Skip to content

Commit 8a5765b

Browse files
committed
under 1 million gas tree parsing
1 parent d5be7be commit 8a5765b

File tree

8 files changed

+201
-285
lines changed

8 files changed

+201
-285
lines changed

foundry.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5+
optimize = true
6+
optimizer_runs = 1000000
57
via_ir = true
68

79
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

src/CborDecode.sol

Lines changed: 117 additions & 245 deletions
Large diffs are not rendered by default.

src/CidCbor.sol

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@ import "./CborDecode.sol";
66
library CidCbor {
77
using CBORDecoder for bytes;
88

9-
uint8 private constant MAJOR_BYTE_STRING = 2;
10-
uint8 private constant MAJOR_TYPE_TAG = 6;
119
uint8 private constant TAG_CID = 42;
1210

13-
uint8 private constant EXPECTED_CID_LENGTH = 37;
14-
1511
uint8 private constant MULTIBASE_FORMAT = 0x00;
1612
uint8 private constant CID_V1 = 0x01;
1713
uint8 private constant MULTICODEC_DAG_CBOR = 0x71;
@@ -32,25 +28,48 @@ library CidCbor {
3228
*/
3329
type CidBytes32 is bytes32;
3430

35-
function expectCidTag(bytes memory cborData, uint byteIdx) internal pure returns (uint) {
36-
uint8 maj;
37-
uint value;
38-
(maj, value, byteIdx) = cborData.parseCborHeader(byteIdx);
39-
require(maj == MAJOR_TYPE_TAG, "expected tag major");
40-
require(value == TAG_CID, "expected tag 42 for CID");
31+
function expectTagCid(bytes memory cborData, uint byteIdx) internal pure returns (uint) {
32+
uint8 head = uint8(cborData[byteIdx]);
33+
uint8 tagValue = uint8(cborData[byteIdx + 1]);
34+
require(head == MajorTag << shiftMajor | MinorExtend1, "expected tag head with 1-byte extension");
35+
require(tagValue == TAG_CID, "expected tag 42 for CID");
36+
return byteIdx + 2;
37+
}
38+
39+
function expect37Bytes(bytes memory cborData, uint byteIdx) internal pure returns (uint) {
40+
require(
41+
uint8(cborData[byteIdx]) == MajorBytes << shiftMajor | MinorExtend1,
42+
"expected byte head with 1-byte extension"
43+
);
44+
require(uint8(cborData[byteIdx + 1]) == 37, "expected 37 bytes for CID");
45+
byteIdx += 2;
4146
return byteIdx;
4247
}
4348

4449
function readCidBytes32(bytes memory cborData, CidIndex idx) internal pure returns (CidBytes32) {
50+
return readCidBytes32_assembly(cborData, idx);
51+
}
52+
53+
function readCidBytes32_loop(bytes memory cborData, CidIndex idx) internal pure returns (CidBytes32) {
4554
uint cidIdx = CidIndex.unwrap(idx);
46-
require(cidIdx != 0, "Can't read a CID hash at index 0");
47-
bytes memory cidBytes = new bytes(MULTIHASH_SIZE_32);
48-
for (uint i = 0; i < MULTIHASH_SIZE_32; i++) {
55+
bytes memory cidBytes = new bytes(32);
56+
for (uint i = 0; i < 32; i++) {
4957
cidBytes[i] = cborData[cidIdx + i];
5058
}
5159
return CidBytes32.wrap(bytes32(cidBytes));
5260
}
5361

62+
function readCidBytes32_assembly(bytes memory cborData, CidIndex idx) internal pure returns (CidBytes32) {
63+
uint cidIdx = CidIndex.unwrap(idx);
64+
require(cidIdx != 0, "Can't read a CID hash at index 0");
65+
66+
bytes32 cidBytes;
67+
assembly ("memory-safe") {
68+
cidBytes := mload(add(cborData, add(0x20, cidIdx)))
69+
}
70+
return CidBytes32.wrap(cidBytes);
71+
}
72+
5473
function readNullableCidIndex(bytes memory cborData, uint byteIdx) internal pure returns (CidIndex, uint) {
5574
if (cborData.isNullNext(byteIdx)) {
5675
return (CidIndex.wrap(0), byteIdx + 1);
@@ -60,20 +79,40 @@ library CidCbor {
6079
}
6180

6281
function readCidIndex(bytes memory cborData, uint byteIdx) internal pure returns (CidIndex, uint) {
63-
(byteIdx) = expectCidTag(cborData, byteIdx);
64-
(uint8 maj, uint len, uint bytesStart) = cborData.parseCborHeader(byteIdx);
82+
return readCidIndex_assembly(cborData, byteIdx);
83+
}
84+
85+
function readCidIndex_assembly(bytes memory cborData, uint byteIdx) internal pure returns (CidIndex, uint) {
86+
bytes9 cidHead;
87+
assembly ("memory-safe") {
88+
cidHead := mload(add(cborData, add(0x20, byteIdx)))
89+
}
90+
91+
// all cids encountered will contain this 9-byte header
92+
// D8 2A cbor tag(42) cid
93+
// 58 25 cbor bytes(37) total length
94+
// 00 multibase format
95+
// 01 CID version
96+
// 71 multicodec DAG-CBOR
97+
// 12 multihash sha-256
98+
// 20 hash size 32 bytes
99+
require(cidHead == hex"D82A58250001711220", "expected CIDv1 dag-cbor sha256 head");
100+
byteIdx += 9;
101+
return (CidIndex.wrap(byteIdx), byteIdx + 32);
102+
}
65103

66-
require(maj == MAJOR_BYTE_STRING, "expected byte string");
67-
require(len == EXPECTED_CID_LENGTH, "expected bytes length 37 for CID");
104+
function readCidIndex_access(bytes memory cborData, uint byteIdx) internal pure returns (CidIndex, uint) {
105+
byteIdx = expectTagCid(cborData, byteIdx);
106+
byteIdx = expect37Bytes(cborData, byteIdx);
68107

69-
// multibase format
70-
require(uint8(cborData[bytesStart]) == MULTIBASE_FORMAT, "expected multibase item");
71-
// cid format
72-
require(uint8(cborData[bytesStart + 1]) == CID_V1, "expected CID v1");
73-
require(uint8(cborData[bytesStart + 2]) == MULTICODEC_DAG_CBOR, "expected CID multicodec DAG-CBOR");
74-
require(uint8(cborData[bytesStart + 3]) == MULTIHASH_SHA_256, "expected CID multihash sha-256");
75-
require(uint8(cborData[bytesStart + 4]) == MULTIHASH_SIZE_32, "expected CID content size 32 bytes");
108+
// uint8 conversion is faster than assembly? lol
109+
require(uint8(cborData[byteIdx]) == MULTIBASE_FORMAT, "expected multibase item");
110+
// cid format always
111+
require(uint8(cborData[byteIdx + 1]) == CID_V1, "expected CID v1");
112+
require(uint8(cborData[byteIdx + 2]) == MULTICODEC_DAG_CBOR, "expected CID multicodec DAG-CBOR");
113+
require(uint8(cborData[byteIdx + 3]) == MULTIHASH_SHA_256, "expected CID multihash sha-256");
114+
require(uint8(cborData[byteIdx + 4]) == MULTIHASH_SIZE_32, "expected CID content size 32 bytes");
76115

77-
return (CidIndex.wrap(bytesStart + 5), bytesStart + len);
116+
return (CidIndex.wrap(byteIdx + 5), byteIdx + 37);
78117
}
79118
}

src/CommitCbor.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ library CommitCbor {
3131
} else if (bytes5(mapKey) == "data") {
3232
(ret.data, byteIdx) = CidCbor.readCidIndex(cborData, byteIdx);
3333
} else if (bytes5(mapKey) == "prev") {
34+
// TODO: skip this field?
3435
(ret.prev, byteIdx) = CidCbor.readNullableCidIndex(cborData, byteIdx);
3536
} else if (bytes4(mapKey) == "did") {
3637
(ret.did, byteIdx) = cborData.readString(byteIdx);
38+
// TODO: other did formats?
3739
require(bytes(ret.did).length == 32, "commit did string must be 32 bytes");
3840
} else if (bytes4(mapKey) == "rev") {
41+
// monotonic commit timestamp
3942
(ret.rev, byteIdx) = cborData.readString(byteIdx);
4043
} else {
4144
revert("unexpected commit field");

src/RecordCbor.sol

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,36 @@ library RecordCbor {
1111
string dollarType;
1212
}
1313

14-
function readRecord(bytes memory cborData, uint byteIdx) internal pure returns (Record memory, uint) {
14+
function readRecord(bytes memory cborData, uint byteIdx) internal pure returns (Record memory ret, uint) {
1515
uint mapLen;
1616
(mapLen, byteIdx) = cborData.readFixedMap(byteIdx);
1717

1818
require(mapLen == 4, "expected 4 fields in record");
1919

20-
string memory text;
21-
string memory dollarType;
22-
2320
for (uint i = 0; i < mapLen; i++) {
2421
bytes memory mapKey;
2522
(mapKey, byteIdx) = cborData.readStringBytes(byteIdx);
2623
if (bytes5(mapKey) == "text") {
27-
(text, byteIdx) = cborData.readString(byteIdx);
24+
(ret.text, byteIdx) = cborData.readString(byteIdx);
2825
} else if (bytes6(mapKey) == "$type") {
29-
(dollarType, byteIdx) = cborData.readString(byteIdx);
26+
(ret.dollarType, byteIdx) = cborData.readString(byteIdx);
3027
} else if (bytes6(mapKey) == "langs") {
3128
uint langsLength;
3229
(langsLength, byteIdx) = cborData.readFixedArray(byteIdx);
3330
for (uint j = 0; j < langsLength; j++) {
31+
// langs string unused.
3432
byteIdx = cborData.skipString(byteIdx);
3533
}
3634
} else if (bytes10(mapKey) == "createdAt") {
35+
// createdAt string unused.
36+
// this field is arbitrary user-defined data. the useful and
37+
// verifiable timestamp is the signed commit repo revision field
3738
byteIdx = cborData.skipString(byteIdx);
3839
} else {
3940
revert("unexpected record key");
4041
}
4142
}
4243

43-
return (Record(text, dollarType), byteIdx);
44+
return (ret, byteIdx);
4445
}
4546
}

src/TreeNodeCbor.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ library TreeNodeCbor {
9696
TreeNodeE[] memory e;
9797
(e, byteIdx) = readNodeE(cborData, byteIdx);
9898
node.entries = buildEntryKeys(e);
99-
//node.entries = dummyEntryKeys(e); // skipping rebuild saves approx 300k gas
10099
}
101100
}
102101
return (node, byteIdx);

test/CborDecode.t.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import "../src/CborDecode.sol";
2929
contract CborDecodeTest {
3030
using CBORDecoder for bytes;
3131

32+
/*
3233
function test_decodeFixedArray() public pure {
3334
bytes memory input = hex"8F0102030405060708090A64746573744401010101F4F6F5";
3435
uint index = 0;
@@ -72,6 +73,7 @@ contract CborDecodeTest {
7273
(str, index) = input.readString(index);
7374
require(keccak256(abi.encodePacked(str)) == keccak256(abi.encodePacked("test")), "str is not 'test'");
7475
}
76+
*/
7577

7678
function test_decodeFalse() public pure {
7779
bytes memory input = hex"f4";
@@ -135,7 +137,9 @@ contract CborDecodeTest {
135137
uint arrayLen = 0;
136138
uint8 num;
137139

140+
console.log("reading fixed array");
138141
(arrayLen, index) = input.readFixedArray(index);
142+
console.log("arrayLen", arrayLen);
139143
require(arrayLen == 5, "array len is not 5");
140144

141145
(num, index) = input.readUInt8(index);

test/CidCbor.t.sol

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,15 @@ import {Test, console} from "forge-std/Test.sol";
55
import "../src/CborDecode.sol";
66
import "../src/CidCbor.sol";
77

8-
contract CidCborTest {
8+
contract CidCborTest is Test {
99
bytes private constant cidCbor =
1010
hex"D82A5825000171122066DA6655BF8DA79B69A87299CF170FED8497FA3059379DC4A8BFE1E28CAB5D93";
1111

1212
function test_readCidIndex_only() public pure {
1313
CidCbor.readCidIndex(cidCbor, 0);
1414
}
1515

16-
function test_readCidIndex_readCidBytes32() public pure {
17-
(CidCbor.CidIndex cidIdx, uint byteIdx) = CidCbor.readCidIndex(cidCbor, 0);
18-
require(byteIdx == cidCbor.length, "expected to read all bytes");
19-
20-
CidCbor.CidBytes32 cid = CidCbor.readCidBytes32(cidCbor, cidIdx);
21-
console.logBytes32(CidCbor.CidBytes32.unwrap(cid));
16+
function test_readCidBytes32_only() public pure {
17+
CidCbor.readCidBytes32(cidCbor, CidCbor.CidIndex.wrap(9));
2218
}
2319
}

0 commit comments

Comments
 (0)