Skip to main content

Nethereum.RLP

NuGet: Nethereum.RLP | Source: src/Nethereum.RLP/

Nethereum.RLP

Recursive Length Prefix (RLP) encoding and decoding for Ethereum data serialization.

Overview​

Nethereum.RLP implements Ethereum's primary data serialization format. RLP encodes arbitrarily nested arrays of binary data into a compact byte representation. It is used throughout Ethereum for encoding transactions, blocks, state tries, and network messages.

Key Features​

  • Encode Elements: Encode single items (bytes, strings, integers) to RLP format
  • Encode Lists: Encode collections and nested structures to RLP format
  • Decode RLP: Deserialize RLP-encoded data back to elements or collections, rejecting non-canonical encodings and bounding nesting depth and element count
  • Stream RLP: RlpReader walks an RLP structure field by field without building a tree
  • Type Conversions: Extension methods for encoding/decoding int, BigInteger, and string types
  • Ethereum Compatibility: Full compliance with Ethereum's RLP specification

Installation​

dotnet add package Nethereum.RLP

Dependencies​

  • Nethereum.Hex - Hexadecimal encoding/decoding

Key Concepts​

What is RLP?​

RLP (Recursive Length Prefix) is a space-efficient encoding method for arbitrarily nested arrays of binary data. It is the main encoding method used to serialize objects in Ethereum.

Key Principles:

  • RLP only encodes structure (nested arrays)
  • Atomic data types (integers, strings) are encoded as byte arrays
  • Integers are represented in big-endian binary form
  • Zero-valued bytes are trimmed from the front of integers

RLP Encoding Rules​

Single Bytes (0x00 - 0x7f)​

For a single byte whose value is in the [0x00, 0x7f] range, that byte is its own RLP encoding.

// Character 'd' (0x64) encodes as itself
RLP.EncodeElement(new byte[] { 0x64 }); // Result: [0x64]

Short Strings (0-55 bytes)​

If a string is 0-55 bytes long, the RLP encoding consists of a single byte with value 0x80 plus the length of the string, followed by the string.

// "dog" = 3 bytes
// Encoded as: 0x83 (0x80 + 3) + "dog" bytes
// Result: 0x83646f67

Long Strings (>55 bytes)​

If a string is more than 55 bytes long, the RLP encoding consists of:

  • Single byte with value 0xb7 plus the length of the length
  • The length of the string
  • The string itself
// 56-byte string encodes with 0xb8 prefix
// 0xb8 (0xb7 + 1) + 0x38 (length = 56) + string bytes

Short Lists (0-55 bytes total payload)​

Lists with total payload 0-55 bytes use a single byte with value 0xc0 plus the length, followed by concatenated RLP encodings of items.

// ["cat", "dog"]
// Encoded: 0xc8 + RLP("cat") + RLP("dog")

Long Lists (>55 bytes total payload)​

Lists with payload >55 bytes use 0xf7 plus length-of-length encoding, similar to long strings.

RLP Elements​

  • RLPItem: Represents a single encoded value (byte array)
  • RLPCollection: Represents a list of RLP elements (can contain RLPItems or nested RLPCollections)

Quick Start​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;

// Encode a string
string text = "dog";
byte[] textBytes = text.ToBytesForRLPEncoding();
byte[] encoded = RLP.EncodeElement(textBytes);
// Result: 0x83646f67

// Encode an integer
int number = 1000;
byte[] numberBytes = number.ToBytesForRLPEncoding();
byte[] encodedNum = RLP.EncodeElement(numberBytes);
// Result: 0x8203e8

// Encode a list
string[] items = { "cat", "dog" };
byte[][] itemBytes = items.ToBytesForRLPEncoding();
byte[][] encodedItems = new byte[itemBytes.Length][];
for (int i = 0; i < itemBytes.Length; i++)
{
encodedItems[i] = RLP.EncodeElement(itemBytes[i]);
}
byte[] encodedList = RLP.EncodeList(encodedItems);
// Result: 0xc88363617483646f67

// Decode RLP
IRLPElement decoded = RLP.Decode(encoded);
string decodedText = decoded.RLPData.ToStringFromRLPDecoded();
// Result: "dog"

Usage Examples​

Example 1: Encoding Strings​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;

// Empty string
string empty = "";
byte[] emptyBytes = empty.ToBytesForRLPEncoding();
byte[] encoded = RLP.EncodeElement(emptyBytes);
// Result: 0x80

// Single character
string single = "d";
byte[] singleBytes = single.ToBytesForRLPEncoding();
byte[] encodedSingle = RLP.EncodeElement(singleBytes);
// Result: 0x64 (single byte in range 0x00-0x7f encodes as itself)

// Short string
string dog = "dog";
byte[] dogBytes = dog.ToBytesForRLPEncoding();
byte[] encodedDog = RLP.EncodeElement(dogBytes);
Assert.Equal("83646f67", encodedDog.ToHex());

// Decode back
IRLPElement decoded = RLP.Decode(encodedDog);
string decodedStr = decoded.RLPData.ToStringFromRLPDecoded();
Assert.Equal("dog", decodedStr);

Example 2: Encoding Integers​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;

// Zero encodes as empty byte array
int zero = 0;
byte[] zeroBytes = zero.ToBytesForRLPEncoding();
byte[] encodedZero = RLP.EncodeElement(zeroBytes);
// Result: 0x80 (empty byte array)

// Small integer (< 128)
int small = 15;
byte[] smallBytes = small.ToBytesForRLPEncoding();
byte[] encodedSmall = RLP.EncodeElement(smallBytes);
// Result: 0x0f (single byte in range encodes as itself)

// Medium integer
int medium = 1000;
byte[] mediumBytes = medium.ToBytesForRLPEncoding();
byte[] encodedMedium = RLP.EncodeElement(mediumBytes);
Assert.Equal("8203e8", encodedMedium.ToHex());

// Decode back
IRLPElement decoded = RLP.Decode(encodedMedium);
int decodedInt = decoded.RLPData.ToIntFromRLPDecoded();
Assert.Equal(1000, decodedInt);

Example 3: Encoding BigInteger​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;
using System.Numerics;

// Large BigInteger
byte[] hexBytes = "100102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
.HexToByteArray();
BigInteger bigInt = hexBytes.ToBigIntegerFromRLPDecoded();

byte[] bigIntBytes = bigInt.ToBytesForRLPEncoding();
byte[] encoded = RLP.EncodeElement(bigIntBytes);
Assert.Equal(
"a0100102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
encoded.ToHex()
);

// Decode back
IRLPElement decoded = RLP.Decode(encoded);
BigInteger decodedBigInt = decoded.RLPData.ToBigIntegerFromRLPDecoded();
Assert.Equal(bigInt, decodedBigInt);

Example 4: Encoding Lists​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;

// Empty list
byte[][] emptyList = new byte[0][];
byte[] encodedEmpty = RLP.EncodeList(emptyList);
Assert.Equal("c0", encodedEmpty.ToHex());

RLPCollection decodedEmpty = RLP.Decode(encodedEmpty) as RLPCollection;
Assert.Equal(0, decodedEmpty.Count);

// Short string list
string[] strings = { "cat", "dog" };
byte[][] stringBytes = strings.ToBytesForRLPEncoding();

// Encode each element
byte[][] encodedElements = new byte[stringBytes.Length][];
for (int i = 0; i < stringBytes.Length; i++)
{
encodedElements[i] = RLP.EncodeElement(stringBytes[i]);
}

// Encode the list
byte[] encodedList = RLP.EncodeList(encodedElements);
Assert.Equal("c88363617483646f67", encodedList.ToHex());

// Decode back
RLPCollection decodedList = RLP.Decode(encodedList) as RLPCollection;
Assert.Equal("cat", decodedList[0].RLPData.ToStringFromRLPDecoded());
Assert.Equal("dog", decodedList[1].RLPData.ToStringFromRLPDecoded());

Example 5: Encoding Long Strings​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;

// String longer than 55 bytes
string longString = "Lorem ipsum dolor sit amet, consectetur adipisicing elit"; // 56 bytes
byte[] longBytes = longString.ToBytesForRLPEncoding();
byte[] encoded = RLP.EncodeElement(longBytes);

// Result starts with 0xb8 (0xb7 + 1 for length-of-length)
// followed by 0x38 (length = 56 in hex)
// followed by the string bytes
Assert.Equal(
"b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974",
encoded.ToHex()
);

// Decode back
IRLPElement decoded = RLP.Decode(encoded);
string decodedStr = decoded.RLPData.ToStringFromRLPDecoded();
Assert.Equal(longString, decodedStr);

Example 6: Mixed List with Long String​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;

// List containing short and long strings
string shortStr = "cat";
string longStr = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
string[] mixed = { shortStr, longStr };

byte[][] mixedBytes = mixed.ToBytesForRLPEncoding();

// Encode each element
byte[][] encodedElements = new byte[mixedBytes.Length][];
for (int i = 0; i < mixedBytes.Length; i++)
{
encodedElements[i] = RLP.EncodeElement(mixedBytes[i]);
}

// Encode the list
byte[] encodedList = RLP.EncodeList(encodedElements);
Assert.Equal(
"f83e83636174b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974",
encodedList.ToHex()
);

// Decode back
RLPCollection decoded = RLP.Decode(encodedList) as RLPCollection;
Assert.Equal("cat", decoded[0].RLPData.ToStringFromRLPDecoded());
Assert.Equal(longStr, decoded[1].RLPData.ToStringFromRLPDecoded());

Example 7: Multiple String List​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;

// List with three short strings
string[] animals = { "dog", "god", "cat" };
byte[][] animalBytes = animals.ToBytesForRLPEncoding();

byte[][] encodedElements = new byte[animalBytes.Length][];
for (int i = 0; i < animalBytes.Length; i++)
{
encodedElements[i] = RLP.EncodeElement(animalBytes[i]);
}

byte[] encodedList = RLP.EncodeList(encodedElements);
Assert.Equal("cc83646f6783676f6483636174", encodedList.ToHex());

// Decode and verify
RLPCollection decoded = RLP.Decode(encodedList) as RLPCollection;
for (int i = 0; i < animals.Length; i++)
{
Assert.Equal(animals[i], decoded[i].RLPData.ToStringFromRLPDecoded());
}

Example 8: Working with Raw Bytes​

using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;

// Encode raw byte array
byte[] data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
byte[] encoded = RLP.EncodeElement(data);

// Decode
IRLPElement decoded = RLP.Decode(encoded);
byte[] decodedBytes = decoded.RLPData;

Assert.Equal(data, decodedBytes);

// Empty byte array
byte[] empty = new byte[0];
byte[] encodedEmpty = RLP.EncodeElement(empty);
Assert.Equal("80", encodedEmpty.ToHex());

IRLPElement decodedEmpty = RLP.Decode(encodedEmpty);
Assert.Null(decodedEmpty.RLPData); // Empty encodes/decodes as null

API Reference​

RLP Class​

Main class for encoding and decoding RLP data.

public class RLP
{
// Constants
public const byte OFFSET_SHORT_LIST = 0xc0;
public static readonly byte[] EMPTY_BYTE_ARRAY = new byte[0];
public static readonly byte[] ZERO_BYTE_ARRAY = { 0 };

// Encoding
public static byte[] EncodeElement(byte[] srcData);
public static byte[] EncodeList(params byte[][] elements);

// Decoding
public static IRLPElement Decode(byte[] msgData);
public static IRLPElement Decode(byte[] msgData, int maxElements);
public static IRLPElement DecodeCollection(byte[] msgData);
public static IRLPElement DecodeCollection(byte[] msgData, int maxElements);

// Limits for untrusted input
public const int MaxDecodeDepth = 64;
public const int MinUntrustedElementBudget = 1024;
public static int UntrustedElementBudget(int inputLength);

// Utilities
public static int ByteArrayToInt(byte[] bytes);
}

Decode and DecodeCollection validate the whole structure before they build anything: a single byte below 0x80 carrying a string prefix, a long-form length under 56, a length with a leading zero, a length larger than the input and list nesting deeper than MaxDecodeDepth all throw Exception("Invalid RLP ...") with the cause as InnerException. The maxElements overloads count elements first and throw once the payload holds more, so a flood of one-byte elements is refused before any element is allocated; UntrustedElementBudget(inputLength) is the budget used for bytes read from a peer, max(MinUntrustedElementBudget, inputLength * 3 / 4). Decode also refuses bytes after the top-level value, since it reads exactly one item; DecodeCollection reads a sequence of items and returns them as an RLPCollection. RLPCollection.RLPData of a nested list is a slice of the input copied on first access.

IRLPElement Interface​

Base interface for RLP elements.

public interface IRLPElement
{
byte[] RLPData { get; }
}

RLPItem Class​

Represents a single RLP-encoded value.

public class RLPItem : IRLPElement
{
public byte[] RLPData { get; }
}

RLPCollection Class​

Represents a list of RLP elements (can be nested).

public class RLPCollection : List<IRLPElement>, IRLPElement
{
public byte[] RLPData { get; set; }

// List operations via base class
public int Count { get; }
public IRLPElement this[int index] { get; }
}

Conversion Extension Methods​

Extensions for converting types to/from RLP encoding.

public static class ConvertorForRLPEncodingExtensions
{
// Encoding (to bytes for RLP)
public static byte[] ToBytesForRLPEncoding(this int number);
public static byte[] ToBytesForRLPEncoding(this long number);
public static byte[] ToBytesForRLPEncoding(this BigInteger bigInteger);
public static byte[] ToBytesForRLPEncoding(this string str);
public static byte[][] ToBytesForRLPEncoding(this string[] strings);

// Decoding (from RLP bytes)
public static int ToIntFromRLPDecoded(this byte[] bytes);
public static long ToLongFromRLPDecoded(this byte[] bytes);
public static BigInteger ToBigIntegerFromRLPDecoded(this byte[] bytes);
public static string ToStringFromRLPDecoded(this byte[] bytes);

public static long ToLongBitsFromRLPDecoded(this byte[] bytes);

// Utilities
public static byte[] TrimZeroBytes(this byte[] bytes);
}

ToLongBitsFromRLPDecoded reads the scalar as a uint64 and returns the same 64 bits as a long, so a value in [2^63, 2^64) does not overflow; a scalar wider than 64 bits still throws.

RlpReader​

A forward-only reader over an RLP byte array for decoding input you do not trust. It shares the canonical header parser of RLP.Decode, allocates nothing for structure, and reads each field in the shape you expect, so a list where a string is expected, a leading-zero integer or a wrong-width fixed field throws InvalidOperationException.

public enum RlpKind { Byte, String, List }

public readonly struct RlpListScope
{
public int Start { get; }
public int End { get; }
public int Length { get; }
}

public readonly struct RlpSegment
{
public RlpSegment(int offset, int length);
public int Offset { get; }
public int Length { get; }
}

public sealed class RlpReader
{
public RlpReader(byte[] data);
public RlpReader(byte[] data, int offset, int length);

public int Position { get; }
public bool IsAtEnd();
public bool IsAtEnd(RlpListScope scope);
public RlpKind PeekKind();
public int PeekSize();

public RlpListScope ReadListHeader();
public void EnsureEnd(RlpListScope scope);
public void EnsureEnd();
public int CountItems(RlpListScope scope);

public byte[] ReadBytes();
public byte[] ReadFixed(int length);
public ulong ReadUInt64();
public BigInteger ReadBigInteger();
public bool ReadBool();

public void Skip();
public RlpSegment ReadRawSegment();
public byte[] ReadRaw();
public byte[] ReadData();
}
var encoded = RLP.EncodeList(
RLP.EncodeElement("cat".ToBytesForRLPEncoding()),
RLP.EncodeElement(1000.ToBytesForRLPEncoding()),
RLP.EncodeElement(new byte[] { 0x01 }));

var reader = new RlpReader(encoded);
var list = reader.ReadListHeader();
var name = Encoding.UTF8.GetString(reader.ReadBytes());
var amount = reader.ReadUInt64();
var flag = reader.ReadBool();
reader.EnsureEnd(list);
reader.EnsureEnd();

Backed by FoundationsRlpDocExampleTests.Given_AnEncodedList_When_ReadWithRlpReader_Then_EachFieldIsReadInOrderAndTheEndIsChecked.

ReadUInt64, ReadBigInteger and ReadBool refuse a leading zero, an integer wider than the target and a boolean other than 0 or 1. ReadFixed(length) refuses any other length. ReadListHeader refuses nesting deeper than RLP.MaxDecodeDepth. EnsureEnd(scope) throws when the list holds more elements than were read and EnsureEnd() when bytes follow the top-level value. ReadRaw returns the encoded bytes of the next element, header included, and ReadData returns the payload of a string or the raw encoding of a list.

var leadingZero = new RlpReader("820001".HexToByteArray());
Assert.Throws<InvalidOperationException>(() => leadingZero.ReadUInt64());

var fixedField = new RlpReader("8301020304".HexToByteArray());
Assert.Throws<InvalidOperationException>(() => fixedField.ReadFixed(32));

Backed by FoundationsRlpDocExampleTests.Given_NonCanonicalOrWrongWidthFields_When_ReadWithRlpReader_Then_ItThrows.

Used By (Consumers)​

RLP is used throughout Nethereum for Ethereum data encoding:

  • Nethereum.Signer - Transaction and message signing uses RLP encoding
  • Nethereum.Model - Transaction and block models use RLP
  • Nethereum.Util - Address derivation uses RLP encoding
  • Nethereum.Merkle.Patricia - Patricia Merkle Trie uses RLP for nodes
  • Nethereum.RPC - Some RPC methods work with RLP-encoded data

Dependencies​

  • Nethereum.Hex - Hexadecimal encoding/decoding for displaying RLP data

Important Notes​

Encoding Behavior​

Empty Values​

  • Empty string/byte array encodes as 0x80
  • Decoded empty value returns null
  • Zero integer encodes as 0x80 (empty byte array)
var empty = RLP.EncodeElement(new byte[0]);
// Result: 0x80

var decoded = RLP.Decode(empty);
// Result: decoded.RLPData == null

Single Bytes​

Single bytes in range 0x00-0x7f encode as themselves:

// Character 'd' (0x64) encodes as itself
var encoded = RLP.EncodeElement(new byte[] { 0x64 });
// Result: [0x64]

Big-Endian Encoding​

All integers are encoded in big-endian format with leading zeros trimmed:

int value = 1000; // 0x000003e8
var bytes = value.ToBytesForRLPEncoding();
// Result: [0x03, 0xe8] (leading zeros removed)

Threshold Values​

The RLP specification uses specific threshold values:

  • 56 bytes: Threshold between short and long item encoding
    • 0-55 bytes: Use single-byte length prefix (0x80-0xb7 or 0xc0-0xf7)
    • 56+ bytes: Use length-of-length prefix (0xb8+ or 0xf8+)

Decoding Collections​

When decoding, check the element type:

var decoded = RLP.Decode(encoded);

if (decoded is RLPCollection collection)
{
// It's a list - iterate items
foreach (var item in collection)
{
var value = item.RLPData;
}
}
else
{
// It's a single item
var value = decoded.RLPData;
}

Untrusted Input​

RLP.Decode rejects non-canonical encodings and nesting deeper than RLP.MaxDecodeDepth, and the maxElements overloads cap the element count:

var budget = RLP.UntrustedElementBudget(payload.Length);

Assert.Throws<Exception>(() => RLP.Decode(payload, 100));
var decoded = (RLPCollection)RLP.Decode(payload, budget);

Assert.Throws<Exception>(() => RLP.Decode("8100".HexToByteArray()));
Assert.Throws<Exception>(() => RLP.Decode("b80101".HexToByteArray()));

Backed by FoundationsRlpDocExampleTests.Given_APayloadWithMoreElementsThanTheBudget_When_Decoded_Then_ItIsRejectedBeforeTheTreeIsBuilt and FoundationsRlpDocExampleTests.Given_NonCanonicalOrOverDeepRlp_When_Decoded_Then_AnExceptionIsThrown.

Null vs Empty​

  • Decoded empty items return null for RLPData
  • Check for null before using decoded data:
var decoded = RLP.Decode(encoded);
string str = decoded.RLPData?.ToStringFromRLPDecoded() ?? "";

Common Use Cases​

Transaction Encoding​

Ethereum transactions are RLP-encoded:

// Transaction: [nonce, gasPrice, gasLimit, to, value, data, v, r, s]
var transaction = new List<byte[]>
{
nonce.ToBytesForRLPEncoding(),
gasPrice.ToBytesForRLPEncoding(),
gasLimit.ToBytesForRLPEncoding(),
to.HexToByteArray(),
value.ToBytesForRLPEncoding(),
data.HexToByteArray(),
v.ToBytesForRLPEncoding(),
r,
s
};

var encodedElements = transaction.Select(RLP.EncodeElement).ToArray();
var rlpTransaction = RLP.EncodeList(encodedElements);

Block Header Encoding​

Block headers use RLP for hashing:

// Block header fields encoded as RLP list
var headerFields = new List<byte[]>
{
parentHash,
unclesHash,
beneficiary,
stateRoot,
transactionsRoot,
receiptsRoot,
logsBloom,
difficulty.ToBytesForRLPEncoding(),
number.ToBytesForRLPEncoding(),
gasLimit.ToBytesForRLPEncoding(),
gasUsed.ToBytesForRLPEncoding(),
timestamp.ToBytesForRLPEncoding(),
extraData,
mixHash,
nonce
};

var encodedHeader = RLP.EncodeList(
headerFields.Select(RLP.EncodeElement).ToArray()
);

Additional Resources​