RLP Encoding and decoding
$ dotnet add package Nethereum.RLPRecursive Length Prefix (RLP) encoding and decoding for Ethereum data serialization.
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.
dotnet add package Nethereum.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:
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]
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
If a string is more than 55 bytes long, the RLP encoding consists of:
// 56-byte string encodes with 0xb8 prefix
// 0xb8 (0xb7 + 1) + 0x38 (length = 56) + string bytes
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")
Lists with payload >55 bytes use 0xf7 plus length-of-length encoding, similar to long strings.
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"
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);
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);
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);
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());
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);
using Nethereum.RLP;
using Nethereum.Hex.HexConvertors.Extensions;
// List containing short and long strings
string short = "cat";
string long = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
string[] mixed = { short, long };
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(long, decoded[1].RLPData.ToStringFromRLPDecoded());
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());
}
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
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[] data);
public static IRLPElement Decode(byte[] data, int position);
// Utilities
public static int ByteArrayToInt(byte[] bytes);
public static byte[] IntToByteArray(int value);
}
Base interface for RLP elements.
public interface IRLPElement
{
byte[] RLPData { get; }
}
Represents a single RLP-encoded value.
public class RLPItem : IRLPElement
{
public byte[] RLPData { get; }
}
Represents a list of RLP elements (can be nested).
public class RLPCollection : List<IRLPElement>, IRLPElement
{
public byte[] RLPData { get; }
// List operations via base class
public int Count { get; }
public IRLPElement this[int index] { get; }
}
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);
// Utilities
public static byte[] TrimZeroBytes(this byte[] bytes);
}
RLP is used throughout Nethereum for Ethereum data encoding:
0x80null0x80 (empty byte array)var empty = RLP.EncodeElement(new byte[0]);
// Result: 0x80
var decoded = RLP.Decode(empty);
// Result: decoded.RLPData == null
Single bytes in range 0x00-0x7f encode as themselves:
// Character 'd' (0x64) encodes as itself
var encoded = RLP.EncodeElement(new byte[] { 0x64 });
// Result: [0x64]
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)
The RLP specification uses specific threshold values:
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;
}
null for RLPDatavar decoded = RLP.Decode(encoded);
string str = decoded.RLPData?.ToStringFromRLPDecoded() ?? "";
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 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()
);