Skip to content

Celestia - v27 #1429

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

Open
wants to merge 5 commits into
base: release-v27
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions da-contracts/celestia_test_cases.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
pragma solidity 0.8.24;

import {IL1DAValidator, L1DAValidatorOutput} from "../../IL1DAValidator.sol";
import {IDAOracle} from "./IDAOracle.sol";
import {ISP1Verifier} from "./ISP1Verifier.sol";
import {CelestiaZKStackInput} from "./types.sol";

contract CelestiaL1DAValidator is IL1DAValidator {
error InvalidProof();
error OperatorDAHashMismatch(bytes32 expected, bytes32 actual);
error DataRootMismatch(bytes32 expected, bytes32 actual);

address public immutable SP1_GROTH_16_VERIFIER;
address public immutable BLOBSTREAM;
bytes32 public immutable eqsVkey;

constructor(
address _sp1Groth16Verifier,
address _blobstream,
bytes32 _eqsVkey
) {
SP1_GROTH_16_VERIFIER = _sp1Groth16Verifier;
BLOBSTREAM = _blobstream;
eqsVkey = _eqsVkey;
}

function checkDA(
uint256 chainId,
uint256 batchNumber,
bytes32 l2DAValidatorOutputHash,
bytes calldata operatorDAInput,
uint256 _maxBlobsSupported
) external returns (L1DAValidatorOutput memory output) {
CelestiaZKStackInput memory input = abi.decode(operatorDAInput[32:], (CelestiaZKStackInput));

bytes memory publicValues = input.publicValues; // get reference to bytes
bytes32 eqKeccakHash;
bytes32 eqDataRoot;
assembly {
let ptr := add(publicValues, 32) // skip length prefix
eqKeccakHash := mload(ptr) // first bytes32
eqDataRoot := mload(add(ptr, 32)) // second bytes32
}

// First verify the equivalency proof (im assuming this call reverts if the proof ins invalid, so we move onward from here)
ISP1Verifier(SP1_GROTH_16_VERIFIER).verifyProof(eqsVkey, input.publicValues, input.equivalenceProof);

// lastly we verify the data root is inside of blobstream
bool valid = IDAOracle(BLOBSTREAM).verifyAttestation(
input.attestationProof.tupleRootNonce,
input.attestationProof.tuple,
input.attestationProof.proof
);

// can use custom error or whatever matter labs likes the most
if (!valid) revert InvalidProof();

output.stateDiffHash = bytes32(operatorDAInput[:32]);

if (l2DAValidatorOutputHash != keccak256(abi.encodePacked(output.stateDiffHash, eqKeccakHash)))
revert OperatorDAHashMismatch(eqKeccakHash, output.stateDiffHash);
if (input.attestationProof.tuple.dataRoot != eqDataRoot)
revert DataRootMismatch(eqDataRoot, input.attestationProof.tuple.dataRoot);

output.blobsLinearHashes = new bytes32[](_maxBlobsSupported);
output.blobsOpeningCommitments = new bytes32[](_maxBlobsSupported);
}
}
98 changes: 98 additions & 0 deletions da-contracts/contracts/da-layers/celestia/CelestiaTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "../../../lib/forge-std/src/Test.sol";
import "../../../lib/forge-std/src/console.sol";
import "./CelestiaL1DAValidator.sol";
import "../../IL1DAValidator.sol";

contract CelestiaL1DAValidatorTest is Test {
CelestiaL1DAValidator public validator;

// Test variables
uint256 public constant MAX_BLOBS_SUPPORTED = 1;

// Fork-related variables
uint256 public mainnetFork;
string public MAINNET_RPC_URL;
bool public forksInitialized;

function setUp() public {
// Get the RPC URL from environment variable
try vm.envString("MAINNET_RPC_URL") returns (string memory url) {
MAINNET_RPC_URL = url;
console.log("Using MAINNET_RPC_URL:", MAINNET_RPC_URL);

// Create a fork of mainnet
mainnetFork = vm.createFork(MAINNET_RPC_URL);
vm.selectFork(mainnetFork);
forksInitialized = true;

console.log("Testing against mainnet fork at block:", block.number);
} catch {
console.log("MAINNET_RPC_URL not set. Mock testing only.");
forksInitialized = false;
}

// Deploy the validator contract
validator = new CelestiaL1DAValidator(
0x397A5f7f3dBd538f23DE225B51f532c34448dA9B, // SP1Groth16Verifier
0xF0c6429ebAB2e7DC6e05DaFB61128bE21f13cb1e, // Blobstream
0x005a902e725cde951470b808cc74ba08d2470219e281b82aec0a1c239da7db7e // EQS Vkey
);
}

function testMultipleCases() public {
// Skip test if fork is not available
if (!forksInitialized) {
console.log("Skipping test - MAINNET_RPC_URL not set");
return;
}

// Read and parse the JSON file
string memory json = vm.readFile("celestia_test_cases.json");
string[] memory testCases = abi.decode(vm.parseJson(json), (string[]));

console.log("Running", testCases.length, "test cases");

// Make sure we're using the fork
vm.selectFork(mainnetFork);

// Run each test case
for (uint i = 0; i < testCases.length; i++) {
console.log("\nTesting case", i);

// Convert hex string to bytes
// first 32 bytes of real-world operator DA input contains state diff hash
bytes memory testCase = vm.parseBytes(testCases[i]);

// first 32 bytes of real-world operator DA input contains state diff hash
// new bytes(32) will
bytes memory operatorDAInput = bytes.concat(new bytes(32), testCase);
console.log("Parsed input");

// Decode the input
CelestiaZKStackInput memory decodedInput = abi.decode(testCase, (CelestiaZKStackInput));
console.log("Decoded input");
// In the real world, the "eqKeccakHash" comes from the L2 DA contract
// we know it's valid because it's proved in the zkEVM.
(bytes32 eqKeccakHash, bytes32 eqDataRoot) = abi.decode(decodedInput.publicValues, (bytes32, bytes32));
console.log("Decoded public values");

console.log("eqKeccakHash", vm.toString(eqKeccakHash));
console.log("eqDataRoot", vm.toString(eqDataRoot));
console.log("Input data length:", operatorDAInput.length);

try validator.checkDA(0, 0, keccak256(abi.encodePacked(new bytes(32), eqKeccakHash)), operatorDAInput, MAX_BLOBS_SUPPORTED) returns (L1DAValidatorOutput memory output) {
console.log("Case", i, "passed!");
console.log("State diff hash:", vm.toString(output.stateDiffHash));
} catch Error(string memory reason) {
console.log("Case", i, "failed with reason:", reason);
fail();
} catch (bytes memory lowLevelData) {
console.log("Case", i, "failed with low level error");
fail();
}
}
}
}
16 changes: 16 additions & 0 deletions da-contracts/contracts/da-layers/celestia/IDAOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {DataRootTuple, BinaryMerkleProof} from "./types.sol";
interface IDAOracle {
/// @notice Verify a Data Availability attestation.
/// @param _tupleRootNonce Nonce of the tuple root to prove against.
/// @param _tuple Data root tuple to prove inclusion of.
/// @param _proof Binary Merkle tree proof that `tuple` is in the root at `_tupleRootNonce`.
/// @return `true` is proof is valid, `false` otherwise.
function verifyAttestation(
uint256 _tupleRootNonce,
DataRootTuple memory _tuple,
BinaryMerkleProof memory _proof
) external view returns (bool);
}
11 changes: 11 additions & 0 deletions da-contracts/contracts/da-layers/celestia/ISP1Verifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pragma solidity 0.8.24;

interface ISP1Verifier {
/// @notice Verifies a proof with given public values and vkey.
/// @dev It is expected that the first 4 bytes of proofBytes must match the first 4 bytes of
/// target verifier's VERIFIER_HASH.
/// @param programVKey The verification key for the RISC-V program.
/// @param publicValues The public values encoded as bytes.
/// @param proofBytes The proof of the program execution the SP1 zkVM encoded as bytes.
function verifyProof(bytes32 programVKey, bytes calldata publicValues, bytes calldata proofBytes) external view;
}
38 changes: 38 additions & 0 deletions da-contracts/contracts/da-layers/celestia/types.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

struct CelestiaZKStackInput {
AttestationProof attestationProof;
bytes equivalenceProof;
bytes publicValues;
}

struct DataRootTuple {
// Celestia block height the data root was included in.
// Genesis block is height = 0.
// First queryable block is height = 1.
uint256 height;
// Data root.
bytes32 dataRoot;
}

/// @notice Contains the necessary parameters needed to verify that a data root tuple
/// was committed to, by the Blobstream smart contract, at some specif nonce.
struct AttestationProof {
// the attestation nonce that commits to the data root tuple.
uint256 tupleRootNonce;
// the data root tuple that was committed to.
DataRootTuple tuple;
// the binary merkle proof of the tuple to the commitment.
BinaryMerkleProof proof;
}

/// @notice Merkle Tree Proof structure.
struct BinaryMerkleProof {
// List of side nodes to verify and calculate tree.
bytes32[] sideNodes;
// The key of the leaf to verify.
uint256 key;
// The number of leaves in the tree
uint256 numLeaves;
}
3 changes: 2 additions & 1 deletion da-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ fs_permissions = [
{ access = "read", path = "../l2-contracts/artifacts-zk/" },
{ access = "read", path = "./script-config" },
{ access = "read-write", path = "./script-out" },
{ access = "read", path = "./out" }
{ access = "read", path = "./out" },
{ access = "read", path = "./celestia_test_cases.json" }
]
cache_path = 'cache-forge'
test = 'test/foundry'
Expand Down
1 change: 1 addition & 0 deletions da-contracts/lib/forge-std
Submodule forge-std added at 3b20d6
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import {IL2DAValidator} from "../interfaces/IL2DAValidator.sol";
import {StateDiffL2DAValidator} from "./StateDiffL2DAValidator.sol";

/// Avail L2 DA validator. It will create a commitment to the pubdata that can later be verified during settlement.
contract CelestiaL2DAValidator is IL2DAValidator, StateDiffL2DAValidator {
function validatePubdata(
// The rolling hash of the user L2->L1 logs.
bytes32,
// The root hash of the user L2->L1 logs.
bytes32,
// The chained hash of the L2->L1 messages
bytes32 _chainedMessagesHash,
// The chained hash of uncompressed bytecodes sent to L1
bytes32 _chainedBytecodesHash,
// Operator data, that is related to the DA itself
bytes calldata _totalL2ToL1PubdataAndStateDiffs
) external returns (bytes32 outputHash) {
(bytes32 stateDiffHash, bytes calldata _totalPubdata, bytes calldata _leftoverSuffix) = _produceStateDiffPubdata(
_chainedMessagesHash,
_chainedBytecodesHash,
_totalL2ToL1PubdataAndStateDiffs
);

bytes32 fullPubdataHash = keccak256(_totalPubdata);
outputHash = keccak256(abi.encodePacked(stateDiffHash, fullPubdataHash));
}
}
5 changes: 5 additions & 0 deletions l2-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ fs_permissions = [
{ access = "read", path = "../system-contracts/bootloader/build/artifacts" },
{ access = "read", path = "../system-contracts/artifacts-zk/contracts-preprocessed" }
]
optimizer = true
optimizer_runs = 100

[profile.default.zksync]
enable_eravm_extensions = true
zksolc = "1.5.7"
optimizer = true
optimizer_runs = 100

1 change: 1 addition & 0 deletions lib/sp1-blobstream
Submodule sp1-blobstream added at 24ac92
1 change: 1 addition & 0 deletions lib/sp1-contracts
Submodule sp1-contracts added at 0885c3