diff --git a/Cargo.lock b/Cargo.lock index 3563ac17a..38e1345c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7809,6 +7809,7 @@ dependencies = [ name = "sov-db" version = "0.6.0" dependencies = [ + "alloy-primitives", "anyhow", "bincode", "borsh", @@ -8013,6 +8014,7 @@ dependencies = [ name = "sov-rollup-interface" version = "0.6.0" dependencies = [ + "alloy-primitives", "anyhow", "async-trait", "borsh", @@ -8021,6 +8023,7 @@ dependencies = [ "futures", "hex", "jmt", + "risc0-zkp", "serde", "serde_json", "sha2", diff --git a/bin/citrea/tests/bitcoin_e2e/light_client_test.rs b/bin/citrea/tests/bitcoin_e2e/light_client_test.rs index 84dcabb52..d6453b673 100644 --- a/bin/citrea/tests/bitcoin_e2e/light_client_test.rs +++ b/bin/citrea/tests/bitcoin_e2e/light_client_test.rs @@ -24,6 +24,7 @@ use rand::{thread_rng, Rng}; use risc0_zkvm::{FakeReceipt, InnerReceipt, MaybePruned, Receipt, ReceiptClaim}; use sov_ledger_rpc::LedgerRpcClient; use sov_rollup_interface::da::{BatchProofMethodId, DaTxRequest}; +use sov_rollup_interface::rpc::BatchProofMethodIdRpcResponse; use sov_rollup_interface::zk::BatchProofCircuitOutput; use super::batch_prover_test::wait_for_zkproofs; @@ -598,14 +599,18 @@ impl TestCase for LightClientBatchProofMethodIdUpdateTest { assert_eq!( lcp_output.batch_proof_method_ids, vec![ - ( - 0, + BatchProofMethodIdRpcResponse::new( + U64::from(0), [ 1129196088, 155917133, 2638897170, 1970178024, 1745057535, 2098237452, 402126456, 572125060 ] + .into() ), - (100, citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID) + BatchProofMethodIdRpcResponse::new( + U64::from(100), + citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID.into() + ) ], ); @@ -647,14 +652,18 @@ impl TestCase for LightClientBatchProofMethodIdUpdateTest { assert_eq!( lcp_output.batch_proof_method_ids, vec![ - ( - 0, + BatchProofMethodIdRpcResponse::new( + U64::from(0), [ 1129196088, 155917133, 2638897170, 1970178024, 1745057535, 2098237452, 402126456, 572125060 - ], + ] + .into(), + ), + BatchProofMethodIdRpcResponse::new( + U64::from(100), + citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID.into() ), - (100, citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID), ] ); @@ -669,15 +678,22 @@ impl TestCase for LightClientBatchProofMethodIdUpdateTest { assert_eq!( lcp_output.batch_proof_method_ids, vec![ - ( - 0, + BatchProofMethodIdRpcResponse::new( + U64::from(0), [ 1129196088, 155917133, 2638897170, 1970178024, 1745057535, 2098237452, 402126456, 572125060 - ], + ] + .into(), ), - (100, citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID), - (200, new_batch_proof_method_id) + BatchProofMethodIdRpcResponse::new( + U64::from(100), + citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID.into() + ), + BatchProofMethodIdRpcResponse::new( + U64::from(200), + new_batch_proof_method_id.into() + ) ] ); @@ -700,15 +716,22 @@ impl TestCase for LightClientBatchProofMethodIdUpdateTest { assert_eq!( lcp_output.batch_proof_method_ids, vec![ - ( - 0, + BatchProofMethodIdRpcResponse::new( + U64::from(0), [ 1129196088, 155917133, 2638897170, 1970178024, 1745057535, 2098237452, 402126456, 572125060 - ], + ] + .into(), + ), + BatchProofMethodIdRpcResponse::new( + U64::from(100), + citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID.into() ), - (100, citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID), - (200, new_batch_proof_method_id) + BatchProofMethodIdRpcResponse::new( + U64::from(200), + new_batch_proof_method_id.into() + ) ] ); @@ -816,13 +839,13 @@ impl TestCase for LightClientUnverifiableBatchProofTest { let method_ids = lcp_output.batch_proof_method_ids; let genesis_state_root = lcp_output.state_root; - let fork1_height = method_ids[1].0; + let fork1_height: u64 = method_ids[1].height.to(); let verifiable_batch_proof = create_serialized_fake_receipt_batch_proof( genesis_state_root, [1u8; 32], fork1_height + 1, - method_ids[1].1, + method_ids[1].method_id.into(), None, false, ); @@ -835,7 +858,7 @@ impl TestCase for LightClientUnverifiableBatchProofTest { [2u8; 32], [3u8; 32], fork1_height * 3, - method_ids[1].1, + method_ids[1].method_id.into(), None, false, ); @@ -849,7 +872,7 @@ impl TestCase for LightClientUnverifiableBatchProofTest { [3u8; 32], [5u8; 32], fork1_height * 4, - method_ids[1].1, + method_ids[1].method_id.into(), None, true, ); @@ -862,7 +885,7 @@ impl TestCase for LightClientUnverifiableBatchProofTest { [1u8; 32], [2u8; 32], fork1_height * 2, - method_ids[1].1, + method_ids[1].method_id.into(), None, false, ); @@ -911,7 +934,7 @@ impl TestCase for LightClientUnverifiableBatchProofTest { // The unverifiable batch proof and malformed journal batch proof should not have updated the state root or the last l2 height assert_eq!(lcp_output.state_root, [3u8; 32]); - assert_eq!(lcp_output.last_l2_height, fork1_height * 3); + assert_eq!(lcp_output.last_l2_height, U64::from(fork1_height * 3)); assert!(lcp_output.unchained_batch_proofs_info.is_empty()); Ok(()) @@ -1025,7 +1048,7 @@ impl TestCase for VerifyChunkedTxsInLightClient { let method_ids = lcp_output.batch_proof_method_ids; let genesis_state_root = lcp_output.state_root; - let fork1_height = method_ids[1].0; + let fork1_height: u64 = method_ids[1].height.to(); // Even though the state diff is 100kb the proof will be 200kb because the fake receipt claim also has the journal // But the compressed size will go down to 100kb @@ -1036,7 +1059,7 @@ impl TestCase for VerifyChunkedTxsInLightClient { genesis_state_root, [1u8; 32], fork1_height + 1, - method_ids[1].1, + method_ids[1].method_id.into(), Some(state_diff_100kb.clone()), false, ); @@ -1074,7 +1097,7 @@ impl TestCase for VerifyChunkedTxsInLightClient { // The batch proof should have updated the state root and the last l2 height assert_eq!(lcp_output.state_root, [1u8; 32]); - assert_eq!(lcp_output.last_l2_height, fork1_height + 1); + assert_eq!(lcp_output.last_l2_height, U64::from(fork1_height + 1)); assert!(lcp_output.unchained_batch_proofs_info.is_empty()); // Now generate another proof but this time: @@ -1089,7 +1112,7 @@ impl TestCase for VerifyChunkedTxsInLightClient { [1u8; 32], [2u8; 32], fork1_height * 2, - method_ids[1].1, + method_ids[1].method_id.into(), Some(state_diff_130kb), false, ); @@ -1159,10 +1182,10 @@ impl TestCase for VerifyChunkedTxsInLightClient { // The batch proof should not have updated the state root and the last l2 height because these are only the chunks assert_eq!(lcp_output.state_root, [1u8; 32]); - assert_eq!(lcp_output.last_l2_height, fork1_height + 1); + assert_eq!(lcp_output.last_l2_height, U64::from(fork1_height + 1)); assert!(lcp_output.unchained_batch_proofs_info.is_empty()); // There are two chunks so the size should be 2 - assert_eq!(lcp_output.mmr_guest.size, 2); + assert_eq!(lcp_output.mmr_guest.size, U64::from(2)); let lcp_last_chunks = light_client_prover .client @@ -1174,10 +1197,10 @@ impl TestCase for VerifyChunkedTxsInLightClient { // The batch proof should not have updated the state root and the last l2 height because these are only the chunks assert_eq!(lcp_output.state_root, [1u8; 32]); - assert_eq!(lcp_output.last_l2_height, fork1_height + 1); + assert_eq!(lcp_output.last_l2_height, U64::from(fork1_height + 1)); assert!(lcp_output.unchained_batch_proofs_info.is_empty()); // There are now four chunks in total so the size should be 4 - assert_eq!(lcp_output.mmr_guest.size, 4); + assert_eq!(lcp_output.mmr_guest.size, U64::from(4)); // Expect light client prover to have generated light client proof let lcp_aggregate = light_client_prover @@ -1190,7 +1213,7 @@ impl TestCase for VerifyChunkedTxsInLightClient { // The batch proof should have updated the state root and the last l2 height assert_eq!(lcp_output.state_root, [2u8; 32]); - assert_eq!(lcp_output.last_l2_height, fork1_height * 2); + assert_eq!(lcp_output.last_l2_height, U64::from(fork1_height * 2)); assert!(lcp_output.unchained_batch_proofs_info.is_empty()); let random_method_id = [1u32; 8]; @@ -1236,7 +1259,7 @@ impl TestCase for VerifyChunkedTxsInLightClient { // The batch proof should NOT have updated the state root and the last l2 height // Because it is not verified assert_eq!(lcp_output.state_root, [2u8; 32]); - assert_eq!(lcp_output.last_l2_height, fork1_height * 2); + assert_eq!(lcp_output.last_l2_height, U64::from(fork1_height * 2)); // Also should not leave unchained outputs assert!(lcp_output.unchained_batch_proofs_info.is_empty()); diff --git a/crates/sovereign-sdk/full-node/db/sov-db/Cargo.toml b/crates/sovereign-sdk/full-node/db/sov-db/Cargo.toml index 67500409d..e22ef3298 100644 --- a/crates/sovereign-sdk/full-node/db/sov-db/Cargo.toml +++ b/crates/sovereign-sdk/full-node/db/sov-db/Cargo.toml @@ -20,6 +20,7 @@ sov-rollup-interface = { path = "../../../rollup-interface", features = ["native sov-schema-db = { path = "../sov-schema-db" } # External +alloy-primitives = { workspace = true, features=["serde"] } anyhow = { workspace = true, default-features = true } bincode = { workspace = true } borsh = { workspace = true, default-features = true, features = ["bytes", "rc"] } diff --git a/crates/sovereign-sdk/full-node/db/sov-db/src/schema/types.rs b/crates/sovereign-sdk/full-node/db/sov-db/src/schema/types.rs index e92acb733..8ac4e897f 100644 --- a/crates/sovereign-sdk/full-node/db/sov-db/src/schema/types.rs +++ b/crates/sovereign-sdk/full-node/db/sov-db/src/schema/types.rs @@ -1,13 +1,14 @@ use std::fmt::Debug; use std::sync::Arc; +use alloy_primitives::{U32, U64}; use borsh::{BorshDeserialize, BorshSerialize}; use sov_rollup_interface::da::LatestDaState; use sov_rollup_interface::mmr::MMRGuest; use sov_rollup_interface::rpc::{ - BatchProofOutputRpcResponse, BatchProofResponse, HexTx, LatestDaStateRpcResponse, - LightClientProofOutputRpcResponse, LightClientProofResponse, SoftConfirmationResponse, - VerifiedBatchProofResponse, + BatchProofMethodIdRpcResponse, BatchProofOutputRpcResponse, BatchProofResponse, HexTx, + LatestDaStateRpcResponse, LightClientProofOutputRpcResponse, LightClientProofResponse, + SoftConfirmationResponse, VerifiedBatchProofResponse, }; use sov_rollup_interface::soft_confirmation::SignedSoftConfirmation; use sov_rollup_interface::zk::{ @@ -103,19 +104,36 @@ impl From for LightClientProofOutputRpcResponse { fn from(value: StoredLightClientProofOutput) -> Self { Self { state_root: value.state_root, - light_client_proof_method_id: value.light_client_proof_method_id, + light_client_proof_method_id: value.light_client_proof_method_id.into(), latest_da_state: LatestDaStateRpcResponse { block_hash: value.latest_da_state.block_hash, - block_height: value.latest_da_state.block_height, + block_height: U64::from(value.latest_da_state.block_height), total_work: value.latest_da_state.total_work, - current_target_bits: value.latest_da_state.current_target_bits, - epoch_start_time: value.latest_da_state.epoch_start_time, - prev_11_timestamps: value.latest_da_state.prev_11_timestamps, + current_target_bits: U32::from(value.latest_da_state.current_target_bits), + epoch_start_time: U32::from(value.latest_da_state.epoch_start_time), + prev_11_timestamps: value + .latest_da_state + .prev_11_timestamps + .into_iter() + .map(U32::from) + .collect::>() + .try_into() + .expect("should have 11 elements"), }, - unchained_batch_proofs_info: value.unchained_batch_proofs_info, - last_l2_height: value.last_l2_height, - batch_proof_method_ids: value.batch_proof_method_ids, - mmr_guest: value.mmr_guest, + unchained_batch_proofs_info: value + .unchained_batch_proofs_info + .into_iter() + .map(Into::into) + .collect(), + last_l2_height: U64::from(value.last_l2_height), + batch_proof_method_ids: value + .batch_proof_method_ids + .into_iter() + .map(|(height, method_id)| { + BatchProofMethodIdRpcResponse::new(U64::from(height), method_id.into()) + }) + .collect(), + mmr_guest: value.mmr_guest.into(), } } } diff --git a/crates/sovereign-sdk/rollup-interface/Cargo.toml b/crates/sovereign-sdk/rollup-interface/Cargo.toml index 92dffae8d..0d06ed4f0 100644 --- a/crates/sovereign-sdk/rollup-interface/Cargo.toml +++ b/crates/sovereign-sdk/rollup-interface/Cargo.toml @@ -17,6 +17,7 @@ resolver = "2" [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } +alloy-primitives = { workspace = true, features=["serde"], optional = true} borsh = { workspace = true } bytes = { workspace = true, optional = true, default-features = true } digest = { workspace = true } @@ -29,13 +30,14 @@ thiserror = { workspace = true, optional = true } # TODO: Remove tokio when https://github.com/Sovereign-Labs/sovereign-sdk/issues/1161 is resolved tokio = { workspace = true, optional = true } tracing = { workspace = true, optional = true } +risc0-zkp = { workspace = true, optional = true } [dev-dependencies] serde_json = { workspace = true } [features] default = ["std"] -native = ["std", "tokio", "futures", "tracing"] +native = ["alloy-primitives", "std", "tokio", "futures", "tracing", "risc0-zkp"] testing = [] std = [ "anyhow/default", diff --git a/crates/sovereign-sdk/rollup-interface/src/node/rpc/mod.rs b/crates/sovereign-sdk/rollup-interface/src/node/rpc/mod.rs index 87c3c39ab..f7384f942 100644 --- a/crates/sovereign-sdk/rollup-interface/src/node/rpc/mod.rs +++ b/crates/sovereign-sdk/rollup-interface/src/node/rpc/mod.rs @@ -7,7 +7,9 @@ use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; +use alloy_primitives::{U32, U64}; use borsh::{BorshDeserialize, BorshSerialize}; +use risc0_zkp::core::digest::Digest; use serde::{Deserialize, Serialize}; use crate::da::SequencerCommitment; @@ -146,16 +148,81 @@ pub struct LatestDaStateRpcResponse { #[serde(with = "hex::serde")] pub block_hash: [u8; 32], /// Height of the blockchain - pub block_height: u64, + pub block_height: U64, /// Total work done in the DA blockchain #[serde(with = "hex::serde")] pub total_work: [u8; 32], /// Current target bits of DA - pub current_target_bits: u32, + pub current_target_bits: U32, /// The time of the first block in the current epoch (the difficulty adjustment timestamp) - pub epoch_start_time: u32, + pub epoch_start_time: U32, /// The UNIX timestamps in seconds of the previous 11 blocks - pub prev_11_timestamps: [u32; 11], + pub prev_11_timestamps: [U32; 11], +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +/// Activation height and method id +pub struct BatchProofMethodIdRpcResponse { + /// Activation height + pub height: U64, + #[serde(with = "hex::serde")] + /// Method id + pub method_id: Digest, +} +impl BatchProofMethodIdRpcResponse { + /// Create a new instance from height and method id + pub fn new(height: U64, method_id: Digest) -> Self { + Self { height, method_id } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +/// Hex serializable BatchProofInfo +pub struct BatchProofInfoRpcResponse { + /// Initial state root of the batch proof + #[serde(with = "hex::serde")] + pub initial_state_root: [u8; 32], + /// Final state root of the batch proof + #[serde(with = "hex::serde")] + pub final_state_root: [u8; 32], + /// The last processed l2 height in the batch proof + pub last_l2_height: U64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +/// Hex serializable Root +pub struct Root(#[serde(with = "hex::serde")] [u8; 32]); + +/// Hex serializable MMRGuest +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MMRGuestRpcResponse { + /// Subroots of the MMR + pub subroots: Vec, + /// Size of the MMR + pub size: U64, +} + +impl From for MMRGuestRpcResponse { + fn from(mmr: MMRGuest) -> Self { + Self { + subroots: mmr.subroots.into_iter().map(Root).collect(), + size: U64::from(mmr.size), + } + } +} + +impl From for BatchProofInfoRpcResponse { + fn from(info: BatchProofInfo) -> Self { + Self { + initial_state_root: info.initial_state_root, + final_state_root: info.final_state_root, + last_l2_height: U64::from(info.last_l2_height), + } + } } /// The output of a light client proof @@ -167,19 +234,20 @@ pub struct LightClientProofOutputRpcResponse { pub state_root: [u8; 32], /// The method id of the light client proof /// This is used to compare the previous light client proof method id with the input (current) method id - pub light_client_proof_method_id: [u32; 8], + #[serde(with = "hex::serde")] + pub light_client_proof_method_id: Digest, /// Latest DA state after proof pub latest_da_state: LatestDaStateRpcResponse, /// Batch proof info from current or previous light client proofs that were not changed and unable to update the state root yet - pub unchained_batch_proofs_info: Vec, + pub unchained_batch_proofs_info: Vec, /// Last l2 height the light client proof verifies - pub last_l2_height: u64, + pub last_l2_height: U64, /// L2 activation height of the fork and the Method ids of the batch proofs that were verified in the light client proof - pub batch_proof_method_ids: Vec<(u64, [u32; 8])>, + pub batch_proof_method_ids: Vec, /// A map from tx hash to chunk data. /// MMRGuest is an impl. MMR, which only needs to hold considerably small amount of data. /// like 32 hashes and some u64 - pub mmr_guest: MMRGuest, + pub mmr_guest: MMRGuestRpcResponse, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -187,6 +255,7 @@ pub struct LightClientProofOutputRpcResponse { /// The response to a JSON-RPC request for a light client proof pub struct LightClientProofResponse { /// The proof + #[serde(with = "hex::serde")] pub proof: ProofRpcResponse, /// The output of the light client proof circuit pub light_client_proof_output: LightClientProofOutputRpcResponse,