diff --git a/Cargo.toml b/Cargo.toml index fec899973..667e9583c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ byteorder = { version = "1.5.0", default-features = false } bytes = { version = "1.2.1", default-features = false } chrono = { version = "0.4.37", default-features = false } clap = { version = "4.4.10", features = ["derive"] } +const-hex = "1.12" crypto-bigint = { version = "0.5.5" } digest = { version = "0.10.6", default-features = false, features = ["alloc"] } derive_more = { version = "0.99.11", default-features = false } diff --git a/bin/citrea/src/rollup/bitcoin.rs b/bin/citrea/src/rollup/bitcoin.rs index 6a6c4232c..c844dd96c 100644 --- a/bin/citrea/src/rollup/bitcoin.rs +++ b/bin/citrea/src/rollup/bitcoin.rs @@ -22,7 +22,7 @@ use sov_modules_api::{Address, SpecId, Zkvm}; use sov_modules_rollup_blueprint::RollupBlueprint; use sov_prover_storage_manager::{ProverStorageManager, SnapshotManager}; use sov_rollup_interface::da::DaVerifier; -use sov_rollup_interface::services::da::SenderWithNotifier; +use sov_rollup_interface::services::da::TxRequestWithNotifier; use sov_state::ProverStorage; use sov_stf_runner::ProverGuestRunConfig; use tokio::sync::broadcast; @@ -118,7 +118,7 @@ impl RollupBlueprint for BitcoinRollup { require_wallet_check: bool, task_manager: &mut TaskManager<()>, ) -> Result, anyhow::Error> { - let (tx, rx) = unbounded_channel::>(); + let (tx, rx) = unbounded_channel::>(); let bitcoin_service = if require_wallet_check { BitcoinService::new_with_wallet_check( diff --git a/bin/citrea/tests/bitcoin_e2e/batch_prover_test.rs b/bin/citrea/tests/bitcoin_e2e/batch_prover_test.rs index ee3b0dc6f..cc7321e0e 100644 --- a/bin/citrea/tests/bitcoin_e2e/batch_prover_test.rs +++ b/bin/citrea/tests/bitcoin_e2e/batch_prover_test.rs @@ -20,7 +20,7 @@ use citrea_e2e::traits::NodeT; use citrea_e2e::Result; use citrea_primitives::{TO_BATCH_PROOF_PREFIX, TO_LIGHT_CLIENT_PREFIX}; use sov_ledger_rpc::LedgerRpcClient; -use sov_rollup_interface::da::{DaData, SequencerCommitment}; +use sov_rollup_interface::da::{DaTxRequest, SequencerCommitment}; use sov_rollup_interface::rpc::VerifiedBatchProofResponse; use tokio::time::sleep; @@ -273,7 +273,7 @@ impl TestCase for SkipPreprovenCommitmentsTest { // Send the same commitment that was already proven. bitcoin_da_service .send_transaction_with_fee_rate( - DaData::SequencerCommitment(commitments.first().unwrap().clone()), + DaTxRequest::SequencerCommitment(commitments.first().unwrap().clone()), 1, ) .await diff --git a/bin/citrea/tests/bitcoin_e2e/light_client_test.rs b/bin/citrea/tests/bitcoin_e2e/light_client_test.rs index 41bc9caf1..1dbbd130c 100644 --- a/bin/citrea/tests/bitcoin_e2e/light_client_test.rs +++ b/bin/citrea/tests/bitcoin_e2e/light_client_test.rs @@ -1,19 +1,25 @@ +use std::sync::Arc; use std::time::Duration; use alloy_primitives::U64; use async_trait::async_trait; -use bitcoin_da::service::FINALITY_DEPTH; +use bitcoin_da::service::{BitcoinService, BitcoinServiceConfig, FINALITY_DEPTH}; +use bitcoin_da::spec::RollupParams; use citrea_batch_prover::rpc::BatchProverRpcClient; use citrea_batch_prover::GroupCommitments; +use citrea_common::tasks::manager::TaskManager; use citrea_e2e::config::{ BatchProverConfig, LightClientProverConfig, SequencerConfig, SequencerMempoolConfig, TestCaseConfig, }; use citrea_e2e::framework::TestFramework; +use citrea_e2e::node::NodeKind; use citrea_e2e::test_case::{TestCase, TestCaseRunner}; use citrea_e2e::Result; use citrea_light_client_prover::rpc::LightClientProverRpcClient; +use citrea_primitives::{TO_BATCH_PROOF_PREFIX, TO_LIGHT_CLIENT_PREFIX}; use sov_ledger_rpc::LedgerRpcClient; +use sov_rollup_interface::da::{BatchProofMethodId, DaTxRequest}; use super::batch_prover_test::wait_for_zkproofs; use super::get_citrea_path; @@ -433,3 +439,247 @@ async fn test_light_client_proving_multiple_proofs() -> Result<()> { .run() .await } + +#[derive(Default)] +struct LightClientBatchProofMethodIdUpdateTest { + task_manager: TaskManager<()>, +} + +#[async_trait] +impl TestCase for LightClientBatchProofMethodIdUpdateTest { + fn test_config() -> TestCaseConfig { + TestCaseConfig { + with_sequencer: true, + with_batch_prover: true, + with_light_client_prover: true, + ..Default::default() + } + } + + fn sequencer_config() -> SequencerConfig { + SequencerConfig { + min_soft_confirmations_per_commitment: 2, + da_update_interval_ms: 500, + ..Default::default() + } + } + + fn batch_prover_config() -> BatchProverConfig { + BatchProverConfig { + enable_recovery: false, + ..Default::default() + } + } + + fn light_client_prover_config() -> LightClientProverConfig { + LightClientProverConfig { + enable_recovery: false, + initial_da_height: 171, + ..Default::default() + } + } + + async fn cleanup(&self) -> Result<()> { + self.task_manager.abort().await; + Ok(()) + } + + async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> { + let da = f.bitcoin_nodes.get(0).unwrap(); + let sequencer = f.sequencer.as_ref().unwrap(); + let batch_prover = f.batch_prover.as_ref().unwrap(); + let light_client_prover = f.light_client_prover.as_ref().unwrap(); + + let da_config = &da.config; + let bitcoin_da_service_config = BitcoinServiceConfig { + node_url: format!( + "http://127.0.0.1:{}/wallet/{}", + da_config.rpc_port, + NodeKind::Bitcoin + ), + node_username: da_config.rpc_user.clone(), + node_password: da_config.rpc_password.clone(), + network: bitcoin::Network::Regtest, + da_private_key: Some( + // This is a random private key matching guest's METHOD_ID_UPGRADE_AUTHORITY + "79122E48DF1A002FB6584B2E94D0D50F95037416C82DAF280F21CD67D17D9077".to_string(), + ), + tx_backup_dir: Self::test_config() + .dir + .join("tx_backup_dir") + .display() + .to_string(), + monitoring: Default::default(), + mempool_space_url: None, + }; + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let bitcoin_da_service = Arc::new( + BitcoinService::new_with_wallet_check( + bitcoin_da_service_config, + RollupParams { + to_light_client_prefix: TO_LIGHT_CLIENT_PREFIX.to_vec(), + to_batch_proof_prefix: TO_BATCH_PROOF_PREFIX.to_vec(), + }, + tx, + ) + .await + .unwrap(), + ); + + self.task_manager + .spawn(|tk| bitcoin_da_service.clone().run_da_queue(rx, tk)); + + let min_soft_confirmations_per_commitment = + sequencer.min_soft_confirmations_per_commitment(); + + // publish min_soft_confirmations_per_commitment confirmations + for _ in 0..min_soft_confirmations_per_commitment { + sequencer.client.send_publish_batch_request().await?; + } + sequencer + .wait_for_l2_height(min_soft_confirmations_per_commitment, None) + .await?; + + // Wait for commitment tx to be submitted to DA + da.wait_mempool_len(2, Some(TEN_MINS)).await?; + + // Finalize the DA block which contains the commitment tx + da.generate(FINALITY_DEPTH).await?; + + let commitment_l1_height = da.get_finalized_height().await?; + + // Wait for batch prover to generate proof for commitment + batch_prover + .wait_for_l1_height(commitment_l1_height, Some(TEN_MINS)) + .await + .unwrap(); + + // Assert that commitment is queryable + let commitments = batch_prover + .client + .http_client() + .get_sequencer_commitments_on_slot_by_number(U64::from(commitment_l1_height)) + .await + .unwrap() + .unwrap(); + assert_eq!(commitments.len(), 1); + + // Ensure that batch proof is submitted to DA + da.wait_mempool_len(2, Some(TEN_MINS)).await?; + + // Finalize the DA block which contains the batch proof tx + da.generate(FINALITY_DEPTH).await?; + + let batch_proof_l1_height = da.get_finalized_height().await?; + + // Wait for light client prover to process batch proofs. + light_client_prover + .wait_for_l1_height(batch_proof_l1_height, Some(TEN_MINS)) + .await + .unwrap(); + + // Expect light client prover to have generated light client proof + let lcp = light_client_prover + .client + .http_client() + .get_light_client_proof_by_l1_height(batch_proof_l1_height) + .await?; + let lcp_output = lcp.unwrap().light_client_proof_output; + // Verify the current batch proof method ids + assert_eq!( + lcp_output.batch_proof_method_ids, + vec![(0, citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID)], + ); + + // Send BatchProofMethodId transaction to da + let new_batch_proof_method_id = [1u32; 8]; + bitcoin_da_service + .send_transaction_with_fee_rate( + DaTxRequest::BatchProofMethodId(BatchProofMethodId { + method_id: new_batch_proof_method_id, + activation_l2_height: 100, + }), + 1, + ) + .await + .unwrap(); + + // Ensure that method id tx is submitted to DA + da.wait_mempool_len(2, Some(TEN_MINS)).await?; + + // Finalize the DA block which contains the method id tx + da.generate(FINALITY_DEPTH).await?; + + let method_id_l1_height = da.get_finalized_height().await?; + + // Wait for light client prover to process method id update + light_client_prover + .wait_for_l1_height(method_id_l1_height, Some(TEN_MINS)) + .await + .unwrap(); + + // Assert that 1 l1 block before method id tx, still has the same batch proof method ids + let lcp = light_client_prover + .client + .http_client() + .get_light_client_proof_by_l1_height(method_id_l1_height - 1) + .await?; + let lcp_output = lcp.unwrap().light_client_proof_output; + // Verify the current batch proof method ids + assert_eq!( + lcp_output.batch_proof_method_ids, + vec![(0, citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID)], + ); + + // Assert that method ids are updated + let lcp = light_client_prover + .client + .http_client() + .get_light_client_proof_by_l1_height(method_id_l1_height) + .await?; + let lcp_output = lcp.unwrap().light_client_proof_output; + // Verify the current batch proof method ids + assert_eq!( + lcp_output.batch_proof_method_ids, + vec![ + (0, citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID), + (100, new_batch_proof_method_id) + ], + ); + + // Generate one more empty l1 block + da.generate(1).await?; + + // Wait for light client to process it + light_client_prover + .wait_for_l1_height(method_id_l1_height + 1, None) + .await + .unwrap(); + + // Verify that previously updated method ids are being used + let lcp = light_client_prover + .client + .http_client() + .get_light_client_proof_by_l1_height(method_id_l1_height + 1) + .await?; + let lcp_output = lcp.unwrap().light_client_proof_output; + assert_eq!( + lcp_output.batch_proof_method_ids, + vec![ + (0, citrea_risc0_batch_proof::BATCH_PROOF_BITCOIN_ID), + (100, new_batch_proof_method_id) + ], + ); + + Ok(()) + } +} + +#[tokio::test] +async fn test_light_client_batch_proof_method_id_update() -> Result<()> { + TestCaseRunner::new(LightClientBatchProofMethodIdUpdateTest::default()) + .set_citrea_path(get_citrea_path()) + .run() + .await +} diff --git a/bin/citrea/tests/bitcoin_e2e/sequencer_commitments.rs b/bin/citrea/tests/bitcoin_e2e/sequencer_commitments.rs index 8750f9dd3..8d5a56237 100644 --- a/bin/citrea/tests/bitcoin_e2e/sequencer_commitments.rs +++ b/bin/citrea/tests/bitcoin_e2e/sequencer_commitments.rs @@ -18,7 +18,7 @@ use citrea_primitives::TO_BATCH_PROOF_PREFIX; use rs_merkle::algorithms::Sha256; use rs_merkle::MerkleTree; use sov_ledger_rpc::LedgerRpcClient; -use sov_rollup_interface::da::{BlobReaderTrait, DaData}; +use sov_rollup_interface::da::{BlobReaderTrait, DaTxRequest}; use sov_rollup_interface::rpc::SequencerCommitmentResponse; use tokio::time::sleep; @@ -309,11 +309,11 @@ impl SequencerSendCommitmentsToDaTest { let data = BlobReaderTrait::full_data(&mut blob); - let commitment = DaData::try_from_slice(data).unwrap(); + let commitment = DaTxRequest::try_from_slice(data).unwrap(); - matches!(commitment, DaData::SequencerCommitment(_)); + matches!(commitment, DaTxRequest::SequencerCommitment(_)); - let DaData::SequencerCommitment(commitment) = commitment else { + let DaTxRequest::SequencerCommitment(commitment) = commitment else { panic!("Expected SequencerCommitment, got {:?}", commitment); }; diff --git a/bin/citrea/tests/test_helpers/mod.rs b/bin/citrea/tests/test_helpers/mod.rs index 1ce84ff95..6e913436f 100644 --- a/bin/citrea/tests/test_helpers/mod.rs +++ b/bin/citrea/tests/test_helpers/mod.rs @@ -15,7 +15,7 @@ use sov_mock_da::{MockAddress, MockBlock, MockDaConfig, MockDaService}; use sov_modules_api::default_signature::private_key::DefaultPrivateKey; use sov_modules_api::PrivateKey; use sov_modules_rollup_blueprint::RollupBlueprint as _; -use sov_rollup_interface::da::{BlobReaderTrait, DaData, SequencerCommitment}; +use sov_rollup_interface::da::{BlobReaderTrait, DaTxRequest, SequencerCommitment}; use sov_rollup_interface::services::da::{DaService, SlotData}; use sov_rollup_interface::zk::Proof; use sov_rollup_interface::Network; @@ -362,10 +362,10 @@ fn extract_da_data( .extract_relevant_blobs(&block) .into_iter() .for_each(|mut tx| { - let data = DaData::try_from_slice(tx.full_data()); - if let Ok(DaData::SequencerCommitment(seq_com)) = data { + let data = DaTxRequest::try_from_slice(tx.full_data()); + if let Ok(DaTxRequest::SequencerCommitment(seq_com)) = data { sequencer_commitments.push(seq_com); - } else if let Ok(DaData::ZKProof(proof)) = data { + } else if let Ok(DaTxRequest::ZKProof(proof)) = data { zk_proofs.push(proof); } else { tracing::warn!( diff --git a/crates/bitcoin-da/src/helpers/builders/light_client_proof_namespace.rs b/crates/bitcoin-da/src/helpers/builders/light_client_proof_namespace.rs index 1abd9199e..3348b31a1 100644 --- a/crates/bitcoin-da/src/helpers/builders/light_client_proof_namespace.rs +++ b/crates/bitcoin-da/src/helpers/builders/light_client_proof_namespace.rs @@ -32,6 +32,8 @@ pub(crate) enum RawLightClientData { /// let chunks = compressed.chunks(MAX_TXBODY_SIZE) /// [borsh(DaDataLightClient::Chunk(chunk)) for chunk in chunks] Chunks(Vec>), + /// borsh(DaDataLightClient::BatchProofMethodId(MethodId)) + BatchProofMethodId(Vec), } /// This is a list of light client tx we need to send to DA @@ -47,6 +49,10 @@ pub(crate) enum LightClientTxs { commit: Transaction, // unsigned reveal: TxWithId, }, + BatchProofMethodId { + commit: Transaction, // unsigned + reveal: TxWithId, + }, } impl TxListWithReveal for LightClientTxs { @@ -85,6 +91,18 @@ impl TxListWithReveal for LightClientTxs { writer.flush()?; Ok(()) } + Self::BatchProofMethodId { commit, reveal } => { + path.push(format!( + "batch_proof_method_id_light_client_inscription_with_reveal_id_{}.txs", + reveal.id + )); + let file = File::create(path)?; + let mut writer: BufWriter<&File> = BufWriter::new(&file); + writer.write_all(&serialize(commit))?; + writer.write_all(&serialize(&reveal.tx))?; + writer.flush()?; + Ok(()) + } } } } @@ -94,7 +112,7 @@ impl TxListWithReveal for LightClientTxs { // Creates the light client transactions (commit and reveal) #[allow(clippy::too_many_arguments)] #[instrument(level = "trace", skip_all, err)] -pub fn create_zkproof_transactions( +pub fn create_light_client_transactions( data: RawLightClientData, da_private_key: SecretKey, prev_utxo: Option, @@ -128,11 +146,21 @@ pub fn create_zkproof_transactions( network, &reveal_tx_prefix, ), + RawLightClientData::BatchProofMethodId(body) => create_inscription_type_2( + body, + &da_private_key, + prev_utxo, + utxos, + change_address, + commit_fee_rate, + reveal_fee_rate, + network, + &reveal_tx_prefix, + ), } } -// TODO: parametrize hardness -// so tests are easier +// TODO: parametrize hardness so tests are easier // Creates the inscription transactions Type 0 - LightClientTxs::Complete #[allow(clippy::too_many_arguments)] #[instrument(level = "trace", skip_all, err)] @@ -295,8 +323,7 @@ pub fn create_inscription_type_0( } } -// TODO: parametrize hardness -// so tests are easier +// TODO: parametrize hardness so tests are easier // Creates the inscription transactions Type 1 - LightClientTxs::Chunked #[allow(clippy::too_many_arguments)] #[instrument(level = "trace", skip_all, err)] @@ -591,3 +618,166 @@ pub fn create_inscription_type_1( nonce += 1; } } + +// TODO: parametrize hardness so tests are easier +// Creates the inscription transactions Type 0 - LightClientTxs::BatchProofMethodId +#[allow(clippy::too_many_arguments)] +#[instrument(level = "trace", skip_all, err)] +pub fn create_inscription_type_2( + body: Vec, + da_private_key: &SecretKey, + prev_utxo: Option, + utxos: Vec, + change_address: Address, + commit_fee_rate: u64, + reveal_fee_rate: u64, + network: Network, + reveal_tx_prefix: &[u8], +) -> Result { + // Create reveal key + let secp256k1 = Secp256k1::new(); + let key_pair = UntweakedKeypair::new(&secp256k1, &mut rand::thread_rng()); + let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); + + let kind = TransactionKindLightClient::BatchProofMethodId; + let kind_bytes = kind.to_bytes(); + + // sign the body for authentication of the sequencer + let (signature, signer_public_key) = sign_blob_with_private_key(&body, da_private_key); + + // start creating inscription content + let mut reveal_script_builder = script::Builder::new() + .push_x_only_key(&public_key) + .push_opcode(OP_CHECKSIGVERIFY) + .push_slice(PushBytesBuf::try_from(kind_bytes).expect("Cannot push header")) + .push_opcode(OP_FALSE) + .push_opcode(OP_IF) + .push_slice(PushBytesBuf::try_from(signature).expect("Cannot push signature")) + .push_slice( + PushBytesBuf::try_from(signer_public_key).expect("Cannot push sequencer public key"), + ); + // push body in chunks of 520 bytes + for chunk in body.chunks(520) { + reveal_script_builder = reveal_script_builder + .push_slice(PushBytesBuf::try_from(chunk.to_vec()).expect("Cannot push body chunk")); + } + // push end if + reveal_script_builder = reveal_script_builder.push_opcode(OP_ENDIF); + + // This envelope is not finished yet. The random number will be added later + + // Start loop to find a 'nonce' i.e. random number that makes the reveal tx hash starting with zeros given length + let mut nonce: i64 = 16; // skip the first digits to avoid OP_PUSHNUM_X + loop { + if nonce % 1000 == 0 { + trace!(nonce, "Trying to find commit & reveal nonce"); + if nonce > 16384 { + warn!("Too many iterations finding nonce"); + } + } + let utxos = utxos.clone(); + let change_address = change_address.clone(); + // ownerships are moved to the loop + let mut reveal_script_builder = reveal_script_builder.clone(); + + // push nonce + reveal_script_builder = reveal_script_builder + .push_slice(nonce.to_le_bytes()) + // drop the second item, bc there is a big chance it's 0 (tx kind) and nonce is >= 16 + .push_opcode(OP_NIP); + + // finalize reveal script + let reveal_script = reveal_script_builder.into_script(); + + let (control_block, merkle_root, tapscript_hash) = + build_taproot(&reveal_script, public_key, &secp256k1); + + // create commit tx address + let commit_tx_address = Address::p2tr(&secp256k1, public_key, merkle_root, network); + + let reveal_value = REVEAL_OUTPUT_AMOUNT; + let fee = get_size_reveal( + change_address.script_pubkey(), + reveal_value, + &reveal_script, + &control_block, + ) as u64 + * reveal_fee_rate; + let reveal_input_value = fee + reveal_value + REVEAL_OUTPUT_THRESHOLD; + + // build commit tx + // we don't need leftover_utxos because they will be requested from bitcoind next call + let (mut unsigned_commit_tx, _leftover_utxos) = build_commit_transaction( + prev_utxo.clone(), + utxos, + commit_tx_address.clone(), + change_address.clone(), + reveal_input_value, + commit_fee_rate, + )?; + + let output_to_reveal = unsigned_commit_tx.output[0].clone(); + + let mut reveal_tx = build_reveal_transaction( + output_to_reveal.clone(), + unsigned_commit_tx.compute_txid(), + 0, + change_address, + reveal_value + REVEAL_OUTPUT_THRESHOLD, + reveal_fee_rate, + &reveal_script, + &control_block, + )?; + + build_witness( + &unsigned_commit_tx, + &mut reveal_tx, + tapscript_hash, + reveal_script, + control_block, + &key_pair, + &secp256k1, + ); + + let min_commit_value = Amount::from_sat(fee + reveal_value); + while unsigned_commit_tx.output[0].value >= min_commit_value { + let reveal_wtxid = reveal_tx.compute_wtxid(); + let reveal_hash = reveal_wtxid.as_raw_hash().to_byte_array(); + // check if first N bytes equal to the given prefix + if reveal_hash.starts_with(reveal_tx_prefix) { + // check if inscription locked to the correct address + let recovery_key_pair = key_pair.tap_tweak(&secp256k1, merkle_root); + let (x_only_pub_key, _parity) = recovery_key_pair.to_inner().x_only_public_key(); + assert_eq!( + Address::p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked(x_only_pub_key), + network, + ), + commit_tx_address + ); + + return Ok(LightClientTxs::BatchProofMethodId { + commit: unsigned_commit_tx, + reveal: TxWithId { + id: reveal_tx.compute_txid(), + tx: reveal_tx, + }, + }); + } else { + unsigned_commit_tx.output[0].value -= Amount::ONE_SAT; + unsigned_commit_tx.output[1].value += Amount::ONE_SAT; + reveal_tx.output[0].value -= Amount::ONE_SAT; + reveal_tx.input[0].previous_output.txid = unsigned_commit_tx.compute_txid(); + update_witness( + &unsigned_commit_tx, + &mut reveal_tx, + tapscript_hash, + &key_pair, + &secp256k1, + ); + } + } + + nonce += 1; + } +} diff --git a/crates/bitcoin-da/src/helpers/builders/tests.rs b/crates/bitcoin-da/src/helpers/builders/tests.rs index 8f8f61ed1..565078311 100644 --- a/crates/bitcoin-da/src/helpers/builders/tests.rs +++ b/crates/bitcoin-da/src/helpers/builders/tests.rs @@ -468,7 +468,7 @@ fn create_inscription_transactions() { let tx_prefix = &[0u8]; let LightClientTxs::Complete { commit, reveal } = - super::light_client_proof_namespace::create_zkproof_transactions( + super::light_client_proof_namespace::create_light_client_transactions( RawLightClientData::Complete(body.clone()), da_private_key, None, diff --git a/crates/bitcoin-da/src/helpers/mod.rs b/crates/bitcoin-da/src/helpers/mod.rs index 02bd20dc3..eda1ed852 100644 --- a/crates/bitcoin-da/src/helpers/mod.rs +++ b/crates/bitcoin-da/src/helpers/mod.rs @@ -16,6 +16,8 @@ enum TransactionKindLightClient { Chunked = 1, /// This type of transaction includes chunk parts of body (>= 400kb) ChunkedPart = 2, + /// This type of transaction includes a new batch proof method_id + BatchProofMethodId = 3, Unknown(NonZeroU16), } @@ -26,6 +28,7 @@ impl TransactionKindLightClient { TransactionKindLightClient::Complete => 0u16.to_le_bytes().to_vec(), TransactionKindLightClient::Chunked => 1u16.to_le_bytes().to_vec(), TransactionKindLightClient::ChunkedPart => 2u16.to_le_bytes().to_vec(), + TransactionKindLightClient::BatchProofMethodId => 3u16.to_le_bytes().to_vec(), TransactionKindLightClient::Unknown(v) => v.get().to_le_bytes().to_vec(), } } @@ -39,6 +42,7 @@ impl TransactionKindLightClient { 0 => Some(TransactionKindLightClient::Complete), 1 => Some(TransactionKindLightClient::Chunked), 2 => Some(TransactionKindLightClient::ChunkedPart), + 3 => Some(TransactionKindLightClient::BatchProofMethodId), n => Some(TransactionKindLightClient::Unknown( NonZeroU16::new(n).expect("Is not zero"), )), diff --git a/crates/bitcoin-da/src/helpers/parsers.rs b/crates/bitcoin-da/src/helpers/parsers.rs index 1dd4d756a..a47adf8b3 100644 --- a/crates/bitcoin-da/src/helpers/parsers.rs +++ b/crates/bitcoin-da/src/helpers/parsers.rs @@ -18,6 +18,8 @@ pub enum ParsedLightClientTransaction { Aggregate(ParsedAggregate), /// Kind 2 Chunk(ParsedChunk), + /// Kind 3 + BatchProverMethodId(ParsedBatchProverMethodId), } #[derive(Debug, Clone)] @@ -54,6 +56,13 @@ pub struct ParsedSequencerCommitment { pub public_key: Vec, } +#[derive(Debug, Clone)] +pub struct ParsedBatchProverMethodId { + pub body: Vec, + pub signature: Vec, + pub public_key: Vec, +} + /// To verify the signature of the inscription and get the hash of the body pub trait VerifyParsed { fn public_key(&self) -> &[u8]; @@ -118,6 +127,18 @@ impl VerifyParsed for ParsedSequencerCommitment { } } +impl VerifyParsed for ParsedBatchProverMethodId { + fn public_key(&self) -> &[u8] { + &self.public_key + } + fn signature(&self) -> &[u8] { + &self.signature + } + fn body(&self) -> &[u8] { + &self.body + } +} + #[derive(Error, Debug, Clone, PartialEq)] pub enum ParserError { #[error("Invalid header length")] @@ -196,6 +217,10 @@ fn parse_relevant_lightclient( TransactionKindLightClient::ChunkedPart => { light_client::parse_type_2_body(instructions).map(ParsedLightClientTransaction::Chunk) } + TransactionKindLightClient::BatchProofMethodId => { + light_client::parse_type_3_body(instructions) + .map(ParsedLightClientTransaction::BatchProverMethodId) + } TransactionKindLightClient::Unknown(n) => Err(ParserError::InvalidHeaderType(n)), } } @@ -262,8 +287,8 @@ mod light_client { use bitcoin::script::Instruction::{Op, PushBytes}; use super::{ - read_instr, read_opcode, read_push_bytes, ParsedAggregate, ParsedChunk, ParsedComplete, - ParserError, + read_instr, read_opcode, read_push_bytes, ParsedAggregate, ParsedBatchProverMethodId, + ParsedChunk, ParsedComplete, ParserError, }; // Parse transaction body of Type0 @@ -417,6 +442,49 @@ mod light_client { Ok(ParsedChunk { body }) } + + // Parse transaction body of Type3 + pub(super) fn parse_type_3_body( + instructions: &mut dyn Iterator, ParserError>>, + ) -> Result { + let op_false = read_push_bytes(instructions)?; + if !op_false.is_empty() { + // OP_FALSE = OP_PUSHBYTES_0 + return Err(ParserError::UnexpectedOpcode); + } + + if OP_IF != read_opcode(instructions)? { + return Err(ParserError::UnexpectedOpcode); + } + + let signature = read_push_bytes(instructions)?; + let public_key = read_push_bytes(instructions)?; + let body = read_push_bytes(instructions)?; + + if OP_ENDIF != read_opcode(instructions)? { + return Err(ParserError::UnexpectedOpcode); + } + + // Nonce + let _nonce = read_push_bytes(instructions)?; + if OP_NIP != read_opcode(instructions)? { + return Err(ParserError::UnexpectedOpcode); + } + // END of transaction + if instructions.next().is_some() { + return Err(ParserError::UnexpectedOpcode); + } + + let signature = signature.as_bytes().to_vec(); + let public_key = public_key.as_bytes().to_vec(); + let body = body.as_bytes().to_vec(); + + Ok(ParsedBatchProverMethodId { + body, + signature, + public_key, + }) + } } mod batch_proof { diff --git a/crates/bitcoin-da/src/service.rs b/crates/bitcoin-da/src/service.rs index 2c6a8c020..d7b6aa933 100644 --- a/crates/bitcoin-da/src/service.rs +++ b/crates/bitcoin-da/src/service.rs @@ -24,9 +24,9 @@ use citrea_primitives::compression::{compress_blob, decompress_blob}; use citrea_primitives::MAX_TXBODY_SIZE; use serde::{Deserialize, Serialize}; use sov_rollup_interface::da::{ - DaData, DaDataBatchProof, DaDataLightClient, DaNamespace, DaSpec, SequencerCommitment, + DaDataBatchProof, DaDataLightClient, DaNamespace, DaSpec, DaTxRequest, SequencerCommitment, }; -use sov_rollup_interface::services::da::{DaService, SenderWithNotifier}; +use sov_rollup_interface::services::da::{DaService, TxRequestWithNotifier}; use sov_rollup_interface::zk::Proof; use tokio::select; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; @@ -39,7 +39,7 @@ use crate::helpers::builders::batch_proof_namespace::{ create_seqcommitment_transactions, BatchProvingTxs, }; use crate::helpers::builders::light_client_proof_namespace::{ - create_zkproof_transactions, LightClientTxs, RawLightClientData, + create_light_client_transactions, LightClientTxs, RawLightClientData, }; use crate::helpers::builders::{TxListWithReveal, TxWithId}; use crate::helpers::merkle_tree; @@ -111,7 +111,7 @@ pub struct BitcoinService { da_private_key: Option, to_light_client_prefix: Vec, to_batch_proof_prefix: Vec, - inscribes_queue: UnboundedSender>, + inscribes_queue: UnboundedSender>, tx_backup_dir: PathBuf, pub monitoring: Arc, fee: FeeService, @@ -122,7 +122,7 @@ impl BitcoinService { pub async fn new_with_wallet_check( config: BitcoinServiceConfig, chain_params: RollupParams, - tx: UnboundedSender>, + tx: UnboundedSender>, ) -> Result { let client = Arc::new( Client::new( @@ -172,7 +172,7 @@ impl BitcoinService { pub async fn new_without_wallet_check( config: BitcoinServiceConfig, chain_params: RollupParams, - tx: UnboundedSender>, + tx: UnboundedSender>, ) -> Result { let client = Arc::new( Client::new( @@ -214,7 +214,7 @@ impl BitcoinService { pub async fn run_da_queue( self: Arc, - mut rx: UnboundedReceiver>, + mut rx: UnboundedReceiver>, token: CancellationToken, ) { trace!("BitcoinDA queue is initialized. Waiting for the first request..."); @@ -241,7 +241,7 @@ impl BitcoinService { }; match self .send_transaction_with_fee_rate( - request.da_data.clone(), + request.tx_request.clone(), fee_sat_per_vbyte, ) .await @@ -325,7 +325,7 @@ impl BitcoinService { #[instrument(level = "trace", fields(prev_utxo), ret, err)] pub async fn send_transaction_with_fee_rate( &self, - da_data: DaData, + tx_request: DaTxRequest, fee_sat_per_vbyte: u64, ) -> Result> { let network = self.network; @@ -344,8 +344,8 @@ impl BitcoinService { .require_network(network) .context("Invalid network for address")?; - match da_data { - DaData::ZKProof(zkproof) => { + match tx_request { + DaTxRequest::ZKProof(zkproof) => { let data = split_proof(zkproof); let reveal_light_client_prefix = self.to_light_client_prefix.clone(); @@ -353,7 +353,7 @@ impl BitcoinService { let inscription_txs = tokio::task::spawn_blocking(move || { // Since this is CPU bound work, we use spawn_blocking // to release the tokio runtime execution - create_zkproof_transactions( + create_light_client_transactions( data, da_private_key, prev_utxo, @@ -383,9 +383,10 @@ impl BitcoinService { self.send_chunked_transaction(commit_chunks, reveal_chunks, commit, reveal) .await } + _ => panic!("ZKProof tx must be either complete or chunked"), } } - DaData::SequencerCommitment(comm) => { + DaTxRequest::SequencerCommitment(comm) => { let data = DaDataBatchProof::SequencerCommitment(comm); let blob = borsh::to_vec(&data).expect("DaDataBatchProof serialize must not fail"); @@ -415,6 +416,40 @@ impl BitcoinService { self.send_complete_transaction(commit, reveal).await } + DaTxRequest::BatchProofMethodId(method_id) => { + let data = DaDataLightClient::BatchProofMethodId(method_id); + let blob = borsh::to_vec(&data).expect("DaDataLightClient serialize must not fail"); + + let prefix = self.to_light_client_prefix.clone(); + + // create inscribe transactions + let inscription_txs = tokio::task::spawn_blocking(move || { + // Since this is CPU bound work, we use spawn_blocking + // to release the tokio runtime execution + create_light_client_transactions( + RawLightClientData::BatchProofMethodId(blob), + da_private_key, + prev_utxo, + utxos, + address, + fee_sat_per_vbyte, + fee_sat_per_vbyte, + network, + prefix, + ) + }) + .await??; + + // write txs to file, it can be used to continue revealing blob if something goes wrong + inscription_txs.write_to_file(self.tx_backup_dir.clone())?; + + match inscription_txs { + LightClientTxs::BatchProofMethodId { commit, reveal } => { + self.send_complete_transaction(commit, reveal).await + } + _ => panic!("Tx must be BatchProofMethodId"), + } + } } } @@ -766,6 +801,9 @@ impl DaService for BitcoinService { ParsedLightClientTransaction::Chunk(_chunk) => { // we ignore them for now } + ParsedLightClientTransaction::BatchProverMethodId(_) => { + // ignore because these are not proofs + } } } } @@ -827,7 +865,8 @@ impl DaService for BitcoinService { body.extend(chunk); } ParsedLightClientTransaction::Complete(_) - | ParsedLightClientTransaction::Aggregate(_) => { + | ParsedLightClientTransaction::Aggregate(_) + | ParsedLightClientTransaction::BatchProverMethodId(_) => { error!("{}:{}: Expected chunk, got other tx kind", tx_id, chunk_id); continue 'aggregate; } @@ -994,6 +1033,17 @@ impl DaService for BitcoinService { ParsedLightClientTransaction::Chunk(_) => { // ignore } + ParsedLightClientTransaction::BatchProverMethodId(method_id) => { + if let Some(hash) = method_id.get_sig_verified_hash() { + let relevant_tx = BlobWithSender::new( + method_id.body, + method_id.public_key, + hash, + ); + + relevant_txs.push(relevant_tx); + } + } } } } @@ -1006,12 +1056,12 @@ impl DaService for BitcoinService { #[instrument(level = "trace", skip_all)] async fn send_transaction( &self, - da_data: DaData, + tx_request: DaTxRequest, ) -> Result<::TransactionId> { let queue = self.get_send_transaction_queue(); let (tx, rx) = oneshot_channel(); - queue.send(SenderWithNotifier { - da_data, + queue.send(TxRequestWithNotifier { + tx_request, notify: tx, })?; rx.await? @@ -1019,7 +1069,7 @@ impl DaService for BitcoinService { fn get_send_transaction_queue( &self, - ) -> UnboundedSender> { + ) -> UnboundedSender> { self.inscribes_queue.clone() } diff --git a/crates/bitcoin-da/src/verifier.rs b/crates/bitcoin-da/src/verifier.rs index cfe57e8a5..ee31a7fcd 100644 --- a/crates/bitcoin-da/src/verifier.rs +++ b/crates/bitcoin-da/src/verifier.rs @@ -29,6 +29,7 @@ const EXPECTED_EPOCH_TIMESPAN: u32 = 60 * 60 * 24 * 14; /// Number of blocks per epoch const BLOCKS_PER_EPOCH: u64 = 2016; +#[derive(Debug)] pub struct BitcoinVerifier { to_batch_proof_prefix: Vec, to_light_client_prefix: Vec, @@ -147,6 +148,16 @@ impl DaVerifier for BitcoinVerifier { ParsedLightClientTransaction::Chunk(_chunk) => { // ignore } + ParsedLightClientTransaction::BatchProverMethodId(method_id) => { + if let Some(blob_content) = + verified_blob_content(&method_id, &mut blobs_iter)? + { + // assert tx content is not modified + if blob_content != method_id.body { + return Err(ValidationError::BlobContentWasModified); + } + } + } } } } diff --git a/crates/bitcoin-da/tests/service.rs b/crates/bitcoin-da/tests/service.rs index 46901bdf3..f0c31072c 100644 --- a/crates/bitcoin-da/tests/service.rs +++ b/crates/bitcoin-da/tests/service.rs @@ -11,7 +11,7 @@ use bitcoin_da::service::get_relevant_blobs_from_txs; use bitcoin_da::spec::RollupParams; use bitcoin_da::verifier::BitcoinVerifier; use citrea_common::tasks::manager::TaskManager; -use citrea_e2e::config::TestCaseConfig; +use citrea_e2e::config::{BitcoinConfig, TestCaseConfig}; use citrea_e2e::framework::TestFramework; use citrea_e2e::test_case::{TestCase, TestCaseRunner}; use citrea_e2e::Result; @@ -35,6 +35,13 @@ impl TestCase for BitcoinServiceTest { } } + fn bitcoin_config() -> BitcoinConfig { + BitcoinConfig { + extra_args: vec!["-limitancestorcount=50", "-limitdescendantcount=50"], + ..Default::default() + } + } + async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> { let mut task_manager = TaskManager::default(); let da_node = f.bitcoin_nodes.get(0).unwrap(); @@ -45,7 +52,7 @@ impl TestCase for BitcoinServiceTest { to_light_client_prefix: TO_LIGHT_CLIENT_PREFIX.to_vec(), }); - let (block, block_commitments, block_proofs) = + let (block, block_commitments, block_proofs, _) = generate_mock_txs(&service, da_node, &mut task_manager).await; let block_wtxids = block .txdata @@ -59,7 +66,7 @@ impl TestCase for BitcoinServiceTest { { let (mut txs, inclusion_proof, completeness_proof) = service.extract_relevant_blobs_with_proof(&block, DaNamespace::ToBatchProver); - assert_eq!(inclusion_proof.wtxids.len(), 29); + assert_eq!(inclusion_proof.wtxids.len(), 33); assert_eq!(inclusion_proof.wtxids[1..], block_wtxids[1..]); // 3 valid commitments, and 1 invalid commitment with wrong public key assert_eq!(txs.len(), 4); @@ -100,15 +107,15 @@ impl TestCase for BitcoinServiceTest { { let (mut txs, inclusion_proof, completeness_proof) = service.extract_relevant_blobs_with_proof(&block, DaNamespace::ToLightClientProver); - assert_eq!(inclusion_proof.wtxids.len(), 29); + assert_eq!(inclusion_proof.wtxids.len(), 33); assert_eq!(inclusion_proof.wtxids[1..], block_wtxids[1..]); - // 2 complete and 2 aggregate proofs - assert_eq!(txs.len(), 4); + // 2 complete, 2 aggregate proofs, and 2 method id txs + assert_eq!(txs.len(), 6); // it is >= due to the probability that one of commit transactions ended up // with the prefix by chance (reveals are guaranteed to have a certain prefix) assert!( - completeness_proof.len() >= 4, - "expected completeness proof to have at least 4 txs, it has {}", + completeness_proof.len() >= 6, + "expected completeness proof to have at least 6 txs, it has {}", completeness_proof.len() ); diff --git a/crates/bitcoin-da/tests/test_utils.rs b/crates/bitcoin-da/tests/test_utils.rs index 7e351a7bf..075224979 100644 --- a/crates/bitcoin-da/tests/test_utils.rs +++ b/crates/bitcoin-da/tests/test_utils.rs @@ -23,7 +23,7 @@ use citrea_e2e::node::NodeKind; use citrea_e2e::traits::NodeT; use citrea_primitives::compression::decompress_blob; use citrea_primitives::{MAX_TXBODY_SIZE, TO_BATCH_PROOF_PREFIX, TO_LIGHT_CLIENT_PREFIX}; -use sov_rollup_interface::da::{DaData, SequencerCommitment}; +use sov_rollup_interface::da::{BatchProofMethodId, DaTxRequest, SequencerCommitment}; use sov_rollup_interface::services::da::DaService; pub const DEFAULT_DA_PRIVATE_KEY: &str = @@ -92,15 +92,21 @@ pub async fn get_service( /// - Valid commitments: 3 (6 txs) /// - Valid complete proofs: 2 (4 txs) /// - Valid chunked proofs: 1 with 2 chunks (6 txs) + 1 with 3 chunks (8 txs) +/// - Valid method id txs: 2 (4 txs) /// - Invalid commitment with wrong public key: 1 (2 txs) /// - Invalid commitment with wrong prefix: 1 (2 txs) /// -/// With coinbase transaction, returned block has total of 29 transactions. +/// With coinbase transaction, returned block has total of 33 transactions. pub async fn generate_mock_txs( da_service: &BitcoinService, da_node: &BitcoinNode, task_manager: &mut TaskManager<()>, -) -> (BitcoinBlock, Vec, Vec>) { +) -> ( + BitcoinBlock, + Vec, + Vec>, + Vec, +) { // Funding wallet requires block generation, hence we do funding at the beginning // to be able to write all transactions into the same block. let wrong_prefix_wallet = "wrong_prefix".to_string(); @@ -132,6 +138,18 @@ pub async fn generate_mock_txs( let mut valid_commitments = vec![]; let mut valid_proofs = vec![]; + let mut valid_method_ids = vec![]; + + // Send method id update tx + let method_id = BatchProofMethodId { + method_id: [0; 8], + activation_l2_height: 0, + }; + valid_method_ids.push(method_id.clone()); + da_service + .send_transaction(DaTxRequest::BatchProofMethodId(method_id)) + .await + .expect("Failed to send transaction"); let commitment = SequencerCommitment { merkle_root: [13; 32], @@ -140,7 +158,7 @@ pub async fn generate_mock_txs( }; valid_commitments.push(commitment.clone()); da_service - .send_transaction(DaData::SequencerCommitment(commitment)) + .send_transaction(DaTxRequest::SequencerCommitment(commitment)) .await .expect("Failed to send transaction"); @@ -151,7 +169,7 @@ pub async fn generate_mock_txs( }; valid_commitments.push(commitment.clone()); da_service - .send_transaction(DaData::SequencerCommitment(commitment)) + .send_transaction(DaTxRequest::SequencerCommitment(commitment)) .await .expect("Failed to send transaction"); @@ -160,7 +178,7 @@ pub async fn generate_mock_txs( valid_proofs.push(blob.clone()); da_service - .send_transaction(DaData::ZKProof(blob)) + .send_transaction(DaTxRequest::ZKProof(blob)) .await .expect("Failed to send transaction"); @@ -170,13 +188,13 @@ pub async fn generate_mock_txs( valid_proofs.push(blob.clone()); da_service - .send_transaction(DaData::ZKProof(blob)) + .send_transaction(DaTxRequest::ZKProof(blob)) .await .expect("Failed to send transaction"); // Sequencer commitment with wrong tx prefix wrong_prefix_da_service - .send_transaction(DaData::SequencerCommitment(SequencerCommitment { + .send_transaction(DaTxRequest::SequencerCommitment(SequencerCommitment { merkle_root: [15; 32], l2_start_block_number: 1246, l2_end_block_number: 1268, @@ -189,13 +207,13 @@ pub async fn generate_mock_txs( valid_proofs.push(blob.clone()); da_service - .send_transaction(DaData::ZKProof(blob)) + .send_transaction(DaTxRequest::ZKProof(blob)) .await .expect("Failed to send transaction"); // Sequencer commitment with wrong key and signature wrong_key_da_service - .send_transaction(DaData::SequencerCommitment(SequencerCommitment { + .send_transaction(DaTxRequest::SequencerCommitment(SequencerCommitment { merkle_root: [15; 32], l2_start_block_number: 1246, l2_end_block_number: 1268, @@ -210,7 +228,7 @@ pub async fn generate_mock_txs( }; valid_commitments.push(commitment.clone()); da_service - .send_transaction(DaData::SequencerCommitment(commitment)) + .send_transaction(DaTxRequest::SequencerCommitment(commitment)) .await .expect("Failed to send transaction"); @@ -220,7 +238,18 @@ pub async fn generate_mock_txs( valid_proofs.push(blob.clone()); da_service - .send_transaction(DaData::ZKProof(blob)) + .send_transaction(DaTxRequest::ZKProof(blob)) + .await + .expect("Failed to send transaction"); + + // Send method id update tx + let method_id = BatchProofMethodId { + method_id: [1; 8], + activation_l2_height: 100, + }; + valid_method_ids.push(method_id.clone()); + da_service + .send_transaction(DaTxRequest::BatchProofMethodId(method_id)) .await .expect("Failed to send transaction"); @@ -228,9 +257,9 @@ pub async fn generate_mock_txs( let block_hash = da_node.generate(1).await.unwrap()[0]; let block = da_service.get_block_by_hash(block_hash).await.unwrap(); - assert_eq!(block.txdata.len(), 29); + assert_eq!(block.txdata.len(), 33); - (block, valid_commitments, valid_proofs) + (block, valid_commitments, valid_proofs, valid_method_ids) } #[allow(unused)] diff --git a/crates/bitcoin-da/tests/verifier.rs b/crates/bitcoin-da/tests/verifier.rs index 83bc17e60..e3b632af5 100644 --- a/crates/bitcoin-da/tests/verifier.rs +++ b/crates/bitcoin-da/tests/verifier.rs @@ -10,7 +10,7 @@ use bitcoin_da::spec::proof::InclusionMultiProof; use bitcoin_da::spec::RollupParams; use bitcoin_da::verifier::{BitcoinVerifier, ValidationError, WITNESS_COMMITMENT_PREFIX}; use citrea_common::tasks::manager::TaskManager; -use citrea_e2e::config::TestCaseConfig; +use citrea_e2e::config::{BitcoinConfig, TestCaseConfig}; use citrea_e2e::framework::TestFramework; use citrea_e2e::test_case::{TestCase, TestCaseRunner}; use citrea_e2e::Result; @@ -35,12 +35,19 @@ impl TestCase for BitcoinVerifierTest { } } + fn bitcoin_config() -> BitcoinConfig { + BitcoinConfig { + extra_args: vec!["-limitancestorcount=50", "-limitdescendantcount=50"], + ..Default::default() + } + } + async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> { let mut task_manager = TaskManager::default(); let da_node = f.bitcoin_nodes.get(0).unwrap(); let service = get_default_service(&mut task_manager, &da_node.config).await; - let (block, _, _) = generate_mock_txs(&service, da_node, &mut task_manager).await; + let (block, _, _, _) = generate_mock_txs(&service, da_node, &mut task_manager).await; let (mut b_txs, b_inclusion_proof, b_completeness_proof) = service.extract_relevant_blobs_with_proof(&block, DaNamespace::ToBatchProver); @@ -580,7 +587,7 @@ impl TestCase for BitcoinVerifierTest { let mut l_txs = l_txs.clone(); let body = { - let parsed = parse_light_client_transaction(&l_completeness_proof[0]).unwrap(); + let parsed = parse_light_client_transaction(&l_completeness_proof[1]).unwrap(); match parsed { ParsedLightClientTransaction::Complete(complete) => complete.body, // normally we should decompress the tx body _ => panic!("Should not select zk proof tx other than complete"), diff --git a/crates/light-client-prover/src/circuit.rs b/crates/light-client-prover/src/circuit.rs index d782855e7..d25501c20 100644 --- a/crates/light-client-prover/src/circuit.rs +++ b/crates/light-client-prover/src/circuit.rs @@ -1,6 +1,6 @@ use borsh::BorshDeserialize; use sov_modules_api::BlobReaderTrait; -use sov_rollup_interface::da::{DaDataLightClient, DaNamespace, DaVerifier}; +use sov_rollup_interface::da::{BatchProofMethodId, DaDataLightClient, DaNamespace, DaVerifier}; use sov_rollup_interface::zk::{ BatchProofCircuitOutput, BatchProofInfo, LightClientCircuitInput, LightClientCircuitOutput, OldBatchProofCircuitOutput, ZkvmGuest, @@ -10,9 +10,9 @@ use sov_rollup_interface::Network; use crate::utils::{collect_unchained_outputs, recursive_match_state_roots}; #[derive(Debug)] -pub enum LightClientVerificationError { - DaTxsCouldntBeVerified, - HeaderChainVerificationFailed, +pub enum LightClientVerificationError { + DaTxsCouldntBeVerified(DaV::Error), + HeaderChainVerificationFailed(DaV::Error), InvalidPreviousLightClientProof, } @@ -25,8 +25,9 @@ pub fn run_circuit( l2_genesis_root: [u8; 32], initial_batch_proof_method_ids: InitialBatchProofMethodIds, batch_prover_da_public_key: &[u8], + method_id_upgrade_authority_da_public_key: &[u8], network: Network, -) -> Result { +) -> Result> { // Extract previous light client proof output let previous_light_client_proof_output = if let Some(journal) = input.previous_light_client_proof_journal { @@ -34,7 +35,7 @@ pub fn run_circuit( &journal, &input.light_client_proof_method_id.into(), ) - .map_err(|_| LightClientVerificationError::InvalidPreviousLightClientProof)?; + .map_err(|_| LightClientVerificationError::::InvalidPreviousLightClientProof)?; // Ensure method IDs match assert_eq!( input.light_client_proof_method_id, @@ -45,7 +46,7 @@ pub fn run_circuit( None }; - let batch_proof_method_ids = previous_light_client_proof_output + let mut batch_proof_method_ids = previous_light_client_proof_output .as_ref() .map_or(initial_batch_proof_method_ids, |o| { o.batch_proof_method_ids.clone() @@ -59,7 +60,7 @@ pub fn run_circuit( &input.da_block_header, network, ) - .map_err(|_| LightClientVerificationError::HeaderChainVerificationFailed)?; + .map_err(|err| LightClientVerificationError::HeaderChainVerificationFailed(err))?; // Verify data from da da_verifier @@ -70,7 +71,7 @@ pub fn run_circuit( input.completeness_proof, DaNamespace::ToLightClientProver, ) - .map_err(|_| LightClientVerificationError::DaTxsCouldntBeVerified)?; + .map_err(|err| LightClientVerificationError::DaTxsCouldntBeVerified(err))?; // Mapping from initial state root to final state root and last L2 height let mut initial_to_final = std::collections::BTreeMap::<[u8; 32], ([u8; 32], u64)>::new(); @@ -176,6 +177,24 @@ pub fn run_circuit( } DaDataLightClient::Aggregate(_) => todo!(), DaDataLightClient::Chunk(_) => todo!(), + DaDataLightClient::BatchProofMethodId(_) => {} // if coming from batch prover, ignore + } + } + } else if blob.sender().as_ref() == method_id_upgrade_authority_da_public_key { + let data = DaDataLightClient::try_from_slice(blob.verified_data()); + + if let Ok(DaDataLightClient::BatchProofMethodId(BatchProofMethodId { + method_id, + activation_l2_height, + })) = data + { + let last_activation_height = batch_proof_method_ids + .last() + .expect("Should be at least one") + .0; + + if activation_l2_height > last_activation_height { + batch_proof_method_ids.push((activation_l2_height, method_id)); } } } diff --git a/crates/light-client-prover/src/da_block_handler.rs b/crates/light-client-prover/src/da_block_handler.rs index c123bc0e6..c157f61f0 100644 --- a/crates/light-client-prover/src/da_block_handler.rs +++ b/crates/light-client-prover/src/da_block_handler.rs @@ -152,6 +152,12 @@ where .da_service .extract_relevant_blobs_with_proof(l1_block, DaNamespace::ToLightClientProver); + // Even though following extract_batch_proofs call does full_data on batch proofs, + // we also need to do it for BatchProofMethodId txs + da_data.iter_mut().for_each(|tx| { + tx.full_data(); + }); + let batch_proofs = self.extract_batch_proofs(&mut da_data, l1_hash).await; tracing::info!( "Block {} has {} batch proofs", diff --git a/crates/light-client-prover/src/tests/mod.rs b/crates/light-client-prover/src/tests/mod.rs index 27aed41cb..be5cdd2a2 100644 --- a/crates/light-client-prover/src/tests/mod.rs +++ b/crates/light-client-prover/src/tests/mod.rs @@ -4,7 +4,7 @@ use sov_mock_da::{MockBlockHeader, MockDaVerifier}; use sov_mock_zkvm::MockZkGuest; use sov_rollup_interface::zk::LightClientCircuitInput; use sov_rollup_interface::Network; -use test_utils::{create_mock_blob, create_prev_lcp_serialized}; +use test_utils::{create_mock_batch_proof, create_new_method_id_tx, create_prev_lcp_serialized}; use crate::circuit::{run_circuit, LightClientVerificationError}; @@ -16,8 +16,8 @@ fn test_light_client_circuit_valid_da_valid_data() { let light_client_proof_method_id = [1u32; 8]; let da_verifier = MockDaVerifier {}; - let blob_1 = create_mock_blob([1u8; 32], [2u8; 32], 2, true); - let blob_2 = create_mock_blob([2u8; 32], [3u8; 32], 3, true); + let blob_1 = create_mock_batch_proof([1u8; 32], [2u8; 32], 2, true); + let blob_2 = create_mock_batch_proof([2u8; 32], [3u8; 32], 3, true); let block_header_1 = MockBlockHeader::from_height(1); @@ -32,6 +32,7 @@ fn test_light_client_circuit_valid_da_valid_data() { let l2_genesis_state_root = [1u8; 32]; let batch_prover_da_pub_key = [9; 32].to_vec(); + let method_id_upgrade_authority = [11u8; 32].to_vec(); let output_1 = run_circuit::<_, MockZkGuest>( da_verifier.clone(), @@ -39,6 +40,7 @@ fn test_light_client_circuit_valid_da_valid_data() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ) .unwrap(); @@ -49,8 +51,8 @@ fn test_light_client_circuit_valid_da_valid_data() { assert_eq!(output_1.last_l2_height, 3); // Now get more proofs to see the previous light client part is also working correctly - let blob_3 = create_mock_blob([3u8; 32], [4u8; 32], 4, true); - let blob_4 = create_mock_blob([4u8; 32], [5u8; 32], 5, true); + let blob_3 = create_mock_batch_proof([3u8; 32], [4u8; 32], 4, true); + let blob_4 = create_mock_batch_proof([4u8; 32], [5u8; 32], 5, true); let block_header_2 = MockBlockHeader::from_height(2); @@ -71,6 +73,7 @@ fn test_light_client_circuit_valid_da_valid_data() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ) .unwrap(); @@ -86,8 +89,8 @@ fn test_wrong_order_da_blocks_should_still_work() { let light_client_proof_method_id = [1u32; 8]; let da_verifier = MockDaVerifier {}; - let blob_1 = create_mock_blob([1u8; 32], [2u8; 32], 2, true); - let blob_2 = create_mock_blob([2u8; 32], [3u8; 32], 3, true); + let blob_1 = create_mock_batch_proof([1u8; 32], [2u8; 32], 2, true); + let blob_2 = create_mock_batch_proof([2u8; 32], [3u8; 32], 3, true); let block_header_1 = MockBlockHeader::from_height(1); @@ -102,6 +105,7 @@ fn test_wrong_order_da_blocks_should_still_work() { let l2_genesis_state_root = [1u8; 32]; let batch_prover_da_pub_key = [9; 32].to_vec(); + let method_id_upgrade_authority = [11u8; 32].to_vec(); let output_1 = run_circuit::<_, MockZkGuest>( da_verifier.clone(), @@ -109,6 +113,7 @@ fn test_wrong_order_da_blocks_should_still_work() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ) .unwrap(); @@ -126,8 +131,8 @@ fn create_unchainable_outputs_then_chain_them_on_next_block() { let block_header_1 = MockBlockHeader::from_height(1); - let blob_1 = create_mock_blob([2u8; 32], [3u8; 32], 3, true); - let blob_2 = create_mock_blob([3u8; 32], [4u8; 32], 4, true); + let blob_1 = create_mock_batch_proof([2u8; 32], [3u8; 32], 3, true); + let blob_2 = create_mock_batch_proof([3u8; 32], [4u8; 32], 4, true); let input = LightClientCircuitInput { previous_light_client_proof_journal: None, @@ -140,6 +145,7 @@ fn create_unchainable_outputs_then_chain_them_on_next_block() { let l2_genesis_state_root = [1u8; 32]; let batch_prover_da_pub_key = [9; 32].to_vec(); + let method_id_upgrade_authority = [11u8; 32].to_vec(); let output_1 = run_circuit::<_, MockZkGuest>( da_verifier.clone(), @@ -147,6 +153,7 @@ fn create_unchainable_outputs_then_chain_them_on_next_block() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ) .unwrap(); @@ -169,7 +176,7 @@ fn create_unchainable_outputs_then_chain_them_on_next_block() { ); // On the next l1 block, give 1-2 transition - let blob_1 = create_mock_blob([1u8; 32], [2u8; 32], 2, true); + let blob_1 = create_mock_batch_proof([1u8; 32], [2u8; 32], 2, true); let block_header_2 = MockBlockHeader::from_height(2); @@ -190,6 +197,7 @@ fn create_unchainable_outputs_then_chain_them_on_next_block() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ) .unwrap(); @@ -206,8 +214,8 @@ fn test_header_chain_proof_height_and_hash() { let light_client_proof_method_id = [1u32; 8]; let da_verifier = MockDaVerifier {}; - let blob_1 = create_mock_blob([1u8; 32], [2u8; 32], 2, true); - let blob_2 = create_mock_blob([2u8; 32], [3u8; 32], 3, true); + let blob_1 = create_mock_batch_proof([1u8; 32], [2u8; 32], 2, true); + let blob_2 = create_mock_batch_proof([2u8; 32], [3u8; 32], 3, true); let block_header_1 = MockBlockHeader::from_height(1); @@ -222,6 +230,7 @@ fn test_header_chain_proof_height_and_hash() { let l2_genesis_state_root = [1u8; 32]; let batch_prover_da_pub_key = [9; 32].to_vec(); + let method_id_upgrade_authority = [11u8; 32].to_vec(); let output_1 = run_circuit::<_, MockZkGuest>( da_verifier.clone(), @@ -229,6 +238,7 @@ fn test_header_chain_proof_height_and_hash() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ) .unwrap(); @@ -239,8 +249,8 @@ fn test_header_chain_proof_height_and_hash() { assert_eq!(output_1.last_l2_height, 3); // Now give l1 block with height 3 - let blob_3 = create_mock_blob([3u8; 32], [4u8; 32], 4, true); - let blob_4 = create_mock_blob([4u8; 32], [5u8; 32], 5, true); + let blob_3 = create_mock_batch_proof([3u8; 32], [4u8; 32], 4, true); + let blob_4 = create_mock_batch_proof([4u8; 32], [5u8; 32], 5, true); let block_header_2 = MockBlockHeader::from_height(3); @@ -262,11 +272,14 @@ fn test_header_chain_proof_height_and_hash() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ); assert!(matches!( res, - Err(LightClientVerificationError::HeaderChainVerificationFailed) + Err(LightClientVerificationError::HeaderChainVerificationFailed( + _ + )) )); } @@ -275,8 +288,8 @@ fn test_unverifiable_batch_proofs() { let light_client_proof_method_id = [1u32; 8]; let da_verifier = MockDaVerifier {}; - let blob_1 = create_mock_blob([1u8; 32], [2u8; 32], 2, true); - let blob_2 = create_mock_blob([2u8; 32], [3u8; 32], 3, false); + let blob_1 = create_mock_batch_proof([1u8; 32], [2u8; 32], 2, true); + let blob_2 = create_mock_batch_proof([2u8; 32], [3u8; 32], 3, false); let block_header_1 = MockBlockHeader::from_height(1); @@ -291,6 +304,7 @@ fn test_unverifiable_batch_proofs() { let l2_genesis_state_root = [1u8; 32]; let batch_prover_da_pub_key = [9; 32].to_vec(); + let method_id_upgrade_authority = [11u8; 32].to_vec(); let output_1 = run_circuit::<_, MockZkGuest>( da_verifier.clone(), @@ -298,6 +312,7 @@ fn test_unverifiable_batch_proofs() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ) .unwrap(); @@ -315,8 +330,8 @@ fn test_unverifiable_prev_light_client_proof() { let light_client_proof_method_id = [1u32; 8]; let da_verifier = MockDaVerifier {}; - let blob_1 = create_mock_blob([1u8; 32], [2u8; 32], 2, true); - let blob_2 = create_mock_blob([2u8; 32], [3u8; 32], 3, false); + let blob_1 = create_mock_batch_proof([1u8; 32], [2u8; 32], 2, true); + let blob_2 = create_mock_batch_proof([2u8; 32], [3u8; 32], 3, false); let block_header_1 = MockBlockHeader::from_height(1); @@ -331,6 +346,7 @@ fn test_unverifiable_prev_light_client_proof() { let l2_genesis_state_root = [1u8; 32]; let batch_prover_da_pub_key = [9; 32].to_vec(); + let method_id_upgrade_authority = [11u8; 32].to_vec(); let output_1 = run_circuit::<_, MockZkGuest>( da_verifier.clone(), @@ -338,6 +354,7 @@ fn test_unverifiable_prev_light_client_proof() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ) .unwrap(); @@ -368,6 +385,7 @@ fn test_unverifiable_prev_light_client_proof() { l2_genesis_state_root, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &batch_prover_da_pub_key, + &method_id_upgrade_authority, Network::Nightly, ); assert!(matches!( @@ -375,3 +393,109 @@ fn test_unverifiable_prev_light_client_proof() { Err(LightClientVerificationError::InvalidPreviousLightClientProof) )); } + +#[test] +fn test_new_method_id_txs() { + let light_client_proof_method_id = [1u32; 8]; + let da_verifier = MockDaVerifier {}; + + let l2_genesis_state_root = [1u8; 32]; + let batch_prover_da_pub_key = [9; 32]; + let method_id_upgrade_authority = [11u8; 32]; + + let blob_1 = create_mock_batch_proof([1u8; 32], [2u8; 32], 2, true); + let blob_2 = create_new_method_id_tx(10, [2u32; 8], method_id_upgrade_authority); + + let block_header_1 = MockBlockHeader::from_height(1); + + let input = LightClientCircuitInput { + previous_light_client_proof_journal: None, + light_client_proof_method_id, + da_block_header: block_header_1, + da_data: vec![blob_1, blob_2], + inclusion_proof: [1u8; 32], + completeness_proof: (), + }; + + let output_1 = run_circuit::<_, MockZkGuest>( + da_verifier.clone(), + input, + l2_genesis_state_root, + INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), + &batch_prover_da_pub_key.clone(), + &method_id_upgrade_authority, + Network::Nightly, + ) + .unwrap(); + + assert_eq!(output_1.batch_proof_method_ids.len(), 2); + assert_eq!( + output_1.batch_proof_method_ids, + vec![(0u64, [0u32; 8]), (10u64, [2u32; 8])] + ); + + // now try wrong method id + let blob_2 = create_new_method_id_tx(10, [3u32; 8], batch_prover_da_pub_key); + + let block_header_2 = MockBlockHeader::from_height(2); + + let input = LightClientCircuitInput { + previous_light_client_proof_journal: Some(create_prev_lcp_serialized(output_1, true)), + light_client_proof_method_id, + da_block_header: block_header_2, + da_data: vec![blob_2], + inclusion_proof: [1u8; 32], + completeness_proof: (), + }; + + let output_2 = run_circuit::<_, MockZkGuest>( + da_verifier.clone(), + input, + l2_genesis_state_root, + INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), + &batch_prover_da_pub_key, + &method_id_upgrade_authority, + Network::Nightly, + ) + .unwrap(); + + // didn't change + assert_eq!(output_2.batch_proof_method_ids.len(), 2); + assert_eq!( + output_2.batch_proof_method_ids, + vec![(0u64, [0u32; 8]), (10u64, [2u32; 8])] + ); + + // now try activation height < last activationg height and activation height = last activation height + let blob_1 = create_new_method_id_tx(10, [2u32; 8], method_id_upgrade_authority); + let blob_2 = create_new_method_id_tx(3, [2u32; 8], method_id_upgrade_authority); + + let block_header_3 = MockBlockHeader::from_height(3); + + let input = LightClientCircuitInput { + previous_light_client_proof_journal: Some(create_prev_lcp_serialized(output_2, true)), + light_client_proof_method_id, + da_block_header: block_header_3, + da_data: vec![blob_1, blob_2], + inclusion_proof: [1u8; 32], + completeness_proof: (), + }; + + let output_3 = run_circuit::<_, MockZkGuest>( + da_verifier.clone(), + input, + l2_genesis_state_root, + INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), + &batch_prover_da_pub_key.clone(), + &method_id_upgrade_authority, + Network::Nightly, + ) + .unwrap(); + + // didn't change + assert_eq!(output_3.batch_proof_method_ids.len(), 2); + assert_eq!( + output_3.batch_proof_method_ids, + vec![(0u64, [0u32; 8]), (10u64, [2u32; 8])] + ); +} diff --git a/crates/light-client-prover/src/tests/test_utils.rs b/crates/light-client-prover/src/tests/test_utils.rs index b83e828ce..688b67738 100644 --- a/crates/light-client-prover/src/tests/test_utils.rs +++ b/crates/light-client-prover/src/tests/test_utils.rs @@ -2,10 +2,10 @@ use std::collections::BTreeMap; use sov_mock_da::{MockAddress, MockBlob, MockDaSpec, MockHash}; use sov_mock_zkvm::{MockCodeCommitment, MockJournal, MockProof}; -use sov_rollup_interface::da::{BlobReaderTrait, DaDataLightClient}; +use sov_rollup_interface::da::{BatchProofMethodId, BlobReaderTrait, DaDataLightClient}; use sov_rollup_interface::zk::{BatchProofCircuitOutput, LightClientCircuitOutput}; -pub(crate) fn create_mock_blob( +pub(crate) fn create_mock_batch_proof( initial_state_root: [u8; 32], final_state_root: [u8; 32], last_l2_height: u64, @@ -61,3 +61,21 @@ pub(crate) fn create_prev_lcp_serialized( false => borsh::to_vec(&MockJournal::Unverifiable(serialized)).unwrap(), } } + +pub(crate) fn create_new_method_id_tx( + activation_height: u64, + new_method_id: [u32; 8], + pub_key: [u8; 32], +) -> MockBlob { + let da_data = DaDataLightClient::BatchProofMethodId(BatchProofMethodId { + method_id: new_method_id, + activation_l2_height: activation_height, + }); + + let da_data_ser = borsh::to_vec(&da_data).expect("should serialize"); + + let mut blob = MockBlob::new(da_data_ser, MockAddress::new(pub_key), [0u8; 32]); + blob.full_data(); + + blob +} diff --git a/crates/prover-services/src/parallel/mod.rs b/crates/prover-services/src/parallel/mod.rs index 30eae50b8..a9d4bd644 100644 --- a/crates/prover-services/src/parallel/mod.rs +++ b/crates/prover-services/src/parallel/mod.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use futures::future; use rand::Rng; use sov_db::ledger_db::LedgerDB; -use sov_rollup_interface::da::DaData; +use sov_rollup_interface::da::DaTxRequest; use sov_rollup_interface::services::da::DaService; use sov_rollup_interface::zk::{Proof, ZkvmHost}; use sov_stf_runner::ProverService; @@ -166,9 +166,9 @@ where } async fn submit_proof(&self, proof: Proof) -> anyhow::Result<::TransactionId> { - let da_data = DaData::ZKProof(proof); + let tx_request = DaTxRequest::ZKProof(proof); self.da_service - .send_transaction(da_data) + .send_transaction(tx_request) .await .map_err(|e| anyhow::anyhow!(e)) } diff --git a/crates/sequencer/src/commitment/mod.rs b/crates/sequencer/src/commitment/mod.rs index 57bbdf7b5..aefba3dfa 100644 --- a/crates/sequencer/src/commitment/mod.rs +++ b/crates/sequencer/src/commitment/mod.rs @@ -11,8 +11,8 @@ use rs_merkle::MerkleTree; use sov_db::ledger_db::SequencerLedgerOps; use sov_db::schema::types::{SlotNumber, SoftConfirmationNumber}; use sov_modules_api::StateDiff; -use sov_rollup_interface::da::{BlockHeaderTrait, DaData, SequencerCommitment}; -use sov_rollup_interface::services::da::{DaService, SenderWithNotifier}; +use sov_rollup_interface::da::{BlockHeaderTrait, DaTxRequest, SequencerCommitment}; +use sov_rollup_interface::services::da::{DaService, TxRequestWithNotifier}; use tokio::select; use tokio::sync::oneshot; use tokio_util::sync::CancellationToken; @@ -138,9 +138,9 @@ where debug!("Sequencer: submitting commitment: {:?}", commitment); - let da_data = DaData::SequencerCommitment(commitment); + let tx_request = DaTxRequest::SequencerCommitment(commitment); let (notify, rx) = oneshot::channel(); - let request = SenderWithNotifier { da_data, notify }; + let request = TxRequestWithNotifier { tx_request, notify }; self.da_service .get_send_transaction_queue() .send(request) diff --git a/crates/sovereign-sdk/adapters/mock-da/src/service.rs b/crates/sovereign-sdk/adapters/mock-da/src/service.rs index df9f92740..a96a4035e 100644 --- a/crates/sovereign-sdk/adapters/mock-da/src/service.rs +++ b/crates/sovereign-sdk/adapters/mock-da/src/service.rs @@ -9,10 +9,10 @@ use borsh::BorshDeserialize; use pin_project::pin_project; use sha2::Digest; use sov_rollup_interface::da::{ - BlobReaderTrait, BlockHeaderTrait, DaData, DaDataBatchProof, DaDataLightClient, DaNamespace, - DaSpec, SequencerCommitment, Time, + BlobReaderTrait, BlockHeaderTrait, DaDataBatchProof, DaDataLightClient, DaNamespace, DaSpec, + DaTxRequest, SequencerCommitment, Time, }; -use sov_rollup_interface::services::da::{DaService, SenderWithNotifier, SlotData}; +use sov_rollup_interface::services::da::{DaService, SlotData, TxRequestWithNotifier}; use sov_rollup_interface::zk::Proof; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio::sync::{broadcast, Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; @@ -155,8 +155,8 @@ impl MockDaService { blocks.prune_above(height); for blob in blobs { - let da_data = DaData::ZKProof(blob); - let blob = borsh::to_vec(&da_data).unwrap(); + let tx_request = DaTxRequest::ZKProof(blob); + let blob = borsh::to_vec(&tx_request).unwrap(); self.add_blob(&blocks, blob, Default::default()).unwrap(); } @@ -477,17 +477,25 @@ impl DaService for MockDaService { } #[tracing::instrument(name = "MockDA", level = "debug", skip_all)] - async fn send_transaction(&self, da_data: DaData) -> Result { - let blob = match da_data { - DaData::ZKProof(proof) => { + async fn send_transaction( + &self, + tx_request: DaTxRequest, + ) -> Result { + let blob = match tx_request { + DaTxRequest::ZKProof(proof) => { tracing::debug!("Adding a zkproof"); - let data = DaDataLightClient::Complete(proof); - borsh::to_vec(&data).unwrap() + let req = DaDataLightClient::Complete(proof); + borsh::to_vec(&req).unwrap() } - DaData::SequencerCommitment(seq_comm) => { + DaTxRequest::SequencerCommitment(seq_comm) => { tracing::debug!("Adding a sequencer commitment"); - let data = DaData::SequencerCommitment(seq_comm); - borsh::to_vec(&data).unwrap() + let req = DaTxRequest::SequencerCommitment(seq_comm); + borsh::to_vec(&req).unwrap() + } + DaTxRequest::BatchProofMethodId(method_id) => { + tracing::debug!("Adding a batch proof method id tx"); + let req = DaTxRequest::BatchProofMethodId(method_id); + borsh::to_vec(&req).unwrap() } }; let blocks = self.blocks.lock().await; @@ -497,12 +505,12 @@ impl DaService for MockDaService { fn get_send_transaction_queue( &self, - ) -> UnboundedSender> { - let (tx, mut rx) = unbounded_channel::>(); + ) -> UnboundedSender> { + let (tx, mut rx) = unbounded_channel::>(); let this = self.clone(); tokio::spawn(async move { while let Some(req) = rx.recv().await { - let res = this.send_transaction(req.da_data).await; + let res = this.send_transaction(req.tx_request).await; let _ = req.notify.send(res); } }); @@ -640,7 +648,7 @@ mod tests { for i in 0..num_blocks { let proof = vec![i as u8; i + 1]; - let published_blob = DaData::ZKProof(proof.clone()); + let published_blob = DaTxRequest::ZKProof(proof.clone()); let height = (i + 1) as u64; da.send_transaction(published_blob.clone()).await.unwrap(); @@ -691,7 +699,7 @@ mod tests { for (i, blob) in blobs.iter().enumerate() { let height = (i + 1) as u64; // Send transaction should pass - da.send_transaction(DaData::ZKProof(blob.to_owned())) + da.send_transaction(DaTxRequest::ZKProof(blob.to_owned())) .await .unwrap(); let last_finalized_block_response = da.get_last_finalized_block_header().await; @@ -777,13 +785,13 @@ mod tests { // 1 -> 2 -> 3 - da.send_transaction(DaData::ZKProof(vec![1, 2, 3, 4])) + da.send_transaction(DaTxRequest::ZKProof(vec![1, 2, 3, 4])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![4, 5, 6, 7])) + da.send_transaction(DaTxRequest::ZKProof(vec![4, 5, 6, 7])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![8, 9, 0, 1])) + da.send_transaction(DaTxRequest::ZKProof(vec![8, 9, 0, 1])) .await .unwrap(); @@ -834,19 +842,19 @@ mod tests { // \ -> 3.2 -> 4.2 // 1 - da.send_transaction(DaData::ZKProof(vec![1, 2, 3, 4])) + da.send_transaction(DaTxRequest::ZKProof(vec![1, 2, 3, 4])) .await .unwrap(); // 2 - da.send_transaction(DaData::ZKProof(vec![4, 5, 6, 7])) + da.send_transaction(DaTxRequest::ZKProof(vec![4, 5, 6, 7])) .await .unwrap(); // 3.1 - da.send_transaction(DaData::ZKProof(vec![8, 9, 0, 1])) + da.send_transaction(DaTxRequest::ZKProof(vec![8, 9, 0, 1])) .await .unwrap(); // 4.1 - da.send_transaction(DaData::ZKProof(vec![2, 3, 4, 5])) + da.send_transaction(DaTxRequest::ZKProof(vec![2, 3, 4, 5])) .await .unwrap(); @@ -876,16 +884,16 @@ mod tests { // 1 -> 2 -> 3 -> 4 - da.send_transaction(DaData::ZKProof(vec![1, 2, 3, 4])) + da.send_transaction(DaTxRequest::ZKProof(vec![1, 2, 3, 4])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![4, 5, 6, 7])) + da.send_transaction(DaTxRequest::ZKProof(vec![4, 5, 6, 7])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![8, 9, 0, 1])) + da.send_transaction(DaTxRequest::ZKProof(vec![8, 9, 0, 1])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![2, 3, 4, 5])) + da.send_transaction(DaTxRequest::ZKProof(vec![2, 3, 4, 5])) .await .unwrap(); @@ -945,13 +953,13 @@ mod tests { assert!(has_planned_fork.is_some()); } - da.send_transaction(DaData::ZKProof(vec![1, 2, 3, 4])) + da.send_transaction(DaTxRequest::ZKProof(vec![1, 2, 3, 4])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![4, 5, 6, 7])) + da.send_transaction(DaTxRequest::ZKProof(vec![4, 5, 6, 7])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![8, 9, 0, 1])) + da.send_transaction(DaTxRequest::ZKProof(vec![8, 9, 0, 1])) .await .unwrap(); @@ -983,19 +991,19 @@ mod tests { PlannedFork::new(4, 2, vec![vec![13, 13, 13, 13], vec![14, 14, 14, 14]]); da.set_planned_fork(planned_fork).await.unwrap(); - da.send_transaction(DaData::ZKProof(vec![1, 1, 1, 1])) + da.send_transaction(DaTxRequest::ZKProof(vec![1, 1, 1, 1])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![2, 2, 2, 2])) + da.send_transaction(DaTxRequest::ZKProof(vec![2, 2, 2, 2])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![3, 3, 3, 3])) + da.send_transaction(DaTxRequest::ZKProof(vec![3, 3, 3, 3])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![4, 4, 4, 4])) + da.send_transaction(DaTxRequest::ZKProof(vec![4, 4, 4, 4])) .await .unwrap(); - da.send_transaction(DaData::ZKProof(vec![5, 5, 5, 5])) + da.send_transaction(DaTxRequest::ZKProof(vec![5, 5, 5, 5])) .await .unwrap(); diff --git a/crates/sovereign-sdk/adapters/mock-da/src/types/mod.rs b/crates/sovereign-sdk/adapters/mock-da/src/types/mod.rs index 61641b31d..9d981dd1c 100644 --- a/crates/sovereign-sdk/adapters/mock-da/src/types/mod.rs +++ b/crates/sovereign-sdk/adapters/mock-da/src/types/mod.rs @@ -160,7 +160,7 @@ pub struct MockDaConfig { pub db_path: PathBuf, } -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] /// DaVerifier used in tests. pub struct MockDaVerifier {} diff --git a/crates/sovereign-sdk/rollup-interface/src/node/services/da.rs b/crates/sovereign-sdk/rollup-interface/src/node/services/da.rs index 88419c419..002c67b3c 100644 --- a/crates/sovereign-sdk/rollup-interface/src/node/services/da.rs +++ b/crates/sovereign-sdk/rollup-interface/src/node/services/da.rs @@ -9,15 +9,15 @@ use tokio::sync::oneshot::Sender as OneshotSender; use crate::da::BlockHeaderTrait; #[cfg(feature = "native")] -use crate::da::{DaData, DaNamespace, DaSpec, DaVerifier, SequencerCommitment}; +use crate::da::{DaNamespace, DaSpec, DaTxRequest, DaVerifier, SequencerCommitment}; #[cfg(feature = "native")] use crate::zk::Proof; /// This type represents a queued request to send_transaction #[cfg(feature = "native")] -pub struct SenderWithNotifier { +pub struct TxRequestWithNotifier { /// Data to send. - pub da_data: DaData, + pub tx_request: DaTxRequest, /// Channel to receive result of the operation. pub notify: OneshotSender>, } @@ -110,12 +110,15 @@ pub trait DaService: Send + Sync + 'static { /// Send a transaction directly to the DA layer. /// blob is the serialized and signed transaction. /// Returns nothing if the transaction was successfully sent. - async fn send_transaction(&self, da_data: DaData) -> Result; + async fn send_transaction( + &self, + tx_request: DaTxRequest, + ) -> Result; /// A tx part of the queue to send transactions in order fn get_send_transaction_queue( &self, - ) -> UnboundedSender> { + ) -> UnboundedSender> { unimplemented!() } diff --git a/crates/sovereign-sdk/rollup-interface/src/state_machine/da.rs b/crates/sovereign-sdk/rollup-interface/src/state_machine/da.rs index 0929dba64..eb15938b0 100644 --- a/crates/sovereign-sdk/rollup-interface/src/state_machine/da.rs +++ b/crates/sovereign-sdk/rollup-interface/src/state_machine/da.rs @@ -22,6 +22,15 @@ pub struct SequencerCommitment { pub l2_end_block_number: u64, } +/// A new batch proof method_id starting to be applied from the l2_block_number (inclusive). +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, BorshDeserialize, BorshSerialize)] +pub struct BatchProofMethodId { + /// New method id of upcoming fork + pub method_id: [u32; 8], + /// Activation L2 height of the new method id + pub activation_l2_height: u64, +} + impl core::cmp::PartialOrd for SequencerCommitment { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -34,16 +43,15 @@ impl core::cmp::Ord for SequencerCommitment { } } -// TODO: rename to da service request smth smth -// DaDataOutgoing -/// Data written to DA can only be one of these two types -/// Data written to DA and read from DA is must be borsh serialization of this enum +/// Transaction request to send to the DA queue. #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, BorshDeserialize, BorshSerialize)] -pub enum DaData { +pub enum DaTxRequest { /// A commitment from the sequencer SequencerCommitment(SequencerCommitment), /// Or a zk proof and state diff ZKProof(Proof), + /// Batch proof method id update for light client + BatchProofMethodId(BatchProofMethodId), } /// Data written to DA and read from DA must be the borsh serialization of this enum @@ -55,6 +63,8 @@ pub enum DaDataLightClient { Aggregate(Vec<[u8; 32]>), /// A chunk of an aggregate Chunk(Vec), + /// A new batch proof method_id + BatchProofMethodId(BatchProofMethodId), } /// Data written to DA and read from DA must be the borsh serialization of this enum diff --git a/guests/risc0/light-client-proof/bitcoin/src/bin/light_client_proof_bitcoin.rs b/guests/risc0/light-client-proof/bitcoin/src/bin/light_client_proof_bitcoin.rs index 35c8230ee..ba0df5bf2 100644 --- a/guests/risc0/light-client-proof/bitcoin/src/bin/light_client_proof_bitcoin.rs +++ b/guests/risc0/light-client-proof/bitcoin/src/bin/light_client_proof_bitcoin.rs @@ -88,6 +88,25 @@ const BATCH_PROVER_DA_PUBLIC_KEY: [u8; 33] = { } }; +pub const METHOD_ID_UPGRADE_AUTHORITY_DA_PUBLIC_KEY: [u8; 33] = { + let hex_pub_key = match NETWORK { + Network::Mainnet => "000000000000000000000000000000000000000000000000000000000000000000", + Network::Testnet => "000000000000000000000000000000000000000000000000000000000000000000", + Network::Devnet => "000000000000000000000000000000000000000000000000000000000000000000", + Network::Nightly => match option_env!("METHOD_ID_UPGRADE_AUTHORITY_DA_PUBLIC_KEY") { + Some(hex_pub_key) => hex_pub_key, + None => "0313c4ff65eb94999e0ac41cfe21592baa52910f5a5ada9074b816de4f560189db", + }, + }; + + match const_hex::const_decode_to_array(hex_pub_key.as_bytes()) { + Ok(pub_key) => pub_key, + Err(_) => { + panic!("METHOD_ID_UPGRADE_AUTHORITY_DA_PUBLIC_KEY must be valid 33-byte hex string") + } + } +}; + pub fn main() { let guest = Risc0Guest::new(); @@ -104,6 +123,7 @@ pub fn main() { L2_GENESIS_ROOT, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &BATCH_PROVER_DA_PUBLIC_KEY, + &METHOD_ID_UPGRADE_AUTHORITY_DA_PUBLIC_KEY, NETWORK, ) .unwrap(); diff --git a/guests/risc0/light-client-proof/mock/src/bin/light_client_proof_mock.rs b/guests/risc0/light-client-proof/mock/src/bin/light_client_proof_mock.rs index 94dd5958e..c2fbdaa3e 100644 --- a/guests/risc0/light-client-proof/mock/src/bin/light_client_proof_mock.rs +++ b/guests/risc0/light-client-proof/mock/src/bin/light_client_proof_mock.rs @@ -26,6 +26,13 @@ const BATCH_PROVER_DA_PUBLIC_KEY: [u8; 33] = match const_hex::const_decode_to_ar Err(_) => panic!("Can't happen"), }; +const METHOD_ID_UPGRADE_AUTHORITY_DA_PUBLIC_KEY: [u8; 33] = match const_hex::const_decode_to_array( + b"0313c4ff65eb94999e0ac41cfe21592baa52910f5a5ada9074b816de4f560189db", +) { + Ok(pub_key) => pub_key, + Err(_) => panic!("Can't happen"), +}; + pub fn main() { let guest = Risc0Guest::new(); @@ -39,6 +46,7 @@ pub fn main() { L2_GENESIS_ROOT, INITIAL_BATCH_PROOF_METHOD_IDS.to_vec(), &BATCH_PROVER_DA_PUBLIC_KEY, + &METHOD_ID_UPGRADE_AUTHORITY_DA_PUBLIC_KEY, NETWORK, ) .unwrap();