Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

createCommitment & blob to shares #329

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c1fbdf9
forge install: forge-std
S1nus Jun 16, 2024
e412e18
Merge remote-tracking branch 'origin'
S1nus Jul 31, 2024
1e2fcd0
blob to shares
S1nus Aug 1, 2024
3c49274
typo
S1nus Aug 1, 2024
b7b79e8
this is annoying, but accelerate
S1nus Aug 4, 2024
443cd52
somewhat functioning test setup. Now to make it work.
S1nus Aug 4, 2024
e3bfc12
test setup works. now to fix the implementation
S1nus Aug 4, 2024
650595b
so close
S1nus Aug 4, 2024
c13cba6
blob to shares
S1nus Aug 5, 2024
396eca4
cleanups
S1nus Aug 5, 2024
95d16ce
comment
S1nus Aug 5, 2024
9fde028
start createCommitment
S1nus Aug 7, 2024
bdf6d18
update libs
S1nus Aug 7, 2024
9ecf545
cooking on createCommitment
S1nus Aug 7, 2024
902471c
subtree roots
S1nus Aug 14, 2024
c1bb1a4
trying to debug a seg fault
S1nus Aug 16, 2024
fab0080
cleanup
S1nus Aug 16, 2024
267a2de
trying to make it work
S1nus Aug 17, 2024
53ba966
ok now it actually returns something
S1nus Aug 17, 2024
d4fa30b
regenerate test vectors, now blobToShares broke
S1nus Aug 19, 2024
2474e6b
add commitment to test vectors
S1nus Aug 19, 2024
51b848b
upgraded testvectors
S1nus Aug 19, 2024
db5a04b
update testVecs again
S1nus Aug 19, 2024
60cc7f8
fix node creation
S1nus Aug 20, 2024
590d14a
bLeafHash
S1nus Aug 20, 2024
b9e0fc8
try prepending namespace bytes
S1nus Aug 20, 2024
ae44d6a
about to try adding tons of logs
S1nus Aug 20, 2024
6f225ec
createCommitment seems to work
S1nus Aug 21, 2024
4fa90fc
createCommitment seems to work, added test vectors up to 29kB blob size
S1nus Aug 21, 2024
2f40a19
cleanup
S1nus Aug 21, 2024
6305985
forge fmt
S1nus Aug 23, 2024
3190a0b
Merge branch 'master' of https://github.com/celestiaorg/blobstream-co…
S1nus Aug 27, 2024
079d8f6
Merge branch 'master' into connor/blob-to-shares
S1nus Aug 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
solc_version = "0.8.22"
via_ir = true
gas_reports = ["*"]
fs_permissions = [{ access = "read", path = "./"}]
2 changes: 1 addition & 1 deletion lib/ds-test
215 changes: 215 additions & 0 deletions src/lib/commitment/Commitment.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Namespace, isReservedNamespace} from "../tree/Types.sol";
import "../tree/namespace/NamespaceMerkleTree.sol";
import "../tree/binary/BinaryMerkleTree.sol";
import "../tree/binary/BinaryMerkleMultiproof.sol";
import "../tree/namespace/NamespaceNode.sol";
import "../tree/namespace/NamespaceMerkleMultiproof.sol";
import {leafDigest} from "../tree/namespace/TreeHasher.sol";
import {leafDigest as bLeafDigest} from "../tree/binary/TreeHasher.sol";
import "../../../lib/openzeppelin-contracts-upgradeable/contracts/utils/math/MathUpgradeable.sol";
import "forge-std/console.sol";

uint256 constant SUBTREE_ROOT_THRESHOLD = 64;

function _divCeil(uint256 x, uint256 y) pure returns (uint256 z) {
z = x / y + (x % y == 0 ? 0 : 1);
}

function _numShares(uint256 blobSize) pure returns (uint256) {
return _divCeil((MathUpgradeable.max(blobSize, 478) - 478), 482) + 1;
}

function _copyNamespace(bytes memory share, bytes29 namespaceBytes) pure {
for (uint256 i = 0; i < namespaceBytes.length; i++) {
share[i] = namespaceBytes[i];
}
}

function _writeInfoByteV0(bytes memory share, bool startingSequence) pure {
share[29] = bytes1(startingSequence ? 1 : 0);
}

function _writeSequenceLength(bytes memory share, uint32 sequenceLength) pure {
// Removed the "reverse", because it didn't work- maybe it's already big-endian?
//bytes4 sequenceLengthBigEndianBytes = bytes4(abi.encodePacked(reverse(sequenceLength)));
bytes4 sequenceLengthBigEndianBytes = bytes4(abi.encodePacked(sequenceLength));
share[30] = sequenceLengthBigEndianBytes[0];
share[31] = sequenceLengthBigEndianBytes[1];
share[32] = sequenceLengthBigEndianBytes[2];
share[33] = sequenceLengthBigEndianBytes[3];
}

function _copyBytes(bytes memory buffer, uint32 cursor, bytes memory data, uint32 length) pure returns (uint32) {
uint256 start = buffer.length - length;
for (uint256 i = start; i < buffer.length; i++) {
if (cursor < data.length) {
buffer[i] = data[cursor];
cursor++;
} else {
buffer[i] = 0;
}
}
return cursor;
}

function _bytesToHexString(bytes memory buffer) pure returns (string memory) {
// Fixed buffer size for hexadecimal convertion
bytes memory converted = new bytes(buffer.length * 2);

bytes memory _base = "0123456789abcdef";

for (uint256 i = 0; i < buffer.length; i++) {
converted[i * 2] = _base[uint8(buffer[i]) / _base.length];
converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length];
}

return string(abi.encodePacked(converted));
}

// Share Version 0
function _bytesToSharesV0(bytes memory blobData, Namespace memory namespace)
pure
returns (bytes[] memory shares, bool err)
{
if (namespace.version != 0) {
return (new bytes[](0), true);
}
if (isReservedNamespace(namespace)) {
return (new bytes[](0), true);
}
// Allocate memory for the shares
uint256 numShares = _numShares(blobData.length);
bytes[] memory _shares = new bytes[](numShares);
for (uint256 i = 0; i < _shares.length; i++) {
_shares[i] = new bytes(512);
}
// Get the namespace bytes
bytes29 namespaceBytes = namespace.toBytes();

// The first share is special, because it has startingSequence set to true and the 4-byte sequence length
_copyNamespace(_shares[0], namespaceBytes);
_writeInfoByteV0(_shares[0], true);
_writeSequenceLength(_shares[0], uint32(blobData.length));
uint32 cursor = 0;
cursor = _copyBytes(_shares[0], cursor, blobData, uint32(478)); //only 478 bytes, because 4 bytes are used for the sequence length

if (shares.length != 1) {
// The remaining shares are all the same
for (uint256 i = 1; i < _shares.length; i++) {
// Copy the namespace
_copyNamespace(_shares[i], namespaceBytes);
// Write the info byte
_writeInfoByteV0(_shares[i], false);
// Copy the data
cursor = _copyBytes(_shares[i], cursor, blobData, uint32(482)); // copy the full 482 bytes
}
}

shares = _shares;
err = false;
}

function _roundDownPowerOfTwo(uint256 x) pure returns (uint256) {
uint256 result = 1;
while (result * 2 <= x) {
result *= 2;
}
return result;
}

function _roundUpPowerOfTwo(uint256 x) pure returns (uint256) {
uint256 result = 1;
while (result < x) {
result *= 2;
}
return result;
}

function _blobMinSquareSize(uint256 shareCount) pure returns (uint256) {
return _roundUpPowerOfTwo(MathUpgradeable.sqrt(shareCount, MathUpgradeable.Rounding.Ceil));
}

function _subtreeWidth(uint256 shareCount, uint256 subtreeRootThreshold) pure returns (uint256) {
uint256 s = shareCount / subtreeRootThreshold;
if (s != 0) {
s++;
}
s = _roundUpPowerOfTwo(s);
return MathUpgradeable.min(s, _blobMinSquareSize(shareCount));
}

function _merkleMountainRangeSizes(uint256 totalSize, uint256 maxTreeSize) pure returns (uint256[] memory) {
// Overestimate size of array
// This is a workaround because Solidity doesn't support dynamic memory arrays like Go or Rust
uint256 bigTrees = totalSize / maxTreeSize;
uint256 leftovers = totalSize % maxTreeSize;
uint256 numTrees;
if (leftovers == 0) {
numTrees = bigTrees;
} else {
numTrees = bigTrees + MathUpgradeable.log2(leftovers) + (leftovers % 2);
}
uint256[] memory treeSizes = new uint256[](numTrees);
uint256 count = 0;
while (totalSize != 0) {
if (totalSize >= maxTreeSize) {
treeSizes[count] = maxTreeSize;
totalSize -= maxTreeSize;
} else {
uint256 treeSize = _roundDownPowerOfTwo(totalSize);
treeSizes[count] = treeSize;
totalSize -= treeSize;
}
count++;
}
return treeSizes;
}

function _createCommitment(bytes[] memory shares, Namespace memory namespace) returns (bytes32 commitment) {
uint256 subtreeWidth = _subtreeWidth(shares.length, SUBTREE_ROOT_THRESHOLD);
uint256[] memory treeSizes = _merkleMountainRangeSizes(shares.length, subtreeWidth);
bytes[][] memory leafSets = new bytes[][](treeSizes.length);
uint256 cursor = 0;
// So much copying...
// This could likely be optimized, but I'm not an EVM expert
// Let's see if the gas is too high, optimize later.
// Stop when we hit 0, the delimeter indicating the end of the array
for (uint256 i = 0; i < treeSizes.length; i++) {
leafSets[i] = new bytes[](treeSizes[i]);
for (uint256 j = 0; j < treeSizes[i]; j++) {
//leafSets[i][j] = new bytes(512);
// Try with 512 + 29, prefixing with the 29 byte namespace
leafSets[i][j] = new bytes(512);
// copy the share
for (uint256 k = 0; k < 512; k++) {
leafSets[i][j][k] = shares[cursor][k];
}
cursor += treeSizes[i];
}
}

//NamespaceNode[] memory subtreeRoots = new NamespaceNode[](leafSets.length);
bytes32[] memory subtreeRoots = new bytes32[](leafSets.length);
// Fore each leafSet, compute the root using _computeRoot. Pass a null value for the "proof" parameter
//NamespaceMerkleMultiproof memory nullproof = NamespaceMerkleMultiproof(0, 0, new NamespaceNode[](0));
for (uint256 i = 0; i < leafSets.length; i++) {
NamespaceNode[] memory leafNamespaceNodes = new NamespaceNode[](leafSets[i].length);
for (uint256 j = 0; j < leafSets[i].length; j++) {
leafNamespaceNodes[j] = leafDigest(namespace, leafSets[i][j]);
}
//NamespaceMerkleMultiproof memory emptyProof = NamespaceMerkleMultiproof(0, leafSets[i].length, new NamespaceNode[](0));
NamespaceMerkleMultiproof memory emptyProof =
NamespaceMerkleMultiproof(0, leafSets[i].length, leafNamespaceNodes);
(NamespaceNode memory root,,,) =
NamespaceMerkleTree._computeRoot(emptyProof, leafNamespaceNodes, 0, leafNamespaceNodes.length, 0, 0);
subtreeRoots[i] = bLeafDigest(bytes(abi.encodePacked(root.min.toBytes(), root.max.toBytes(), root.digest)));
}
//BinaryMerkleMultiproof memory nullBinaryProof = BinaryMerkleMultiproof(new bytes32[](0), 0, 0);
BinaryMerkleMultiproof memory emptyBinaryProof = BinaryMerkleMultiproof(new bytes32[](0), 0, subtreeRoots.length);
(bytes32 binaryTreeRoot,,,) =
BinaryMerkleTree._computeRootMulti(emptyBinaryProof, subtreeRoots, 0, subtreeRoots.length, 0, 0);
commitment = binaryTreeRoot;
}
93 changes: 93 additions & 0 deletions src/lib/commitment/test/Commitment.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
pragma solidity ^0.8.22;

import "ds-test/test.sol";
import "forge-std/Vm.sol";
import "forge-std/console.sol";
import {_bytesToSharesV0, _createCommitment, _bytesToHexString} from "../Commitment.sol";
import {toNamespace, Namespace} from "../../tree/Types.sol";
import "../../tree/namespace/NamespaceMerkleTree.sol";
import "../../tree/namespace/NamespaceMerkleMultiproof.sol";

contract CommitmentTest is DSTest {
Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));

struct TestVector {
string commitment;
string data;
string namespace;
string shares;
}

function fromHexChar(uint8 c) public pure returns (uint8) {
if (bytes1(c) >= bytes1("0") && bytes1(c) <= bytes1("9")) {
return c - uint8(bytes1("0"));
}
if (bytes1(c) >= bytes1("a") && bytes1(c) <= bytes1("f")) {
return 10 + c - uint8(bytes1("a"));
}
if (bytes1(c) >= bytes1("A") && bytes1(c) <= bytes1("F")) {
return 10 + c - uint8(bytes1("A"));
}
revert("fail");
}

function fromHex(string memory s) public pure returns (bytes memory) {
bytes memory ss = bytes(s);
require(ss.length % 2 == 0); // length must be even
bytes memory r = new bytes(ss.length / 2);
for (uint256 i = 0; i < ss.length / 2; ++i) {
r[i] = bytes1(fromHexChar(uint8(ss[2 * i])) * 16 + fromHexChar(uint8(ss[2 * i + 1])));
}
return r;
}

function compareStrings(string memory a, string memory b) public pure returns (bool) {
return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b))));
}

function testBytesToSharesV0() external view {
// test vectors were generated here: https://github.com/S1nus/share-test-vec-gen
string memory path = "./src/lib/commitment/test/testVectors.json";
string memory jsonData = vm.readFile(path);
bytes memory vecsData = vm.parseJson(jsonData);
TestVector[] memory vecs = abi.decode(vecsData, (TestVector[]));

for (uint256 i = 0; i < vecs.length; i++) {
bytes29 nsString = bytes29(fromHex(vecs[i].namespace));
Namespace memory ns = toNamespace(nsString);
bytes memory data = fromHex(vecs[i].data);
(bytes[] memory shares, bool err) = _bytesToSharesV0(data, ns);
string memory out = "";
for (uint256 j = 0; j < shares.length; j++) {
out = string.concat(out, _bytesToHexString(shares[j]));
}
// none of the test vectors should cause an error
if (!compareStrings(out, vecs[i].shares)) {
console.log("expected: ", vecs[i].shares);
console.log("got: ", out);
revert();
}
}
}

function testCreateCommitmentV0() external {
string memory path = "./src/lib/commitment/test/testVectors2.json";
string memory jsonData = vm.readFile(path);
bytes memory vecsData = vm.parseJson(jsonData);
TestVector[] memory vecs = abi.decode(vecsData, (TestVector[]));

for (uint256 i = 0; i < vecs.length; i++) {
bytes29 nsString = bytes29(fromHex(vecs[i].namespace));
Namespace memory ns = toNamespace(nsString);
bytes memory data = fromHex(vecs[i].data);
(bytes[] memory shares, bool err) = _bytesToSharesV0(data, ns);
bytes32 commitment = _createCommitment(shares, ns);
if (!compareStrings(_bytesToHexString(abi.encodePacked(commitment)), vecs[i].commitment)) {
console.log("Commitment mismatch for vector ", i);
console.log("expected: ", vecs[i].commitment);
console.log("got: ", _bytesToHexString(abi.encodePacked(commitment)));
revert();
}
}
}
}
1 change: 1 addition & 0 deletions src/lib/commitment/test/testVectors.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/lib/commitment/test/testVectors2.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/lib/tree/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ function toBytes(Namespace memory n) pure returns (bytes29) {
return bytes29(abi.encodePacked(n.version, n.id));
}

function isReservedNamespace(Namespace memory n) pure returns (bool) {
bytes29 upper = hex"00000000000000000000000000000000000000000000000000000000FF";
bytes29 lower = hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00";
return n.toBytes() < upper || n.toBytes() > lower;
}

function toNamespace(bytes29 n) pure returns (Namespace memory) {
bytes memory id = new bytes(28);
for (uint256 i = 1; i < 29; i++) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tree/binary/BinaryMerkleTree.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ library BinaryMerkleTree {
uint256 end,
uint256 headProof,
uint256 headLeaves
) private pure returns (bytes32, uint256, uint256, bool) {
) public pure returns (bytes32, uint256, uint256, bool) {
// reached a leaf
if (end - begin == 1) {
// if current range overlaps with proof range, pop and return a leaf
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tree/namespace/NamespaceMerkleTree.sol
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ library NamespaceMerkleTree {
uint256 end,
uint256 headProof,
uint256 headLeaves
) private pure returns (NamespaceNode memory, uint256, uint256, bool) {
) public pure returns (NamespaceNode memory, uint256, uint256, bool) {
// reached a leaf
if (end - begin == 1) {
// if current range overlaps with proof range, pop and return a leaf
Expand Down
Loading