using System; using System.Collections.Generic; using System.Linq; using Nethereum.Hex.HexConvertors.Extensions; namespace Nethereum.RLP { /// /// Recursive Length Prefix (RLP) encoding. /// /// The purpose of RLP is to encode arbitrarily nested arrays of binary data, and /// RLP is the main encoding method used to serialize objects in Ethereum. The /// only purpose of RLP is to encode structure; encoding specific atomic data /// types (eg. strings, integers, floats) is left up to higher-order protocols; in /// Ethereum the standard is that integers are represented in big endian binary /// form. If one wishes to use RLP to encode a dictionary, the two suggested /// canonical forms are to either use [[k1,v1],[k2,v2]...] with keys in /// lexicographic order or to use the higher-level Patricia Tree encoding as /// Ethereum does. /// /// /// The RLP encoding function takes in an item. An item is defined as follows: /// /// /// - A string (ie. byte array) is an item - A list of items is an item /// /// /// For example, an empty string is an item, as is the string containing the word /// "cat", a list containing any number of strings, as well as more complex data /// structures like ["cat",["puppy","cow"],"horse",[[]],"pig",[""],"sheep"]. Note /// that in the context of the rest of this article, "string" will be used as a /// synonym for "a certain number of bytes of binary data"; no special encodings /// are used and no knowledge about the content of the strings is implied. /// /// /// See: /// /// /// public class RLP { /// /// Reason for threshold according to Vitalik Buterin: /// - 56 bytes maximizes the benefit of both options /// - if we went with 60 then we would have only had 4 slots for long strings /// so RLP would not have been able to store objects above 4gb /// - if we went with 48 then RLP would be fine for 2^128 space, but that's way too much /// - so 56 and 2^64 space seems like the right place to put the cutoff /// - also, that's where Bitcoin's varint does the cutof /// private const int SIZE_THRESHOLD = 56; /* RLP encoding rules are defined as follows: * For a single byte whose value is in the [0x00, 0x7f] range, that byte is * its own RLP encoding. */ /// /// [0x80] /// 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. The range of the first byte is thus [0x80, 0xb7]. /// private const byte OFFSET_SHORT_ITEM = 0x80; /// /// [0xb7] /// If a string is more than 55 bytes long, the RLP encoding consists of a /// single byte with value 0xb7 plus the length of the length of the string /// in binary form, followed by the length of the string, followed by the /// string. For example, a length-1024 string would be encoded as /// \xb9\x04\x00 followed by the string. The range of the first byte is thus /// [0xb8, 0xbf]. /// private const byte OFFSET_LONG_ITEM = 0xb7; /// /// [0xc0] /// If the total payload of a list (i.e. the combined length of all its /// items) is 0-55 bytes long, the RLP encoding consists of a single byte /// with value 0xc0 plus the length of the list followed by the concatenation /// of the RLP encodings of the items. The range of the first byte is thus /// [0xc0, 0xf7]. /// public const byte OFFSET_SHORT_LIST = 0xc0; /// /// [0xf7] /// If the total payload of a list is more than 55 bytes long, the RLP /// encoding consists of a single byte with value 0xf7 plus the length of the /// length of the list in binary form, followed by the length of the list, /// followed by the concatenation of the RLP encodings of the items. The /// range of the first byte is thus [0xf8, 0xff]. /// private const byte OFFSET_LONG_LIST = 0xf7; public static readonly byte[] EMPTY_BYTE_ARRAY = new byte[0]; public static readonly byte[] ZERO_BYTE_ARRAY = {0}; public static int ByteArrayToInt(byte[] bytes) { if (BitConverter.IsLittleEndian) Array.Reverse(bytes); return BitConverter.ToInt32(bytes, 0); } public static IRLPElement Decode(byte[] msgData) { var rlpCollection = new RLPCollection(); Decode(msgData, 0, 0, msgData.Length, 1, rlpCollection); return rlpCollection[0]; } public static IRLPElement DecodeCollection(byte[] msgData) { var rlpCollection = new RLPCollection(); Decode(msgData, 0, 0, msgData.Length, 1, rlpCollection); return rlpCollection; } /// /// Decodes a message from a starting point to an end point /// public static void Decode(byte[] msgData, int level, int startPosition, int endPosition, int levelToIndex, RLPCollection rlpCollection) { if (msgData == null || msgData.Length == 0) return; var currentData = new byte[endPosition - startPosition]; Array.Copy(msgData, startPosition, currentData, 0, currentData.Length); try { var currentPosition = startPosition; while (currentPosition < endPosition) { if (IsListBiggerThan55Bytes(msgData, currentPosition)) { currentPosition = ProcessListBiggerThan55Bytes(msgData, level, levelToIndex, rlpCollection, currentPosition); continue; } if (IsListLessThan55Bytes(msgData, currentPosition)) { currentPosition = ProcessListLessThan55Bytes(msgData, level, levelToIndex, rlpCollection, currentPosition); continue; } if (IsItemBiggerThan55Bytes(msgData, currentPosition)) { currentPosition = ProcessItemBiggerThan55Bytes(msgData, rlpCollection, currentPosition); continue; } if (IsItemLessThan55Bytes(msgData, currentPosition)) { currentPosition = ProcessItemLessThan55Bytes(msgData, rlpCollection, currentPosition); continue; } if (IsNullItem(msgData, currentPosition)) { currentPosition = ProcessNullItem(rlpCollection, currentPosition); continue; } if (IsSigleByteItem(msgData, currentPosition)) currentPosition = ProcessSingleByteItem(msgData, rlpCollection, currentPosition); } } catch (Exception ex) { throw new Exception( "Invalid RLP " + currentData.ToHex(), ex); } } /// /// data[0] - 0xF7 = how many next bytes allocated /// for the length of the list /// private static bool IsListBiggerThan55Bytes(byte[] msgData, int currentPosition) { return msgData[currentPosition] > OFFSET_LONG_LIST; } private static bool IsListLessThan55Bytes(byte[] msgData, int currentPosition) { return msgData[currentPosition] >= OFFSET_SHORT_LIST && msgData[currentPosition] <= OFFSET_LONG_LIST; } // It's an item with a payload more than 55 bytes // data[0] - 0xB7 = how much next bytes allocated for // the length of the string private static bool IsItemBiggerThan55Bytes(byte[] msgData, int currentPosition) { return msgData[currentPosition] > OFFSET_LONG_ITEM && msgData[currentPosition] < OFFSET_SHORT_LIST; } // data[0] - 0x80 == length of the item private static bool IsItemLessThan55Bytes(byte[] msgData, int currentPosition) { return msgData[currentPosition] > OFFSET_SHORT_ITEM && msgData[currentPosition] <= OFFSET_LONG_ITEM; } private static bool IsNullItem(byte[] msgData, int currentPosition) { return msgData[currentPosition] == OFFSET_SHORT_ITEM; } private static bool IsSigleByteItem(byte[] msgData, int currentPosition) { return msgData[currentPosition] < OFFSET_SHORT_ITEM; } private static int ProcessSingleByteItem(byte[] msgData, RLPCollection rlpCollection, int currentPosition) { byte[] item = {msgData[currentPosition]}; var rlpItem = new RLPItem(item); rlpCollection.Add(rlpItem); currentPosition += 1; return currentPosition; } private static int ProcessNullItem(RLPCollection rlpCollection, int currentPosition) { var item = EMPTY_BYTE_ARRAY; var rlpItem = new RLPItem(item); rlpCollection.Add(rlpItem); currentPosition += 1; return currentPosition; } private static int ProcessItemLessThan55Bytes(byte[] msgData, RLPCollection rlpCollection, int currentPosition) { var length = (byte) (msgData[currentPosition] - OFFSET_SHORT_ITEM); var item = new byte[length]; Array.Copy(msgData, currentPosition + 1, item, 0, length); var rlpPrefix = new byte[2]; Array.Copy(msgData, currentPosition, rlpPrefix, 0, 2); var rlpItem = new RLPItem(item); rlpCollection.Add(rlpItem); currentPosition += 1 + length; return currentPosition; } private static int ProcessItemBiggerThan55Bytes(byte[] msgData, RLPCollection rlpCollection, int currentPosition) { var lengthOfLength = (byte) (msgData[currentPosition] - OFFSET_LONG_ITEM); var length = CalculateLength(lengthOfLength, msgData, currentPosition); // now we can parse an item for data[1]..data[length] var item = new byte[length]; Array.Copy(msgData, currentPosition + lengthOfLength + 1, item, 0, length); var rlpPrefix = new byte[lengthOfLength + 1]; Array.Copy(msgData, currentPosition, rlpPrefix, 0, lengthOfLength + 1); var rlpItem = new RLPItem(item); rlpCollection.Add(rlpItem); currentPosition += lengthOfLength + length + 1; return currentPosition; } private static int ProcessListLessThan55Bytes(byte[] msgData, int level, int levelToIndex, RLPCollection rlpCollection, int currentPosition) { var length = msgData[currentPosition] - OFFSET_SHORT_LIST; var rlpDataLength = length + 1; var rlpData = new byte[length + 1]; Array.Copy(msgData, currentPosition, rlpData, 0, rlpDataLength); var newLevelCollection = new RLPCollection {RLPData = rlpData}; if (length > 0) Decode(msgData, level + 1, currentPosition + 1, currentPosition + rlpDataLength, levelToIndex, newLevelCollection); rlpCollection.Add(newLevelCollection); currentPosition += rlpDataLength; return currentPosition; } private static int ProcessListBiggerThan55Bytes(byte[] msgData, int level, int levelToIndex, RLPCollection rlpCollection, int currentPosition) { var lengthOfLength = (byte) (msgData[currentPosition] - OFFSET_LONG_LIST); var length = CalculateLength(lengthOfLength, msgData, currentPosition); var rlpDataLength = lengthOfLength + length + 1; var rlpData = new byte[rlpDataLength]; Array.Copy(msgData, currentPosition, rlpData, 0, rlpDataLength); var newLevelCollection = new RLPCollection {RLPData = rlpData}; Decode(msgData, level + 1, currentPosition + lengthOfLength + 1, currentPosition + rlpDataLength, levelToIndex, newLevelCollection); rlpCollection.Add(newLevelCollection); currentPosition += rlpDataLength; return currentPosition; } public static IRLPElement DecodeFirstElement(byte[] msgData, int startPos) { var rlpCollection = new RLPCollection(); Decode(msgData, 0, startPos, startPos + 1, 1, rlpCollection); return rlpCollection[0]; } public static byte[] EncodeByte(byte singleByte) { if (singleByte == 0) return new[] {OFFSET_SHORT_ITEM}; if (singleByte <= 0x7F) return new[] {singleByte}; return new[] {(byte) (OFFSET_SHORT_ITEM + 1), singleByte}; } public static byte[] EncodeElement(byte[] srcData) { if (IsNullOrZeroArray(srcData)) return new[] {OFFSET_SHORT_ITEM}; if (IsSingleZero(srcData)) return srcData; if (srcData.Length == 1 && srcData[0] < 0x80) return srcData; if (srcData.Length < SIZE_THRESHOLD) { // length = 8X var length = (byte) (OFFSET_SHORT_ITEM + srcData.Length); var data = new byte[srcData.Length + 1]; Array.Copy(srcData, 0, data, 1, srcData.Length); data[0] = length; return data; } else { // length of length = BX // prefix = [BX, [length]] var tmpLength = srcData.Length; byte byteNum = 0; while (tmpLength != 0) { ++byteNum; tmpLength = tmpLength >> 8; } var lenBytes = new byte[byteNum]; for (var i = 0; i < byteNum; ++i) lenBytes[byteNum - 1 - i] = (byte) (srcData.Length >> (8 * i)); // first byte = F7 + bytes.length var data = new byte[srcData.Length + 1 + byteNum]; Array.Copy(srcData, 0, data, 1 + byteNum, srcData.Length); data[0] = (byte) (OFFSET_LONG_ITEM + byteNum); Array.Copy(lenBytes, 0, data, 1, lenBytes.Length); return data; } } public static byte[] EncodeDataItemsAsElementOrListAndCombineAsList(byte[][] dataItems, int[] indexOfListDataItems = null) { if(indexOfListDataItems == null) return EncodeList(dataItems.Select(EncodeElement).ToArray()); var encodedData = new List(); for (var i = 0; i < dataItems.Length; i++) { if (indexOfListDataItems.Contains(i)) { var item = dataItems[i]; encodedData.Add(EncodeList(item)); } else { encodedData.Add(EncodeElement(dataItems[i])); } } return EncodeList(encodedData.ToArray()); } public static byte[] EncodeList(params byte[][] items) { if (items == null || (items.Length == 1 && items[0] == null)) return new[] {OFFSET_SHORT_LIST}; var totalLength = 0; for (var i = 0; i < items.Length; i++) totalLength += items[i].Length; byte[] data; int copyPos; if (totalLength < SIZE_THRESHOLD) { var dataLength = 1 + totalLength; data = new byte[dataLength]; //single byte length data[0] = (byte) (OFFSET_SHORT_LIST + totalLength); copyPos = 1; } else { // length of length = BX // prefix = [BX, [length]] var tmpLength = totalLength; byte byteNum = 0; while (tmpLength != 0) { ++byteNum; tmpLength = tmpLength >> 8; } tmpLength = totalLength; var lenBytes = new byte[byteNum]; for (var i = 0; i < byteNum; ++i) lenBytes[byteNum - 1 - i] = (byte) (tmpLength >> (8 * i)); // first byte = F7 + bytes.length data = new byte[1 + lenBytes.Length + totalLength]; data[0] = (byte) (OFFSET_LONG_LIST + byteNum); Array.Copy(lenBytes, 0, data, 1, lenBytes.Length); copyPos = lenBytes.Length + 1; } //Combine all elements foreach (var item in items) { Array.Copy(item, 0, data, copyPos, item.Length); copyPos += item.Length; } return data; } public static bool IsNullOrZeroArray(byte[] array) { return array == null || array.Length == 0; } public static bool IsSingleZero(byte[] array) { return array.Length == 1 && array[0] == 0; } private static int CalculateLength(int lengthOfLength, byte[] msgData, int pos) { var pow = (byte) (lengthOfLength - 1); var length = 0; for (var i = 1; i <= lengthOfLength; ++i) { length += msgData[pos + i] << (8 * pow); pow--; } return length; } } }