From 1e71cdb81629b5d0104205d2e00325056d06bd43 Mon Sep 17 00:00:00 2001 From: 0xatomFusion <179967211+0xatomFusion@users.noreply.github.com> Date: Sat, 9 Aug 2025 14:56:11 +0100 Subject: [PATCH 1/8] feat: refactor btc indexer and create system wallet resource --- core/lib/types/src/via_bootstrap.rs | 70 +++ core/lib/types/src/via_wallet.rs | 23 +- core/lib/via_btc_client/Cargo.toml | 6 +- core/lib/via_btc_client/examples/bootstrap.rs | 2 +- .../examples/upgrade_system_contracts.rs | 2 +- core/lib/via_btc_client/src/bootstrap/mod.rs | 119 +++++ core/lib/via_btc_client/src/indexer/mod.rs | 427 ++++++------------ core/lib/via_btc_client/src/indexer/parser.rs | 345 ++++++++++++-- core/lib/via_btc_client/src/inscriber/mod.rs | 5 + .../src/inscriber/script_builder.rs | 33 ++ core/lib/via_btc_client/src/lib.rs | 1 + core/lib/via_btc_client/src/types.rs | 94 +++- .../implementations/layers/via_btc_watch.rs | 12 +- .../layers/via_consistency_checker.rs | 6 +- .../src/implementations/layers/via_indexer.rs | 25 +- .../implementations/layers/via_l1_indexer.rs | 11 +- .../layers/via_verifier_btc_watch.rs | 11 +- .../layers/via_verifier_storage_init.rs | 24 +- .../src/implementations/resources/mod.rs | 1 + .../message_processors/governance_upgrade.rs | 1 + core/node/via_consistency_checker/src/lib.rs | 2 +- .../lib/via_musig2/examples/coordinator.rs | 6 +- .../via_musig2/examples/verify_partial_sig.rs | 1 + .../message_processors/governance_upgrade.rs | 1 + 24 files changed, 827 insertions(+), 401 deletions(-) create mode 100644 core/lib/types/src/via_bootstrap.rs create mode 100644 core/lib/via_btc_client/src/bootstrap/mod.rs diff --git a/core/lib/types/src/via_bootstrap.rs b/core/lib/types/src/via_bootstrap.rs new file mode 100644 index 000000000..9d884cd2c --- /dev/null +++ b/core/lib/types/src/via_bootstrap.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; + +use bitcoin::{Address, Txid}; +use zksync_basic_types::H256; + +use crate::via_wallet::SystemWallets; + +#[derive(Debug, Clone, Default)] +pub struct BootstrapState { + pub wallets: Option, + pub sequencer_proposal_tx_id: Option, + pub bootstrap_tx_id: Option, + pub sequencer_votes: HashMap, + pub starting_block_number: u32, + pub bootloader_hash: Option, + pub abstract_account_hash: Option, +} + +impl BootstrapState { + pub fn validate(&self) -> anyhow::Result<()> { + let wallets = self + .wallets + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Wallets missing"))?; + + if !wallets.sequencer.script_pubkey().is_p2wpkh() { + anyhow::bail!("Sequencer must be P2WPKH"); + } + if !wallets.bridge.script_pubkey().is_p2tr() { + anyhow::bail!("Bridge must be Taproot"); + } + if !wallets.governance.script_pubkey().is_p2wsh() { + anyhow::bail!("Governance must be P2WSH"); + } + if !wallets + .verifiers + .iter() + .all(|a| a.script_pubkey().is_p2wpkh()) + { + anyhow::bail!("All verifiers must be P2WPKH"); + } + if self.starting_block_number == 0 { + anyhow::bail!("Starting block number must be > 0"); + } + if !self.has_majority_votes(wallets.verifiers.len()) { + anyhow::bail!("Majority votes missing"); + } + if self.sequencer_proposal_tx_id.is_none() { + anyhow::bail!("sequencer_proposal_tx_id missing"); + } + if self.bootstrap_tx_id.is_none() { + anyhow::bail!("bootstrap_tx_id missing"); + } + if self.bootloader_hash.is_none() { + anyhow::bail!("Bootloader hash missing"); + } + if self.abstract_account_hash.is_none() { + anyhow::bail!("Abstract account hash missing"); + } + + Ok(()) + } + + fn has_majority_votes(&self, verifier_count: usize) -> bool { + let total_votes = self.sequencer_votes.len(); + let positive_votes = self.sequencer_votes.values().filter(|&&v| v).count(); + + positive_votes * 2 > total_votes && total_votes == verifier_count + } +} diff --git a/core/lib/types/src/via_wallet.rs b/core/lib/types/src/via_wallet.rs index 4efedf72c..501706842 100644 --- a/core/lib/types/src/via_wallet.rs +++ b/core/lib/types/src/via_wallet.rs @@ -75,6 +75,17 @@ pub struct SystemWallets { } impl SystemWallets { + pub fn is_valid_bridge_address(&self, bridge_address: Address) -> anyhow::Result<()> { + if !self.verifiers.contains(&bridge_address) { + anyhow::bail!( + "bridge address mismatch, expected one of {:?}, found {}", + &self.bridge, + bridge_address + ); + } + Ok(()) + } + pub fn is_valid_verifier_address(&self, verifier: Address) -> anyhow::Result<()> { if !self.verifiers.contains(&verifier) { anyhow::bail!( @@ -129,22 +140,22 @@ impl TryFrom<&BootstrapState> for SystemWalletsDetails { .clone() .ok_or_else(|| anyhow::anyhow!("Wallets missing"))?; - if let Some(seq_txid) = &state.sequencer_proposal_tx_id { + if let Some(txid) = &state.sequencer_proposal_tx_id { map.insert( WalletRole::Sequencer, WalletInfo { addresses: vec![wallets.sequencer.clone()], - txid: seq_txid.clone(), + txid: *txid, }, ); } - if let Some(boot_txid) = &state.bootstrap_tx_id { + if let Some(txid) = &state.bootstrap_tx_id { map.insert( WalletRole::Bridge, WalletInfo { addresses: vec![wallets.bridge.clone()], - txid: boot_txid.clone(), + txid: *txid, }, ); @@ -152,7 +163,7 @@ impl TryFrom<&BootstrapState> for SystemWalletsDetails { WalletRole::Gov, WalletInfo { addresses: vec![wallets.governance.clone()], - txid: boot_txid.clone(), + txid: *txid, }, ); @@ -160,7 +171,7 @@ impl TryFrom<&BootstrapState> for SystemWalletsDetails { WalletRole::Verifier, WalletInfo { addresses: wallets.verifiers.clone(), - txid: boot_txid.clone(), + txid: *txid, }, ); } diff --git a/core/lib/via_btc_client/Cargo.toml b/core/lib/via_btc_client/Cargo.toml index 9f6f01122..4054c4e39 100644 --- a/core/lib/via_btc_client/Cargo.toml +++ b/core/lib/via_btc_client/Cargo.toml @@ -86,4 +86,8 @@ path = "examples/upgrade_system_contracts.rs" [[example]] name = "upgrade_inscription_parsing" -path = "examples/upgrade_inscription_parsing.rs" \ No newline at end of file +path = "examples/upgrade_inscription_parsing.rs" + +[[example]] +name = "propose_new_bridge" +path = "examples/propose_new_bridge.rs" diff --git a/core/lib/via_btc_client/examples/bootstrap.rs b/core/lib/via_btc_client/examples/bootstrap.rs index 03245f2f5..787e69f95 100644 --- a/core/lib/via_btc_client/examples/bootstrap.rs +++ b/core/lib/via_btc_client/examples/bootstrap.rs @@ -261,7 +261,7 @@ pub async fn attest_sequencer( .await?; let mut parser = MessageParser::new(network); - let res = parser.parse_system_transaction(&tx, 0); + let res = parser.parse_system_transaction(&tx, 0, None); if res.is_empty() { anyhow::bail!( "Inscription not found {:?}", diff --git a/core/lib/via_btc_client/examples/upgrade_system_contracts.rs b/core/lib/via_btc_client/examples/upgrade_system_contracts.rs index 54802669a..f21301df5 100644 --- a/core/lib/via_btc_client/examples/upgrade_system_contracts.rs +++ b/core/lib/via_btc_client/examples/upgrade_system_contracts.rs @@ -127,7 +127,7 @@ async fn main() -> Result<()> { .get_transaction(&system_contract_upgrade_info.final_reveal_tx.txid) .await?; - let upgrade_inscription = parser.parse_system_transaction(&tx, 0); + let upgrade_inscription = parser.parse_system_transaction(&tx, 0, None); info!("upgrade_inscription: {:?}", upgrade_inscription); Ok(()) diff --git a/core/lib/via_btc_client/src/bootstrap/mod.rs b/core/lib/via_btc_client/src/bootstrap/mod.rs new file mode 100644 index 000000000..cfd75b314 --- /dev/null +++ b/core/lib/via_btc_client/src/bootstrap/mod.rs @@ -0,0 +1,119 @@ +use std::{str::FromStr, sync::Arc}; + +use bitcoin::{Address, Txid}; +use zksync_config::configs::via_consensus::ViaGenesisConfig; +use zksync_types::{via_bootstrap::BootstrapState, via_wallet::SystemWallets}; + +use crate::{ + client::BitcoinClient, + indexer::MessageParser, + traits::BitcoinOps, + types::{FullInscriptionMessage, Vote}, +}; + +#[derive(Debug, Clone)] +pub struct ViaBootstrap { + pub config: ViaGenesisConfig, + pub client: Arc, +} + +impl ViaBootstrap { + pub fn new(client: Arc, config: ViaGenesisConfig) -> Self { + Self { client, config } + } + + pub async fn process_bootstrap_messages(&self) -> anyhow::Result { + let network = self.client.get_network(); + let mut parser = MessageParser::new(network); + let mut state = BootstrapState::default(); + + let mut sequencer: Option
= None; + let mut bridge: Option
= None; + let mut governance: Option
= None; + let mut verifiers: Vec
= vec![]; + + for txid_str in self.config.bootstrap_txids.clone() { + let txid = Txid::from_str(&txid_str)?; + let tx = self.client.get_transaction(&txid).await?; + let messages = parser.parse_system_transaction(&tx, 0, None); + + for message in messages { + match message { + FullInscriptionMessage::SystemBootstrapping(sb) => { + tracing::debug!("Processing SystemBootstrapping message"); + + let verifier_addresses = sb + .input + .verifier_p2wpkh_addresses + .iter() + .map(|addr| addr.clone().require_network(network).unwrap()) + .collect::>(); + verifiers.extend(verifier_addresses); + + bridge = Some( + sb.input + .bridge_musig2_address + .require_network(network) + .unwrap(), + ); + governance = Some( + sb.input + .governance_address + .require_network(network) + .unwrap(), + ); + + state.starting_block_number = sb.input.start_block_height; + state.bootloader_hash = Some(sb.input.bootloader_hash); + state.abstract_account_hash = Some(sb.input.abstract_account_hash); + + state.bootstrap_tx_id = Some(txid); + } + FullInscriptionMessage::ProposeSequencer(ps) => { + tracing::debug!("Processing ProposeSequencer message"); + let sequencer_address = ps + .input + .sequencer_new_p2wpkh_address + .require_network(network) + .unwrap(); + sequencer = Some(sequencer_address); + state.sequencer_proposal_tx_id = Some(txid); + } + FullInscriptionMessage::ValidatorAttestation(va) => { + let p2wpkh_address = va + .common + .p2wpkh_address + .as_ref() + .expect("ValidatorAttestation must have a p2wpkh address"); + + if verifiers.contains(p2wpkh_address) + && va.input.reference_txid == state.sequencer_proposal_tx_id.unwrap() + { + state + .sequencer_votes + .insert(p2wpkh_address.clone(), va.input.attestation == Vote::Ok); + } + } + _ => { + tracing::debug!("Ignoring non-bootstrap message during bootstrap process"); + } + } + } + } + + // Construct SystemWallets and put them into state + if let (Some(seq), Some(br), Some(gov)) = (sequencer, bridge, governance) { + state.wallets = Some(SystemWallets { + sequencer: seq, + bridge: br, + governance: gov, + verifiers, + }); + } + + // Validate the final state + state.validate()?; + + Ok(state) + } +} diff --git a/core/lib/via_btc_client/src/indexer/mod.rs b/core/lib/via_btc_client/src/indexer/mod.rs index da8e80108..e2fd40d9a 100644 --- a/core/lib/via_btc_client/src/indexer/mod.rs +++ b/core/lib/via_btc_client/src/indexer/mod.rs @@ -1,121 +1,41 @@ -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; -use bitcoin::{Address, Amount, BlockHash, Network, Transaction as BitcoinTransaction, Txid}; -use tracing::{debug, error, info, instrument, warn}; +use bitcoin::{Address, Amount, BlockHash, OutPoint, Transaction as BitcoinTransaction, Txid}; +use tracing::{debug, info, instrument, warn}; mod parser; pub use parser::{get_eth_address, MessageParser}; use zksync_basic_types::L1BatchNumber; -use zksync_config::configs::via_btc_client::ViaBtcClientConfig; -use zksync_types::H256; +use zksync_types::via_wallet::SystemWallets; use crate::{ client::BitcoinClient, traits::BitcoinOps, types::{ - self, BitcoinIndexerResult, BridgeWithdrawal, FullInscriptionMessage, L1ToL2Message, - SystemContractUpgrade, TransactionWithMetadata, Vote, + BitcoinIndexerResult, BridgeWithdrawal, FullInscriptionMessage, L1ToL2Message, + SystemTransactions, TransactionWithMetadata, }, }; -/// Represents the state during the bootstrap process -#[derive(Debug, Clone)] -pub struct BootstrapState { - pub verifier_addresses: Vec
, - pub proposed_sequencer: Option
, - pub proposed_sequencer_txid: Option, - pub sequencer_votes: HashMap, - pub bridge_address: Option
, - pub starting_block_number: u32, - pub bootloader_hash: Option, - pub abstract_account_hash: Option, - pub proposed_governance: Option
, -} - -impl BootstrapState { - pub fn new() -> Self { - Self { - verifier_addresses: Vec::new(), - proposed_sequencer: None, - proposed_sequencer_txid: None, - sequencer_votes: HashMap::new(), - bridge_address: None, - starting_block_number: 0, - bootloader_hash: None, - abstract_account_hash: None, - proposed_governance: None, - } - } - - pub fn is_complete(&self) -> bool { - !self.verifier_addresses.is_empty() - && self.proposed_sequencer.is_some() - && self.bridge_address.is_some() - && self.starting_block_number > 0 - && self.has_majority_votes() - && self.bootloader_hash.is_some() - && self.abstract_account_hash.is_some() - && self.proposed_governance.is_some() - } - - fn has_majority_votes(&self) -> bool { - let total_votes = self.sequencer_votes.len(); - let positive_votes = self - .sequencer_votes - .values() - .filter(|&v| *v == Vote::Ok) - .count(); - positive_votes * 2 > total_votes && total_votes == self.verifier_addresses.len() - } -} - /// The main indexer struct for processing Bitcoin inscriptions #[derive(Debug, Clone)] pub struct BitcoinInscriptionIndexer { client: Arc, + wallets: Arc, parser: MessageParser, - bridge_address: Address, - sequencer_address: Address, - governance_address: Address, - verifier_addresses: Vec
, - starting_block_number: u32, } impl BitcoinInscriptionIndexer { - #[instrument(skip(client, config, bootstrap_txids), target = "bitcoin_indexer")] - pub async fn new( - client: Arc, - config: ViaBtcClientConfig, - bootstrap_txids: Vec, - ) -> BitcoinIndexerResult - where - Self: Sized, - { - info!("Creating new BitcoinInscriptionIndexer"); - let mut parser = MessageParser::new(config.network()); - let mut bootstrap_state = BootstrapState::new(); - - for txid in bootstrap_txids { - debug!("Processing bootstrap transaction: {}", txid); - let tx = client.get_transaction(&txid).await?; - let messages = parser.parse_system_transaction(&tx, 0); - - for message in messages { - Self::process_bootstrap_message( - &mut bootstrap_state, - message, - txid, - config.network(), - ); - } - - if bootstrap_state.is_complete() { - info!("Bootstrap process completed"); - break; - } + #[instrument( + skip(client, wallets) + target = "bitcoin_indexer" + )] + pub fn new(client: Arc, wallets: Arc) -> Self { + Self { + client: client.clone(), + parser: MessageParser::new(client.get_network()), + wallets, } - - Self::create_indexer(bootstrap_state, client, parser) } #[instrument(skip(self), target = "bitcoin_indexer")] @@ -136,16 +56,43 @@ impl BitcoinInscriptionIndexer { Ok(res) } + #[instrument(skip(self), target = "bitcoin_indexer")] + pub fn update_system_wallets( + &mut self, + sequencer_opt: Option
, + bridge_opt: Option
, + verifiers_opt: Option>, + governance_opt: Option
, + ) { + let mut new_wallets = SystemWallets { + ..(*self.wallets).clone() + }; + + if let Some(sequencer) = sequencer_opt { + new_wallets.sequencer = sequencer; + } + + if let Some(bridge) = bridge_opt { + new_wallets.bridge = bridge; + } + + if let Some(verifiers) = verifiers_opt { + new_wallets.verifiers = verifiers; + } + + if let Some(governance) = governance_opt { + new_wallets.governance = governance; + } + + self.wallets = Arc::new(new_wallets); + } + #[instrument(skip(self), target = "bitcoin_indexer")] pub async fn process_block( &mut self, block_height: u32, ) -> BitcoinIndexerResult> { debug!("Processing block at height {}", block_height); - if block_height < self.starting_block_number { - error!("Attempted to process block before starting block"); - return Err(types::IndexerError::InvalidBlockHeight(block_height)); - } let block = self.client.fetch_block(block_height as u128).await?; // TODO: check block header is belong to a valid chain of blocks (reorg detection and management) @@ -153,15 +100,16 @@ impl BitcoinInscriptionIndexer { let mut valid_messages = Vec::new(); - let (gov_txs, system_tx, bridge_tx) = self.extract_important_transactions(&block.txdata); + let mut system_txs = self.extract_important_transactions(&block.txdata); - // Parse protocol upgrade messages - if let Some(gov_txs) = gov_txs { - let parsed_messages: Vec<_> = gov_txs + // Parse protocol upgrade messages (Upgrade system contracts, bridge addresses, sequencer address) + if !system_txs.governance_txs.is_empty() { + let parsed_messages: Vec<_> = system_txs + .governance_txs .iter() .flat_map(|tx| { self.parser - .parse_protocol_upgrade_transaction(tx, block_height) + .parse_protocol_upgrade_transactions(tx, block_height) }) .collect(); @@ -175,10 +123,14 @@ impl BitcoinInscriptionIndexer { valid_messages.extend(messages); } - if let Some(system_tx) = system_tx { - let parsed_messages: Vec<_> = system_tx + if !system_txs.system_txs.is_empty() { + let parsed_messages: Vec<_> = system_txs + .system_txs .iter() - .flat_map(|tx| self.parser.parse_system_transaction(&tx.tx, block_height)) + .flat_map(|tx| { + self.parser + .parse_system_transaction(&tx.tx, block_height, Some(&self.wallets)) + }) .collect(); let messages: Vec<_> = parsed_messages @@ -189,10 +141,14 @@ impl BitcoinInscriptionIndexer { valid_messages.extend(messages); } - if let Some(mut bridge_tx) = bridge_tx { - let parsed_messages: Vec<_> = bridge_tx + if !system_txs.bridge_txs.is_empty() { + let parsed_messages: Vec<_> = system_txs + .bridge_txs .iter_mut() - .flat_map(|tx| self.parser.parse_bridge_transaction(tx, block_height)) + .flat_map(|tx| { + self.parser + .parse_bridge_transaction(tx, block_height, &self.wallets) + }) .collect(); let mut messages = vec![]; @@ -216,11 +172,7 @@ impl BitcoinInscriptionIndexer { fn extract_important_transactions( &self, transactions: &[BitcoinTransaction], - ) -> ( - Option>, - Option>, - Option>, - ) { + ) -> SystemTransactions { // We only care about the transactions that sequencer, verifiers are sending and the bridge is receiving let system_txs: Vec = transactions .iter() @@ -228,8 +180,8 @@ impl BitcoinInscriptionIndexer { .filter_map(|(tx_index, tx)| { let is_valid = tx.input.iter().any(|input| { if let Some(btc_address) = self.parser.parse_p2wpkh(&input.witness) { - btc_address == self.sequencer_address - || self.verifier_addresses.contains(&btc_address) + btc_address == self.wallets.sequencer + || self.wallets.verifiers.contains(&btc_address) } else { false } @@ -250,7 +202,7 @@ impl BitcoinInscriptionIndexer { let is_bridge_output = tx .output .iter() - .any(|output| output.script_pubkey == self.bridge_address.script_pubkey()); + .any(|output| output.script_pubkey == self.wallets.bridge.script_pubkey()); if is_bridge_output { Some(TransactionWithMetadata::new(tx.clone(), tx_index)) @@ -260,14 +212,14 @@ impl BitcoinInscriptionIndexer { }) .collect(); - let gov_txs: Vec = transactions + let governance_txs: Vec = transactions .iter() .enumerate() .filter_map(|(tx_index, tx)| { let is_bridge_output = tx .output .iter() - .any(|output| output.script_pubkey == self.governance_address.script_pubkey()); + .any(|output| output.script_pubkey == self.wallets.governance.script_pubkey()); if is_bridge_output { Some(TransactionWithMetadata::new(tx.clone(), tx_index)) @@ -277,25 +229,11 @@ impl BitcoinInscriptionIndexer { }) .collect(); - let gov_txs = if !gov_txs.is_empty() { - Some(gov_txs) - } else { - None - }; - - let system_txs = if !system_txs.is_empty() { - Some(system_txs) - } else { - None - }; - - let bridge_txs = if !bridge_txs.is_empty() { - Some(bridge_txs) - } else { - None - }; - - (gov_txs, system_txs, bridge_txs) + SystemTransactions { + system_txs, + bridge_txs, + governance_txs, + } } #[instrument(skip(self), target = "bitcoin_indexer")] @@ -318,13 +256,8 @@ impl BitcoinInscriptionIndexer { self.client.fetch_block_height().await.map_err(|e| e.into()) } - pub fn get_state(&self) -> (Address, Address, Vec
, u32) { - ( - self.bridge_address.clone(), - self.sequencer_address.clone(), - self.verifier_addresses.clone(), - self.starting_block_number, - ) + pub fn get_state(&self) -> Arc { + self.wallets.clone() } pub async fn get_l1_batch_number( @@ -345,7 +278,7 @@ impl BitcoinInscriptionIndexer { } pub fn get_number_of_verifiers(&self) -> usize { - self.verifier_addresses.len() + self.wallets.verifiers.len() } pub async fn parse_transaction( @@ -353,49 +286,13 @@ impl BitcoinInscriptionIndexer { tx: &Txid, ) -> BitcoinIndexerResult> { let tx = self.client.get_transaction(tx).await?; - Ok(self.parser.parse_system_transaction(&tx, 0)) + Ok(self + .parser + .parse_system_transaction(&tx, 0, Some(&self.wallets))) } } impl BitcoinInscriptionIndexer { - pub fn create_indexer( - bootstrap_state: BootstrapState, - client: Arc, - parser: MessageParser, - ) -> BitcoinIndexerResult { - if bootstrap_state.is_complete() { - if let (Some(bridge), Some(sequencer), Some(governance)) = ( - bootstrap_state.bridge_address.clone(), - bootstrap_state.proposed_sequencer.clone(), - bootstrap_state.proposed_governance.clone(), - ) { - info!("BitcoinInscriptionIndexer successfully created"); - Ok(Self { - client, - parser, - bridge_address: bridge, - sequencer_address: sequencer, - governance_address: governance, - verifier_addresses: bootstrap_state.verifier_addresses, - starting_block_number: bootstrap_state.starting_block_number, - }) - } else { - error!("Incomplete bootstrap process despite state being marked as complete"); - error!("state: {:?}", bootstrap_state); - Err(types::IndexerError::IncompleteBootstrap( - "Incomplete bootstrap process despite state being marked as complete" - .to_string(), - )) - } - } else { - error!("Bootstrap process did not complete with provided transactions"); - error!("state: {:?}", bootstrap_state); - Err(types::IndexerError::IncompleteBootstrap( - "Bootstrap process did not complete with provided transactions".to_string(), - )) - } - } - #[instrument(skip(self, message), target = "bitcoin_indexer")] fn is_valid_system_message(&self, message: &FullInscriptionMessage) -> bool { match message { @@ -403,22 +300,22 @@ impl BitcoinInscriptionIndexer { .common .p2wpkh_address .as_ref() - .map_or(false, |addr| self.verifier_addresses.contains(addr)), + .map_or(false, |addr| self.wallets.verifiers.contains(addr)), FullInscriptionMessage::ValidatorAttestation(m) => m .common .p2wpkh_address .as_ref() - .map_or(false, |addr| self.verifier_addresses.contains(addr)), + .map_or(false, |addr| self.wallets.verifiers.contains(addr)), FullInscriptionMessage::L1BatchDAReference(m) => m .common .p2wpkh_address .as_ref() - .map_or(false, |addr| addr == &self.sequencer_address), + .map_or(false, |addr| addr == &self.wallets.sequencer), FullInscriptionMessage::ProofDAReference(m) => m .common .p2wpkh_address .as_ref() - .map_or(false, |addr| addr == &self.sequencer_address), + .map_or(false, |addr| addr == &self.wallets.sequencer), FullInscriptionMessage::SystemBootstrapping(_) => { debug!("SystemBootstrapping message is always valid"); true @@ -438,86 +335,17 @@ impl BitcoinInscriptionIndexer { } async fn is_valid_gov_message(&self, message: &FullInscriptionMessage) -> bool { - match message { - FullInscriptionMessage::SystemContractUpgrade(m) => { - self.is_valid_gov_upgrade(m).await.unwrap_or(false) - } - _ => false, - } - } + let maybe_input = match message { + FullInscriptionMessage::SystemContractUpgrade(m) => m.input.inputs.first(), + FullInscriptionMessage::UpdateBridge(m) => m.input.inputs.first(), + FullInscriptionMessage::UpdateSequencer(m) => m.input.inputs.first(), + FullInscriptionMessage::UpdateGovernance(m) => m.input.inputs.first(), + _ => return false, + }; - #[instrument(skip(state, message), target = "bitcoin_indexer")] - fn process_bootstrap_message( - state: &mut BootstrapState, - message: FullInscriptionMessage, - txid: Txid, - network: Network, - ) { - match message { - FullInscriptionMessage::SystemBootstrapping(sb) => { - debug!("Processing SystemBootstrapping message"); - // convert the verifier addresses to the correct network - // since the bootstrap message should run on the bootstrapping phase of sequencer - // i consume it's ok to using unwrap - let verifier_addresses = sb - .input - .verifier_p2wpkh_addresses - .iter() - .map(|addr| addr.clone().require_network(network).unwrap()) - .collect(); - - state.verifier_addresses = verifier_addresses; - - let bridge_address = sb - .input - .bridge_musig2_address - .require_network(network) - .unwrap(); - - let governance_address = sb - .input - .governance_address - .require_network(network) - .unwrap(); - - state.bridge_address = Some(bridge_address); - state.starting_block_number = sb.input.start_block_height; - state.bootloader_hash = Some(sb.input.bootloader_hash); - state.abstract_account_hash = Some(sb.input.abstract_account_hash); - state.proposed_governance = Some(governance_address); - } - FullInscriptionMessage::ProposeSequencer(ps) => { - debug!("Processing ProposeSequencer message"); - let sequencer_address = ps - .input - .sequencer_new_p2wpkh_address - .require_network(network) - .unwrap(); - state.proposed_sequencer = Some(sequencer_address); - state.proposed_sequencer_txid = Some(txid); - } - FullInscriptionMessage::ValidatorAttestation(va) => { - let p2wpkh_address = va - .common - .p2wpkh_address - .as_ref() - .expect("ValidatorAttestation message must have a p2wpkh address"); - if state.verifier_addresses.contains(p2wpkh_address) - && state.proposed_sequencer.is_some() - { - if let Some(proposed_txid) = state.proposed_sequencer_txid { - if va.input.reference_txid == proposed_txid { - state - .sequencer_votes - .insert(p2wpkh_address.clone(), va.input.attestation); - } - } - } - } - _ => { - debug!("Ignoring non-bootstrap message during bootstrap process"); - } - } + self.is_valid_gov_upgrade(maybe_input) + .await + .unwrap_or(false) } #[instrument(skip(self, message), target = "bitcoin_indexer")] @@ -525,13 +353,13 @@ impl BitcoinInscriptionIndexer { let is_valid_receiver = message .tx_outputs .iter() - .any(|output| output.script_pubkey == self.bridge_address.script_pubkey()); + .any(|output| output.script_pubkey == self.wallets.bridge.script_pubkey()); debug!("L1ToL2Message transfer validity: {}", is_valid_receiver); let total_bridge_amount = message .tx_outputs .iter() - .filter(|output| output.script_pubkey == self.bridge_address.script_pubkey()) + .filter(|output| output.script_pubkey == self.wallets.bridge.script_pubkey()) .map(|output| output.value) .sum::(); @@ -549,18 +377,18 @@ impl BitcoinInscriptionIndexer { if let Some(outpoint) = message.input.inputs.first() { let tx = self.client.get_transaction(&outpoint.txid).await?; if let Some(txout) = tx.output.get(outpoint.vout as usize) { - return Ok(txout.script_pubkey == self.bridge_address.script_pubkey()); + return Ok(txout.script_pubkey == self.wallets.bridge.script_pubkey()); } } Ok(false) } - #[instrument(skip(self, message), target = "bitcoin_indexer")] - async fn is_valid_gov_upgrade(&self, message: &SystemContractUpgrade) -> anyhow::Result { - if let Some(outpoint) = message.input.inputs.first() { + #[instrument(skip(self, outpoint_opt), target = "bitcoin_indexer")] + async fn is_valid_gov_upgrade(&self, outpoint_opt: Option<&OutPoint>) -> anyhow::Result { + if let Some(outpoint) = outpoint_opt { let tx = self.client.get_transaction(&outpoint.txid).await?; if let Some(txout) = tx.output.get(outpoint.vout as usize) { - return Ok(txout.script_pubkey == self.governance_address.script_pubkey()); + return Ok(txout.script_pubkey == self.wallets.governance.script_pubkey()); } } Ok(false) @@ -571,7 +399,9 @@ impl BitcoinInscriptionIndexer { txid: &Txid, ) -> anyhow::Result { let a = self.client.get_transaction(txid).await?; - let b = self.parser.parse_system_transaction(&a, 0); + let b = self + .parser + .parse_system_transaction(&a, 0, Some(&self.wallets)); let msg = b .first() .ok_or_else(|| anyhow::anyhow!("No message found"))?; @@ -587,7 +417,9 @@ impl BitcoinInscriptionIndexer { txid: &Txid, ) -> anyhow::Result { let a = self.client.get_transaction(txid).await?; - let b = self.parser.parse_system_transaction(&a, 0); + let b = self + .parser + .parse_system_transaction(&a, 0, Some(&self.wallets)); let msg = b .first() .ok_or_else(|| anyhow::anyhow!("No message found"))?; @@ -607,14 +439,15 @@ mod tests { use async_trait::async_trait; use bitcoin::{ - block::Header, hashes::Hash, Amount, Block, OutPoint, ScriptBuf, Transaction, TxMerkleNode, - TxOut, + block::Header, hashes::Hash, Address, Amount, Block, Network, OutPoint, ScriptBuf, + Transaction, TxMerkleNode, TxOut, }; use bitcoincore_rpc::json::GetBlockStatsResult; use mockall::{mock, predicate::*}; + use zksync_types::H256; use super::*; - use crate::types::{BitcoinClientResult, CommonFields}; + use crate::types::{self, BitcoinClientResult, CommonFields, Vote}; mock! { BitcoinOps {} @@ -659,19 +492,17 @@ mod tests { } fn get_indexer_with_mock(mock_client: MockBitcoinOps) -> BitcoinInscriptionIndexer { - let parser = MessageParser::new(Network::Testnet); - let bridge_address = get_test_addr(); - let sequencer_address = get_test_addr(); - let governance_address = get_test_addr(); + let wallets = Arc::new(SystemWallets { + bridge: get_test_addr(), + sequencer: get_test_addr(), + governance: get_test_addr(), + verifiers: vec![], + }); BitcoinInscriptionIndexer { client: Arc::new(mock_client), - parser, - bridge_address, - sequencer_address, - governance_address, - verifier_addresses: vec![], - starting_block_number: 0, + parser: MessageParser::new(Network::Testnet), + wallets, } } @@ -749,7 +580,8 @@ mod tests { common: get_test_common_fields(), input: types::ProposeSequencerInput { sequencer_new_p2wpkh_address: indexer - .sequencer_address + .wallets + .sequencer .clone() .as_unchecked() .to_owned(), @@ -799,7 +631,7 @@ mod tests { }, tx_outputs: vec![TxOut { value: Amount::from_sat(1000), - script_pubkey: indexer.bridge_address.script_pubkey(), + script_pubkey: indexer.wallets.bridge.script_pubkey(), }], }); assert!(indexer.is_valid_bridge_message(&l1_to_l2_message).await); @@ -809,12 +641,13 @@ mod tests { common: get_test_common_fields(), input: types::SystemBootstrappingInput { start_block_height: 0, - bridge_musig2_address: indexer.bridge_address.clone().as_unchecked().to_owned(), + bridge_musig2_address: indexer.wallets.bridge.clone().as_unchecked().to_owned(), verifier_p2wpkh_addresses: vec![], bootloader_hash: H256::zero(), abstract_account_hash: H256::zero(), governance_address: indexer - .governance_address + .wallets + .governance .clone() .as_unchecked() .to_owned(), @@ -837,7 +670,7 @@ mod tests { }, tx_outputs: vec![TxOut { value: Amount::from_sat(1000), - script_pubkey: indexer.bridge_address.script_pubkey(), + script_pubkey: indexer.wallets.bridge.script_pubkey(), }], }; assert!(indexer.is_valid_l1_to_l2_transfer(&valid_message)); diff --git a/core/lib/via_btc_client/src/indexer/parser.rs b/core/lib/via_btc_client/src/indexer/parser.rs index 27776d562..66c403fa3 100644 --- a/core/lib/via_btc_client/src/indexer/parser.rs +++ b/core/lib/via_btc_client/src/indexer/parser.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use bitcoin::{ address::NetworkUnchecked, hashes::Hash, @@ -8,7 +10,8 @@ use bitcoin::{ use tracing::{debug, instrument, warn}; use zksync_basic_types::H256; use zksync_types::{ - protocol_version::ProtocolSemanticVersion, Address as EVMAddress, L1BatchNumber, U256, + protocol_version::ProtocolSemanticVersion, via_wallet::SystemWallets, Address as EVMAddress, + L1BatchNumber, U256, }; use crate::types::{ @@ -17,11 +20,16 @@ use crate::types::{ ProofDAReference, ProofDAReferenceInput, ProposeSequencer, ProposeSequencerInput, SystemBootstrapping, SystemBootstrappingInput, SystemContractUpgrade, SystemContractUpgradeInput, SystemContractUpgradeProposal, SystemContractUpgradeProposalInput, - TransactionWithMetadata, ValidatorAttestation, ValidatorAttestationInput, Vote, + TransactionWithMetadata, UpdateBridge, UpdateBridgeInput, UpdateBridgeProposal, + UpdateBridgeProposalInput, UpdateGovernance, UpdateGovernanceInput, UpdateSequencer, + UpdateSequencerInput, ValidatorAttestation, ValidatorAttestationInput, Vote, }; const OP_RETURN_WITHDRAW_PREFIX: &[u8] = b"VIA_PROTOCOL:WITHDRAWAL"; const OP_RETURN_UPGRADE_PROTOCOL_PREFIX: &[u8] = b"VIA_PROTOCOL:UPGRADE"; +const OP_RETURN_UPDATE_SEQUENCER_PREFIX: &[u8] = b"VIA_PROTOCOL:SEQ"; +const OP_RETURN_UPDATE_BRIDGE_PREFIX: &[u8] = b"VIA_PROTOCOL:BRI"; +const OP_RETURN_UPDATE_GOVERNANCE_PREFIX: &[u8] = b"VIA_PROTOCOL:GOV"; // Using constants to define the minimum number of instructions can help to make parsing more quick const MIN_WITNESS_LENGTH: usize = 3; @@ -32,19 +40,16 @@ const MIN_L1_BATCH_DA_REFERENCE_INSTRUCTIONS: usize = 7; const MIN_PROOF_DA_REFERENCE_INSTRUCTIONS: usize = 5; const MIN_L1_TO_L2_MESSAGE_INSTRUCTIONS: usize = 5; const MIN_SYSTEM_CONTRACT_UPGRADE_PROPOSAL: usize = 6; +const MIN_UPDATE_BRIDGE_PROPOSAL: usize = 5; #[derive(Debug, Clone)] pub struct MessageParser { network: Network, - bridge_address: Option
, } impl MessageParser { pub fn new(network: Network) -> Self { - Self { - network, - bridge_address: None, - } + Self { network } } #[instrument(skip(self, tx), target = "bitcoin_indexer::parser")] @@ -52,6 +57,7 @@ impl MessageParser { &mut self, tx: &Transaction, block_height: u32, + wallets: Option<&SystemWallets>, ) -> Vec { // parsing btc address let mut sender_addresses: Option
= None; @@ -68,7 +74,7 @@ impl MessageParser { tx.input .iter() .filter_map(|input| { - self.parse_system_input(input, tx, block_height, address.clone()) + self.parse_system_input(input, tx, block_height, address.clone(), wallets) }) .collect() } @@ -79,18 +85,32 @@ impl MessageParser { } #[instrument(skip(self, tx), target = "bitcoin_indexer::parser")] - pub fn parse_protocol_upgrade_transaction( + pub fn parse_protocol_upgrade_transactions( &mut self, tx: &TransactionWithMetadata, block_height: u32, ) -> Vec { let mut messages = Vec::new(); - // Try to parse upgrade protocol. + if let Some(update_sequencer) = self.parse_op_return_update_governance(&tx.tx, block_height) + { + messages.push(update_sequencer); + } + if let Some(upgrade_protocol) = self.parse_op_return_protocol_upgrade(&tx.tx, block_height) { messages.push(upgrade_protocol); } + + if let Some(update_bridge) = self.parse_op_return_update_bridge(&tx.tx, block_height) { + messages.push(update_bridge); + } + + if let Some(update_sequencer) = self.parse_op_return_update_sequencer(&tx.tx, block_height) + { + messages.push(update_sequencer); + } + messages } @@ -99,16 +119,13 @@ impl MessageParser { &mut self, tx: &mut TransactionWithMetadata, block_height: u32, + wallets: &SystemWallets, ) -> Vec { let mut messages = Vec::new(); let vout = match tx.tx.output.iter().enumerate().find_map(|(index, output)| { - if let Some(bridge_addr) = &self.bridge_address { - if output.script_pubkey == bridge_addr.script_pubkey() { - Some(index) - } else { - None - } + if output.script_pubkey == wallets.bridge.script_pubkey() { + Some(index) } else { None } @@ -123,7 +140,8 @@ impl MessageParser { let bridge_output = &tx.tx.output[vout]; // Try to parse as inscription-based deposit first - if let Some(inscription_message) = self.parse_inscription_deposit(tx, block_height) { + if let Some(inscription_message) = self.parse_inscription_deposit(tx, block_height, wallets) + { messages.push(inscription_message); } @@ -135,7 +153,9 @@ impl MessageParser { } // Try to parse withdrawals processed by the bridge address. - if let Some(bridge_withdrawals) = self.parse_op_return_withdrawal(&tx.tx, block_height) { + if let Some(bridge_withdrawals) = + self.parse_op_return_withdrawal(&tx.tx, block_height, wallets) + { messages.push(bridge_withdrawals); } @@ -149,6 +169,7 @@ impl MessageParser { tx: &Transaction, block_height: u32, address: Address, + wallets: Option<&SystemWallets>, ) -> Option { let witness = &input.witness; if witness.len() < MIN_WITNESS_LENGTH { @@ -191,7 +212,7 @@ impl MessageParser { output_vout: None, }; - self.parse_system_message(tx, &instructions[via_index..], &common_fields) + self.parse_system_message(tx, &instructions[via_index..], &common_fields, wallets) } #[instrument(skip(self), target = "bitcoin_indexer::parser")] @@ -215,6 +236,7 @@ impl MessageParser { tx: &Transaction, instructions: &[Instruction], common_fields: &CommonFields, + wallets: Option<&SystemWallets>, ) -> Option { let message_type = instructions.get(1)?; @@ -251,7 +273,7 @@ impl MessageParser { } Instruction::PushBytes(bytes) if bytes.as_bytes() == types::L1_TO_L2_MSG.as_bytes() => { debug!("Parsing L1 to L2 message"); - self.parse_l1_to_l2_message(tx, instructions, common_fields) + self.parse_l1_to_l2_message(tx, instructions, common_fields, wallets) } Instruction::PushBytes(bytes) if bytes.as_bytes() == types::SYSTEM_CONTRACT_UPGRADE_MSG.as_bytes() => @@ -259,6 +281,12 @@ impl MessageParser { debug!("Parsing System contract upgrade proposal"); self.parse_system_contract_upgrade_message(instructions, common_fields) } + Instruction::PushBytes(bytes) + if bytes.as_bytes() == types::UPGRADE_BRIDGE_MSG.as_bytes() => + { + debug!("Parsing update bridge proposal"); + self.parse_update_bridge_proposal_message(instructions, common_fields) + } Instruction::PushBytes(bytes) => { warn!("Unknown message type for system transaction parser"); warn!( @@ -330,14 +358,6 @@ impl MessageParser { } })?; - // Save the bridge address for later use - self.bridge_address = Some( - network_unchecked_bridge_address - .clone() - .require_network(self.network) - .ok()?, - ); - debug!("Parsed bridge address"); let bootloader_hash = H256::from_slice( @@ -591,7 +611,10 @@ impl MessageParser { tx: &Transaction, instructions: &[Instruction], common_fields: &CommonFields, + wallets_opt: Option<&SystemWallets>, ) -> Option { + let wallets = wallets_opt?; + if instructions.len() < MIN_L1_TO_L2_MESSAGE_INSTRUCTIONS { warn!("Insufficient instructions for L1 to L2 message"); return None; @@ -612,13 +635,8 @@ impl MessageParser { .output .iter() .find(|output| { - if let Some(address) = self.bridge_address.as_ref() { - output.script_pubkey.is_p2tr() - && output.script_pubkey == address.script_pubkey() - } else { - tracing::error!("Bridge address not found"); - false - } + output.script_pubkey.is_p2tr() + && output.script_pubkey == wallets.bridge.script_pubkey() }) .map(|output| output.value) .unwrap_or(Amount::ZERO); @@ -690,10 +708,66 @@ impl MessageParser { )) } + #[instrument( + skip(self, instructions, common_fields), + target = "bitcoin_indexer::parser" + )] + fn parse_update_bridge_proposal_message( + &self, + instructions: &[Instruction], + common_fields: &CommonFields, + ) -> Option { + if instructions.len() < MIN_UPDATE_BRIDGE_PROPOSAL { + return None; + } + + // network unchecked is required to enable serde serialization and deserialization on the library structs + let verifier_p2wpkh_addresses = instructions[1..instructions.len() - 2] + .iter() + .filter_map(|instr| { + if let Instruction::PushBytes(bytes) = instr { + std::str::from_utf8(bytes.as_bytes()) + .ok() + .and_then(|s| s.parse::>().ok()) + } else { + None + } + }) + .collect::>(); + + debug!( + "Parsed {} verifier addresses", + verifier_p2wpkh_addresses.len() + ); + + let bridge_musig2_address = instructions.get(instructions.len() - 2).and_then(|instr| { + if let Instruction::PushBytes(bytes) = instr { + std::str::from_utf8(bytes.as_bytes()) + .ok() + .and_then(|s| s.parse::>().ok()) + } else { + None + } + })?; + + debug!("Parsed bridge address"); + + Some(FullInscriptionMessage::UpdateBridgeProposal( + UpdateBridgeProposal { + common: common_fields.clone(), + input: UpdateBridgeProposalInput { + bridge_musig2_address, + verifier_p2wpkh_addresses, + }, + }, + )) + } + fn parse_inscription_deposit( &self, tx: &TransactionWithMetadata, block_height: u32, + wallets: &SystemWallets, ) -> Option { // Try to find any witness data that contains a valid inscription for input in tx.tx.input.iter() { @@ -724,7 +798,12 @@ impl MessageParser { }; // Parse L1ToL2Message from instructions - return self.parse_l1_to_l2_message(&tx.tx, &instructions[via_index..], &common_fields); + return self.parse_l1_to_l2_message( + &tx.tx, + &instructions[via_index..], + &common_fields, + Some(wallets), + ); } None @@ -751,7 +830,12 @@ impl MessageParser { // Parse OP_RETURN data if let Some(op_return_data) = op_return_output.script_pubkey.as_bytes().get(2..) { - if op_return_data.starts_with(OP_RETURN_WITHDRAW_PREFIX) { + if op_return_data.starts_with(OP_RETURN_WITHDRAW_PREFIX) + || op_return_data.starts_with(OP_RETURN_UPGRADE_PROTOCOL_PREFIX) + || op_return_data.starts_with(OP_RETURN_UPDATE_SEQUENCER_PREFIX) + || op_return_data.starts_with(OP_RETURN_UPDATE_BRIDGE_PREFIX) + || op_return_data.starts_with(OP_RETURN_UPDATE_GOVERNANCE_PREFIX) + { return None; } // Parse receiver address from OP_RETURN data @@ -796,6 +880,7 @@ impl MessageParser { &self, tx: &Transaction, block_height: u32, + wallets: &SystemWallets, ) -> Option { // Find OP_RETURN output let op_return_output = tx @@ -836,12 +921,8 @@ impl MessageParser { Err(_) => continue, }; - if let Some(bridge_address) = self.bridge_address.clone() { - if address == bridge_address { - continue; - } - } else { - return None; + if address == wallets.bridge { + continue; } withdrawals.push((address.to_string(), output.value.to_sat() as i64)); @@ -929,6 +1010,158 @@ impl MessageParser { } None } + + fn parse_op_return_update_bridge( + &self, + tx: &Transaction, + block_height: u32, + ) -> Option { + // Find OP_RETURN output + let op_return_output = tx + .output + .iter() + .find(|output| output.script_pubkey.is_op_return())?; + + // Parse OP_RETURN data + if let Some(op_return_data) = op_return_output.script_pubkey.as_bytes().get(2..) { + if !op_return_data.starts_with(OP_RETURN_UPDATE_BRIDGE_PREFIX) { + return None; + } + + let start = OP_RETURN_UPDATE_BRIDGE_PREFIX.len() + 1; + if op_return_data.len() < start + 32 { + return None; + } + + // Parse proposal_tx_id from OP_RETURN data + let proposal_tx_id = match Txid::from_slice(&op_return_data[start..start + 32]) { + Ok(tx_id) => tx_id, + Err(_) => return None, + }; + + let input = UpdateBridgeInput { + inputs: tx.input.iter().map(|input| input.previous_output).collect(), + proposal_tx_id, + }; + + // Create common fields with empty signature for OP_RETURN + let common = CommonFields { + schnorr_signature: TaprootSignature::from_slice(&[0; 64]).ok()?, + encoded_public_key: PushBytesBuf::new(), + block_height, + tx_id: tx.compute_ntxid().into(), + p2wpkh_address: None, + tx_index: None, + output_vout: None, + }; + + return Some(FullInscriptionMessage::UpdateBridge(UpdateBridge { + common, + input, + })); + } + None + } + + fn parse_op_return_update_sequencer( + &self, + tx: &Transaction, + block_height: u32, + ) -> Option { + // Find OP_RETURN output + let op_return_output = tx + .output + .iter() + .find(|output| output.script_pubkey.is_op_return())?; + + // Parse OP_RETURN data + if let Some(op_return_data) = op_return_output.script_pubkey.as_bytes().get(2..) { + if !op_return_data.starts_with(OP_RETURN_UPDATE_SEQUENCER_PREFIX) { + return None; + } + + let start = OP_RETURN_UPDATE_SEQUENCER_PREFIX.len() + 1; + + // Parse sequencer address from OP_RETURN data + let address_str = std::str::from_utf8(&op_return_data[start..]).ok()?; + let address = match Address::from_str(address_str) { + Ok(address) => address, + Err(_) => return None, + }; + + let input = UpdateSequencerInput { + inputs: tx.input.iter().map(|input| input.previous_output).collect(), + address, + }; + + // Create common fields with empty signature for OP_RETURN + let common = CommonFields { + schnorr_signature: TaprootSignature::from_slice(&[0; 64]).ok()?, + encoded_public_key: PushBytesBuf::new(), + block_height, + tx_id: tx.compute_ntxid().into(), + p2wpkh_address: None, + tx_index: None, + output_vout: None, + }; + + return Some(FullInscriptionMessage::UpdateSequencer(UpdateSequencer { + common, + input, + })); + } + None + } + + fn parse_op_return_update_governance( + &self, + tx: &Transaction, + block_height: u32, + ) -> Option { + // Find OP_RETURN output + let op_return_output = tx + .output + .iter() + .find(|output| output.script_pubkey.is_op_return())?; + + // Parse OP_RETURN data + if let Some(op_return_data) = op_return_output.script_pubkey.as_bytes().get(2..) { + if !op_return_data.starts_with(OP_RETURN_UPDATE_GOVERNANCE_PREFIX) { + return None; + } + + let start = OP_RETURN_UPDATE_GOVERNANCE_PREFIX.len() + 1; + + // Parse sequencer address from OP_RETURN data + let address_str = std::str::from_utf8(&op_return_data[start..]).ok()?; + let address = match Address::from_str(address_str) { + Ok(address) => address, + Err(_) => return None, + }; + + let input = UpdateGovernanceInput { + inputs: tx.input.iter().map(|input| input.previous_output).collect(), + address, + }; + + // Create common fields with empty signature for OP_RETURN + let common = CommonFields { + schnorr_signature: TaprootSignature::from_slice(&[0; 64]).ok()?, + encoded_public_key: PushBytesBuf::new(), + block_height, + tx_id: tx.compute_ntxid().into(), + p2wpkh_address: None, + tx_index: None, + output_vout: None, + }; + + return Some(FullInscriptionMessage::UpdateGovernance(UpdateGovernance { + common, + input, + })); + } + None + } } #[instrument(skip(instructions), target = "bitcoin_indexer::parser")] @@ -972,6 +1205,25 @@ mod tests { deserialize(&Vec::from_hex(tx_hex).unwrap()).unwrap() } + fn system_wallets() -> SystemWallets { + SystemWallets { + sequencer: Address::from_str("bcrt1qw2mvkvm6alfhe86yf328kgvr7mupdx4vln7kpv") + .unwrap() + .assume_checked(), + bridge: Address::from_str( + "bcrt1pcx974cg2w66cqhx67zadf85t8k4sd2wp68l8x8agd3aj4tuegsgsz97amg", + ) + .unwrap() + .assume_checked(), + governance: Address::from_str( + "bcrt1q92gkfme6k9dkpagrkwt76etkaq29hvf02w5m38f6shs4ddpw7hzqp347zm", + ) + .unwrap() + .assume_checked(), + verifiers: vec![], + } + } + #[ignore] #[test] fn test_parse_transaction() { @@ -979,7 +1231,7 @@ mod tests { let mut parser = MessageParser::new(network); let tx = setup_test_transaction(); - let messages = parser.parse_system_transaction(&tx, 0); + let messages = parser.parse_system_transaction(&tx, 0, Some(&system_wallets())); assert_eq!(messages.len(), 1); } @@ -990,8 +1242,9 @@ mod tests { let mut parser = MessageParser::new(network); let tx = setup_test_transaction(); - if let Some(FullInscriptionMessage::SystemBootstrapping(bootstrapping)) = - parser.parse_system_transaction(&tx, 0).pop() + if let Some(FullInscriptionMessage::SystemBootstrapping(bootstrapping)) = parser + .parse_system_transaction(&tx, 0, Some(&system_wallets())) + .pop() { assert_eq!(bootstrapping.input.start_block_height, 10); assert_eq!(bootstrapping.input.verifier_p2wpkh_addresses.len(), 1); diff --git a/core/lib/via_btc_client/src/inscriber/mod.rs b/core/lib/via_btc_client/src/inscriber/mod.rs index 4bf7d5123..20412daa7 100644 --- a/core/lib/via_btc_client/src/inscriber/mod.rs +++ b/core/lib/via_btc_client/src/inscriber/mod.rs @@ -814,6 +814,11 @@ impl Inscriber { pub async fn get_client(&self) -> &dyn BitcoinOps { &*self.client } + + #[instrument(skip(self), target = "bitcoin_inscriber")] + pub fn inscriber_address(&self) -> anyhow::Result
{ + Ok(self.signer.get_p2wpkh_address()?) + } } #[cfg(test)] diff --git a/core/lib/via_btc_client/src/inscriber/script_builder.rs b/core/lib/via_btc_client/src/inscriber/script_builder.rs index c7cf32d68..25fc23224 100644 --- a/core/lib/via_btc_client/src/inscriber/script_builder.rs +++ b/core/lib/via_btc_client/src/inscriber/script_builder.rs @@ -140,6 +140,9 @@ impl InscriptionData { types::InscriptionMessage::SystemContractUpgradeProposal(input) => { Self::build_system_contract_upgrade_message_script(basic_script, input) } + types::InscriptionMessage::UpdateBridgeProposal(input) => { + Self::build_update_bridge_script(basic_script, input, network)? + } }; let final_script = final_script_result.push_opcode(all::OP_ENDIF).into_script(); @@ -340,6 +343,36 @@ impl InscriptionData { basic_script } + #[instrument( + skip(basic_script, input), + target = "bitcoin_inscriber::script_builder" + )] + fn build_update_bridge_script( + basic_script: ScriptBuilder, + input: &types::UpdateBridgeProposalInput, + network: Network, + ) -> anyhow::Result { + debug!("Building Bridge address script"); + + let mut script = basic_script.push_slice(&*types::UPGRADE_BRIDGE_MSG); + + for verifier_p2wpkh_address in &input.verifier_p2wpkh_addresses { + let network_checked_address = + verifier_p2wpkh_address.clone().require_network(network)?; + let address_encoded = + Self::encode_push_bytes(network_checked_address.to_string().as_bytes()); + script = script.push_slice(address_encoded); + } + + let bridge_address = input + .bridge_musig2_address + .clone() + .require_network(network)?; + let bridge_address_encoded = Self::encode_push_bytes(bridge_address.to_string().as_bytes()); + + Ok(script.push_slice(bridge_address_encoded)) + } + #[instrument(skip(data), target = "bitcoin_inscriber::script_builder")] fn encode_push_bytes(data: &[u8]) -> PushBytesBuf { let mut encoded = PushBytesBuf::with_capacity(data.len()); diff --git a/core/lib/via_btc_client/src/lib.rs b/core/lib/via_btc_client/src/lib.rs index e43fa5bb8..195b85472 100644 --- a/core/lib/via_btc_client/src/lib.rs +++ b/core/lib/via_btc_client/src/lib.rs @@ -1,6 +1,7 @@ pub mod traits; pub mod types; +pub mod bootstrap; pub mod client; pub mod indexer; pub mod inscriber; diff --git a/core/lib/via_btc_client/src/types.rs b/core/lib/via_btc_client/src/types.rs index 17cb20a8b..463c60264 100644 --- a/core/lib/via_btc_client/src/types.rs +++ b/core/lib/via_btc_client/src/types.rs @@ -89,6 +89,57 @@ pub struct SystemBootstrappingInput { pub governance_address: BitcoinAddress, } +#[derive(Clone, Debug, PartialEq)] +pub struct UpdateSequencer { + pub common: CommonFields, + pub input: UpdateSequencerInput, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct UpdateSequencerInput { + /// The input utxos. + pub inputs: Vec, + pub address: BitcoinAddress, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct UpdateGovernance { + pub common: CommonFields, + pub input: UpdateGovernanceInput, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct UpdateGovernanceInput { + /// The input utxos. + pub inputs: Vec, + pub address: BitcoinAddress, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct UpdateBridge { + pub common: CommonFields, + pub input: UpdateBridgeInput, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct UpdateBridgeInput { + /// The input utxos. + pub inputs: Vec, + pub proposal_tx_id: Txid, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct UpdateBridgeProposal { + pub common: CommonFields, + pub input: UpdateBridgeProposalInput, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct UpdateBridgeProposalInput { + pub bridge_musig2_address: BitcoinAddress, + pub verifier_p2wpkh_addresses: Vec>, +} + #[derive(Clone, Debug, PartialEq)] pub struct SystemBootstrapping { pub common: CommonFields, @@ -188,6 +239,7 @@ pub enum InscriptionMessage { ProposeSequencer(ProposeSequencerInput), L1ToL2Message(L1ToL2MessageInput), SystemContractUpgradeProposal(SystemContractUpgradeProposalInput), + UpdateBridgeProposal(UpdateBridgeProposalInput), } impl Serializable for InscriptionMessage { @@ -224,8 +276,40 @@ pub enum FullInscriptionMessage { ProposeSequencer(ProposeSequencer), L1ToL2Message(L1ToL2Message), SystemContractUpgradeProposal(SystemContractUpgradeProposal), - SystemContractUpgrade(SystemContractUpgrade), BridgeWithdrawal(BridgeWithdrawal), + UpdateBridgeProposal(UpdateBridgeProposal), + UpdateGovernance(UpdateGovernance), + UpdateSequencer(UpdateSequencer), + SystemContractUpgrade(SystemContractUpgrade), + UpdateBridge(UpdateBridge), +} + +impl FullInscriptionMessage { + /// Return a numeric sort key that reflects the enum declaration order + fn order_key(&self) -> usize { + match self { + FullInscriptionMessage::L1BatchDAReference(_) => 0, + FullInscriptionMessage::ProofDAReference(_) => 1, + FullInscriptionMessage::ValidatorAttestation(_) => 2, + FullInscriptionMessage::SystemBootstrapping(_) => 3, + FullInscriptionMessage::ProposeSequencer(_) => 4, + FullInscriptionMessage::L1ToL2Message(_) => 5, + FullInscriptionMessage::SystemContractUpgradeProposal(_) => 6, + FullInscriptionMessage::BridgeWithdrawal(_) => 7, + FullInscriptionMessage::UpdateBridgeProposal(_) => 8, + + // System inscriptions must be ordered to ensure the indexer always uses the latest wallet state. + FullInscriptionMessage::UpdateGovernance(_) => 9, + FullInscriptionMessage::UpdateSequencer(_) => 10, + FullInscriptionMessage::SystemContractUpgrade(_) => 11, + FullInscriptionMessage::UpdateBridge(_) => 12, + } + } + + pub fn sort_messages(mut msgs: Vec) -> Vec { + msgs.sort_by_key(|msg| msg.order_key()); + msgs + } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -254,6 +338,7 @@ lazy_static! { pub static ref L1_TO_L2_MSG: PushBytesBuf = PushBytesBuf::from(b"L1ToL2Message"); pub static ref SYSTEM_CONTRACT_UPGRADE_MSG: PushBytesBuf = PushBytesBuf::from(b"SystemContractUpgradeProposal"); + pub static ref UPGRADE_BRIDGE_MSG: PushBytesBuf = PushBytesBuf::from(b"UpgradeBridgeProposal"); } pub(crate) const VIA_INSCRIPTION_PROTOCOL: &str = "via_inscription_protocol"; @@ -416,3 +501,10 @@ pub type BitcoinSignerResult = Result; pub type BitcoinInscriberResult = Result; pub type BitcoinTransactionBuilderResult = Result; + +#[derive(Debug, Clone)] +pub struct SystemTransactions { + pub system_txs: Vec, + pub bridge_txs: Vec, + pub governance_txs: Vec, +} diff --git a/core/node/node_framework/src/implementations/layers/via_btc_watch.rs b/core/node/node_framework/src/implementations/layers/via_btc_watch.rs index 54a91d472..8d1daeab8 100644 --- a/core/node/node_framework/src/implementations/layers/via_btc_watch.rs +++ b/core/node/node_framework/src/implementations/layers/via_btc_watch.rs @@ -10,6 +10,7 @@ use crate::{ pools::{MasterPool, PoolResource}, via_btc_client::BtcClientResource, via_btc_indexer::BtcIndexerResource, + via_system_wallet::ViaSystemWalletsResource, }, service::StopReceiver, task::{Task, TaskId}, @@ -32,6 +33,7 @@ pub struct BtcWatchLayer { pub struct Input { pub master_pool: PoolResource, pub btc_client_resource: BtcClientResource, + pub system_wallets_resource: ViaSystemWalletsResource, } #[derive(Debug, IntoContext)] @@ -68,13 +70,9 @@ impl WiringLayer for BtcWatchLayer { async fn wire(self, input: Self::Input) -> Result { let main_pool = input.master_pool.get().await?; let client = input.btc_client_resource.btc_sender.unwrap(); - let indexer = BitcoinInscriptionIndexer::new( - client.clone(), - self.via_btc_client.clone(), - self.via_genesis_config.bootstrap_txids()?, - ) - .await - .map_err(|e| WiringError::Internal(e.into()))?; + let system_wallets = input.system_wallets_resource.0; + let indexer = BitcoinInscriptionIndexer::new(client.clone(), system_wallets); + let btc_indexer_resource = BtcIndexerResource::from(indexer.clone()); // We should not set block_confirmations to 0 for mainnet, // because we need to wait for some confirmations to be sure that the transaction is included in a block. diff --git a/core/node/node_framework/src/implementations/layers/via_consistency_checker.rs b/core/node/node_framework/src/implementations/layers/via_consistency_checker.rs index b633407b6..757f4203d 100644 --- a/core/node/node_framework/src/implementations/layers/via_consistency_checker.rs +++ b/core/node/node_framework/src/implementations/layers/via_consistency_checker.rs @@ -5,7 +5,7 @@ use crate::{ healthcheck::AppHealthCheckResource, pools::{MasterPool, PoolResource}, via_btc_client::BtcClientResource, - via_btc_indexer::BtcIndexerResource, + via_system_wallet::ViaSystemWalletsResource, }, service::StopReceiver, task::{Task, TaskId}, @@ -22,7 +22,7 @@ pub struct ViaConsistencyCheckerLayer { #[derive(Debug, FromContext)] #[context(crate = crate)] pub struct Input { - pub btc_indexer_resource: BtcIndexerResource, + pub system_wallets_resource: ViaSystemWalletsResource, pub btc_client_resource: BtcClientResource, pub master_pool: PoolResource, #[context(default)] @@ -60,7 +60,7 @@ impl WiringLayer for ViaConsistencyCheckerLayer { let singleton_pool = input.master_pool.get_singleton().await?; let consistency_checker = ConsistencyChecker::new( - input.btc_indexer_resource.0.get_state().1, + input.system_wallets_resource.0.sequencer.clone(), "celestia".into(), btc_client, self.max_batches_to_recheck, diff --git a/core/node/node_framework/src/implementations/layers/via_indexer.rs b/core/node/node_framework/src/implementations/layers/via_indexer.rs index c6604835d..0ea533812 100644 --- a/core/node/node_framework/src/implementations/layers/via_indexer.rs +++ b/core/node/node_framework/src/implementations/layers/via_indexer.rs @@ -1,24 +1,22 @@ use via_btc_client::indexer::BitcoinInscriptionIndexer; -use zksync_config::configs::{via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig}; use crate::{ implementations::resources::{ via_btc_client::BtcClientResource, via_btc_indexer::BtcIndexerResource, + via_system_wallet::ViaSystemWalletsResource, }, wiring_layer::{WiringError, WiringLayer}, FromContext, IntoContext, }; #[derive(Debug)] -pub struct ViaIndexerLayer { - via_genesis_config: ViaGenesisConfig, - via_btc_client: ViaBtcClientConfig, -} +pub struct ViaIndexerLayer {} #[derive(Debug, FromContext)] #[context(crate = crate)] pub struct Input { pub btc_client_resource: BtcClientResource, + pub system_wallets_resource: ViaSystemWalletsResource, } #[derive(Debug, IntoContext)] @@ -28,11 +26,8 @@ pub struct Output { } impl ViaIndexerLayer { - pub fn new(via_genesis_config: ViaGenesisConfig, via_btc_client: ViaBtcClientConfig) -> Self { - Self { - via_genesis_config, - via_btc_client, - } + pub fn new() -> Self { + Self {} } } @@ -47,13 +42,9 @@ impl WiringLayer for ViaIndexerLayer { async fn wire(self, input: Self::Input) -> Result { let client = input.btc_client_resource.default; - let indexer = BitcoinInscriptionIndexer::new( - client, - self.via_btc_client.clone(), - self.via_genesis_config.bootstrap_txids()?, - ) - .await - .map_err(|e| WiringError::Internal(e.into()))?; + let system_wallets = input.system_wallets_resource.0; + let indexer = BitcoinInscriptionIndexer::new(client, system_wallets); + let btc_indexer_resource = BtcIndexerResource::from(indexer.clone()); Ok(Output { diff --git a/core/node/node_framework/src/implementations/layers/via_l1_indexer.rs b/core/node/node_framework/src/implementations/layers/via_l1_indexer.rs index 18082c000..22eb30723 100644 --- a/core/node/node_framework/src/implementations/layers/via_l1_indexer.rs +++ b/core/node/node_framework/src/implementations/layers/via_l1_indexer.rs @@ -10,6 +10,7 @@ use crate::{ pools::{IndexerPool, PoolResource}, via_btc_client::BtcClientResource, via_btc_indexer::BtcIndexerResource, + via_system_wallet::ViaSystemWalletsResource, }, service::StopReceiver, task::{Task, TaskId}, @@ -29,6 +30,7 @@ pub struct L1IndexerLayer { pub struct Input { pub master_pool: PoolResource, pub btc_client_resource: BtcClientResource, + pub system_wallets_resource: ViaSystemWalletsResource, } #[derive(Debug, IntoContext)] @@ -65,13 +67,8 @@ impl WiringLayer for L1IndexerLayer { async fn wire(self, input: Self::Input) -> Result { let main_pool = input.master_pool.get().await?; let client = input.btc_client_resource.bridge.unwrap(); - let indexer = BitcoinInscriptionIndexer::new( - client.clone(), - self.via_btc_client.clone(), - self.via_genesis_config.bootstrap_txids()?, - ) - .await - .map_err(|e| WiringError::Internal(e.into()))?; + let system_wallets = input.system_wallets_resource.0; + let indexer = BitcoinInscriptionIndexer::new(client.clone(), system_wallets); let btc_indexer_resource = BtcIndexerResource::from(indexer.clone()); let l1_indexer = L1Indexer::new( diff --git a/core/node/node_framework/src/implementations/layers/via_verifier_btc_watch.rs b/core/node/node_framework/src/implementations/layers/via_verifier_btc_watch.rs index 3c470d2f6..46142542e 100644 --- a/core/node/node_framework/src/implementations/layers/via_verifier_btc_watch.rs +++ b/core/node/node_framework/src/implementations/layers/via_verifier_btc_watch.rs @@ -11,6 +11,7 @@ use crate::{ pools::{PoolResource, VerifierPool}, via_btc_client::BtcClientResource, via_btc_indexer::BtcIndexerResource, + via_system_wallet::ViaSystemWalletsResource, }, service::StopReceiver, task::{Task, TaskId}, @@ -33,6 +34,7 @@ pub struct VerifierBtcWatchLayer { pub struct Input { pub master_pool: PoolResource, pub btc_client_resource: BtcClientResource, + pub system_wallets_resource: ViaSystemWalletsResource, } #[derive(Debug, IntoContext)] @@ -69,13 +71,8 @@ impl WiringLayer for VerifierBtcWatchLayer { async fn wire(self, input: Self::Input) -> Result { let main_pool = input.master_pool.get().await?; let client = input.btc_client_resource.btc_sender.unwrap(); - let indexer = BitcoinInscriptionIndexer::new( - client.clone(), - self.via_btc_client.clone(), - self.via_genesis_config.bootstrap_txids()?, - ) - .await - .map_err(|e| WiringError::Internal(e.into()))?; + let system_wallets = input.system_wallets_resource.0; + let indexer = BitcoinInscriptionIndexer::new(client.clone(), system_wallets); let btc_indexer_resource = BtcIndexerResource::from(indexer.clone()); diff --git a/core/node/node_framework/src/implementations/layers/via_verifier_storage_init.rs b/core/node/node_framework/src/implementations/layers/via_verifier_storage_init.rs index d6e7f5b6a..588ba468e 100644 --- a/core/node/node_framework/src/implementations/layers/via_verifier_storage_init.rs +++ b/core/node/node_framework/src/implementations/layers/via_verifier_storage_init.rs @@ -1,8 +1,14 @@ +use std::sync::Arc; + use via_verifier_storage_init::ViaVerifierStorageInitializer; -use zksync_config::GenesisConfig; +use zksync_config::{configs::via_consensus::ViaGenesisConfig, GenesisConfig}; use crate::{ - implementations::resources::pools::{PoolResource, VerifierPool}, + implementations::resources::{ + pools::{PoolResource, VerifierPool}, + via_btc_client::BtcClientResource, + via_system_wallet::ViaSystemWalletsResource, + }, task::TaskKind, wiring_layer::{WiringError, WiringLayer}, FromContext, IntoContext, StopReceiver, Task, TaskId, @@ -11,6 +17,7 @@ use crate::{ /// Wiring layer for via verifier initialization. #[derive(Debug)] pub struct ViaVerifierInitLayer { + pub via_genesis_config: ViaGenesisConfig, pub genesis: GenesisConfig, } @@ -18,6 +25,7 @@ pub struct ViaVerifierInitLayer { #[context(crate = crate)] pub struct Input { pub master_pool: PoolResource, + pub btc_client_resource: BtcClientResource, } #[derive(Debug, IntoContext)] @@ -27,6 +35,7 @@ pub struct Output { pub initializer: ViaVerifierStorageInitializer, #[context(task)] pub precondition: ViaVerifierStorageInitializerPrecondition, + pub system_wallets_resource: ViaSystemWalletsResource, } #[async_trait::async_trait] @@ -40,12 +49,21 @@ impl WiringLayer for ViaVerifierInitLayer { async fn wire(self, input: Self::Input) -> Result { let pool = input.master_pool.get().await?; - let initializer = ViaVerifierStorageInitializer::new(self.genesis, pool); + let client = input.btc_client_resource.verifier.unwrap(); + + let initializer = + ViaVerifierStorageInitializer::new(self.via_genesis_config, self.genesis, pool, client); + + let system_wallets = + ViaSystemWalletsResource(Arc::new(initializer.indexer_wallets().await.unwrap())); + + let system_wallets_resource = ViaSystemWalletsResource::from(system_wallets); let precondition = ViaVerifierStorageInitializerPrecondition(initializer.clone()); Ok(Output { initializer, precondition, + system_wallets_resource, }) } } diff --git a/core/node/node_framework/src/implementations/resources/mod.rs b/core/node/node_framework/src/implementations/resources/mod.rs index d49f6bed6..a4d629105 100644 --- a/core/node/node_framework/src/implementations/resources/mod.rs +++ b/core/node/node_framework/src/implementations/resources/mod.rs @@ -18,4 +18,5 @@ pub mod via_btc_client; pub mod via_btc_indexer; pub mod via_gas_adjuster; pub mod via_state_keeper; +pub mod via_system_wallet; pub mod web3_api; diff --git a/core/node/via_btc_watch/src/message_processors/governance_upgrade.rs b/core/node/via_btc_watch/src/message_processors/governance_upgrade.rs index 656c3a5bf..9c6617204 100644 --- a/core/node/via_btc_watch/src/message_processors/governance_upgrade.rs +++ b/core/node/via_btc_watch/src/message_processors/governance_upgrade.rs @@ -72,6 +72,7 @@ impl MessageProcessor for GovernanceUpgradesEventProcessor { let messages = self.message_parser.parse_system_transaction( &proposal_tx, system_contract_upgrade_msg.common.block_height, + None, ); for message in messages { diff --git a/core/node/via_consistency_checker/src/lib.rs b/core/node/via_consistency_checker/src/lib.rs index 1fd63a228..e68f23bd7 100644 --- a/core/node/via_consistency_checker/src/lib.rs +++ b/core/node/via_consistency_checker/src/lib.rs @@ -214,7 +214,7 @@ impl ConsistencyChecker { .map_err(CheckError::Internal)?; let mut parser = MessageParser::new(self.btc_client.config.network()); - let inscriptions = parser.parse_system_transaction(&tx, 0); + let inscriptions = parser.parse_system_transaction(&tx, 0, None); for inscription in inscriptions { if let FullInscriptionMessage::L1BatchDAReference(msg) = inscription { diff --git a/via_verifier/lib/via_musig2/examples/coordinator.rs b/via_verifier/lib/via_musig2/examples/coordinator.rs index 8f28494ae..e18dd63df 100644 --- a/via_verifier/lib/via_musig2/examples/coordinator.rs +++ b/via_verifier/lib/via_musig2/examples/coordinator.rs @@ -86,7 +86,7 @@ async fn main() -> anyhow::Result<()> { let other_pubkey_2 = PublicKey::from_secret_key(&secp, &SecretKey::new(&mut rng)); let all_pubkeys = vec![public_key, other_pubkey_1, other_pubkey_2]; - let coordinator_signer = Signer::new(secret_key, 0, all_pubkeys.clone())?; + let coordinator_signer = Signer::new(secret_key, 0, all_pubkeys.clone(), None)?; // Create test bridge address let bridge_address = @@ -118,11 +118,11 @@ async fn main() -> anyhow::Result<()> { // Each verifier has their own keys: let mut rng = thread_rng(); let verifier1_sk = SecretKey::new(&mut rng); - let verifier1_signer = Signer::new(verifier1_sk, 1, all_pubkeys.clone())?; + let verifier1_signer = Signer::new(verifier1_sk, 1, all_pubkeys.clone(), None)?; let mut rng = thread_rng(); let verifier2_sk = SecretKey::new(&mut rng); - let verifier2_signer = Signer::new(verifier2_sk, 2, all_pubkeys)?; + let verifier2_signer = Signer::new(verifier2_sk, 2, all_pubkeys, None)?; // Spawn tasks for verifier polling let verifier1_task = tokio::spawn(run_verifier_polling( diff --git a/via_verifier/lib/via_musig2/examples/verify_partial_sig.rs b/via_verifier/lib/via_musig2/examples/verify_partial_sig.rs index e3e7623ad..45b581b2e 100644 --- a/via_verifier/lib/via_musig2/examples/verify_partial_sig.rs +++ b/via_verifier/lib/via_musig2/examples/verify_partial_sig.rs @@ -231,6 +231,7 @@ async fn main() -> Result<(), Box> { pubkeys_str, PartialSignature::from_slice(&partial_sig_2.to_vec())?, sighash.as_byte_array(), + None, )?; let partial_sig_3: [u8; 32] = second_round_3.our_signature(); diff --git a/via_verifier/node/via_btc_watch/src/message_processors/governance_upgrade.rs b/via_verifier/node/via_btc_watch/src/message_processors/governance_upgrade.rs index bb33dd071..9214cc4a8 100644 --- a/via_verifier/node/via_btc_watch/src/message_processors/governance_upgrade.rs +++ b/via_verifier/node/via_btc_watch/src/message_processors/governance_upgrade.rs @@ -63,6 +63,7 @@ impl MessageProcessor for GovernanceUpgradesEventProcessor { let messages = self.message_parser.parse_system_transaction( &proposal_tx, system_contract_upgrade_msg.common.block_height, + None, ); for message in messages { From 2db90435a843e17847efad0c0b1639582f609c6e Mon Sep 17 00:00:00 2001 From: 0xatomFusion <179967211+0xatomFusion@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:06:35 +0100 Subject: [PATCH 2/8] feat: add system wallet loading to the init layer for sequencer and verifier --- core/bin/via_server/src/node_builder.rs | 10 +++ .../src/implementations/layers/mod.rs | 1 + .../layers/via_node_storage_init.rs | 60 ++++++++++++++++ .../resources/via_system_wallet.rs | 20 ++++++ core/node/via_node_storage_init/src/lib.rs | 72 +++++++++++++++++++ .../bin/verifier_server/src/node_builder.rs | 2 +- .../node/via_verifier_storage_init/src/lib.rs | 26 +++++-- .../via_verifier_storage_init/src/wallets.rs | 67 +++++++++++++++++ 8 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 core/node/node_framework/src/implementations/layers/via_node_storage_init.rs create mode 100644 core/node/node_framework/src/implementations/resources/via_system_wallet.rs create mode 100644 core/node/via_node_storage_init/src/lib.rs create mode 100644 via_verifier/node/via_verifier_storage_init/src/wallets.rs diff --git a/core/bin/via_server/src/node_builder.rs b/core/bin/via_server/src/node_builder.rs index 42e7ea30b..dbf606dea 100644 --- a/core/bin/via_server/src/node_builder.rs +++ b/core/bin/via_server/src/node_builder.rs @@ -36,6 +36,7 @@ use zksync_node_framework::{ via_da_dispatcher::DataAvailabilityDispatcherLayer, via_gas_adjuster::ViaGasAdjusterLayer, via_l1_gas::ViaL1GasLayer, + via_node_storage_init::ViaNodeStorageInitializerLayer, via_state_keeper::{ main_batch_executor::MainBatchExecutorLayer, mempool_io::MempoolIOLayer, output_handler::OutputHandlerLayer, RocksdbStorageOptions, StateKeeperLayer, @@ -175,6 +176,14 @@ impl ViaNodeBuilder { } // VIA related layers + fn add_init_node_storage_layer(mut self) -> anyhow::Result { + let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + + self.node + .add_layer(ViaNodeStorageInitializerLayer::new(via_genesis_config)); + Ok(self) + } + fn add_btc_client_layer(mut self) -> anyhow::Result { let via_btc_client_config = try_load_config!(self.configs.via_btc_client_config); let secrets = self.secrets.via_l1.clone().unwrap(); @@ -613,6 +622,7 @@ impl ViaNodeBuilder { ViaComponent::Btc => { self = self .add_btc_client_layer()? + .add_init_node_storage_layer()? .add_gas_adjuster_layer()? .add_btc_watcher_layer()? .add_btc_sender_layer()? diff --git a/core/node/node_framework/src/implementations/layers/mod.rs b/core/node/node_framework/src/implementations/layers/mod.rs index 06811b2d1..1d82497b5 100644 --- a/core/node/node_framework/src/implementations/layers/mod.rs +++ b/core/node/node_framework/src/implementations/layers/mod.rs @@ -45,6 +45,7 @@ pub mod via_indexer; pub mod via_l1_gas; pub mod via_l1_indexer; pub mod via_main_node_fee_params_fetcher; +pub mod via_node_storage_init; pub mod via_state_keeper; pub mod via_validate_chain_ids; pub mod via_verifier_btc_watch; diff --git a/core/node/node_framework/src/implementations/layers/via_node_storage_init.rs b/core/node/node_framework/src/implementations/layers/via_node_storage_init.rs new file mode 100644 index 000000000..66ae8fc62 --- /dev/null +++ b/core/node/node_framework/src/implementations/layers/via_node_storage_init.rs @@ -0,0 +1,60 @@ +use via_node_storage_init::ViaMainNodeStorageInitializer; +use zksync_config::configs::via_consensus::ViaGenesisConfig; + +use crate::{ + implementations::resources::{ + pools::{MasterPool, PoolResource}, + via_btc_client::BtcClientResource, + via_system_wallet::ViaSystemWalletsResource, + }, + wiring_layer::{WiringError, WiringLayer}, + FromContext, IntoContext, +}; + +#[derive(Debug)] +pub struct ViaNodeStorageInitializerLayer { + via_genesis_config: ViaGenesisConfig, +} + +#[derive(Debug, FromContext)] +#[context(crate = crate)] +pub struct Input { + pub master_pool: PoolResource, + pub btc_client_resource: BtcClientResource, +} + +#[derive(Debug, IntoContext)] +#[context(crate = crate)] +pub struct Output { + pub system_wallets_resource: ViaSystemWalletsResource, +} + +impl ViaNodeStorageInitializerLayer { + pub fn new(via_genesis_config: ViaGenesisConfig) -> Self { + Self { via_genesis_config } + } +} + +#[async_trait::async_trait] +impl WiringLayer for ViaNodeStorageInitializerLayer { + type Input = Input; + type Output = Output; + + fn layer_name(&self) -> &'static str { + "Via_node_storage_initializer" + } + + async fn wire(self, input: Self::Input) -> Result { + let client = input.btc_client_resource.default; + let pool = input.master_pool.get().await?; + + let initializer = + ViaMainNodeStorageInitializer::new(pool, client.clone(), self.via_genesis_config); + let system_wallets = initializer.indexer_wallets().await?; + let system_wallets_resource = ViaSystemWalletsResource::from(system_wallets); + + Ok(Output { + system_wallets_resource, + }) + } +} diff --git a/core/node/node_framework/src/implementations/resources/via_system_wallet.rs b/core/node/node_framework/src/implementations/resources/via_system_wallet.rs new file mode 100644 index 000000000..8d31315b3 --- /dev/null +++ b/core/node/node_framework/src/implementations/resources/via_system_wallet.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use zksync_types::via_wallet::SystemWallets; + +use crate::Resource; + +#[derive(Debug, Clone)] +pub struct ViaSystemWalletsResource(pub Arc); + +impl Resource for ViaSystemWalletsResource { + fn name() -> String { + "via_system_wallets_resource".into() + } +} + +impl From for ViaSystemWalletsResource { + fn from(wallets: SystemWallets) -> Self { + Self(Arc::new(wallets)) + } +} diff --git a/core/node/via_node_storage_init/src/lib.rs b/core/node/via_node_storage_init/src/lib.rs new file mode 100644 index 000000000..861212241 --- /dev/null +++ b/core/node/via_node_storage_init/src/lib.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use via_btc_client::{bootstrap::ViaBootstrap, client::BitcoinClient}; +use zksync_config::configs::via_consensus::ViaGenesisConfig; +use zksync_dal::{ConnectionPool, Core, CoreDal}; +use zksync_types::via_wallet::{SystemWallets, SystemWalletsDetails}; + +#[derive(Debug)] +pub struct ViaMainNodeStorageInitializer { + pool: ConnectionPool, + bootstrap: ViaBootstrap, +} + +impl ViaMainNodeStorageInitializer { + pub fn new( + pool: ConnectionPool, + client: Arc, + config: ViaGenesisConfig, + ) -> Self { + let bootstrap = ViaBootstrap::new(client, config); + + Self { pool, bootstrap } + } + + pub async fn indexer_wallets(&self) -> anyhow::Result { + if let Some(system_wallets) = self.fetch_indexer_wallets_from_db().await? { + return Ok(system_wallets); + } + Ok(self.init_indexer_wallets().await?) + } + + pub async fn init_indexer_wallets(&self) -> anyhow::Result { + let state = self.bootstrap.process_bootstrap_messages().await?; + + let indexer_wallets_details = SystemWalletsDetails::try_from(&state)?; + + self.pool + .connection() + .await? + .via_wallet_dal() + .insert_wallets(&indexer_wallets_details) + .await?; + + let wallets = state + .wallets + .clone() + .ok_or_else(|| anyhow::anyhow!("Wallets missing when init_indexer_wallets"))?; + + tracing::info!("Loaded the indexer wallets from bootstrap inscriptions"); + + Ok(wallets) + } + + async fn fetch_indexer_wallets_from_db(&self) -> anyhow::Result> { + let system_wallet_raw_opt = self + .pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await?; + + let Some(system_wallet_raw) = system_wallet_raw_opt else { + return Ok(None); + }; + + let parsed_system_wallets = SystemWallets::try_from(system_wallet_raw)?; + tracing::info!("Loaded the indexer wallets from DB"); + + Ok(Some(parsed_system_wallets)) + } +} diff --git a/via_verifier/bin/verifier_server/src/node_builder.rs b/via_verifier/bin/verifier_server/src/node_builder.rs index 2bc909bfe..84e017921 100644 --- a/via_verifier/bin/verifier_server/src/node_builder.rs +++ b/via_verifier/bin/verifier_server/src/node_builder.rs @@ -210,8 +210,8 @@ impl ViaNodeBuilder { .add_circuit_breaker_checker_layer()? .add_prometheus_exporter_layer()? .add_pools_layer()? - .add_storage_initialization_layer()? .add_btc_client_layer()? + .add_storage_initialization_layer()? .add_btc_sender_layer()? .add_verifier_btc_watcher_layer()? .add_via_celestia_da_client_layer()? diff --git a/via_verifier/node/via_verifier_storage_init/src/lib.rs b/via_verifier/node/via_verifier_storage_init/src/lib.rs index d362de658..738e38d00 100644 --- a/via_verifier/node/via_verifier_storage_init/src/lib.rs +++ b/via_verifier/node/via_verifier_storage_init/src/lib.rs @@ -1,25 +1,43 @@ mod genesis; +mod wallets; use std::{future::Future, sync::Arc, time::Duration}; use genesis::VerifierGenesis; use tokio::sync::watch; +use via_btc_client::client::BitcoinClient; use via_verifier_dal::{ConnectionPool, Verifier}; -use zksync_config::GenesisConfig; +use wallets::ViaWalletsInitializer; +use zksync_config::{configs::via_consensus::ViaGenesisConfig, GenesisConfig}; use zksync_node_storage_init::InitializeStorage; +use zksync_types::via_wallet::SystemWallets; #[derive(Debug, Clone)] pub struct ViaVerifierStorageInitializer { genesis: Arc, + wallets: ViaWalletsInitializer, } impl ViaVerifierStorageInitializer { - pub fn new(genesis_config: GenesisConfig, pool: ConnectionPool) -> Self { + pub fn new( + via_genesis_config: ViaGenesisConfig, + genesis_config: GenesisConfig, + pool: ConnectionPool, + client: Arc, + ) -> Self { let genesis = Arc::new(VerifierGenesis { genesis_config, - pool, + pool: pool.clone(), }); - Self { genesis } + let wallets = ViaWalletsInitializer::new(pool, client, via_genesis_config); + Self { genesis, wallets } + } + + pub async fn indexer_wallets(&self) -> anyhow::Result { + if let Some(system_wallets) = self.wallets.fetch_indexer_wallets_from_db().await? { + return Ok(system_wallets); + } + Ok(self.wallets.init_indexer_wallets().await?) } pub async fn run(self, stop_receiver: watch::Receiver) -> anyhow::Result<()> { diff --git a/via_verifier/node/via_verifier_storage_init/src/wallets.rs b/via_verifier/node/via_verifier_storage_init/src/wallets.rs new file mode 100644 index 000000000..0a38fe56c --- /dev/null +++ b/via_verifier/node/via_verifier_storage_init/src/wallets.rs @@ -0,0 +1,67 @@ +use std::sync::Arc; + +use via_btc_client::{bootstrap::ViaBootstrap, client::BitcoinClient}; +use via_verifier_dal::{ConnectionPool, Verifier, VerifierDal}; +use zksync_config::configs::via_consensus::ViaGenesisConfig; +use zksync_types::via_wallet::{SystemWallets, SystemWalletsDetails}; + +#[derive(Debug, Clone)] +pub struct ViaWalletsInitializer { + pool: ConnectionPool, + bootstrap: ViaBootstrap, +} + +impl ViaWalletsInitializer { + pub fn new( + pool: ConnectionPool, + client: Arc, + config: ViaGenesisConfig, + ) -> Self { + let bootstrap = ViaBootstrap::new(client, config); + + Self { pool, bootstrap } + } + + pub(crate) async fn init_indexer_wallets(&self) -> anyhow::Result { + let state = self.bootstrap.process_bootstrap_messages().await?; + + let indexer_wallets_details = SystemWalletsDetails::try_from(&state)?; + + self.pool + .connection() + .await? + .via_wallet_dal() + .insert_wallets(&indexer_wallets_details) + .await?; + + let wallets = state + .wallets + .clone() + .ok_or_else(|| anyhow::anyhow!("Wallets missing when init_indexer_wallets"))?; + + tracing::info!("Loaded the indexer wallets from bootstrap inscriptions"); + + Ok(wallets) + } + + pub(crate) async fn fetch_indexer_wallets_from_db( + &self, + ) -> anyhow::Result> { + let system_wallet_raw_opt = self + .pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await?; + + let Some(system_wallet_raw) = system_wallet_raw_opt else { + return Ok(None); + }; + + let parsed_system_wallets = SystemWallets::try_from(system_wallet_raw)?; + tracing::info!("Loaded the indexer wallets from DB"); + + Ok(Some(parsed_system_wallets)) + } +} From 0ed90d9bde1b08fe68178b249b617c8a8c85b0db Mon Sep 17 00:00:00 2001 From: 0xatomFusion <179967211+0xatomFusion@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:21:46 +0100 Subject: [PATCH 3/8] refactor: bridge config --- core/bin/via_external_node/src/config/mod.rs | 1 - .../bin/via_external_node/src/node_builder.rs | 16 +--------- core/bin/via_server/src/node_builder.rs | 12 ++++---- core/lib/config/src/configs/mod.rs | 1 + core/lib/config/src/configs/via_bridge.rs | 30 +++++++++++++++++++ core/lib/config/src/configs/via_consensus.rs | 24 ++------------- core/lib/config/src/configs/via_general.rs | 3 ++ core/lib/config/src/configs/via_l1_indexer.rs | 2 ++ core/lib/config/src/configs/via_verifier.rs | 21 ++++++++++++- core/lib/env_config/src/lib.rs | 1 + core/lib/env_config/src/via_bridge.rs | 9 ++++++ .../src/temp_config_store/mod.rs | 3 ++ core/node/api_server/src/web3/tests/mod.rs | 4 +-- core/node/api_server/src/web3/tests/ws.rs | 4 +-- .../implementations/layers/via_btc_watch.rs | 12 ++++---- .../implementations/layers/via_l1_indexer.rs | 19 ++++-------- .../layers/via_verifier/coordinator_api.rs | 18 +++++------ .../layers/via_verifier_btc_watch.rs | 10 +++---- .../layers/via_zk_verification.rs | 10 +++---- etc/env/base/via_bridge.toml | 14 +++++++++ etc/env/base/via_genesis.toml | 13 -------- etc/env/base/via_verifier.toml | 2 ++ via_indexer/bin/indexer/src/main.rs | 2 ++ via_indexer/bin/indexer/src/node_builder.rs | 14 ++++----- via_indexer/node/indexer/src/lib.rs | 6 ++-- .../bin/verifier_server/src/node_builder.rs | 23 +++++++------- .../lib/via_musig2/src/transaction_builder.rs | 2 +- .../src/coordinator/api_decl.rs | 4 ++- 28 files changed, 155 insertions(+), 125 deletions(-) create mode 100644 core/lib/config/src/configs/via_bridge.rs create mode 100644 core/lib/env_config/src/via_bridge.rs create mode 100644 etc/env/base/via_bridge.toml diff --git a/core/bin/via_external_node/src/config/mod.rs b/core/bin/via_external_node/src/config/mod.rs index 0de384bf0..25e56fa4a 100644 --- a/core/bin/via_external_node/src/config/mod.rs +++ b/core/bin/via_external_node/src/config/mod.rs @@ -13,7 +13,6 @@ use zksync_config::{ api::{MaxResponseSize, MaxResponseSizeOverrides}, consensus::{ConsensusConfig, ConsensusSecrets}, en_config::ENConfig, - via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig, via_secrets::ViaL1Secrets, GeneralConfig, diff --git a/core/bin/via_external_node/src/node_builder.rs b/core/bin/via_external_node/src/node_builder.rs index f41f1a5a8..b9e487d33 100644 --- a/core/bin/via_external_node/src/node_builder.rs +++ b/core/bin/via_external_node/src/node_builder.rs @@ -21,9 +21,7 @@ use zksync_node_framework::{ block_reverter::BlockReverterLayer, commitment_generator::CommitmentGeneratorLayer, consensus::ExternalNodeConsensusLayer, - consistency_checker::ConsistencyCheckerLayer, healtcheck_server::HealthCheckLayer, - l1_batch_commitment_mode_validation::L1BatchCommitmentModeValidationLayer, logs_bloom_backfill::LogsBloomBackfillLayer, main_node_client::MainNodeClientLayer, metadata_calculator::MetadataCalculatorLayer, @@ -42,7 +40,6 @@ use zksync_node_framework::{ }, sync_state_updater::SyncStateUpdaterLayer, tree_data_fetcher::TreeDataFetcherLayer, - validate_chain_ids::ValidateChainIdsLayer, via_btc_client::BtcClientLayer, via_consistency_checker::ViaConsistencyCheckerLayer, via_indexer::ViaIndexerLayer, @@ -284,18 +281,7 @@ impl ExternalNodeBuilder { } fn add_btc_indexer_layer(mut self) -> anyhow::Result { - let via_btc_client_config = ViaBtcClientConfig { - network: self.config.remote.via_network.to_string(), - external_apis: vec![], - fee_strategies: vec![], - use_rpc_for_fee_rate: None, - }; - println!("{:?}", &self.config); - let via_genesis_config = self.config.via_genesis_config.clone().unwrap(); - self.node.add_layer(ViaIndexerLayer::new( - via_genesis_config, - via_btc_client_config, - )); + self.node.add_layer(ViaIndexerLayer::new()); Ok(self) } diff --git a/core/bin/via_server/src/node_builder.rs b/core/bin/via_server/src/node_builder.rs index dbf606dea..699a97fae 100644 --- a/core/bin/via_server/src/node_builder.rs +++ b/core/bin/via_server/src/node_builder.rs @@ -198,12 +198,12 @@ impl ViaNodeBuilder { } fn add_btc_watcher_layer(mut self) -> anyhow::Result { - let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let via_bridge_config = try_load_config!(self.configs.via_bridge_config); let via_btc_client_config = try_load_config!(self.configs.via_btc_client_config); let via_btc_watch_config = try_load_config!(self.configs.via_btc_watch_config); self.node.add_layer(BtcWatchLayer::new( - via_genesis_config, + via_bridge_config, via_btc_client_config, via_btc_watch_config, )); @@ -312,7 +312,7 @@ impl ViaNodeBuilder { response_body_size_limit: Some(rpc_config.max_response_body_size()), ..Default::default() }; - let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let via_bridge_config = try_load_config!(self.configs.via_bridge_config); let via_btc_client_config = try_load_config!(self.configs.via_btc_client_config); self.node.add_layer(Web3ServerLayer::http( @@ -321,7 +321,7 @@ impl ViaNodeBuilder { &rpc_config, &self.contracts_config, &self.genesis_config, - Some(via_genesis_config.bridge_address), + Some(via_bridge_config.bridge_address), Some(via_btc_client_config.network()), ), optional_config, @@ -520,7 +520,7 @@ impl ViaNodeBuilder { with_extended_tracing: rpc_config.extended_api_tracing, ..Default::default() }; - let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let via_bridge_config = try_load_config!(self.configs.via_bridge_config); let via_btc_client_config = try_load_config!(self.configs.via_btc_client_config); self.node.add_layer(Web3ServerLayer::ws( @@ -529,7 +529,7 @@ impl ViaNodeBuilder { &rpc_config, &self.contracts_config, &self.genesis_config, - Some(via_genesis_config.bridge_address), + Some(via_bridge_config.bridge_address), Some(via_btc_client_config.network()), ), optional_config, diff --git a/core/lib/config/src/configs/mod.rs b/core/lib/config/src/configs/mod.rs index b44e84d21..9884d1126 100644 --- a/core/lib/config/src/configs/mod.rs +++ b/core/lib/config/src/configs/mod.rs @@ -69,6 +69,7 @@ pub mod secrets; pub mod snapshot_recovery; pub mod snapshots_creator; pub mod utils; +pub mod via_bridge; pub mod via_btc_client; pub mod via_btc_sender; pub mod via_btc_watch; diff --git a/core/lib/config/src/configs/via_bridge.rs b/core/lib/config/src/configs/via_bridge.rs new file mode 100644 index 000000000..2fe815ad6 --- /dev/null +++ b/core/lib/config/src/configs/via_bridge.rs @@ -0,0 +1,30 @@ +use std::str::FromStr; + +use bitcoin::Address; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct ViaBridgeConfig { + /// The coordinator public key. + pub coordinator_pub_key: String, + + /// The verifiers public keys. + pub verifiers_pub_keys: Vec, + + /// The bridge address. + pub bridge_address: String, + + /// The minimum required signers to process a musig2 session. + pub required_signers: usize, + + /// The agreement threshold required for the verifier to finalize an L1 batch. + pub zk_agreement_threshold: f64, +} + +impl ViaBridgeConfig { + pub fn bridge_address(&self) -> anyhow::Result
{ + Ok(Address::from_str(&self.bridge_address) + .expect("Invalid bridge address") + .assume_checked()) + } +} diff --git a/core/lib/config/src/configs/via_consensus.rs b/core/lib/config/src/configs/via_consensus.rs index 653e649e0..06febf2d7 100644 --- a/core/lib/config/src/configs/via_consensus.rs +++ b/core/lib/config/src/configs/via_consensus.rs @@ -1,36 +1,16 @@ use std::str::FromStr; -use bitcoin::{Address, Txid}; +use bitcoin::Txid; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ViaGenesisConfig { - /// The coordinator public key. - pub coordinator_pub_key: String, - - /// The verifiers public keys. - pub verifiers_pub_keys: Vec, - - /// The bridge address. - pub bridge_address: String, - - /// The minimum required signers to process a musig2 session. - pub required_signers: usize, - - /// The agreement threshold required for the verifier to finalize an L1 batch. - pub zk_agreement_threshold: f64, - /// List of transaction IDs to bootstrap the indexer. pub bootstrap_txids: Vec, } impl ViaGenesisConfig { - pub fn bridge_address(&self) -> anyhow::Result
{ - Ok(Address::from_str(&self.bridge_address) - .expect("Invalid bridge address") - .assume_checked()) - } - + /// Get the bootstrap transaction IDs. pub fn bootstrap_txids(&self) -> anyhow::Result> { self.bootstrap_txids .iter() diff --git a/core/lib/config/src/configs/via_general.rs b/core/lib/config/src/configs/via_general.rs index 5eb21205b..3ee2002f0 100644 --- a/core/lib/config/src/configs/via_general.rs +++ b/core/lib/config/src/configs/via_general.rs @@ -10,6 +10,7 @@ use crate::{ prover_job_monitor::ProverJobMonitorConfig, pruning::PruningConfig, snapshot_recovery::SnapshotRecoveryConfig, + via_bridge::ViaBridgeConfig, vm_runner::{BasicWitnessInputProducerConfig, ProtectiveReadsWriterConfig}, CommitmentGeneratorConfig, ExperimentalVmConfig, ExternalPriceApiClientConfig, FriProofCompressorConfig, FriProverConfig, FriProverGatewayConfig, @@ -63,6 +64,7 @@ pub struct ViaGeneralConfig { pub via_celestia_config: Option, pub via_verifier_config: Option, pub via_genesis_config: Option, + pub via_bridge_config: Option, } impl From for ViaGeneralConfig { @@ -108,6 +110,7 @@ impl From for ViaGeneralConfig { via_verifier_config: None, via_btc_client_config: None, via_genesis_config: None, + via_bridge_config: None, } } } diff --git a/core/lib/config/src/configs/via_l1_indexer.rs b/core/lib/config/src/configs/via_l1_indexer.rs index f9379bdf8..974f72904 100644 --- a/core/lib/config/src/configs/via_l1_indexer.rs +++ b/core/lib/config/src/configs/via_l1_indexer.rs @@ -3,9 +3,11 @@ use super::{ via_secrets::ViaSecrets, ObservabilityConfig, PostgresConfig, PrometheusConfig, ViaBtcWatchConfig, }; +use crate::configs::via_bridge::ViaBridgeConfig; #[derive(Debug, Clone, PartialEq)] pub struct ViaIndexerConfig { + pub via_bridge_config: ViaBridgeConfig, pub via_genesis_config: ViaGenesisConfig, pub via_btc_client_config: ViaBtcClientConfig, pub via_btc_watch_config: ViaBtcWatchConfig, diff --git a/core/lib/config/src/configs/via_verifier.rs b/core/lib/config/src/configs/via_verifier.rs index 50f7b1ba5..3ded6008e 100644 --- a/core/lib/config/src/configs/via_verifier.rs +++ b/core/lib/config/src/configs/via_verifier.rs @@ -1,9 +1,10 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, time::Duration, }; -use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; +use bitcoin::{policy::MAX_STANDARD_TX_WEIGHT, Address, TapNodeHash}; use serde::{Deserialize, Serialize}; use zksync_basic_types::via_roles::ViaNodeRole; @@ -30,6 +31,9 @@ pub struct ViaVerifierConfig { /// The verifier btc wallet address. pub wallet_address: String, + /// The bridge address merkle root. + pub bridge_address_merkle_root: Option, + /// The session timeout. pub session_timeout: u64, @@ -41,6 +45,10 @@ impl ViaVerifierConfig { pub fn polling_interval(&self) -> Duration { Duration::from_millis(self.poll_interval) } + + pub fn wallet_address(&self) -> anyhow::Result
{ + Ok(Address::from_str(&self.wallet_address)?.assume_checked()) + } } impl ViaVerifierConfig { @@ -68,6 +76,17 @@ impl ViaVerifierConfig { wallet_address: "".into(), session_timeout: 30, max_tx_weight: None, + bridge_address_merkle_root: None, + } + } + + pub fn bridge_address_merkle_root(&self) -> Option { + if let Some(merkle_root) = self.bridge_address_merkle_root.clone() { + if merkle_root.is_empty() { + return None; + } + return Some(TapNodeHash::from_str(&merkle_root).expect("Invalid signer merkle root")); } + None } } diff --git a/core/lib/env_config/src/lib.rs b/core/lib/env_config/src/lib.rs index 9d6631310..8ad050b9e 100644 --- a/core/lib/env_config/src/lib.rs +++ b/core/lib/env_config/src/lib.rs @@ -20,6 +20,7 @@ mod observability; mod proof_data_handler; mod snapshots_creator; mod utils; +mod via_bridge; mod via_btc_client; mod via_btc_sender; mod via_celestia; diff --git a/core/lib/env_config/src/via_bridge.rs b/core/lib/env_config/src/via_bridge.rs new file mode 100644 index 000000000..222ced8cc --- /dev/null +++ b/core/lib/env_config/src/via_bridge.rs @@ -0,0 +1,9 @@ +use zksync_config::configs::via_bridge::ViaBridgeConfig; + +use crate::{envy_load, FromEnv}; + +impl FromEnv for ViaBridgeConfig { + fn from_env() -> anyhow::Result { + envy_load("via_bridge", "VIA_BRIDGE_") + } +} diff --git a/core/lib/zksync_core_leftovers/src/temp_config_store/mod.rs b/core/lib/zksync_core_leftovers/src/temp_config_store/mod.rs index b13f11c50..7d24d0f14 100644 --- a/core/lib/zksync_core_leftovers/src/temp_config_store/mod.rs +++ b/core/lib/zksync_core_leftovers/src/temp_config_store/mod.rs @@ -10,6 +10,7 @@ use zksync_config::{ }, fri_prover_group::FriProverGroupConfig, house_keeper::HouseKeeperConfig, + via_bridge::ViaBridgeConfig, via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig, vm_runner::BasicWitnessInputProducerConfig, @@ -225,6 +226,8 @@ impl ViaTempConfigStore { Some(ViaCelestiaConfig::from_env().context("Failed to load celestia config")?); via_general_config.via_genesis_config = Some(ViaGenesisConfig::from_env().context("Failed to load genesis config")?); + via_general_config.via_bridge_config = + Some(ViaBridgeConfig::from_env().context("Failed to load bridge config")?); Ok(via_general_config) } } diff --git a/core/node/api_server/src/web3/tests/mod.rs b/core/node/api_server/src/web3/tests/mod.rs index ac4cb2bc1..237081fe7 100644 --- a/core/node/api_server/src/web3/tests/mod.rs +++ b/core/node/api_server/src/web3/tests/mod.rs @@ -248,8 +248,8 @@ async fn test_http_server(test: impl HttpTest) { &web3_config, &contracts_config, &genesis, - "".into(), - bitcoin::Network::Regtest, + Some("".into()), + Some(bitcoin::Network::Regtest), ); api_config.filters_disabled = test.filters_disabled(); let mut server_handles = spawn_http_server( diff --git a/core/node/api_server/src/web3/tests/ws.rs b/core/node/api_server/src/web3/tests/ws.rs index e7fdb6063..fd48e29fe 100644 --- a/core/node/api_server/src/web3/tests/ws.rs +++ b/core/node/api_server/src/web3/tests/ws.rs @@ -172,8 +172,8 @@ async fn test_ws_server(test: impl WsTest) { &web3_config, &contracts_config, &genesis_config, - "".into(), - bitcoin::Network::Regtest, + Some("".into()), + Some(bitcoin::Network::Regtest), ); let mut storage = pool.connection().await.unwrap(); test.storage_initialization() diff --git a/core/node/node_framework/src/implementations/layers/via_btc_watch.rs b/core/node/node_framework/src/implementations/layers/via_btc_watch.rs index 8d1daeab8..e52141fb4 100644 --- a/core/node/node_framework/src/implementations/layers/via_btc_watch.rs +++ b/core/node/node_framework/src/implementations/layers/via_btc_watch.rs @@ -1,7 +1,7 @@ use via_btc_client::indexer::BitcoinInscriptionIndexer; use via_btc_watch::{BitcoinNetwork, BtcWatch}; use zksync_config::{ - configs::{via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig}, + configs::{via_bridge::ViaBridgeConfig, via_btc_client::ViaBtcClientConfig}, ViaBtcWatchConfig, }; @@ -23,7 +23,7 @@ use crate::{ /// Responsible for initializing and running of [`BtcWatch`] component, that polls the Bitcoin node for the relevant events. #[derive(Debug)] pub struct BtcWatchLayer { - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, via_btc_client: ViaBtcClientConfig, btc_watch_config: ViaBtcWatchConfig, } @@ -46,12 +46,12 @@ pub struct Output { impl BtcWatchLayer { pub fn new( - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, via_btc_client: ViaBtcClientConfig, btc_watch_config: ViaBtcWatchConfig, ) -> Self { Self { - via_genesis_config, + via_bridge_config, via_btc_client, btc_watch_config, } @@ -89,8 +89,8 @@ impl WiringLayer for BtcWatchLayer { indexer, client, main_pool, - self.via_genesis_config.bridge_address()?, - self.via_genesis_config.zk_agreement_threshold, + self.via_bridge_config.bridge_address()?, + self.via_bridge_config.zk_agreement_threshold, ) .await?; diff --git a/core/node/node_framework/src/implementations/layers/via_l1_indexer.rs b/core/node/node_framework/src/implementations/layers/via_l1_indexer.rs index 22eb30723..094ea73e9 100644 --- a/core/node/node_framework/src/implementations/layers/via_l1_indexer.rs +++ b/core/node/node_framework/src/implementations/layers/via_l1_indexer.rs @@ -1,9 +1,6 @@ use via_btc_client::indexer::BitcoinInscriptionIndexer; use via_indexer::L1Indexer; -use zksync_config::{ - configs::{via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig}, - ViaBtcWatchConfig, -}; +use zksync_config::{configs::via_bridge::ViaBridgeConfig, ViaBtcWatchConfig}; use crate::{ implementations::resources::{ @@ -20,8 +17,7 @@ use crate::{ #[derive(Debug)] pub struct L1IndexerLayer { - via_genesis_config: ViaGenesisConfig, - via_btc_client: ViaBtcClientConfig, + via_bridge_config: ViaBridgeConfig, btc_watch_config: ViaBtcWatchConfig, } @@ -42,14 +38,9 @@ pub struct Output { } impl L1IndexerLayer { - pub fn new( - via_genesis_config: ViaGenesisConfig, - via_btc_client: ViaBtcClientConfig, - btc_watch_config: ViaBtcWatchConfig, - ) -> Self { + pub fn new(via_bridge_config: ViaBridgeConfig, btc_watch_config: ViaBtcWatchConfig) -> Self { Self { - via_genesis_config, - via_btc_client, + via_bridge_config, btc_watch_config, } } @@ -73,7 +64,7 @@ impl WiringLayer for L1IndexerLayer { let l1_indexer = L1Indexer::new( self.btc_watch_config, - self.via_genesis_config, + self.via_bridge_config, indexer, client, main_pool, diff --git a/core/node/node_framework/src/implementations/layers/via_verifier/coordinator_api.rs b/core/node/node_framework/src/implementations/layers/via_verifier/coordinator_api.rs index 00a337d7f..2eac21c80 100644 --- a/core/node/node_framework/src/implementations/layers/via_verifier/coordinator_api.rs +++ b/core/node/node_framework/src/implementations/layers/via_verifier/coordinator_api.rs @@ -4,7 +4,7 @@ use via_btc_client::traits::BitcoinOps; use via_verifier_dal::{ConnectionPool, Verifier}; use via_withdrawal_client::client::WithdrawalClient; use zksync_config::{ - configs::{via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig}, + configs::{via_bridge::ViaBridgeConfig, via_btc_client::ViaBtcClientConfig}, ViaVerifierConfig, }; @@ -23,7 +23,7 @@ use crate::{ /// Wiring layer for coordinator api #[derive(Debug)] pub struct ViaCoordinatorApiLayer { - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, via_btc_client: ViaBtcClientConfig, verifier_config: ViaVerifierConfig, } @@ -45,12 +45,12 @@ pub struct Output { impl ViaCoordinatorApiLayer { pub fn new( - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, via_btc_client: ViaBtcClientConfig, verifier_config: ViaVerifierConfig, ) -> Self { Self { - via_genesis_config, + via_bridge_config, via_btc_client, verifier_config, } @@ -78,7 +78,7 @@ impl WiringLayer for ViaCoordinatorApiLayer { master_pool, btc_client, withdrawal_client, - via_genesis_config: self.via_genesis_config, + via_bridge_config: self.via_bridge_config, }; Ok(Output { via_coordinator_api_task, @@ -92,7 +92,7 @@ pub struct ViaCoordinatorApiTask { master_pool: ConnectionPool, btc_client: Arc, withdrawal_client: WithdrawalClient, - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, } #[async_trait::async_trait] @@ -107,9 +107,9 @@ impl Task for ViaCoordinatorApiTask { self.master_pool, self.btc_client, self.withdrawal_client, - self.via_genesis_config.bridge_address()?, - self.via_genesis_config.verifiers_pub_keys.clone(), - self.via_genesis_config.required_signers, + self.via_bridge_config.bridge_address()?, + self.via_bridge_config.verifiers_pub_keys.clone(), + self.via_bridge_config.required_signers, stop_receiver.0, ) .await diff --git a/core/node/node_framework/src/implementations/layers/via_verifier_btc_watch.rs b/core/node/node_framework/src/implementations/layers/via_verifier_btc_watch.rs index 46142542e..6ec3f8c0b 100644 --- a/core/node/node_framework/src/implementations/layers/via_verifier_btc_watch.rs +++ b/core/node/node_framework/src/implementations/layers/via_verifier_btc_watch.rs @@ -2,7 +2,7 @@ use via_btc_client::indexer::BitcoinInscriptionIndexer; use via_btc_watch::BitcoinNetwork; use via_verifier_btc_watch::VerifierBtcWatch; use zksync_config::{ - configs::{via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig}, + configs::{via_bridge::ViaBridgeConfig, via_btc_client::ViaBtcClientConfig}, ViaBtcWatchConfig, }; @@ -24,7 +24,7 @@ use crate::{ /// Responsible for initializing and running of [`VerifierBtcWatch`] component, that polls the Bitcoin node for the relevant events. #[derive(Debug)] pub struct VerifierBtcWatchLayer { - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, via_btc_client: ViaBtcClientConfig, btc_watch_config: ViaBtcWatchConfig, } @@ -47,12 +47,12 @@ pub struct Output { impl VerifierBtcWatchLayer { pub fn new( - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, via_btc_client: ViaBtcClientConfig, btc_watch_config: ViaBtcWatchConfig, ) -> Self { Self { - via_genesis_config, + via_bridge_config, via_btc_client, btc_watch_config, } @@ -91,7 +91,7 @@ impl WiringLayer for VerifierBtcWatchLayer { indexer, client, main_pool, - self.via_genesis_config.zk_agreement_threshold, + self.via_bridge_config.zk_agreement_threshold, ) .await?; diff --git a/core/node/node_framework/src/implementations/layers/via_zk_verification.rs b/core/node/node_framework/src/implementations/layers/via_zk_verification.rs index 8412c70cd..6072c6907 100644 --- a/core/node/node_framework/src/implementations/layers/via_zk_verification.rs +++ b/core/node/node_framework/src/implementations/layers/via_zk_verification.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use via_zk_verifier::ViaVerifier; -use zksync_config::{configs::via_consensus::ViaGenesisConfig, ViaVerifierConfig}; +use zksync_config::{configs::via_bridge::ViaBridgeConfig, ViaVerifierConfig}; use crate::{ implementations::resources::{ @@ -16,7 +16,7 @@ use crate::{ #[derive(Debug)] pub struct ViaBtcProofVerificationLayer { - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, verifier_config: ViaVerifierConfig, } @@ -36,10 +36,10 @@ pub struct ProofVerificationOutput { } impl ViaBtcProofVerificationLayer { - pub fn new(verifier_config: ViaVerifierConfig, via_genesis_config: ViaGenesisConfig) -> Self { + pub fn new(verifier_config: ViaVerifierConfig, via_bridge_config: ViaBridgeConfig) -> Self { Self { verifier_config, - via_genesis_config, + via_bridge_config, } } } @@ -61,7 +61,7 @@ impl WiringLayer for ViaBtcProofVerificationLayer { input.btc_indexer_resource.0.as_ref().clone(), main_pool, input.da_client.0, - self.via_genesis_config.zk_agreement_threshold, + self.via_bridge_config.zk_agreement_threshold, ) .await .map_err(WiringError::internal)?; diff --git a/etc/env/base/via_bridge.toml b/etc/env/base/via_bridge.toml new file mode 100644 index 000000000..360b89115 --- /dev/null +++ b/etc/env/base/via_bridge.toml @@ -0,0 +1,14 @@ +[via_bridge] +# The bridge address +bridge_address = "bcrt1p3s7m76wp5seprjy4gdxuxrr8pjgd47q5s8lu9vefxmp0my2p4t9qh6s8kq" +# The coordinator address +coordinator_pub_key = "03d8e2443ef58aa80fb6256bf3b94d2ecf9117f19cb17661ec60ad35fd84ff4a8b" +# The verifiers public keys +verifiers_pub_keys = [ + "03d8e2443ef58aa80fb6256bf3b94d2ecf9117f19cb17661ec60ad35fd84ff4a8b", + "02043f839b8ecd9ffd79f26ec7d05750555cd0d1e0777cfc84a29b7e38e6324662", +] +# Minimum required signers to finalise a musig2 session +required_signers = 2 +# Minimum vote threshold to finalise an L1 batch +zk_agreement_threshold = 0.5 diff --git a/etc/env/base/via_genesis.toml b/etc/env/base/via_genesis.toml index ddcee5000..0f5553de6 100644 --- a/etc/env/base/via_genesis.toml +++ b/etc/env/base/via_genesis.toml @@ -1,16 +1,3 @@ [via_genesis] -# The bridge address -bridge_address = "bcrt1p3s7m76wp5seprjy4gdxuxrr8pjgd47q5s8lu9vefxmp0my2p4t9qh6s8kq" -# The coordinator address -coordinator_pub_key = "03d8e2443ef58aa80fb6256bf3b94d2ecf9117f19cb17661ec60ad35fd84ff4a8b" -# The verifiers public keys -verifiers_pub_keys = [ - "03d8e2443ef58aa80fb6256bf3b94d2ecf9117f19cb17661ec60ad35fd84ff4a8b", - "02043f839b8ecd9ffd79f26ec7d05750555cd0d1e0777cfc84a29b7e38e6324662", -] -# Minimum required signers to finalise a musig2 session -required_signers = 2 -# Minimum vote threshold to finalise an L1 batch -zk_agreement_threshold = 0.5 # The bootstraping inscriptions bootstrap_txids = [] diff --git a/etc/env/base/via_verifier.toml b/etc/env/base/via_verifier.toml index 0673c2a13..102692f1f 100644 --- a/etc/env/base/via_verifier.toml +++ b/etc/env/base/via_verifier.toml @@ -15,3 +15,5 @@ test_zk_proof_invalid_l1_batch_numbers = [] session_timeout = 30 # The transaction weight limit max_tx_weight = 380000 +# The bridge address merkle root. +bridge_address_merkle_root = "" diff --git a/via_indexer/bin/indexer/src/main.rs b/via_indexer/bin/indexer/src/main.rs index c47827e1d..9e34292ce 100644 --- a/via_indexer/bin/indexer/src/main.rs +++ b/via_indexer/bin/indexer/src/main.rs @@ -1,6 +1,7 @@ use zksync_config::{ configs::{ api::HealthCheckConfig, + via_bridge::ViaBridgeConfig, via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig, via_l1_indexer::ViaIndexerConfig, @@ -35,6 +36,7 @@ fn main() -> anyhow::Result<()> { via_l1: ViaL1Secrets::from_env().ok(), via_da: None, }, + via_bridge_config: ViaBridgeConfig::from_env()?, }; let node_builder = node_builder::ViaNodeBuilder::new(via_indexer_config.clone())?; diff --git a/via_indexer/bin/indexer/src/node_builder.rs b/via_indexer/bin/indexer/src/node_builder.rs index 79ea02cc6..cd583502b 100644 --- a/via_indexer/bin/indexer/src/node_builder.rs +++ b/via_indexer/bin/indexer/src/node_builder.rs @@ -64,25 +64,21 @@ impl ViaNodeBuilder { fn add_btc_client_layer(mut self) -> anyhow::Result { let secrets = self.via_indexer_config.secrets.clone(); let via_btc_client_config = self.via_indexer_config.via_btc_client_config.clone(); - let via_genesis_config = self.via_indexer_config.via_genesis_config.clone(); + let via_bridge_config = self.via_indexer_config.via_bridge_config.clone(); self.node.add_layer(BtcClientLayer::new( via_btc_client_config, secrets.via_l1.unwrap(), ViaWallets::default(), - Some(via_genesis_config.bridge_address.clone()), + Some(via_bridge_config.bridge_address.clone()), )); Ok(self) } fn add_l1_indexer_layer(mut self) -> anyhow::Result { - let via_genesis_config = self.via_indexer_config.via_genesis_config.clone(); - let via_btc_client_config = self.via_indexer_config.via_btc_client_config.clone(); + let via_bridge_config = self.via_indexer_config.via_bridge_config.clone(); let via_btc_watch_config = self.via_indexer_config.via_btc_watch_config.clone(); - let indexer_layer = L1IndexerLayer::new( - via_genesis_config, - via_btc_client_config, - via_btc_watch_config, - ); + + let indexer_layer = L1IndexerLayer::new(via_bridge_config, via_btc_watch_config); self.node.add_layer(indexer_layer); Ok(self) } diff --git a/via_indexer/node/indexer/src/lib.rs b/via_indexer/node/indexer/src/lib.rs index a61061bb5..2fcc92a33 100644 --- a/via_indexer/node/indexer/src/lib.rs +++ b/via_indexer/node/indexer/src/lib.rs @@ -10,7 +10,7 @@ use tokio::sync::watch; pub use via_btc_client::types::BitcoinNetwork; use via_btc_client::{client::BitcoinClient, indexer::BitcoinInscriptionIndexer}; use via_indexer_dal::{Connection, ConnectionPool, Indexer, IndexerDal}; -use zksync_config::{configs::via_consensus::ViaGenesisConfig, ViaBtcWatchConfig}; +use zksync_config::{configs::via_bridge::ViaBridgeConfig, ViaBtcWatchConfig}; use self::message_processors::MessageProcessor; use crate::message_processors::L1ToL2MessageProcessor; @@ -29,7 +29,7 @@ pub struct L1Indexer { impl L1Indexer { pub async fn new( config: ViaBtcWatchConfig, - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, indexer: BitcoinInscriptionIndexer, client: Arc, pool: ConnectionPool, @@ -47,7 +47,7 @@ impl L1Indexer { let message_processors: Vec> = vec![ Box::new(L1ToL2MessageProcessor::new(client.clone())), Box::new(WithdrawalProcessor::new( - via_genesis_config.bridge_address()?, + via_bridge_config.bridge_address()?, client, )), ]; diff --git a/via_verifier/bin/verifier_server/src/node_builder.rs b/via_verifier/bin/verifier_server/src/node_builder.rs index 84e017921..1ce656712 100644 --- a/via_verifier/bin/verifier_server/src/node_builder.rs +++ b/via_verifier/bin/verifier_server/src/node_builder.rs @@ -75,13 +75,13 @@ impl ViaNodeBuilder { fn add_btc_client_layer(mut self) -> anyhow::Result { let via_btc_client_config = try_load_config!(self.configs.via_btc_client_config); let secrets = self.secrets.via_l1.clone().unwrap(); - let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let via_bridge_config = try_load_config!(self.configs.via_bridge_config); self.node.add_layer(BtcClientLayer::new( via_btc_client_config, secrets, self.wallets.clone(), - Some(via_genesis_config.bridge_address), + Some(via_bridge_config.bridge_address), )); Ok(self) } @@ -131,12 +131,12 @@ impl ViaNodeBuilder { // VIA related layers fn add_verifier_btc_watcher_layer(mut self) -> anyhow::Result { - let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let via_bridge_config = try_load_config!(self.configs.via_bridge_config); let via_btc_client_config = try_load_config!(self.configs.via_btc_client_config); let via_btc_watch_config = try_load_config!(self.configs.via_btc_watch_config); self.node.add_layer(VerifierBtcWatchLayer::new( - via_genesis_config, + via_bridge_config, via_btc_client_config, via_btc_watch_config, )); @@ -154,12 +154,12 @@ impl ViaNodeBuilder { } fn add_verifier_coordinator_api_layer(mut self) -> anyhow::Result { - let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let via_bridge_config = try_load_config!(self.configs.via_bridge_config); let via_btc_client_config = try_load_config!(self.configs.via_btc_client_config); let via_verifier_config = try_load_config!(self.configs.via_verifier_config); self.node.add_layer(ViaCoordinatorApiLayer::new( - via_genesis_config, + via_bridge_config, via_btc_client_config, via_verifier_config, )); @@ -167,7 +167,7 @@ impl ViaNodeBuilder { } fn add_withdrawal_verifier_task_layer(mut self) -> anyhow::Result { - let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let via_bridge_config = try_load_config!(self.configs.via_bridge_config); let via_btc_client_config = try_load_config!(self.configs.via_btc_client_config); let via_verifier_config = try_load_config!(self.configs.via_verifier_config); let wallet = self @@ -177,7 +177,7 @@ impl ViaNodeBuilder { .expect("Empty verifier wallet"); self.node.add_layer(ViaWithdrawalVerifierLayer::new( - via_genesis_config, + via_bridge_config, via_btc_client_config, via_verifier_config, wallet, @@ -187,17 +187,20 @@ impl ViaNodeBuilder { fn add_zkp_verification_layer(mut self) -> anyhow::Result { let via_verifier_config = try_load_config!(self.configs.via_verifier_config); - let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let via_bridge_config = try_load_config!(self.configs.via_bridge_config); self.node.add_layer(ViaBtcProofVerificationLayer::new( via_verifier_config, - via_genesis_config, + via_bridge_config, )); Ok(self) } fn add_storage_initialization_layer(mut self) -> anyhow::Result { + let via_genesis_config = try_load_config!(self.configs.via_genesis_config); + let layer = ViaVerifierInitLayer { genesis: self.genesis_config.clone(), + via_genesis_config, }; self.node.add_layer(layer); Ok(self) diff --git a/via_verifier/lib/via_musig2/src/transaction_builder.rs b/via_verifier/lib/via_musig2/src/transaction_builder.rs index fd5be6296..dbdb64280 100644 --- a/via_verifier/lib/via_musig2/src/transaction_builder.rs +++ b/via_verifier/lib/via_musig2/src/transaction_builder.rs @@ -26,7 +26,7 @@ use crate::{ #[derive(Debug, Clone)] pub struct TransactionBuilder { pub utxo_manager: UtxoManager, - bridge_address: Address, + pub bridge_address: Address, } impl TransactionBuilder { diff --git a/via_verifier/node/via_verifier_coordinator/src/coordinator/api_decl.rs b/via_verifier/node/via_verifier_coordinator/src/coordinator/api_decl.rs index 5a0156648..ca55a4913 100644 --- a/via_verifier/node/via_verifier_coordinator/src/coordinator/api_decl.rs +++ b/via_verifier/node/via_verifier_coordinator/src/coordinator/api_decl.rs @@ -20,6 +20,7 @@ use crate::{ }; pub struct RestApi { + pub config: ViaVerifierConfig, pub state: ViaWithdrawalState, pub session_manager: SessionManager, pub master_connection_pool: ConnectionPool, @@ -52,7 +53,7 @@ impl RestApi { Arc::new(TransactionBuilder::new(btc_client.clone(), bridge_address)?); let withdrawal_session = WithdrawalSession::new( - config, + config.clone(), master_connection_pool.clone(), transaction_builder.clone(), withdrawal_client.clone(), @@ -67,6 +68,7 @@ impl RestApi { .collect(); Ok(Self { + config, session_manager: SessionManager::new(sessions), state, master_connection_pool, From 4465e91a3f3418be45331ec98c52abd82653d63e Mon Sep 17 00:00:00 2001 From: 0xatomFusion <179967211+0xatomFusion@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:29:29 +0100 Subject: [PATCH 4/8] fea(via_system_wallets): impl system wallet message processor for sequencer and verifier --- Cargo.lock | 20 ++ Cargo.toml | 2 + core/lib/via_test_utils/Cargo.toml | 1 + core/lib/via_test_utils/src/utils.rs | 184 ++++++++--- core/node/node_framework/Cargo.toml | 1 + .../layers/via_verifier/verifier.rs | 11 +- .../src/btc_inscription_manager.rs | 27 +- core/node/via_btc_watch/Cargo.toml | 1 + core/node/via_btc_watch/src/lib.rs | 29 +- .../message_processors/governance_upgrade.rs | 6 +- .../src/message_processors/l1_to_l2.rs | 66 ++-- .../src/message_processors/mod.rs | 12 +- .../src/message_processors/system_wallet.rs | 274 +++++++++++++++++ .../src/message_processors/votable.rs | 4 +- core/node/via_btc_watch/src/test/mod.rs | 1 + .../via_btc_watch/src/test/system_wallets.rs | 286 ++++++++++++++++++ core/node/via_node_storage_init/Cargo.toml | 22 ++ core/node/via_node_storage_init/README.md | 3 + via_verifier/lib/via_musig2/Cargo.toml | 1 + .../lib/via_musig2/examples/withdrawal.rs | 2 +- via_verifier/lib/via_musig2/src/lib.rs | 39 ++- via_verifier/lib/via_musig2/src/utils.rs | 8 +- via_verifier/node/via_btc_watch/src/lib.rs | 29 +- .../message_processors/governance_upgrade.rs | 4 +- .../src/message_processors/l1_to_l2.rs | 6 +- .../src/message_processors/mod.rs | 4 +- .../src/message_processors/system_wallet.rs | 275 +++++++++++++++++ .../src/message_processors/verifier.rs | 4 +- .../src/message_processors/withdrawal.rs | 6 +- .../node/via_btc_watch/src/test/mod.rs | 2 +- .../via_btc_watch/src/test/system_wallets.rs | 286 ++++++++++++++++++ .../node/via_btc_watch/src/test/verifier.rs | 19 +- .../src/coordinator/api_impl.rs | 3 + .../src/verifier/mod.rs | 58 +++- .../node/via_verifier_storage_init/Cargo.toml | 4 +- via_verifier/node/via_zk_verifier/src/lib.rs | 23 +- 36 files changed, 1584 insertions(+), 139 deletions(-) create mode 100644 core/node/via_btc_watch/src/message_processors/system_wallet.rs create mode 100644 core/node/via_btc_watch/src/test/mod.rs create mode 100644 core/node/via_btc_watch/src/test/system_wallets.rs create mode 100644 core/node/via_node_storage_init/Cargo.toml create mode 100644 core/node/via_node_storage_init/README.md create mode 100644 via_verifier/node/via_btc_watch/src/message_processors/system_wallet.rs create mode 100644 via_verifier/node/via_btc_watch/src/test/system_wallets.rs diff --git a/Cargo.lock b/Cargo.lock index ef93f81a2..ff466887b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9563,6 +9563,7 @@ dependencies = [ "tokio", "tracing", "via_btc_client", + "via_test_utils", "vise", "zksync_config", "zksync_contracts", @@ -9846,6 +9847,7 @@ dependencies = [ "bitcoin", "bitcoincore-rpc", "byteorder", + "clap 4.5.16", "dotenv", "hex", "hyper 0.14.30", @@ -9868,6 +9870,20 @@ dependencies = [ "zksync_dal", ] +[[package]] +name = "via_node_storage_init" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "tokio", + "tracing", + "via_btc_client", + "zksync_config", + "zksync_dal", + "zksync_types", +] + [[package]] name = "via_server" version = "0.1.0" @@ -9935,6 +9951,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bitcoin", "tokio", "tracing", "via_btc_client", @@ -10094,6 +10111,8 @@ dependencies = [ "anyhow", "async-trait", "tokio", + "tracing", + "via_btc_client", "via_verifier_dal", "zksync_config", "zksync_node_genesis", @@ -12191,6 +12210,7 @@ dependencies = [ "via_indexer", "via_indexer_dal", "via_musig2", + "via_node_storage_init", "via_state_keeper", "via_verifier_btc_sender", "via_verifier_btc_watch", diff --git a/Cargo.toml b/Cargo.toml index d73b8cb75..cbf89e144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ members = [ 'core/node/via_consistency_checker', "core/lib/via_mempool", "core/lib/via_test_utils", + "core/node/via_node_storage_init", # VIA Verifier "via_verifier/bin/verifier_server", @@ -351,6 +352,7 @@ via_block_reverter = { version = "0.1.0", path = "core/node/via_block_reverter" via_consistency_checker = { version = "0.1.0", path = "core/node/via_consistency_checker" } via_mempool = { version = "0.1.0", path = "core/lib/via_mempool" } via_test_utils = { version = "0.1.0", path = "core/lib/via_test_utils" } +via_node_storage_init = { version = "0.1.0", path = "core/node/via_node_storage_init" } # VIA Verifier via_withdrawal_client = { version = "0.1.0", path = "via_verifier/lib/via_withdrawal_client" } diff --git a/core/lib/via_test_utils/Cargo.toml b/core/lib/via_test_utils/Cargo.toml index 8cd90bdb4..3080eb490 100644 --- a/core/lib/via_test_utils/Cargo.toml +++ b/core/lib/via_test_utils/Cargo.toml @@ -10,6 +10,7 @@ keywords.workspace = true categories.workspace = true [dependencies] +bitcoin = { version = "0.32.2", features = ["serde"] } via_btc_client.workspace = true zksync_types.workspace = true zksync_config.workspace = true diff --git a/core/lib/via_test_utils/src/utils.rs b/core/lib/via_test_utils/src/utils.rs index 18cb42a79..2659381c3 100644 --- a/core/lib/via_test_utils/src/utils.rs +++ b/core/lib/via_test_utils/src/utils.rs @@ -1,9 +1,16 @@ use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration}; +use bitcoin::{ + address::NetworkUnchecked, + key::rand, + script::PushBytesBuf, + secp256k1::{self, SecretKey}, + CompressedPublicKey, PrivateKey, +}; use tokio::time::sleep; use via_btc_client::{ client::BitcoinClient, - indexer::{BitcoinInscriptionIndexer, BootstrapState, MessageParser}, + indexer::{BitcoinInscriptionIndexer, MessageParser}, inscriber::Inscriber, traits::BitcoinOps, types::{ @@ -12,12 +19,16 @@ use via_btc_client::{ hex::{Case, DisplayHex}, Hash, }, - BitcoinTxid, FullInscriptionMessage, InscriptionMessage, L1BatchDAReferenceInput, NodeAuth, - ProofDAReferenceInput, Vote, + BitcoinTxid, CommonFields, FullInscriptionMessage, InscriptionMessage, + L1BatchDAReferenceInput, NodeAuth, ProofDAReferenceInput, UpdateBridge, UpdateBridgeInput, + UpdateBridgeProposalInput, UpdateGovernance, UpdateGovernanceInput, UpdateSequencer, + UpdateSequencerInput, }, }; use zksync_config::configs::via_btc_client::ViaBtcClientConfig; -use zksync_types::{BitcoinNetwork, L1BatchNumber, H256}; +use zksync_types::{ + via_bootstrap::BootstrapState, via_wallet::SystemWallets, BitcoinNetwork, L1BatchNumber, H256, +}; const RPC_URL: &str = "http://0.0.0.0:18443"; const RPC_USERNAME: &str = "rpcuser"; @@ -69,37 +80,131 @@ pub fn test_verifier_add_2() -> BitcoinAddress { .assume_checked() } +/// Verifier 3 address +pub fn test_verifier_add_3() -> BitcoinAddress { + BitcoinAddress::from_str(&"bcrt1q23lgaa90s85jvtl6dsrkvn0g949cwjkwuyzwdm") + .unwrap() + .assume_checked() +} + +pub fn test_wallets() -> SystemWallets { + let (proposed_sequencer, _) = test_sequencer_wallet(); + SystemWallets { + sequencer: proposed_sequencer, + bridge: BitcoinAddress::from_str( + &"bcrt1p3s7m76wp5seprjy4gdxuxrr8pjgd47q5s8lu9vefxmp0my2p4t9qh6s8kq", + ) + .unwrap() + .assume_checked(), + governance: BitcoinAddress::from_str( + &"bcrt1q92gkfme6k9dkpagrkwt76etkaq29hvf02w5m38f6shs4ddpw7hzqp347zm", + ) + .unwrap() + .assume_checked(), + verifiers: vec![ + test_verifier_add_1(), + test_verifier_add_2(), + test_verifier_add_3(), + ], + } +} + pub fn bootstrap_state_mock() -> BootstrapState { let mut sequencer_votes = HashMap::new(); - sequencer_votes.insert(test_verifier_add_1(), Vote::Ok); - sequencer_votes.insert(test_verifier_add_2(), Vote::Ok); + sequencer_votes.insert(test_verifier_add_1(), true); + sequencer_votes.insert(test_verifier_add_2(), true); - let (proposed_sequencer, _) = test_sequencer_wallet(); BootstrapState { - verifier_addresses: vec![test_verifier_add_1(), test_verifier_add_2()], - proposed_sequencer: Some(proposed_sequencer), - proposed_sequencer_txid: Some(BitcoinTxid::all_zeros()), + wallets: Some(test_wallets()), + sequencer_proposal_tx_id: Some(BitcoinTxid::all_zeros()), + bootstrap_tx_id: Some(BitcoinTxid::all_zeros()), sequencer_votes, - bridge_address: Some( - BitcoinAddress::from_str( - &"bcrt1p3s7m76wp5seprjy4gdxuxrr8pjgd47q5s8lu9vefxmp0my2p4t9qh6s8kq", - ) - .unwrap() - .assume_checked(), - ), starting_block_number: 1, bootloader_hash: Some(H256::zero()), abstract_account_hash: Some(H256::zero()), - proposed_governance: Some( - BitcoinAddress::from_str( - &"bcrt1q92gkfme6k9dkpagrkwt76etkaq29hvf02w5m38f6shs4ddpw7hzqp347zm", - ) - .unwrap() - .assume_checked(), - ), } } +/// Create a update sequencer address inscription +pub fn create_update_sequencer_inscription(address: BitcoinAddress) -> FullInscriptionMessage { + FullInscriptionMessage::UpdateSequencer(UpdateSequencer { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]) + .ok() + .unwrap(), + encoded_public_key: PushBytesBuf::new(), + block_height: 0, + tx_id: BitcoinTxid::all_zeros(), + p2wpkh_address: None, + tx_index: None, + output_vout: None, + }, + input: UpdateSequencerInput { + inputs: vec![], + address: address.as_unchecked().clone(), + }, + }) +} + +/// Create a update governance address inscription +pub fn create_update_governance_inscription(address: BitcoinAddress) -> FullInscriptionMessage { + FullInscriptionMessage::UpdateGovernance(UpdateGovernance { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]) + .ok() + .unwrap(), + encoded_public_key: PushBytesBuf::new(), + block_height: 0, + tx_id: BitcoinTxid::all_zeros(), + p2wpkh_address: None, + tx_index: None, + output_vout: None, + }, + input: UpdateGovernanceInput { + inputs: vec![], + address: address.as_unchecked().clone(), + }, + }) +} + +/// Create a update bridge address inscription +pub async fn create_update_bridge_inscription( + new_bridge: BitcoinAddress, + new_verifiers: Vec, +) -> anyhow::Result { + let mut inscriber = test_sequencer_inscriber().await?; + + sleep(Duration::from_millis(500)).await; + + let input = UpdateBridgeProposalInput { + bridge_musig2_address: new_bridge.as_unchecked().clone(), + verifier_p2wpkh_addresses: new_verifiers + .iter() + .map(|address| address.as_unchecked().clone()) + .collect::>>(), + }; + + let result = inscriber + .inscribe(InscriptionMessage::UpdateBridgeProposal(input)) + .await?; + + Ok(FullInscriptionMessage::UpdateBridge(UpdateBridge { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: PushBytesBuf::new(), + block_height: 0, + tx_id: BitcoinTxid::all_zeros(), + p2wpkh_address: None, + tx_index: None, + output_vout: None, + }, + input: UpdateBridgeInput { + inputs: vec![], + proposal_tx_id: result.final_reveal_tx.txid, + }, + })) +} + /// Returns the inscriptions and the last block hash pub async fn create_chained_inscriptions( start: usize, @@ -150,21 +255,32 @@ pub async fn create_chained_inscriptions( sleep(Duration::from_millis(500)).await; let tx = client.get_transaction(&result.final_reveal_tx.txid).await?; - msgs.extend(parser.parse_system_transaction(&tx, 0)); + msgs.extend(parser.parse_system_transaction(&tx, 0, None)); } Ok((msgs, prev_l1_batch_hash)) } -pub async fn test_create_indexer() -> anyhow::Result { - let bootstrap_state = bootstrap_state_mock(); +pub fn test_create_indexer() -> BitcoinInscriptionIndexer { + BitcoinInscriptionIndexer::new(Arc::new(test_bitcoin_client()), Arc::new(test_wallets())) +} - let client = test_bitcoin_client(); - let parser = MessageParser::new(BitcoinNetwork::Regtest); +pub fn random_bitcoin_wallet() -> (PrivateKey, BitcoinAddress) { + // Initialize secp256k1 context + let secp = secp256k1::Secp256k1::new(); + + // Generate a random secret key + let secret_key = SecretKey::new(&mut rand::thread_rng()); + + // Wrap it into a Bitcoin private key + let private_key = PrivateKey { + compressed: true, + network: NETWORK.into(), + inner: secret_key.clone(), + }; + + let cpk = CompressedPublicKey::from_private_key(&secp, &private_key).unwrap(); + let address = BitcoinAddress::p2wpkh(&cpk, NETWORK); - Ok(BitcoinInscriptionIndexer::create_indexer( - bootstrap_state, - Arc::new(client.clone()), - parser.clone(), - )?) + (private_key, address) } diff --git a/core/node/node_framework/Cargo.toml b/core/node/node_framework/Cargo.toml index a9b53e93b..9fc8d1b06 100644 --- a/core/node/node_framework/Cargo.toml +++ b/core/node/node_framework/Cargo.toml @@ -72,6 +72,7 @@ via_verifier_btc_watch.workspace = true via_verifier_btc_sender.workspace = true via_musig2.workspace = true via_verifier_storage_init.workspace = true +via_node_storage_init.workspace = true via_indexer_dal.workspace = true via_indexer.workspace = true diff --git a/core/node/node_framework/src/implementations/layers/via_verifier/verifier.rs b/core/node/node_framework/src/implementations/layers/via_verifier/verifier.rs index edba1df96..4500d2f47 100644 --- a/core/node/node_framework/src/implementations/layers/via_verifier/verifier.rs +++ b/core/node/node_framework/src/implementations/layers/via_verifier/verifier.rs @@ -3,7 +3,7 @@ use via_verifier_coordinator::verifier::ViaWithdrawalVerifier; use via_withdrawal_client::client::WithdrawalClient; use zksync_config::{ configs::{ - via_btc_client::ViaBtcClientConfig, via_consensus::ViaGenesisConfig, via_wallets::ViaWallet, + via_bridge::ViaBridgeConfig, via_btc_client::ViaBtcClientConfig, via_wallets::ViaWallet, }, ViaVerifierConfig, }; @@ -23,7 +23,7 @@ use crate::{ /// Wiring layer for verifier task #[derive(Debug)] pub struct ViaWithdrawalVerifierLayer { - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, via_btc_client: ViaBtcClientConfig, verifier_config: ViaVerifierConfig, wallet: ViaWallet, @@ -46,13 +46,13 @@ pub struct Output { impl ViaWithdrawalVerifierLayer { pub fn new( - via_genesis_config: ViaGenesisConfig, + via_bridge_config: ViaBridgeConfig, via_btc_client: ViaBtcClientConfig, verifier_config: ViaVerifierConfig, wallet: ViaWallet, ) -> Self { Self { - via_genesis_config, + via_bridge_config, via_btc_client, verifier_config, wallet, @@ -83,8 +83,7 @@ impl WiringLayer for ViaWithdrawalVerifierLayer { master_pool, btc_client, withdrawal_client, - self.via_genesis_config.bridge_address()?, - self.via_genesis_config.verifiers_pub_keys, + self.via_bridge_config, ) .context("Error to init the via withdrawal verifier")?; diff --git a/core/node/via_btc_sender/src/btc_inscription_manager.rs b/core/node/via_btc_sender/src/btc_inscription_manager.rs index a4e61f3aa..9d0873315 100644 --- a/core/node/via_btc_sender/src/btc_inscription_manager.rs +++ b/core/node/via_btc_sender/src/btc_inscription_manager.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anyhow::{Context, Result}; use bincode::serialize; use bitcoin::hashes::Hash; @@ -8,7 +10,7 @@ use zksync_dal::{Connection, ConnectionPool, Core, CoreDal}; use zksync_shared_metrics::BlockL1Stage; use zksync_types::{ btc_inscription_operations::ViaBtcInscriptionRequestType, - via_btc_sender::ViaBtcInscriptionRequest, + via_btc_sender::ViaBtcInscriptionRequest, via_wallet::SystemWallets, }; use crate::metrics::METRICS; @@ -81,6 +83,12 @@ impl ViaBtcInscriptionManager { let mut report_blocked_l1_batch_inscription: Option = None; + let Some(wallets_map) = storage.via_wallet_dal().get_system_wallets_raw().await? else { + anyhow::bail!("System wallets not found"); + }; + + self.validate_inscriber_address(wallets_map)?; + for inscription_id in inflight_inscriptions_ids { if let Some(last_inscription_history) = storage .btc_sender_dal() @@ -269,4 +277,21 @@ impl ViaBtcInscriptionManager { METRICS.pending_inscription_requests.dec_by(1); Ok(()) } + + fn validate_inscriber_address( + &self, + wallets_map: HashMap, + ) -> anyhow::Result<()> { + let wallets = SystemWallets::try_from(wallets_map)?; + + let inscriber_address = self.inscriber.inscriber_address()?; + if wallets.sequencer != inscriber_address { + anyhow::bail!( + "BTC sender inscriber wallets is not valid, expected {} found {}", + wallets.sequencer, + inscriber_address + ) + } + Ok(()) + } } diff --git a/core/node/via_btc_watch/Cargo.toml b/core/node/via_btc_watch/Cargo.toml index 80ca286fd..66b5459ae 100644 --- a/core/node/via_btc_watch/Cargo.toml +++ b/core/node/via_btc_watch/Cargo.toml @@ -28,3 +28,4 @@ tracing.workspace = true sqlx.workspace = true [dev-dependencies] +via_test_utils.workspace = true diff --git a/core/node/via_btc_watch/src/lib.rs b/core/node/via_btc_watch/src/lib.rs index 859b2f110..69e6231c6 100644 --- a/core/node/via_btc_watch/src/lib.rs +++ b/core/node/via_btc_watch/src/lib.rs @@ -13,9 +13,13 @@ use via_btc_client::{ use zksync_config::{configs::via_btc_watch::L1_BLOCKS_CHUNK, ViaBtcWatchConfig}; use zksync_dal::{Connection, ConnectionPool, Core, CoreDal}; +#[cfg(test)] +mod test; + use self::message_processors::{ L1ToL2MessageProcessor, MessageProcessor, MessageProcessorError, VotableMessageProcessor, }; +use crate::message_processors::SystemWalletProcessor; #[derive(Debug)] struct BtcWatchState { @@ -28,6 +32,7 @@ pub struct BtcWatch { indexer: BitcoinInscriptionIndexer, pool: ConnectionPool, state: BtcWatchState, + system_wallet_processor: Box, message_processors: Vec>, } @@ -38,7 +43,7 @@ impl BtcWatch { indexer: BitcoinInscriptionIndexer, btc_client: Arc, pool: ConnectionPool, - bridge_address: BitcoinAddress, + _bridge_address: BitcoinAddress, zk_agreement_threshold: f64, ) -> anyhow::Result { let mut storage = pool.connection_tagged(BtcWatch::module_name()).await?; @@ -59,13 +64,15 @@ impl BtcWatch { drop(storage); + let system_wallet_processor = Box::new(SystemWalletProcessor::new(btc_client.clone())); + // Only build message processors that match the actor role: let message_processors: Vec> = vec![ Box::new(GovernanceUpgradesEventProcessor::new( btc_client, protocol_semantic_version, )), - Box::new(L1ToL2MessageProcessor::new(bridge_address)), + Box::new(L1ToL2MessageProcessor::default()), Box::new(VotableMessageProcessor::new(zk_agreement_threshold)), ]; @@ -75,6 +82,7 @@ impl BtcWatch { pool, state, message_processors, + system_wallet_processor, }) } @@ -154,12 +162,27 @@ impl BtcWatch { to_block = current_l1_block_number; } - let messages = self + let mut messages = self .indexer .process_blocks(self.state.last_processed_bitcoin_block + 1, to_block) .await .map_err(|e| MessageProcessorError::Internal(e.into()))?; + // Re-process blocks if system wallets were updated, since the new wallet state + // may change how subsequent messages are interpreted. + if self + .system_wallet_processor + .process_messages(storage, messages.clone(), &mut self.indexer) + .await + .map_err(|e| MessageProcessorError::Internal(e.into()))? + { + messages = self + .indexer + .process_blocks(self.state.last_processed_bitcoin_block + 1, to_block) + .await + .map_err(|e| MessageProcessorError::Internal(e.into()))?; + } + for processor in self.message_processors.iter_mut() { processor .process_messages(storage, messages.clone(), &mut self.indexer) diff --git a/core/node/via_btc_watch/src/message_processors/governance_upgrade.rs b/core/node/via_btc_watch/src/message_processors/governance_upgrade.rs index 9c6617204..71633ec52 100644 --- a/core/node/via_btc_watch/src/message_processors/governance_upgrade.rs +++ b/core/node/via_btc_watch/src/message_processors/governance_upgrade.rs @@ -52,7 +52,7 @@ impl MessageProcessor for GovernanceUpgradesEventProcessor { storage: &mut Connection<'_, Core>, msgs: Vec, _: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError> { + ) -> Result { let mut upgrades = Vec::new(); for msg in msgs { if let FullInscriptionMessage::SystemContractUpgrade(system_contract_upgrade_msg) = &msg @@ -131,7 +131,7 @@ impl MessageProcessor for GovernanceUpgradesEventProcessor { } let Some(last_upgrade) = upgrades.last() else { - return Ok(()); + return Ok(false); }; let last_version = last_upgrade.0.version; @@ -171,6 +171,6 @@ impl MessageProcessor for GovernanceUpgradesEventProcessor { } self.last_seen_protocol_version = last_version; - Ok(()) + Ok(true) } } diff --git a/core/node/via_btc_watch/src/message_processors/l1_to_l2.rs b/core/node/via_btc_watch/src/message_processors/l1_to_l2.rs index f328f06c8..43d16addf 100644 --- a/core/node/via_btc_watch/src/message_processors/l1_to_l2.rs +++ b/core/node/via_btc_watch/src/message_processors/l1_to_l2.rs @@ -1,6 +1,6 @@ use via_btc_client::{ indexer::BitcoinInscriptionIndexer, - types::{BitcoinAddress, FullInscriptionMessage, L1ToL2Message}, + types::{FullInscriptionMessage, L1ToL2Message}, }; use zksync_dal::{Connection, Core, CoreDal}; use zksync_types::{ @@ -13,16 +13,8 @@ use crate::{ metrics::{InscriptionStage, METRICS}, }; -#[derive(Debug)] -pub struct L1ToL2MessageProcessor { - bridge_address: BitcoinAddress, -} - -impl L1ToL2MessageProcessor { - pub fn new(bridge_address: BitcoinAddress) -> Self { - Self { bridge_address } - } -} +#[derive(Debug, Default)] +pub struct L1ToL2MessageProcessor {} #[async_trait::async_trait] impl MessageProcessor for L1ToL2MessageProcessor { @@ -31,43 +23,37 @@ impl MessageProcessor for L1ToL2MessageProcessor { storage: &mut Connection<'_, Core>, msgs: Vec, _: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError> { + ) -> Result { let mut priority_ops = Vec::new(); for msg in msgs { if let FullInscriptionMessage::L1ToL2Message(l1_to_l2_msg) = msg { - if l1_to_l2_msg - .tx_outputs - .iter() - .any(|output| output.script_pubkey == self.bridge_address.script_pubkey()) - { - let mut tx_id_bytes = l1_to_l2_msg.common.tx_id.as_raw_hash()[..].to_vec(); - tx_id_bytes.reverse(); - let tx_id = H256::from_slice(&tx_id_bytes); - - if storage - .via_transactions_dal() - .transaction_exists_with_txid(&tx_id) - .await - .map_err(|e| MessageProcessorError::DatabaseError(e.to_string()))? - { - tracing::info!( - "Transaction with tx_id {} already processed, skipping", - tx_id - ); - continue; - } - let Some(l1_tx) = self.create_l1_tx_from_message(&l1_to_l2_msg)? else { - tracing::warn!("Invalid deposit, l1 tx_id {}", &l1_to_l2_msg.common.tx_id); - continue; - }; + let mut tx_id_bytes = l1_to_l2_msg.common.tx_id.as_raw_hash()[..].to_vec(); + tx_id_bytes.reverse(); + let tx_id = H256::from_slice(&tx_id_bytes); - priority_ops.push((l1_tx, tx_id)); + if storage + .via_transactions_dal() + .transaction_exists_with_txid(&tx_id) + .await + .map_err(|e| MessageProcessorError::DatabaseError(e.to_string()))? + { + tracing::info!( + "Transaction with tx_id {} already processed, skipping", + tx_id + ); + continue; } + let Some(l1_tx) = self.create_l1_tx_from_message(&l1_to_l2_msg)? else { + tracing::warn!("Invalid deposit, l1 tx_id {}", &l1_to_l2_msg.common.tx_id); + continue; + }; + + priority_ops.push((l1_tx, tx_id)); } } if priority_ops.is_empty() { - return Ok(()); + return Ok(false); } for (new_op, txid) in priority_ops { @@ -80,7 +66,7 @@ impl MessageProcessor for L1ToL2MessageProcessor { .map_err(|e| MessageProcessorError::DatabaseError(e.to_string()))?; } - Ok(()) + Ok(true) } } diff --git a/core/node/via_btc_watch/src/message_processors/mod.rs b/core/node/via_btc_watch/src/message_processors/mod.rs index 3e56d1f9d..3eacd372a 100644 --- a/core/node/via_btc_watch/src/message_processors/mod.rs +++ b/core/node/via_btc_watch/src/message_processors/mod.rs @@ -1,15 +1,17 @@ pub(crate) use governance_upgrade::GovernanceUpgradesEventProcessor; pub(crate) use l1_to_l2::L1ToL2MessageProcessor; +pub(crate) use system_wallet::SystemWalletProcessor; use via_btc_client::{ indexer::BitcoinInscriptionIndexer, types::{BitcoinTxid, FullInscriptionMessage}, }; pub(crate) use votable::VotableMessageProcessor; -use zksync_dal::{Connection, Core}; +use zksync_dal::{Connection, Core, DalError}; use zksync_types::H256; mod governance_upgrade; mod l1_to_l2; +mod system_wallet; mod votable; #[derive(Debug, thiserror::Error)] @@ -20,6 +22,12 @@ pub(super) enum MessageProcessorError { DatabaseError(String), } +impl From for MessageProcessorError { + fn from(err: DalError) -> Self { + MessageProcessorError::DatabaseError(err.to_string()) + } +} + #[async_trait::async_trait] pub(super) trait MessageProcessor: 'static + std::fmt::Debug + Send + Sync { async fn process_messages( @@ -27,7 +35,7 @@ pub(super) trait MessageProcessor: 'static + std::fmt::Debug + Send + Sync { storage: &mut Connection<'_, Core>, msgs: Vec, indexer: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError>; + ) -> Result; } pub(crate) fn convert_txid_to_h256(txid: BitcoinTxid) -> H256 { diff --git a/core/node/via_btc_watch/src/message_processors/system_wallet.rs b/core/node/via_btc_watch/src/message_processors/system_wallet.rs new file mode 100644 index 000000000..235f7514a --- /dev/null +++ b/core/node/via_btc_watch/src/message_processors/system_wallet.rs @@ -0,0 +1,274 @@ +use std::sync::Arc; + +use via_btc_client::{ + client::BitcoinClient, + indexer::{BitcoinInscriptionIndexer, MessageParser}, + traits::BitcoinOps, + types::{ + BitcoinAddress, FullInscriptionMessage, UpdateBridge, UpdateGovernance, UpdateSequencer, + }, +}; +use zksync_dal::{Connection, Core, CoreDal}; +use zksync_types::via_wallet::{SystemWallets, SystemWalletsDetails, WalletInfo, WalletRole}; + +use crate::message_processors::{MessageProcessor, MessageProcessorError}; + +#[derive(Debug)] +pub struct SystemWalletProcessor { + /// BTC client + btc_client: Arc, +} + +impl SystemWalletProcessor { + pub fn new(btc_client: Arc) -> Self { + Self { btc_client } + } +} + +#[async_trait::async_trait] +impl MessageProcessor for SystemWalletProcessor { + async fn process_messages( + &mut self, + storage: &mut Connection<'_, Core>, + msgs: Vec, + indexer: &mut BitcoinInscriptionIndexer, + ) -> Result { + let mut wallets_updated = false; + + let msgs = FullInscriptionMessage::sort_messages(msgs); + + for msg in msgs { + match msg { + FullInscriptionMessage::UpdateGovernance(msg) => { + let updated = self.handle_update_governance(storage, msg, indexer).await?; + // Make sure to not override the wallets_updated if "wallets_updated" it's already true. + if updated { + wallets_updated = updated; + } + } + FullInscriptionMessage::UpdateSequencer(msg) => { + let updated = self.handle_update_sequencer(storage, msg, indexer).await?; + if updated { + wallets_updated = updated; + } + } + FullInscriptionMessage::UpdateBridge(msg) => { + let updated = self + .handle_update_bridge_proposal(storage, msg, indexer) + .await?; + if updated { + wallets_updated = updated; + } + } + + _ => {} + } + } + Ok(wallets_updated) + } +} + +impl SystemWalletProcessor { + async fn handle_update_bridge_proposal( + &self, + storage: &mut Connection<'_, Core>, + update_bridge_msg: UpdateBridge, + indexer: &mut BitcoinInscriptionIndexer, + ) -> Result { + let proposal_tx_id = update_bridge_msg.input.proposal_tx_id; + + let proposal_tx = match self.btc_client.get_transaction(&proposal_tx_id).await { + Ok(proposal_tx) => proposal_tx, + Err(err) => { + tracing::warn!( + "Failed to fetch update bridge proposal transaction: {}, error {}", + proposal_tx_id, + err + ); + return Ok(false); + } + }; + + let mut message_parser = MessageParser::new(self.btc_client.get_network()); + + let messages = message_parser.parse_system_transaction( + &proposal_tx, + update_bridge_msg.common.block_height, + None, + ); + + for message in messages { + match message { + FullInscriptionMessage::UpdateBridgeProposal(update_bridge_msg) => { + let system_wallets_map = + match storage.via_wallet_dal().get_system_wallets_raw().await? { + Some(map) => map, + None => Default::default(), + }; + + let system_wallets = SystemWallets::try_from(system_wallets_map)?; + + let new_bridge_address = match update_bridge_msg + .input + .bridge_musig2_address + .require_network(self.btc_client.get_network()) + { + Ok(address) => address, + Err(err) => { + tracing::error!("Failed to parse bridge address: {}", err); + return Ok(false); + } + }; + + // Skip if bridge already registered + if system_wallets.bridge == new_bridge_address { + tracing::info!("Bridge wallet already exists, skipping"); + return Ok(false); + } + + let mut wallets_details = SystemWalletsDetails::default(); + + wallets_details.0.insert( + WalletRole::Bridge, + WalletInfo { + addresses: vec![new_bridge_address.clone()], + txid: update_bridge_msg.common.tx_id.clone(), + }, + ); + + let verifier_addresses = update_bridge_msg + .input + .verifier_p2wpkh_addresses + .iter() + .map(|addr| addr.clone().assume_checked()) + .collect::>(); + + wallets_details.0.insert( + WalletRole::Verifier, + WalletInfo { + addresses: verifier_addresses.clone(), + txid: update_bridge_msg.common.tx_id.clone(), + }, + ); + + storage + .via_wallet_dal() + .insert_wallets(&wallets_details) + .await?; + + indexer.update_system_wallets( + None, + Some(new_bridge_address), + Some(verifier_addresses), + None, + ); + + tracing::info!("New bridge address updated: {:?}", &wallets_details); + + return Ok(true); + } + _ => return Ok(false), + } + } + Ok(false) + } + + async fn handle_update_sequencer( + &self, + storage: &mut Connection<'_, Core>, + update_sequencer_msg: UpdateSequencer, + indexer: &mut BitcoinInscriptionIndexer, + ) -> Result { + tracing::info!("Received UpdateSequencer message"); + let system_wallets_map = match storage + .via_wallet_dal() + .get_system_wallets_raw() + .await + .unwrap() + { + Some(map) => map, + None => Default::default(), + }; + + let system_wallets = SystemWallets::try_from(system_wallets_map)?; + + let new_sequencer_address = update_sequencer_msg.input.address.assume_checked(); + + // Skip if sequencer already registered + if system_wallets.sequencer == new_sequencer_address { + tracing::info!("Sequencer wallet already exists, skipping"); + return Ok(false); + } + + let mut wallets_details = SystemWalletsDetails::default(); + + wallets_details.0.insert( + WalletRole::Sequencer, + WalletInfo { + addresses: vec![new_sequencer_address.clone()], + txid: update_sequencer_msg.common.tx_id.clone(), + }, + ); + + storage + .via_wallet_dal() + .insert_wallets(&wallets_details) + .await?; + + indexer.update_system_wallets(Some(new_sequencer_address), None, None, None); + + tracing::info!("New sequencer address updated: {:?}", &wallets_details); + + Ok(true) + } + + async fn handle_update_governance( + &self, + storage: &mut Connection<'_, Core>, + update_governance_msg: UpdateGovernance, + indexer: &mut BitcoinInscriptionIndexer, + ) -> Result { + tracing::info!("Received UpdateGovernance message"); + + let system_wallets_map = match storage + .via_wallet_dal() + .get_system_wallets_raw() + .await + .unwrap() + { + Some(map) => map, + None => Default::default(), + }; + + let system_wallets = SystemWallets::try_from(system_wallets_map)?; + + let new_governance_address = update_governance_msg.input.address.assume_checked(); + + // Skip if sequencer already registered + if system_wallets.governance == new_governance_address { + tracing::info!("Sequencer wallet already exists, skipping"); + return Ok(false); + } + + let mut wallets_details = SystemWalletsDetails::default(); + + wallets_details.0.insert( + WalletRole::Gov, + WalletInfo { + addresses: vec![new_governance_address.clone()], + txid: update_governance_msg.common.tx_id.clone(), + }, + ); + + storage + .via_wallet_dal() + .insert_wallets(&wallets_details) + .await?; + + indexer.update_system_wallets(None, None, None, Some(new_governance_address)); + + tracing::info!("New governance address updated: {:?}", &wallets_details); + + Ok(true) + } +} diff --git a/core/node/via_btc_watch/src/message_processors/votable.rs b/core/node/via_btc_watch/src/message_processors/votable.rs index 42703f696..341d13b06 100644 --- a/core/node/via_btc_watch/src/message_processors/votable.rs +++ b/core/node/via_btc_watch/src/message_processors/votable.rs @@ -24,7 +24,7 @@ impl MessageProcessor for VotableMessageProcessor { storage: &mut Connection<'_, Core>, msgs: Vec, indexer: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError> { + ) -> Result { for msg in msgs { match msg { ref f @ FullInscriptionMessage::ValidatorAttestation(ref attestation_msg) => { @@ -104,6 +104,6 @@ impl MessageProcessor for VotableMessageProcessor { _ => (), } } - Ok(()) + Ok(true) } } diff --git a/core/node/via_btc_watch/src/test/mod.rs b/core/node/via_btc_watch/src/test/mod.rs new file mode 100644 index 000000000..a574b6e4b --- /dev/null +++ b/core/node/via_btc_watch/src/test/mod.rs @@ -0,0 +1 @@ +mod system_wallets; diff --git a/core/node/via_btc_watch/src/test/system_wallets.rs b/core/node/via_btc_watch/src/test/system_wallets.rs new file mode 100644 index 000000000..332fd30d7 --- /dev/null +++ b/core/node/via_btc_watch/src/test/system_wallets.rs @@ -0,0 +1,286 @@ +#[cfg(test)] +mod tests { + use std::{str::FromStr, sync::Arc}; + + use via_btc_client::types::BitcoinAddress; + use via_test_utils::utils::{ + create_update_bridge_inscription, create_update_governance_inscription, + create_update_sequencer_inscription, random_bitcoin_wallet, test_bitcoin_client, + test_create_indexer, test_wallets, + }; + use zksync_dal::{ConnectionPool, Core, CoreDal}; + use zksync_types::via_wallet::{SystemWallets, SystemWalletsDetails}; + + use crate::{message_processors::SystemWalletProcessor, MessageProcessor}; + + #[tokio::test] + async fn test_update_sequencer_wallet() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + let new_sequencer_address = random_bitcoin_wallet().1; + let msg = create_update_sequencer_inscription(new_sequencer_address.clone()); + + let old_wallets = indexer.get_state(); + + processor + .process_messages(&mut pool.connection().await?, vec![msg], &mut indexer) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.sequencer, new_sequencer_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } + + #[tokio::test] + async fn test_update_governance_wallet() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + let new_governance_address = random_bitcoin_wallet().1; + let msg = create_update_governance_inscription(new_governance_address.clone()); + + let old_wallets = indexer.get_state(); + + processor + .process_messages(&mut pool.connection().await?, vec![msg], &mut indexer) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.governance, new_governance_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } + + #[tokio::test] + async fn test_update_bridge_wallet_with_4_new_verifiers_when_old_3() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + let new_bridge_address = BitcoinAddress::from_str( + &"bcrt1pcx974cg2w66cqhx67zadf85t8k4sd2wp68l8x8agd3aj4tuegsgsz97amg", + )? + .assume_checked(); + + let new_verifier_1 = random_bitcoin_wallet().1; + let new_verifier_2 = random_bitcoin_wallet().1; + let new_verifier_3 = random_bitcoin_wallet().1; + let new_verifier_4 = random_bitcoin_wallet().1; + + let new_verifiers = vec![ + new_verifier_1, + new_verifier_2, + new_verifier_3, + new_verifier_4, + ]; + + let msg = + create_update_bridge_inscription(new_bridge_address.clone(), new_verifiers.clone()) + .await?; + + let old_wallets = indexer.get_state(); + + processor + .process_messages(&mut pool.connection().await?, vec![msg], &mut indexer) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.bridge, new_bridge_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } + + #[tokio::test] + async fn test_update_bridge_wallet_with_2_new_verifiers_when_old_3() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + let new_bridge_address = BitcoinAddress::from_str( + &"bcrt1pcx974cg2w66cqhx67zadf85t8k4sd2wp68l8x8agd3aj4tuegsgsz97amg", + )? + .assume_checked(); + + let new_verifier_1 = random_bitcoin_wallet().1; + let new_verifier_2 = random_bitcoin_wallet().1; + + let new_verifiers = vec![new_verifier_1, new_verifier_2]; + + let msg = + create_update_bridge_inscription(new_bridge_address.clone(), new_verifiers.clone()) + .await?; + + let old_wallets = indexer.get_state(); + + processor + .process_messages(&mut pool.connection().await?, vec![msg], &mut indexer) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.bridge, new_bridge_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } + + #[tokio::test] + async fn test_update_governance_bridge_wallet_and_sequencer() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + + let new_governance_address = random_bitcoin_wallet().1; + let gov_msg = create_update_governance_inscription(new_governance_address.clone()); + + let new_sequencer_address = random_bitcoin_wallet().1; + let sequencer_msg = create_update_sequencer_inscription(new_sequencer_address.clone()); + + let new_bridge_address = BitcoinAddress::from_str( + &"bcrt1pcx974cg2w66cqhx67zadf85t8k4sd2wp68l8x8agd3aj4tuegsgsz97amg", + )? + .assume_checked(); + + let new_verifier_1 = random_bitcoin_wallet().1; + let new_verifier_2 = random_bitcoin_wallet().1; + let new_verifier_3 = random_bitcoin_wallet().1; + let new_verifier_4 = random_bitcoin_wallet().1; + + let new_verifiers = vec![ + new_verifier_1, + new_verifier_2, + new_verifier_3, + new_verifier_4, + ]; + + let bridge_msg = + create_update_bridge_inscription(new_bridge_address.clone(), new_verifiers.clone()) + .await?; + + let old_wallets = indexer.get_state(); + + processor + .process_messages( + &mut pool.connection().await?, + vec![bridge_msg, sequencer_msg, gov_msg], + &mut indexer, + ) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.bridge, new_bridge_address); + assert_eq!(new_wallets.sequencer, new_sequencer_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } +} diff --git a/core/node/via_node_storage_init/Cargo.toml b/core/node/via_node_storage_init/Cargo.toml new file mode 100644 index 000000000..6e12dfcf9 --- /dev/null +++ b/core/node/via_node_storage_init/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "via_node_storage_init" +description = "VIA node storage initialization" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +zksync_config.workspace = true +zksync_dal.workspace = true +via_btc_client.workspace = true +zksync_types.workspace = true + +anyhow.workspace = true +async-trait.workspace = true +tokio.workspace = true +tracing.workspace = true diff --git a/core/node/via_node_storage_init/README.md b/core/node/via_node_storage_init/README.md new file mode 100644 index 000000000..8178710ca --- /dev/null +++ b/core/node/via_node_storage_init/README.md @@ -0,0 +1,3 @@ +# `via_node_storage_init` + +A set of actions to ensure that any VIA node has initialized storage and can start running. diff --git a/via_verifier/lib/via_musig2/Cargo.toml b/via_verifier/lib/via_musig2/Cargo.toml index f4a5b63b1..042430092 100644 --- a/via_verifier/lib/via_musig2/Cargo.toml +++ b/via_verifier/lib/via_musig2/Cargo.toml @@ -47,6 +47,7 @@ zksync_config.workspace = true mockall = "0.13.0" bitcoincore-rpc = "0.19.0" rand = "0.8" +clap = { workspace = true, features = ["derive"] } [[example]] name = "key_generation_setup" diff --git a/via_verifier/lib/via_musig2/examples/withdrawal.rs b/via_verifier/lib/via_musig2/examples/withdrawal.rs index 624e584aa..6f6b3a8fa 100644 --- a/via_verifier/lib/via_musig2/examples/withdrawal.rs +++ b/via_verifier/lib/via_musig2/examples/withdrawal.rs @@ -227,7 +227,7 @@ async fn main() -> Result<(), Box> { /// We lock the spend output to the key associated with this address. /// /// (FWIW this is an arbitrary mainnet address from block 805222.) -fn receivers_address() -> Address { +pub(crate) fn receivers_address() -> Address { Address::from_str("bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va") .expect("a valid address") .require_network(Network::Bitcoin) diff --git a/via_verifier/lib/via_musig2/src/lib.rs b/via_verifier/lib/via_musig2/src/lib.rs index d89850c79..68349428b 100644 --- a/via_verifier/lib/via_musig2/src/lib.rs +++ b/via_verifier/lib/via_musig2/src/lib.rs @@ -1,7 +1,7 @@ use std::{fmt, str::FromStr}; use anyhow::Context; -use bitcoin::{PrivateKey, TapTweakHash}; +use bitcoin::{PrivateKey, TapNodeHash, TapTweakHash}; use musig2::{ verify_single, CompactSignature, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonceSpices, SecondRound, @@ -75,6 +75,7 @@ impl Signer { secret_key: SecretKey, signer_index: usize, all_pubkeys: Vec, + merkle_root: Option, ) -> Result { let secp = Secp256k1::new(); let public_key = PublicKey::from_secret_key(&secp, &secret_key); @@ -105,7 +106,7 @@ impl Signer { })?; // Calculate taproot tweak - let tap_tweak = TapTweakHash::from_key_and_tweak(internal_key, None); + let tap_tweak = TapTweakHash::from_key_and_tweak(internal_key, merkle_root); let tweak = tap_tweak.to_scalar(); let tweak_bytes = tweak.to_be_bytes(); let musig2_compatible_tweak = secp256k1_musig2::Scalar::from_be_bytes(tweak_bytes).unwrap(); @@ -280,7 +281,35 @@ pub fn get_signer( } } - let signer = Signer::new(secret_key, signer_index, all_pubkeys.clone())?; + let signer = Signer::new(secret_key, signer_index, all_pubkeys.clone(), None)?; + Ok(signer) +} + +pub fn get_signer_with_merkle_root( + private_key_wif: &str, + verifiers_pub_keys_str: Vec, + merkle_root: Option, +) -> anyhow::Result { + let private_key = PrivateKey::from_wif(private_key_wif)?; + let secret_key = + secp256k1_musig2::SecretKey::from_byte_array(&private_key.inner.secret_bytes()) + .with_context(|| "Error to compute the coordinator sk")?; + let secp = secp256k1_musig2::Secp256k1::new(); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + let mut all_pubkeys = Vec::new(); + + let mut signer_index = 0; + + for (i, key) in verifiers_pub_keys_str.iter().enumerate() { + let pk = PublicKey::from_str(key)?; + all_pubkeys.push(pk); + if pk == public_key { + signer_index = i; + } + } + + let signer = Signer::new(secret_key, signer_index, all_pubkeys.clone(), merkle_root)?; Ok(signer) } @@ -302,8 +331,8 @@ mod tests { let pubkeys = vec![public_key_1, public_key_2]; - let mut signer1 = Signer::new(secret_key_1, 0, pubkeys.clone())?; - let mut signer2 = Signer::new(secret_key_2, 1, pubkeys)?; + let mut signer1 = Signer::new(secret_key_1, 0, pubkeys.clone(), None)?; + let mut signer2 = Signer::new(secret_key_2, 1, pubkeys, None)?; // Generate and exchange nonces let message = b"test message".to_vec(); diff --git a/via_verifier/lib/via_musig2/src/utils.rs b/via_verifier/lib/via_musig2/src/utils.rs index 4200e0116..21cec938b 100644 --- a/via_verifier/lib/via_musig2/src/utils.rs +++ b/via_verifier/lib/via_musig2/src/utils.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use bitcoin::TapTweakHash; +use bitcoin::{TapNodeHash, TapTweakHash}; use musig2::{verify_partial, AggNonce, KeyAggContext, PartialSignature, PubNonce}; use secp256k1_musig2::PublicKey; @@ -11,6 +11,7 @@ pub fn verify_partial_signature( pubkeys_str: Vec, partial_sig: PartialSignature, message: &[u8], + merkle_root: Option, ) -> anyhow::Result<()> { let pubkeys = pubkeys_str .iter() @@ -27,7 +28,7 @@ pub fn verify_partial_signature( let internal_key = bitcoin::XOnlyPublicKey::from_slice(&xonly_agg_key.serialize())?; // Calculate taproot tweak - let tap_tweak = TapTweakHash::from_key_and_tweak(internal_key, None); + let tap_tweak = TapTweakHash::from_key_and_tweak(internal_key, merkle_root); let tweak = tap_tweak.to_scalar(); let tweak_bytes = tweak.to_be_bytes(); let tweak = secp256k1_musig2::Scalar::from_be_bytes(tweak_bytes).unwrap(); @@ -283,6 +284,7 @@ mod tests { pubkeys_str.clone(), PartialSignature::from_slice(&partial_sig_2.to_vec())?, sighash.as_byte_array(), + None, )?; verify_partial_signature( @@ -295,6 +297,7 @@ mod tests { pubkeys_str.clone(), PartialSignature::from_slice(&partial_sig_1.to_vec())?, sighash.as_byte_array(), + None, )?; verify_partial_signature( @@ -307,6 +310,7 @@ mod tests { pubkeys_str.clone(), PartialSignature::from_slice(&partial_sig_3.to_vec())?, sighash.as_byte_array(), + None, )?; second_round_1.receive_signature(1, Scalar::from_slice(&partial_sig_2).unwrap())?; diff --git a/via_verifier/node/via_btc_watch/src/lib.rs b/via_verifier/node/via_btc_watch/src/lib.rs index 5d067cba7..579e71377 100644 --- a/via_verifier/node/via_btc_watch/src/lib.rs +++ b/via_verifier/node/via_btc_watch/src/lib.rs @@ -13,7 +13,9 @@ use via_verifier_types::protocol_version::check_if_supported_sequencer_version; use zksync_config::{configs::via_btc_watch::L1_BLOCKS_CHUNK, ViaBtcWatchConfig}; use self::message_processors::{MessageProcessor, MessageProcessorError}; -use crate::message_processors::{L1ToL2MessageProcessor, VerifierMessageProcessor}; +use crate::message_processors::{ + L1ToL2MessageProcessor, SystemWalletProcessor, VerifierMessageProcessor, +}; #[cfg(test)] mod test; @@ -29,6 +31,7 @@ pub struct VerifierBtcWatch { indexer: BitcoinInscriptionIndexer, last_processed_bitcoin_block: u32, pool: ConnectionPool, + system_wallet_processor: Box, message_processors: Vec>, } @@ -53,9 +56,13 @@ impl VerifierBtcWatch { drop(storage); + let system_wallet_processor = Box::new(SystemWalletProcessor::new(btc_client.clone())); + let message_processors: Vec> = vec![ Box::new(GovernanceUpgradesEventProcessor::new(btc_client)), - Box::new(L1ToL2MessageProcessor::new(indexer.get_state().0)), + Box::new(L1ToL2MessageProcessor::new( + indexer.get_state().bridge.clone(), + )), Box::new(VerifierMessageProcessor::new(zk_agreement_threshold)), Box::new(WithdrawalProcessor::new()), ]; @@ -65,6 +72,7 @@ impl VerifierBtcWatch { indexer, last_processed_bitcoin_block: state.last_processed_bitcoin_block, pool, + system_wallet_processor, message_processors, }) } @@ -157,12 +165,27 @@ impl VerifierBtcWatch { to_block = current_l1_block_number; } - let messages = self + let mut messages = self .indexer .process_blocks(self.last_processed_bitcoin_block + 1, to_block) .await .map_err(|e| MessageProcessorError::Internal(e.into()))?; + // Re-process blocks if system wallets were updated, since the new wallet state + // may change how subsequent messages are interpreted. + if self + .system_wallet_processor + .process_messages(storage, messages.clone(), &mut self.indexer) + .await + .map_err(|e| MessageProcessorError::Internal(e.into()))? + { + messages = self + .indexer + .process_blocks(self.last_processed_bitcoin_block + 1, to_block) + .await + .map_err(|e| MessageProcessorError::Internal(e.into()))?; + } + for processor in self.message_processors.iter_mut() { processor .process_messages(storage, messages.clone(), &mut self.indexer) diff --git a/via_verifier/node/via_btc_watch/src/message_processors/governance_upgrade.rs b/via_verifier/node/via_btc_watch/src/message_processors/governance_upgrade.rs index 9214cc4a8..02d9e2583 100644 --- a/via_verifier/node/via_btc_watch/src/message_processors/governance_upgrade.rs +++ b/via_verifier/node/via_btc_watch/src/message_processors/governance_upgrade.rs @@ -43,7 +43,7 @@ impl MessageProcessor for GovernanceUpgradesEventProcessor { storage: &mut Connection<'_, Verifier>, msgs: Vec, _: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError> { + ) -> Result { let mut upgrades = Vec::new(); for msg in msgs { if let FullInscriptionMessage::SystemContractUpgrade(system_contract_upgrade_msg) = &msg @@ -135,6 +135,6 @@ impl MessageProcessor for GovernanceUpgradesEventProcessor { .await .map_err(DalError::generalize)?; } - Ok(()) + Ok(true) } } diff --git a/via_verifier/node/via_btc_watch/src/message_processors/l1_to_l2.rs b/via_verifier/node/via_btc_watch/src/message_processors/l1_to_l2.rs index f0099d341..a89184fee 100644 --- a/via_verifier/node/via_btc_watch/src/message_processors/l1_to_l2.rs +++ b/via_verifier/node/via_btc_watch/src/message_processors/l1_to_l2.rs @@ -38,7 +38,7 @@ impl MessageProcessor for L1ToL2MessageProcessor { storage: &mut Connection<'_, Verifier>, msgs: Vec, _: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError> { + ) -> Result { let mut priority_ops = Vec::new(); for msg in msgs { @@ -75,7 +75,7 @@ impl MessageProcessor for L1ToL2MessageProcessor { } if priority_ops.is_empty() { - return Ok(()); + return Ok(false); } for new_op in priority_ops { @@ -93,7 +93,7 @@ impl MessageProcessor for L1ToL2MessageProcessor { .map_err(|e| MessageProcessorError::DatabaseError(e.to_string()))?; } - Ok(()) + Ok(true) } } diff --git a/via_verifier/node/via_btc_watch/src/message_processors/mod.rs b/via_verifier/node/via_btc_watch/src/message_processors/mod.rs index bca7d5677..4d626334c 100644 --- a/via_verifier/node/via_btc_watch/src/message_processors/mod.rs +++ b/via_verifier/node/via_btc_watch/src/message_processors/mod.rs @@ -1,5 +1,6 @@ pub(crate) use governance_upgrade::GovernanceUpgradesEventProcessor; pub(crate) use l1_to_l2::L1ToL2MessageProcessor; +pub(crate) use system_wallet::SystemWalletProcessor; pub(crate) use verifier::VerifierMessageProcessor; use via_btc_client::{ indexer::BitcoinInscriptionIndexer, @@ -11,6 +12,7 @@ use zksync_types::H256; mod governance_upgrade; mod l1_to_l2; +mod system_wallet; mod verifier; mod withdrawal; @@ -43,7 +45,7 @@ pub(super) trait MessageProcessor: 'static + std::fmt::Debug + Send + Sync { storage: &mut Connection<'_, Verifier>, msgs: Vec, indexer: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError>; + ) -> Result; } pub(crate) fn convert_txid_to_h256(txid: BitcoinTxid) -> H256 { diff --git a/via_verifier/node/via_btc_watch/src/message_processors/system_wallet.rs b/via_verifier/node/via_btc_watch/src/message_processors/system_wallet.rs new file mode 100644 index 000000000..5ef42537d --- /dev/null +++ b/via_verifier/node/via_btc_watch/src/message_processors/system_wallet.rs @@ -0,0 +1,275 @@ +use std::sync::Arc; + +use via_btc_client::{ + client::BitcoinClient, + indexer::{BitcoinInscriptionIndexer, MessageParser}, + traits::BitcoinOps, + types::{ + BitcoinAddress, FullInscriptionMessage, UpdateBridge, UpdateGovernance, UpdateSequencer, + }, +}; +use via_verifier_dal::{Connection, Verifier, VerifierDal}; +use zksync_types::via_wallet::{SystemWallets, SystemWalletsDetails, WalletInfo, WalletRole}; + +use crate::message_processors::{MessageProcessor, MessageProcessorError}; + +#[derive(Debug)] +pub struct SystemWalletProcessor { + /// BTC client + btc_client: Arc, +} + +impl SystemWalletProcessor { + pub fn new(btc_client: Arc) -> Self { + Self { btc_client } + } +} + +#[async_trait::async_trait] +impl MessageProcessor for SystemWalletProcessor { + async fn process_messages( + &mut self, + storage: &mut Connection<'_, Verifier>, + msgs: Vec, + indexer: &mut BitcoinInscriptionIndexer, + ) -> Result { + let mut wallets_updated = false; + + let msgs = FullInscriptionMessage::sort_messages(msgs); + + for msg in msgs { + match msg { + FullInscriptionMessage::UpdateGovernance(msg) => { + let updated = self.handle_update_governance(storage, msg, indexer).await?; + // Make sure to not override the wallets_updated if "wallets_updated" it's already true. + if updated { + wallets_updated = updated; + } + } + FullInscriptionMessage::UpdateSequencer(msg) => { + let updated = self.handle_update_sequencer(storage, msg, indexer).await?; + if updated { + wallets_updated = updated; + } + } + FullInscriptionMessage::UpdateBridge(msg) => { + let updated = self + .handle_update_bridge_proposal(storage, msg, indexer) + .await?; + if updated { + wallets_updated = updated; + } + } + + _ => {} + } + } + Ok(wallets_updated) + } +} + +impl SystemWalletProcessor { + async fn handle_update_bridge_proposal( + &self, + storage: &mut Connection<'_, Verifier>, + update_bridge_msg: UpdateBridge, + indexer: &mut BitcoinInscriptionIndexer, + ) -> Result { + let proposal_tx_id = update_bridge_msg.input.proposal_tx_id; + + let proposal_tx = match self.btc_client.get_transaction(&proposal_tx_id).await { + Ok(proposal_tx) => proposal_tx, + Err(err) => { + tracing::warn!( + "Failed to fetch update bridge proposal transaction: {}, error {}", + proposal_tx_id, + err + ); + return Ok(false); + } + }; + + let mut message_parser = MessageParser::new(self.btc_client.get_network()); + + let messages = message_parser.parse_system_transaction( + &proposal_tx, + update_bridge_msg.common.block_height, + None, + ); + + for message in messages { + match message { + FullInscriptionMessage::UpdateBridgeProposal(update_bridge_msg) => { + let system_wallets_map = + match storage.via_wallet_dal().get_system_wallets_raw().await? { + Some(map) => map, + None => Default::default(), + }; + + let system_wallets = SystemWallets::try_from(system_wallets_map)?; + + let new_bridge_address = match update_bridge_msg + .input + .bridge_musig2_address + .require_network(self.btc_client.get_network()) + { + Ok(address) => address, + Err(err) => { + tracing::error!("Failed to parse bridge address: {}", err); + return Ok(false); + } + }; + + // Skip if bridge already registered + if system_wallets.bridge == new_bridge_address { + tracing::info!("Bridge wallet already exists, skipping"); + return Ok(false); + } + + let mut wallets_details = SystemWalletsDetails::default(); + + wallets_details.0.insert( + WalletRole::Bridge, + WalletInfo { + addresses: vec![new_bridge_address.clone()], + txid: update_bridge_msg.common.tx_id.clone(), + }, + ); + + let verifier_addresses = update_bridge_msg + .input + .verifier_p2wpkh_addresses + .iter() + .map(|addr| addr.clone().assume_checked()) + .collect::>(); + + wallets_details.0.insert( + WalletRole::Verifier, + WalletInfo { + addresses: verifier_addresses.clone(), + txid: update_bridge_msg.common.tx_id.clone(), + }, + ); + + storage + .via_wallet_dal() + .insert_wallets(&wallets_details) + .await?; + + indexer.update_system_wallets( + None, + Some(new_bridge_address), + Some(verifier_addresses), + None, + ); + + tracing::info!("New bridge address updated: {:?}", &wallets_details); + + return Ok(true); + } + _ => return Ok(false), + } + } + Ok(false) + } + + async fn handle_update_sequencer( + &self, + storage: &mut Connection<'_, Verifier>, + update_sequencer_msg: UpdateSequencer, + indexer: &mut BitcoinInscriptionIndexer, + ) -> Result { + tracing::info!("Received UpdateSequencer message"); + + let system_wallets_map = match storage + .via_wallet_dal() + .get_system_wallets_raw() + .await + .unwrap() + { + Some(map) => map, + None => Default::default(), + }; + + let system_wallets = SystemWallets::try_from(system_wallets_map)?; + + let new_sequencer_address = update_sequencer_msg.input.address.assume_checked(); + + // Skip if sequencer already registered + if system_wallets.sequencer == new_sequencer_address { + tracing::info!("Sequencer wallet already exists, skipping"); + return Ok(false); + } + + let mut wallets_details = SystemWalletsDetails::default(); + + wallets_details.0.insert( + WalletRole::Sequencer, + WalletInfo { + addresses: vec![new_sequencer_address.clone()], + txid: update_sequencer_msg.common.tx_id.clone(), + }, + ); + + storage + .via_wallet_dal() + .insert_wallets(&wallets_details) + .await?; + + indexer.update_system_wallets(Some(new_sequencer_address), None, None, None); + + tracing::info!("New sequencer address updated: {:?}", &wallets_details); + + Ok(true) + } + + async fn handle_update_governance( + &self, + storage: &mut Connection<'_, Verifier>, + update_governance_msg: UpdateGovernance, + indexer: &mut BitcoinInscriptionIndexer, + ) -> Result { + tracing::info!("Received UpdateGovernance message"); + + let system_wallets_map = match storage + .via_wallet_dal() + .get_system_wallets_raw() + .await + .unwrap() + { + Some(map) => map, + None => Default::default(), + }; + + let system_wallets = SystemWallets::try_from(system_wallets_map)?; + + let new_governance_address = update_governance_msg.input.address.assume_checked(); + + // Skip if sequencer already registered + if system_wallets.governance == new_governance_address { + tracing::info!("Sequencer wallet already exists, skipping"); + return Ok(false); + } + + let mut wallets_details = SystemWalletsDetails::default(); + + wallets_details.0.insert( + WalletRole::Gov, + WalletInfo { + addresses: vec![new_governance_address.clone()], + txid: update_governance_msg.common.tx_id.clone(), + }, + ); + + storage + .via_wallet_dal() + .insert_wallets(&wallets_details) + .await?; + + indexer.update_system_wallets(None, None, None, Some(new_governance_address)); + + tracing::info!("New governance address updated: {:?}", &wallets_details); + + Ok(true) + } +} diff --git a/via_verifier/node/via_btc_watch/src/message_processors/verifier.rs b/via_verifier/node/via_btc_watch/src/message_processors/verifier.rs index 6580ab224..b74d6d586 100644 --- a/via_verifier/node/via_btc_watch/src/message_processors/verifier.rs +++ b/via_verifier/node/via_btc_watch/src/message_processors/verifier.rs @@ -24,7 +24,7 @@ impl MessageProcessor for VerifierMessageProcessor { storage: &mut Connection<'_, Verifier>, msgs: Vec, indexer: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError> { + ) -> Result { for msg in msgs { match msg { FullInscriptionMessage::ProofDAReference(ref proof_msg) => { @@ -202,6 +202,6 @@ impl MessageProcessor for VerifierMessageProcessor { _ => (), } } - Ok(()) + Ok(false) } } diff --git a/via_verifier/node/via_btc_watch/src/message_processors/withdrawal.rs b/via_verifier/node/via_btc_watch/src/message_processors/withdrawal.rs index 3eb5da114..0898b2288 100644 --- a/via_verifier/node/via_btc_watch/src/message_processors/withdrawal.rs +++ b/via_verifier/node/via_btc_watch/src/message_processors/withdrawal.rs @@ -24,7 +24,7 @@ impl MessageProcessor for WithdrawalProcessor { storage: &mut Connection<'_, Verifier>, msgs: Vec, _: &mut BitcoinInscriptionIndexer, - ) -> Result<(), MessageProcessorError> { + ) -> Result { for msg in msgs { if let FullInscriptionMessage::BridgeWithdrawal(withdrawal_msg) = msg { tracing::info!("Processing withdrawal bridge transaction..."); @@ -62,7 +62,7 @@ impl MessageProcessor for WithdrawalProcessor { "Withdrawal already processed for batch {}. Skipping.", l1_batch_number ); - return Ok(()); + continue; } return Err(MessageProcessorError::SyncError(format!( @@ -90,6 +90,6 @@ impl MessageProcessor for WithdrawalProcessor { } } - Ok(()) + Ok(true) } } diff --git a/via_verifier/node/via_btc_watch/src/test/mod.rs b/via_verifier/node/via_btc_watch/src/test/mod.rs index f6de38ffc..6ffa95f8f 100644 --- a/via_verifier/node/via_btc_watch/src/test/mod.rs +++ b/via_verifier/node/via_btc_watch/src/test/mod.rs @@ -1,2 +1,2 @@ -#[cfg(test)] +mod system_wallets; mod verifier; diff --git a/via_verifier/node/via_btc_watch/src/test/system_wallets.rs b/via_verifier/node/via_btc_watch/src/test/system_wallets.rs new file mode 100644 index 000000000..00c8c610b --- /dev/null +++ b/via_verifier/node/via_btc_watch/src/test/system_wallets.rs @@ -0,0 +1,286 @@ +#[cfg(test)] +mod tests { + use std::{str::FromStr, sync::Arc}; + + use via_btc_client::types::BitcoinAddress; + use via_test_utils::utils::{ + create_update_bridge_inscription, create_update_governance_inscription, + create_update_sequencer_inscription, random_bitcoin_wallet, test_bitcoin_client, + test_create_indexer, test_wallets, + }; + use via_verifier_dal::{ConnectionPool, Verifier, VerifierDal}; + use zksync_types::via_wallet::{SystemWallets, SystemWalletsDetails}; + + use crate::{message_processors::SystemWalletProcessor, MessageProcessor}; + + #[tokio::test] + async fn test_update_sequencer_wallet() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + let new_sequencer_address = random_bitcoin_wallet().1; + let msg = create_update_sequencer_inscription(new_sequencer_address.clone()); + + let old_wallets = indexer.get_state(); + + processor + .process_messages(&mut pool.connection().await?, vec![msg], &mut indexer) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.sequencer, new_sequencer_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } + + #[tokio::test] + async fn test_update_governance_wallet() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + let new_governance_address = random_bitcoin_wallet().1; + let msg = create_update_governance_inscription(new_governance_address.clone()); + + let old_wallets = indexer.get_state(); + + processor + .process_messages(&mut pool.connection().await?, vec![msg], &mut indexer) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.governance, new_governance_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } + + #[tokio::test] + async fn test_update_bridge_wallet_with_4_new_verifiers_when_old_3() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + let new_bridge_address = BitcoinAddress::from_str( + &"bcrt1pcx974cg2w66cqhx67zadf85t8k4sd2wp68l8x8agd3aj4tuegsgsz97amg", + )? + .assume_checked(); + + let new_verifier_1 = random_bitcoin_wallet().1; + let new_verifier_2 = random_bitcoin_wallet().1; + let new_verifier_3 = random_bitcoin_wallet().1; + let new_verifier_4 = random_bitcoin_wallet().1; + + let new_verifiers = vec![ + new_verifier_1, + new_verifier_2, + new_verifier_3, + new_verifier_4, + ]; + + let msg = + create_update_bridge_inscription(new_bridge_address.clone(), new_verifiers.clone()) + .await?; + + let old_wallets = indexer.get_state(); + + processor + .process_messages(&mut pool.connection().await?, vec![msg], &mut indexer) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.bridge, new_bridge_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } + + #[tokio::test] + async fn test_update_bridge_wallet_with_2_new_verifiers_when_old_3() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + let new_bridge_address = BitcoinAddress::from_str( + &"bcrt1pcx974cg2w66cqhx67zadf85t8k4sd2wp68l8x8agd3aj4tuegsgsz97amg", + )? + .assume_checked(); + + let new_verifier_1 = random_bitcoin_wallet().1; + let new_verifier_2 = random_bitcoin_wallet().1; + + let new_verifiers = vec![new_verifier_1, new_verifier_2]; + + let msg = + create_update_bridge_inscription(new_bridge_address.clone(), new_verifiers.clone()) + .await?; + + let old_wallets = indexer.get_state(); + + processor + .process_messages(&mut pool.connection().await?, vec![msg], &mut indexer) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.bridge, new_bridge_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } + + #[tokio::test] + async fn test_update_governance_bridge_wallet_and_sequencer() -> anyhow::Result<()> { + let pool = ConnectionPool::::test_pool().await; + let mut indexer = test_create_indexer(); + + let system_wallet_map = SystemWalletsDetails::try_from(test_wallets())?; + pool.connection() + .await? + .via_wallet_dal() + .insert_wallets(&system_wallet_map) + .await?; + + let mut processor = SystemWalletProcessor::new(Arc::new(test_bitcoin_client())); + + let new_governance_address = random_bitcoin_wallet().1; + let gov_msg = create_update_governance_inscription(new_governance_address.clone()); + + let new_sequencer_address = random_bitcoin_wallet().1; + let sequencer_msg = create_update_sequencer_inscription(new_sequencer_address.clone()); + + let new_bridge_address = BitcoinAddress::from_str( + &"bcrt1pcx974cg2w66cqhx67zadf85t8k4sd2wp68l8x8agd3aj4tuegsgsz97amg", + )? + .assume_checked(); + + let new_verifier_1 = random_bitcoin_wallet().1; + let new_verifier_2 = random_bitcoin_wallet().1; + let new_verifier_3 = random_bitcoin_wallet().1; + let new_verifier_4 = random_bitcoin_wallet().1; + + let new_verifiers = vec![ + new_verifier_1, + new_verifier_2, + new_verifier_3, + new_verifier_4, + ]; + + let bridge_msg = + create_update_bridge_inscription(new_bridge_address.clone(), new_verifiers.clone()) + .await?; + + let old_wallets = indexer.get_state(); + + processor + .process_messages( + &mut pool.connection().await?, + vec![bridge_msg, sequencer_msg, gov_msg], + &mut indexer, + ) + .await?; + + let new_wallets = indexer.get_state(); + + assert_ne!(new_wallets, old_wallets); + assert_eq!(new_wallets.bridge, new_bridge_address); + assert_eq!(new_wallets.sequencer, new_sequencer_address); + + let system_wallets_db_map = pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + .unwrap(); + + let system_wallets_db = Arc::new(SystemWallets::try_from(system_wallets_db_map.clone())?); + + assert_eq!(system_wallets_db, new_wallets); + + Ok(()) + } +} diff --git a/via_verifier/node/via_btc_watch/src/test/verifier.rs b/via_verifier/node/via_btc_watch/src/test/verifier.rs index f692a6f0a..18b72c571 100644 --- a/via_verifier/node/via_btc_watch/src/test/verifier.rs +++ b/via_verifier/node/via_btc_watch/src/test/verifier.rs @@ -1,18 +1,17 @@ #[cfg(test)] mod tests { + use via_test_utils::utils::{ + create_chained_inscriptions, test_create_indexer, test_verifier_add_1, test_verifier_add_2, + }; use via_verifier_dal::{ConnectionPool, Verifier, VerifierDal}; use zksync_types::H256; use crate::{message_processors::VerifierMessageProcessor, MessageProcessor}; - use via_test_utils::utils::{ - create_chained_inscriptions, test_create_indexer, test_verifier_add_1, test_verifier_add_2, - }; - #[tokio::test] async fn test_insert_first_batch() -> anyhow::Result<()> { let pool = ConnectionPool::::test_pool().await; - let mut indexer = test_create_indexer().await?; + let mut indexer = test_create_indexer(); let start = 1; let end = 1; @@ -46,7 +45,7 @@ mod tests { #[tokio::test] async fn test_insert_two_times_first_batch() -> anyhow::Result<()> { let pool = ConnectionPool::::test_pool().await; - let mut indexer = test_create_indexer().await?; + let mut indexer = test_create_indexer(); let start = 1; let end = 1; @@ -84,7 +83,7 @@ mod tests { #[tokio::test] async fn test_insert_multiple_batches() -> anyhow::Result<()> { let pool = ConnectionPool::::test_pool().await; - let mut indexer = test_create_indexer().await?; + let mut indexer = test_create_indexer(); let mut processor = VerifierMessageProcessor::new(1.0); let start = 1; @@ -121,7 +120,7 @@ mod tests { #[tokio::test] async fn test_should_fail_to_insert_batch_with_invalid_prev_batch_hash() -> anyhow::Result<()> { let pool = ConnectionPool::::test_pool().await; - let mut indexer = test_create_indexer().await?; + let mut indexer = test_create_indexer(); let mut processor = VerifierMessageProcessor::new(1.0); let start = 1; let end = 2; @@ -303,7 +302,7 @@ mod tests { #[tokio::test] async fn test_should_not_insert_batch_zero() -> anyhow::Result<()> { let pool = ConnectionPool::::test_pool().await; - let mut indexer = test_create_indexer().await?; + let mut indexer = test_create_indexer(); let start = 0; let end = 0; @@ -332,7 +331,7 @@ mod tests { ) -> anyhow::Result<()> { let pool = ConnectionPool::::test_pool().await; - let mut indexer = test_create_indexer().await?; + let mut indexer = test_create_indexer(); let mut processor = VerifierMessageProcessor::new(1.0); let start = 1; let end = 2; diff --git a/via_verifier/node/via_verifier_coordinator/src/coordinator/api_impl.rs b/via_verifier/node/via_verifier_coordinator/src/coordinator/api_impl.rs index a42940e7d..2123ded07 100644 --- a/via_verifier/node/via_verifier_coordinator/src/coordinator/api_impl.rs +++ b/via_verifier/node/via_verifier_coordinator/src/coordinator/api_impl.rs @@ -194,7 +194,10 @@ impl RestApi { pubkeys_str.clone(), partial_sig, message, + self_.config.bridge_address_merkle_root(), ) { + drop(session); + self_.reset_session().await; METRICS.verifier_errors[&VerifierErrorLabel { diff --git a/via_verifier/node/via_verifier_coordinator/src/verifier/mod.rs b/via_verifier/node/via_verifier_coordinator/src/verifier/mod.rs index 67b9f248b..2b7bffed3 100644 --- a/via_verifier/node/via_verifier_coordinator/src/verifier/mod.rs +++ b/via_verifier/node/via_verifier_coordinator/src/verifier/mod.rs @@ -5,17 +5,21 @@ use std::{ }; use anyhow::{anyhow, Context, Result}; -use bitcoin::{Address, TapSighashType, Witness}; +use bitcoin::{TapSighashType, Witness}; use musig2::{CompactSignature, PartialSignature}; use reqwest::{header, Client, StatusCode}; use tokio::sync::watch; use via_btc_client::traits::{BitcoinOps, Serializable}; -use via_musig2::{get_signer, transaction_builder::TransactionBuilder, verify_signature, Signer}; +use via_musig2::{ + get_signer_with_merkle_root, transaction_builder::TransactionBuilder, verify_signature, Signer, +}; use via_verifier_dal::{ConnectionPool, Verifier, VerifierDal}; use via_verifier_types::{protocol_version::get_sequencer_version, transaction::UnsignedBridgeTx}; use via_withdrawal_client::client::WithdrawalClient; -use zksync_config::configs::{via_verifier::ViaVerifierConfig, via_wallets::ViaWallet}; -use zksync_types::{via_roles::ViaNodeRole, H256}; +use zksync_config::configs::{ + via_bridge::ViaBridgeConfig, via_verifier::ViaVerifierConfig, via_wallets::ViaWallet, +}; +use zksync_types::{via_roles::ViaNodeRole, via_wallet::SystemWallets, H256}; use zksync_utils::time::seconds_since_epoch; use crate::{ @@ -37,7 +41,7 @@ pub struct ViaWithdrawalVerifier { client: Client, signer_per_utxo_input: BTreeMap, final_sig_per_utxo_input: BTreeMap, - verifiers_pub_keys: Vec, + via_bridge_config: ViaBridgeConfig, } impl ViaWithdrawalVerifier { @@ -47,11 +51,12 @@ impl ViaWithdrawalVerifier { master_connection_pool: ConnectionPool, btc_client: Arc, withdrawal_client: WithdrawalClient, - bridge_address: Address, - verifiers_pub_keys: Vec, + via_bridge_config: ViaBridgeConfig, ) -> anyhow::Result { - let transaction_builder = - Arc::new(TransactionBuilder::new(btc_client.clone(), bridge_address)?); + let transaction_builder = Arc::new(TransactionBuilder::new( + btc_client.clone(), + via_bridge_config.bridge_address()?, + )?); let withdrawal_session = WithdrawalSession::new( verifier_config.clone(), @@ -77,7 +82,7 @@ impl ViaWithdrawalVerifier { client: Client::new(), signer_per_utxo_input: BTreeMap::new(), final_sig_per_utxo_input: BTreeMap::new(), - verifiers_pub_keys, + via_bridge_config, }) } @@ -102,6 +107,8 @@ impl ViaWithdrawalVerifier { Ok(()) } async fn loop_iteration(&mut self) -> Result<(), anyhow::Error> { + self.validate_verifier_addresses().await?; + if self.sync_in_progress().await? { return Ok(()); } @@ -273,7 +280,11 @@ impl ViaWithdrawalVerifier { fn create_request_headers(&self) -> anyhow::Result { let mut headers = header::HeaderMap::new(); let timestamp = chrono::Utc::now().timestamp().to_string(); - let signer = get_signer(&self.wallet.private_key, self.verifiers_pub_keys.clone())?; + let signer = get_signer_with_merkle_root( + &self.wallet.private_key, + self.via_bridge_config.verifiers_pub_keys.clone(), + self.verifier_config.bridge_address_merkle_root(), + )?; let verifier_index = signer.signer_index().to_string(); let sequencer_version = get_sequencer_version().to_string(); @@ -551,7 +562,11 @@ impl ViaWithdrawalVerifier { for i in 0..count { self.signer_per_utxo_input.insert( i, - get_signer(&self.wallet.private_key, self.verifiers_pub_keys.clone())?, + get_signer_with_merkle_root( + &self.wallet.private_key, + self.via_bridge_config.verifiers_pub_keys.clone(), + self.verifier_config.bridge_address_merkle_root(), + )?, ); } Ok(()) @@ -766,4 +781,23 @@ impl ViaWithdrawalVerifier { } Ok(false) } + + /// Check if the verifier is in the verifier set and the bridge address is correct. + async fn validate_verifier_addresses(&self) -> anyhow::Result<()> { + let Some(wallets_map) = self + .master_connection_pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + else { + anyhow::bail!("System wallets not found") + }; + + let wallets = SystemWallets::try_from(wallets_map)?; + + wallets.is_valid_verifier_address(self.verifier_config.wallet_address()?)?; + wallets.is_valid_bridge_address(self.via_bridge_config.bridge_address()?) + } } diff --git a/via_verifier/node/via_verifier_storage_init/Cargo.toml b/via_verifier/node/via_verifier_storage_init/Cargo.toml index 561a07981..1ff690e40 100644 --- a/via_verifier/node/via_verifier_storage_init/Cargo.toml +++ b/via_verifier/node/via_verifier_storage_init/Cargo.toml @@ -15,7 +15,9 @@ zksync_config.workspace = true zksync_node_genesis.workspace = true zksync_node_storage_init.workspace = true zksync_types.workspace = true +via_btc_client.workspace = true async-trait.workspace = true tokio.workspace = true -anyhow.workspace = true \ No newline at end of file +anyhow.workspace = true +tracing.workspace = true diff --git a/via_verifier/node/via_zk_verifier/src/lib.rs b/via_verifier/node/via_zk_verifier/src/lib.rs index 052117157..8691885d6 100644 --- a/via_verifier/node/via_zk_verifier/src/lib.rs +++ b/via_verifier/node/via_zk_verifier/src/lib.rs @@ -18,8 +18,8 @@ use via_verifier_types::protocol_version::check_if_supported_sequencer_version; use zksync_config::ViaVerifierConfig; use zksync_da_client::{types::InclusionData, DataAvailabilityClient}; use zksync_types::{ - commitment::L1BatchWithMetadata, protocol_version::ProtocolSemanticVersion, ProtocolVersionId, - H160, H256, + commitment::L1BatchWithMetadata, protocol_version::ProtocolSemanticVersion, + via_wallet::SystemWallets, ProtocolVersionId, H160, H256, }; mod metrics; @@ -96,6 +96,8 @@ impl ViaVerifier { &mut self, storage: &mut Connection<'_, Verifier>, ) -> anyhow::Result<()> { + self.validate_verifier_address().await?; + if let Some((l1_batch_number, mut raw_tx_id)) = storage .via_votes_dal() .get_first_not_verified_l1_batch_in_canonical_inscription_chain() @@ -450,4 +452,21 @@ impl ViaVerifier { } Ok(true) } + + /// Check if the wallet is in the verifier set. + async fn validate_verifier_address(&self) -> anyhow::Result<()> { + let Some(wallets_map) = self + .pool + .connection() + .await? + .via_wallet_dal() + .get_system_wallets_raw() + .await? + else { + anyhow::bail!("System wallets not found") + }; + + let wallets = SystemWallets::try_from(wallets_map)?; + wallets.is_valid_verifier_address(self.config.wallet_address()?) + } } From 0633b5a655fe0f1b2e1d74b1d4b1e0af484cd9c3 Mon Sep 17 00:00:00 2001 From: 0xatomFusion <179967211+0xatomFusion@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:36:45 +0100 Subject: [PATCH 5/8] feat: add the gov tx builder to the via cli and make the bridge address as option param for deposit --- core/lib/via_btc_client/examples/deposit.rs | 8 +- .../examples/deposit_opreturn.rs | 7 +- .../examples/propose_new_bridge.rs | 116 ++++++ docker-compose-via.yml | 2 +- docs/via_guides/musig2.md | 27 ++ docs/via_guides/upgrade.md | 130 +++++- infrastructure/via/src/constants.ts | 6 + infrastructure/via/src/debug.ts | 10 +- infrastructure/via/src/multisig.ts | 137 ++++++- infrastructure/via/src/token.ts | 19 +- via_verifier/lib/via_musig2/Cargo.toml | 8 + .../lib/via_musig2/examples/compute_musig2.rs | 158 ++++++++ .../examples/musig2_with_script_path.rs | 373 ++++++++++++++++++ 13 files changed, 973 insertions(+), 28 deletions(-) create mode 100644 core/lib/via_btc_client/examples/propose_new_bridge.rs create mode 100644 docs/via_guides/musig2.md create mode 100644 via_verifier/lib/via_musig2/examples/compute_musig2.rs create mode 100644 via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs diff --git a/core/lib/via_btc_client/examples/deposit.rs b/core/lib/via_btc_client/examples/deposit.rs index 102c3de92..2abcf3870 100644 --- a/core/lib/via_btc_client/examples/deposit.rs +++ b/core/lib/via_btc_client/examples/deposit.rs @@ -7,7 +7,7 @@ use std::{ }; use anyhow::{Context, Result}; -use bitcoin::{address::NetworkUnchecked, Amount}; +use bitcoin::Amount; use tracing::info; use via_btc_client::{ client::BitcoinClient, @@ -51,10 +51,10 @@ async fn main() -> Result<()> { let rpc_url = args[5].clone(); let rpc_username = args[6].clone(); let rpc_password = args[7].clone(); + let bridge_musig2_address_str = args[8].clone(); - let bridge_musig2_address = "bcrt1p3s7m76wp5seprjy4gdxuxrr8pjgd47q5s8lu9vefxmp0my2p4t9qh6s8kq" - .parse::>()? - .require_network(network)?; + let bridge_musig2_address = + BitcoinAddress::from_str(&bridge_musig2_address_str)?.require_network(network)?; // Load the previous context from the file if it exists let context = load_context_from_file(CONTEXT_FILE)?; diff --git a/core/lib/via_btc_client/examples/deposit_opreturn.rs b/core/lib/via_btc_client/examples/deposit_opreturn.rs index 3bccbf129..c70e011b4 100644 --- a/core/lib/via_btc_client/examples/deposit_opreturn.rs +++ b/core/lib/via_btc_client/examples/deposit_opreturn.rs @@ -3,7 +3,6 @@ use std::{env, str::FromStr, sync::Arc}; use anyhow::Result; use bitcoin::{ absolute, - address::NetworkUnchecked, consensus::encode::serialize_hex, secp256k1::{Message, Secp256k1}, sighash::{EcdsaSighashType, SighashCache}, @@ -47,6 +46,7 @@ async fn main() -> Result<()> { let rpc_url = args[5].clone(); let rpc_username = args[6].clone(); let rpc_password = args[7].clone(); + let bridge_musig2_address_str = args[8].clone(); let private_key = PrivateKey::from_wif(&depositor_private_key).map_err(|e| anyhow::anyhow!(e.to_string()))?; @@ -55,9 +55,8 @@ async fn main() -> Result<()> { .map_err(|e| anyhow::anyhow!(e.to_string()))?; let address = Address::p2wpkh(&compressed_pk, network); - let bridge_musig2_address = "bcrt1p3s7m76wp5seprjy4gdxuxrr8pjgd47q5s8lu9vefxmp0my2p4t9qh6s8kq" - .parse::>()? - .require_network(network)?; + let bridge_musig2_address = + BitcoinAddress::from_str(&bridge_musig2_address_str)?.require_network(network)?; let auth = NodeAuth::UserPass(rpc_username.to_string(), rpc_password.to_string()); let config = ViaBtcClientConfig { diff --git a/core/lib/via_btc_client/examples/propose_new_bridge.rs b/core/lib/via_btc_client/examples/propose_new_bridge.rs new file mode 100644 index 000000000..e5a560835 --- /dev/null +++ b/core/lib/via_btc_client/examples/propose_new_bridge.rs @@ -0,0 +1,116 @@ +use std::{env, str::FromStr, sync::Arc}; + +use anyhow::{Context, Result}; +use bitcoin::{address::NetworkUnchecked, Address}; +use tracing::info; +use via_btc_client::{ + client::BitcoinClient, + indexer::MessageParser, + inscriber::Inscriber, + types::{BitcoinNetwork, InscriptionMessage, NodeAuth, UpdateBridgeProposalInput}, +}; +use zksync_config::configs::via_btc_client::ViaBtcClientConfig; + +const TIMEOUT: u64 = 5; + +async fn create_inscriber( + signer_private_key: &str, + rpc_url: &str, + rpc_username: &str, + rpc_password: &str, + network: BitcoinNetwork, +) -> anyhow::Result { + let auth = NodeAuth::UserPass(rpc_username.to_string(), rpc_password.to_string()); + let config = ViaBtcClientConfig { + network: network.to_string(), + external_apis: vec![], + fee_strategies: vec![], + use_rpc_for_fee_rate: None, + }; + let client = Arc::new(BitcoinClient::new(rpc_url, auth, config)?); + Inscriber::new(client, signer_private_key, None) + .await + .context("Failed to create Inscriber") +} + +// Example: +// cargo run --example propose_new_bridge \ +// regtest \ +// http://0.0.0.0:18443 \ +// rpcuser \ +// rpcpassword \ +// cVZduZu265sWeAqFYygoDEE1FZ7wV9rpW5qdqjRkUehjaUMWLT1R \ +// bcrt1p5kp3mnv8tjdly0yyxmed5pl34gy8ufeh9kaf4vk3e7atxrcaq93s7w4xwq \ +// bcrt1q08v0vm5w3rftefqutgtwlyslhy35ms8ftuay80,bcrt1q50xmdcwlmt8qhwczxptaq2h5cn3zchcrvqd35v,bcrt1q9l2wcyaquvvxuzxenae75q24yx4uhzhq3mrlfe + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .init(); + + let args: Vec = env::args().collect(); + let network = BitcoinNetwork::from_str(&args[1].clone())?; + let rpc_url = args[2].clone(); + let rpc_username = args[3].clone(); + let rpc_password = args[4].clone(); + let private_key = args[5].clone(); + + let bridge_address = Address::from_str(&args[6])?.require_network(network)?; + let verifiers = args[7] + .clone() + .split(",") + .collect::>() + .iter() + .map(|x| { + Address::from_str(x) + .unwrap() + .require_network(network) + .unwrap() + }) + .map(|x| x.as_unchecked().clone()) + .collect::>>(); + + info!("Create an propose bridge transaction",); + + let mut inscriber = create_inscriber( + &private_key, + &rpc_url, + &rpc_username, + &rpc_password, + network, + ) + .await?; + + // System contracts Upgrade message + let input = UpdateBridgeProposalInput { + bridge_musig2_address: bridge_address.as_unchecked().clone(), + verifier_p2wpkh_addresses: verifiers, + }; + let update_bridge_address_proposal = inscriber + .inscribe(InscriptionMessage::UpdateBridgeProposal(input)) + .await?; + info!( + "Update Bridge address proposal: {:?}", + update_bridge_address_proposal.final_reveal_tx.txid.clone() + ); + + tokio::time::sleep(std::time::Duration::from_secs(TIMEOUT)).await; + + info!("*************************************************************************"); + + let mut parser = MessageParser::new(network); + let tx = inscriber + .get_client() + .await + .get_transaction(&update_bridge_address_proposal.final_reveal_tx.txid) + .await?; + + let update_bridge_address_proposal = parser.parse_system_transaction(&tx, 0, None); + info!( + "Update bridge address proposal: {:?}", + update_bridge_address_proposal + ); + + Ok(()) +} diff --git a/docker-compose-via.yml b/docker-compose-via.yml index ca9309c85..c34e33835 100644 --- a/docker-compose-via.yml +++ b/docker-compose-via.yml @@ -132,7 +132,7 @@ services: - VERIFIER_2_ADDRESS=bcrt1qk8mkhrmgtq24nylzyzejznfzws6d98g4kmuuh4 - VERIFIER_3_ADDRESS=bcrt1q23lgaa90s85jvtl6dsrkvn0g949cwjkwuyzwdm - BRIDGE_TEST_ADDRESS=bcrt1pcx974cg2w66cqhx67zadf85t8k4sd2wp68l8x8agd3aj4tuegsgsz97amg - - BRIDGE_TEST_ADDRESS2=bcrt1pm4rre0xv8ryr9lr5lrnzx5tpyk0xr43kfw3aja68c0845vsu5wus3u40fp + - BRIDGE_TEST_ADDRESS2=bcrt1p5kp3mnv8tjdly0yyxmed5pl34gy8ufeh9kaf4vk3e7atxrcaq93s7w4xwq - GOV_ADDRESS=bcrt1q92gkfme6k9dkpagrkwt76etkaq29hvf02w5m38f6shs4ddpw7hzqp347zm - VIA_LOADNEXT_TEST_ADDRESS=bcrt1q8tuqv885kehnzucdfskuw6mrhxcj7cjs4gfk5z - RPC_ARGS=-chain=regtest -rpcconnect=bitcoind -rpcwait -rpcuser=rpcuser -rpcpassword=rpcpassword diff --git a/docs/via_guides/musig2.md b/docs/via_guides/musig2.md new file mode 100644 index 000000000..3c72639cd --- /dev/null +++ b/docs/via_guides/musig2.md @@ -0,0 +1,27 @@ +# MuSig2 Bridge Wallet + +This module creates a **bridge address** that supports two spending methods: + +1. **Key Path Spend (Key Hash)** + +- Uses a MuSig2 aggregate public key. +- Requires **N-of-N signers** to jointly produce a valid signature. +- Primary purpose: **processing withdrawals**. + +2. **Script Path Spend (Script Hash)** + +- Uses an alternative script-based spending condition. +- Intended for **governance control**, allowing governance participants to transfer or reassign UTXOs if necessary. + +This design provides both operational security (via MuSig2 key-path spending) and governance flexibility (via +script-path spending). + +## Example + +```sh +cargo run --example compute_musig2 -- \ + --signers 025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241,03c2871e18d4fb503ead90461da747b40df5e28da0fd3e067f3731f1a28da60ddf \ + --governance-keys 025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241,03c2871e18d4fb503ead90461da747b40df5e28da0fd3e067f3731f1a28da60ddf,03445c516584d751643442bea558be2c5d77a6c3377e86fe6e78e3b992dd68ac62 \ + --threshold 2 \ + --output my_wallet.json +``` diff --git a/docs/via_guides/upgrade.md b/docs/via_guides/upgrade.md index c6723e8ba..af9171913 100644 --- a/docs/via_guides/upgrade.md +++ b/docs/via_guides/upgrade.md @@ -109,7 +109,7 @@ via multisig create-upgrade-tx \ 3. Sign the transaction using the signer-1 `Privatekey`. ```sh -via multisig sign-upgrade-tx --privateKey cQnW8oDqEME4gxJHC4MC9HvJECcF7Ju8oanWdjWLGxDbkfWo7vZa +via multisig sign-tx --privateKey cQnW8oDqEME4gxJHC4MC9HvJECcF7Ju8oanWdjWLGxDbkfWo7vZa ``` After signing the tx send the `upgrade_tx_exec.json` to signer-2 @@ -117,13 +117,13 @@ After signing the tx send the `upgrade_tx_exec.json` to signer-2 4. Sign the transaction using the signer-2 `Privatekey`. ```sh -via multisig sign-upgrade-tx --privateKey cVJYEHTzmfdRPoX6fL3vRnZVmqy4D1sWaT5WL9U25oZhQktoeHgo +via multisig sign-tx --privateKey cVJYEHTzmfdRPoX6fL3vRnZVmqy4D1sWaT5WL9U25oZhQktoeHgo ``` 5. The signer-2 finalize the transaction ```sh -via multisig finalize-upgrade-tx +via multisig finalize-tx ``` 6. The signer-2 broadcast the transaction @@ -212,3 +212,127 @@ select number, encode(bootloader_code_hash, 'hex'), encode(default_aa_code_hash, 18. Start the verifier and restart it with version 26. The withdrawal is processed 19. Execute a withdrawal. 20. Done :) + +--- + +## Upgrade the sequencer address + +1. Start the sequencer and the verifiers +2. Deposit BTC and wait the sequencer process the batch. + +```sh +via token deposit --amount 10 --receiver-l2-address 0x36615Cf349d7F6344891B1e7CA7C72883F5dc049 --bridge-address bcrt1p3s7m76wp5seprjy4gdxuxrr8pjgd47q5s8lu9vefxmp0my2p4t9qh6s8kq +``` + +3. withdraw 1 BTC. + +### update the sequencer on localhost + +1. Create an update sequencer proposal. Follow the doc on how to sign a multisig tx (update just the 2 with the + following cmd) [here](#How-to-execute-an-upgrade-proposal) + +```sh +via multisig create-update-sequencer \ +--inputTxId \ +--inputVout \ +--inputAmount \ +--sequencerAddress bcrt1qw2mvkvm6alfhe86yf328kgvr7mupdx4vln7kpv \ +--fee 500 +``` + +2. Withdraw 1 BTC, the sequencer (btc_sender) should throw and error + `BTC sender inscriber wallets is not valid, expected...`, this error is because we did not yet update the sequencer + Private key. Stop the sequencer and update this ENV in `via.env`: + +```sh +VIA_BTC_SENDER_PRIVATE_KEY=cRaUbRSn8P8cXUcg6cMZ7oTZ1wbDjktYTsbdGw62tuqqD9ttQWMm +VIA_BTC_SENDER_WALLET_ADDRESS=bcrt1qw2mvkvm6alfhe86yf328kgvr7mupdx4vln7kpv +``` + +4. Execute an other deposit and withdrawal, the sequencer and verifiers process the batches. +5. Done. + +--- + +### Update the bridge address on localhost + +1. Create a new bridge address, follow this [doc](musig2.md). You should have a json file on your local `my_wallet.json` +2. Create a proposal update bridge. + +```sh +cargo run --example propose_new_bridge \ + regtest \ + http://0.0.0.0:18443 \ + rpcuser \ + rpcpassword \ + cVZduZu265sWeAqFYygoDEE1FZ7wV9rpW5qdqjRkUehjaUMWLT1R \ + bcrt1pfk264lnycy2v48h3we2jajyg7kyuvha9yfkd4qmxfrgywz3meyhqhdhmj8 \ + bcrt1q08v0vm5w3rftefqutgtwlyslhy35ms8ftuay80,bcrt1q50xmdcwlmt8qhwczxptaq2h5cn3zchcrvqd35v +``` + +3. Copy the txid of the upgrade proposal and create an upgrade using the governance wallet. Follow the doc on how to + sign a multisig tx (update just the 2 with the following cmd) [here](#How-to-execute-an-upgrade-proposal) + +```sh +via multisig create-update-bridge \ +--inputTxId \ +--inputVout \ +--inputAmount \ +--proposalTxid bf8731b79d50b8b4862ae91ee6e3e2beae805ba534dd03daa78de0734a3dc8b8 \ +--fee 500 +``` + +4. The verifier should start throwing an Error because the current signer doesn't match the new bridge address. + +```error +Failed to process verifier withdrawal task: Verifier address not found in the verifiers set, expected one of [bcrt1q08v0vm5w3rftefqutgtwlyslhy35ms8ftuay80, bcrt1q50xmdcwlmt8qhwczxptaq2h5cn3zchcrvqd35v], found bcrt1qw2mvkvm6alfhe86yf328kgvr7mupdx4vln7kpv +``` + +5. Transfer some BTC to the new verifier addresses + +```sh +curl --user rpcuser:rpcpassword \ + --data-binary '{"jsonrpc":"1.0","id":"sendbtc","method":"sendtoaddress","params":["bcrt1q50xmdcwlmt8qhwczxptaq2h5cn3zchcrvqd35v", 0.1]}' \ + -H 'content-type: text/plain;' \ + http://127.0.0.1:18443/wallet/Alice + +curl --user rpcuser:rpcpassword \ + --data-binary '{"jsonrpc":"1.0","id":"sendbtc","method":"sendtoaddress","params":["bcrt1q08v0vm5w3rftefqutgtwlyslhy35ms8ftuay80", 0.1]}' \ + -H 'content-type: text/plain;' \ + http://127.0.0.1:18443/wallet/Alice +``` + +6. Update the ENVs for verifier and coordinator + +```sh +VIA_BTC_SENDER_PRIVATE_KEY=cQnW8oDqEME4gxJHC4MC9HvJECcF7Ju8oanWdjWLGxDbkfWo7vZa +VIA_BTC_SENDER_WALLET_ADDRESS=bcrt1q08v0vm5w3rftefqutgtwlyslhy35ms8ftuay80 +VIA_VERIFIER_PRIVATE_KEY=cQnW8oDqEME4gxJHC4MC9HvJECcF7Ju8oanWdjWLGxDbkfWo7vZa +VIA_VERIFIER_WALLET_ADDRESS=bcrt1q08v0vm5w3rftefqutgtwlyslhy35ms8ftuay80 +VIA_VERIFIER_BRIDGE_ADDRESS_MERKLE_ROOT=2aa187093ce1f9e55ad02aa804480cc01beb9c570781133b768d8cfb12177e25 +VIA_BRIDGE_VERIFIERS_PUB_KEYS=025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241,03c2871e18d4fb503ead90461da747b40df5e28da0fd3e067f3731f1a28da60ddf +VIA_BRIDGE_COORDINATOR_PUB_KEY=025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241 +VIA_BRIDGE_BRIDGE_ADDRESS=bcrt1pfk264lnycy2v48h3we2jajyg7kyuvha9yfkd4qmxfrgywz3meyhqhdhmj8 +``` + +and coordinator: + +```sh +VIA_BTC_SENDER_PRIVATE_KEY=cVJYEHTzmfdRPoX6fL3vRnZVmqy4D1sWaT5WL9U25oZhQktoeHgo +VIA_BTC_SENDER_WALLET_ADDRESS=bcrt1q50xmdcwlmt8qhwczxptaq2h5cn3zchcrvqd35v +VIA_VERIFIER_PRIVATE_KEY=cVJYEHTzmfdRPoX6fL3vRnZVmqy4D1sWaT5WL9U25oZhQktoeHgo +VIA_VERIFIER_WALLET_ADDRESS=bcrt1q50xmdcwlmt8qhwczxptaq2h5cn3zchcrvqd35v +VIA_VERIFIER_BRIDGE_ADDRESS_MERKLE_ROOT=2aa187093ce1f9e55ad02aa804480cc01beb9c570781133b768d8cfb12177e25 +VIA_GENESIS_VERIFIERS_PUB_KEYS=025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241,03c2871e18d4fb503ead90461da747b40df5e28da0fd3e067f3731f1a28da60ddf +VIA_GENESIS_COORDINATOR_PUB_KEY=025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241 +VIA_BRIDGE_BRIDGE_ADDRESS=bcrt1pfk264lnycy2v48h3we2jajyg7kyuvha9yfkd4qmxfrgywz3meyhqhdhmj8 +``` + +7. Deposit BTC to the **new bridge address** + +```sh +via token deposit --amount 10 --receiver-l2-address 0x36615Cf349d7F6344891B1e7CA7C72883F5dc049 --bridge-address bcrt1pfk264lnycy2v48h3we2jajyg7kyuvha9yfkd4qmxfrgywz3meyhqhdhmj8 +``` + +8. Withdraw 1 BTC. +9. The verifier and coordinator process the batch and sequencer finalize the batch. diff --git a/infrastructure/via/src/constants.ts b/infrastructure/via/src/constants.ts index 1e45f3e5d..d650de96d 100644 --- a/infrastructure/via/src/constants.ts +++ b/infrastructure/via/src/constants.ts @@ -11,3 +11,9 @@ export const DEFAULT_L2_RPC_URL = 'http://0.0.0.0:3050'; export const L2_BASE_TOKEN = '0x000000000000000000000000000000000000800a'; export const REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE = 800; export const L1_BTC_DECIMALS = 8; + +export const OP_RETURN_WITHDRAW_PREFIX = 'VIA_PROTOCOL:WITHDRAWAL'; +export const OP_RETURN_UPGRADE_PROTOCOL_PREFIX = 'VIA_PROTOCOL:UPGRADE'; +export const OP_RETURN_UPDATE_SEQUENCER_PREFIX = 'VIA_PROTOCOL:SEQ'; +export const OP_RETURN_UPDATE_BRIDGE_PREFIX = 'VIA_PROTOCOL:BRI'; +export const OP_RETURN_UPDATE_GOVERNANCE_PREFIX = 'VIA_PROTOCOL:GOV'; diff --git a/infrastructure/via/src/debug.ts b/infrastructure/via/src/debug.ts index 7881f97fc..082fed807 100644 --- a/infrastructure/via/src/debug.ts +++ b/infrastructure/via/src/debug.ts @@ -20,7 +20,8 @@ export async function depositMany( l1RpcUrl: string, l2RpcUrl: string, rpcUsername: string, - rpcPassword: string + rpcPassword: string, + bridgeAddress: string ) { const amountPerDeposit = Number(amount) / count; for (let i = 0; i < count; i++) { @@ -32,7 +33,8 @@ export async function depositMany( l1RpcUrl, l2RpcUrl, rpcUsername, - rpcPassword + rpcPassword, + bridgeAddress ); console.log(`\n\nDeposit ${i + 1}/${count}`); @@ -67,6 +69,7 @@ command .description('deposit BTC to l2') .requiredOption('--amount ', 'amount of BTC to deposit', parseFloat) .requiredOption('--receiver-l2-address ', 'receiver l2 address') + .requiredOption('--bridge-address ', 'The bridge address') .option('--sender-private-key ', 'sender private key', DEFAULT_DEPOSITOR_PRIVATE_KEY) .option('--network ', 'network', DEFAULT_NETWORK) .option('--l1-rpc-url ', 'RPC URL', DEFAULT_L1_RPC_URL) @@ -84,7 +87,8 @@ command cmd.l1RpcUrl, cmd.l2RpcUrl, cmd.rpcUsername, - cmd.rpcPassword + cmd.rpcPassword, + cmd.bridgeAddress ) ); diff --git a/infrastructure/via/src/multisig.ts b/infrastructure/via/src/multisig.ts index c5b6ee34c..e761a4058 100644 --- a/infrastructure/via/src/multisig.ts +++ b/infrastructure/via/src/multisig.ts @@ -5,7 +5,13 @@ import { ECPairFactory, ECPairInterface } from 'ecpair'; import { generateBitcoinWallet, getNetwork, readJsonFile, writeJsonFile } from './helpers'; import { Psbt } from 'bitcoinjs-lib'; import axios from 'axios'; -import { DEFAULT_NETWORK } from './constants'; +import { + DEFAULT_NETWORK, + OP_RETURN_UPDATE_BRIDGE_PREFIX, + OP_RETURN_UPDATE_GOVERNANCE_PREFIX, + OP_RETURN_UPDATE_SEQUENCER_PREFIX, + OP_RETURN_UPGRADE_PROTOCOL_PREFIX +} from './constants'; // Initialize the elliptic curve library bitcoin.initEccLib(ecc); @@ -49,11 +55,56 @@ const compute_multisig_address = ( writeJsonFile(outDir, { multisig }); }; +const createUnsignedUpdateSequencerTransaction = ( + utxoInput: UtxoInput, + fee: number, + newSequencerAddress: string, + outDir: string, + networkStr: string = 'regtest' +) => { + const opReturnData = Buffer.from(newSequencerAddress, 'utf8'); + _createUnsignedTransaction(utxoInput, fee, outDir, opReturnData, OP_RETURN_UPDATE_SEQUENCER_PREFIX, networkStr); +}; + +const createUnsignedUpdateGovernanceTransaction = ( + utxoInput: UtxoInput, + fee: number, + newGovernanceAddress: string, + outDir: string, + networkStr: string = 'regtest' +) => { + const opReturnData = Buffer.from(newGovernanceAddress, 'utf8'); + _createUnsignedTransaction(utxoInput, fee, outDir, opReturnData, OP_RETURN_UPDATE_GOVERNANCE_PREFIX, networkStr); +}; + +const createUnsignedUpdateBridgeTransaction = ( + utxoInput: UtxoInput, + fee: number, + updateBridgeProposalTxid: string, + outDir: string, + networkStr: string = 'regtest' +) => { + const opReturnData = Buffer.from(updateBridgeProposalTxid, 'hex').reverse(); + _createUnsignedTransaction(utxoInput, fee, outDir, opReturnData, OP_RETURN_UPDATE_BRIDGE_PREFIX, networkStr); +}; + const createUnsignedUpgradeTransaction = ( utxoInput: UtxoInput, + fee: number, + outDir: string, upgradeTxId: string, + networkStr: string = 'regtest' +) => { + const opReturnData = Buffer.from(upgradeTxId, 'hex').reverse(); + _createUnsignedTransaction(utxoInput, fee, outDir, opReturnData, OP_RETURN_UPGRADE_PROTOCOL_PREFIX, networkStr); +}; + +const _createUnsignedTransaction = ( + utxoInput: UtxoInput, fee: number, outDir: string, + opReturnData: Buffer, + opReturnPrefix: string, networkStr: string = 'regtest' ) => { const dataFile: any = readJsonFile(outDir); @@ -73,9 +124,9 @@ const createUnsignedUpgradeTransaction = ( }); // 2. Create and add the OP_RETURN output - const upgradeTxIdBuffer = Buffer.from(upgradeTxId, 'hex').reverse(); - const prefixBuffer = Buffer.from('VIA_PROTOCOL:UPGRADE', 'utf8'); - const embed = bitcoin.payments.embed({ data: [prefixBuffer, upgradeTxIdBuffer] }); + // const suffixBuffer = Buffer.from(opReturnData, 'hex').reverse(); + const prefixBuffer = Buffer.from(opReturnPrefix, 'utf8'); + const embed = bitcoin.payments.embed({ data: [prefixBuffer, opReturnData] }); if (!embed.output) throw new Error('Could not create OP_RETURN script.'); psbt.addOutput({ @@ -96,7 +147,7 @@ const createUnsignedUpgradeTransaction = ( writeJsonFile(outDir, { ...dataFile, tx: psbt.toBase64() }); - console.log('Successfully created an unsigned PSBT for the upgrade transaction.'); + console.log('Successfully created an unsigned PSBT transaction.'); }; /** @@ -226,15 +277,87 @@ command amountSatoshis: Number(cmd.inputAmount), vout: Number(cmd.inputVout) }, + Number(cmd.fee), + cmd.outDir, cmd.upgradeProposalTxId, + cmd.network + ) + ); + +command + .command('create-update-sequencer') + .description('Generate an unsigned multisig transaction for updating the sequencer') + .requiredOption('--inputTxId ', 'The input txid used to pay fee') + .requiredOption('--inputVout ', 'The input vout used to pay fee') + .requiredOption('--inputAmount ', 'The input amount used to pay fee') + .requiredOption('--fee ', 'The transaction fee') + .requiredOption('--sequencerAddress ', 'The new sequencer address') + .option('--outDir ', 'The output dir', './upgrade_tx_exec.json') + .option('--network ', 'network', DEFAULT_NETWORK) + .action((cmd: Command) => + createUnsignedUpdateSequencerTransaction( + { + txid: cmd.inputTxId, + amountSatoshis: Number(cmd.inputAmount), + vout: Number(cmd.inputVout) + }, + Number(cmd.fee), + cmd.sequencerAddress, + cmd.outDir, + cmd.network + ) + ); + +command + .command('create-update-bridge') + .description('Generate an unsigned multisig transaction for updating the bridge') + .requiredOption('--inputTxId ', 'The input txid used to pay fee') + .requiredOption('--inputVout ', 'The input vout used to pay fee') + .requiredOption('--inputAmount ', 'The input amount used to pay fee') + .requiredOption('--fee ', 'The transaction fee') + .requiredOption('--proposalTxid ', 'The bridge proposal id') + .option('--outDir ', 'The output dir', './upgrade_tx_exec.json') + .option('--network ', 'network', DEFAULT_NETWORK) + .action((cmd: Command) => + createUnsignedUpdateBridgeTransaction( + { + txid: cmd.inputTxId, + amountSatoshis: Number(cmd.inputAmount), + vout: Number(cmd.inputVout) + }, + Number(cmd.fee), + cmd.proposalTxid, + cmd.outDir, + cmd.network + ) + ); + +command + .command('create-update-gov') + .description('Generate an unsigned multisig transaction for updating the governance') + .requiredOption('--inputTxId ', 'The input txid used to pay fee') + .requiredOption('--inputVout ', 'The input vout used to pay fee') + .requiredOption('--inputAmount ', 'The input amount used to pay fee') + .requiredOption('--fee ', 'The transaction fee') + .requiredOption('--governanceAddress ', 'The new governance address') + .option('--outDir ', 'The output dir', './upgrade_tx_exec.json') + .option('--network ', 'network', DEFAULT_NETWORK) + .action((cmd: Command) => + createUnsignedUpdateGovernanceTransaction( + { + txid: cmd.inputTxId, + amountSatoshis: Number(cmd.inputAmount), + vout: Number(cmd.inputVout) + }, Number(cmd.fee), + cmd.governanceAddress, cmd.outDir, cmd.network ) ); command - .command('sign-upgrade-tx') + .command('sign-tx') .description('Sign and upgrade transaction') .requiredOption('--privateKey ', 'The signer private key') .option('--outDir ', 'The output dir', './upgrade_tx_exec.json') @@ -242,7 +365,7 @@ command .action((cmd: Command) => signPsbt(cmd.privateKey, cmd.outDir, cmd.network)); command - .command('finalize-upgrade-tx') + .command('finalize-tx') .description('finalize the upgrade transaction') .option('--outDir ', 'The output dir', './upgrade_tx_exec.json') .option('--network ', 'network', DEFAULT_NETWORK) diff --git a/infrastructure/via/src/token.ts b/infrastructure/via/src/token.ts index 8ea85f929..b3258cd79 100644 --- a/infrastructure/via/src/token.ts +++ b/infrastructure/via/src/token.ts @@ -23,7 +23,8 @@ export async function deposit( l1RpcUrl: string, l2RpcUrl: string, rpcUsername: string, - rpcPassword: string + rpcPassword: string, + bridgeAddress: string ) { if (isNaN(amount)) { console.error('Error: Invalid deposit amount. Please provide a valid number.'); @@ -40,7 +41,7 @@ export async function deposit( process.chdir(`${process.env.VIA_HOME}`); await utils.spawn( - `cargo run --example deposit -- ${amountWithFees} ${receiverL2Address} ${senderPrivateKey} ${network} ${l1RpcUrl} ${rpcUsername} ${rpcPassword}` + `cargo run --example deposit -- ${amountWithFees} ${receiverL2Address} ${senderPrivateKey} ${network} ${l1RpcUrl} ${rpcUsername} ${rpcPassword} ${bridgeAddress}` ); } @@ -52,7 +53,8 @@ async function depositWithOpReturn( l1RpcUrl: string, l2RpcUrl: string, rpcUsername: string, - rpcPassword: string + rpcPassword: string, + bridgeAddress: string ) { if (isNaN(amount)) { console.error('Error: Invalid deposit amount. Please provide a valid number.'); @@ -69,7 +71,7 @@ async function depositWithOpReturn( process.chdir(`${process.env.VIA_HOME}`); await utils.spawn( - `cargo run --example deposit_opreturn -- ${amountWithFees} ${receiverL2Address} ${senderPrivateKey} ${network} ${l1RpcUrl} ${rpcUsername} ${rpcPassword}` + `cargo run --example deposit_opreturn -- ${amountWithFees} ${receiverL2Address} ${senderPrivateKey} ${network} ${l1RpcUrl} ${rpcUsername} ${rpcPassword} ${bridgeAddress}` ); } @@ -171,12 +173,14 @@ command .description('deposit BTC to l2') .requiredOption('--amount ', 'amount of BTC to deposit', parseFloat) .requiredOption('--receiver-l2-address ', 'receiver l2 address') + .requiredOption('--bridge-address ', 'The bridge address') .option('--sender-private-key ', 'sender private key', DEFAULT_DEPOSITOR_PRIVATE_KEY) .option('--network ', 'network', DEFAULT_NETWORK) .option('--l1-rpc-url ', 'RPC URL', DEFAULT_L1_RPC_URL) .option('--l2-rpc-url ', 'RPC URL', DEFAULT_L2_RPC_URL) .option('--rpc-username ', 'RPC username', DEFAULT_RPC_USERNAME) .option('--rpc-password ', 'RPC password', DEFAULT_RPC_PASSWORD) + .action((cmd: Command) => deposit( cmd.amount, @@ -186,7 +190,8 @@ command cmd.l1RpcUrl, cmd.l2RpcUrl, cmd.rpcUsername, - cmd.rpcPassword + cmd.rpcPassword, + cmd.bridgeAddress ) ); @@ -195,6 +200,7 @@ command .description('deposit BTC to l2 with op-return') .requiredOption('--amount ', 'amount of BTC to deposit', parseFloat) .requiredOption('--receiver-l2-address ', 'receiver l2 address') + .requiredOption('--bridge-address ', 'The bridge address') .option('--sender-private-key ', 'sender private key', DEFAULT_DEPOSITOR_PRIVATE_KEY) .option('--network ', 'network', DEFAULT_NETWORK) .option('--l1-rpc-url ', 'RPC URL', DEFAULT_L1_RPC_URL) @@ -210,7 +216,8 @@ command cmd.l1RpcUrl, cmd.l2RpcUrl, cmd.rpcUsername, - cmd.rpcPassword + cmd.rpcPassword, + cmd.bridgeAddress ) ); diff --git a/via_verifier/lib/via_musig2/Cargo.toml b/via_verifier/lib/via_musig2/Cargo.toml index 042430092..29cede6a3 100644 --- a/via_verifier/lib/via_musig2/Cargo.toml +++ b/via_verifier/lib/via_musig2/Cargo.toml @@ -62,3 +62,11 @@ path = "examples/withdrawal.rs" [[example]] name = "coordinator" path = "examples/coordinator.rs" + +[[example]] +name = "musig2_with_script_path" +path = "examples/musig2_with_script_path.rs" + +[[example]] +name = "compute_musig2" +path = "examples/compute_musig2.rs" diff --git a/via_verifier/lib/via_musig2/examples/compute_musig2.rs b/via_verifier/lib/via_musig2/examples/compute_musig2.rs new file mode 100644 index 000000000..2334c2962 --- /dev/null +++ b/via_verifier/lib/via_musig2/examples/compute_musig2.rs @@ -0,0 +1,158 @@ +use std::{fs::File, io::Write, str::FromStr}; + +use bitcoin::{ + blockdata::{opcodes::all::*, script::Builder}, + hex::DisplayHex, + secp256k1::Secp256k1, + taproot::LeafVersion, + Address, Network, PublicKey, ScriptBuf, XOnlyPublicKey, +}; +use clap::Parser; +use musig2::KeyAggContext; +use serde::Serialize; + +#[derive(Parser, Debug)] +#[command( + author, + version = "0.0.1", + about = "Compute a musig 2 wallet with key hash as primary spending and the script path for governance spending hash" +)] +struct Args { + /// Comma separated signer public keys (compressed hex, 33 bytes) + #[arg(long)] + signers: String, + + /// Comma separated governance x-only public keys (hex, N keys) + #[arg(long)] + governance_keys: String, + + /// Governance threshold M (e.g. 2 for 2-of-N) + #[arg(long)] + threshold: usize, + + /// Bitcoin network (regtest, testnet, mainnet, signet) + #[arg(long, default_value = "regtest")] + network: String, + + /// Output JSON file path + #[arg(long, default_value = "wallet_info.json")] + output: String, +} + +#[derive(Serialize)] +struct ExportData { + aggregated_internal_key: String, + governance_script_hex: String, + taproot_output_key: String, + merkle_root: Option, + taproot_address: String, + control_block: String, + threshold: usize, + total_governance_keys: usize, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let secp = Secp256k1::verification_only(); + + // --- Parse signer full pubkeys (MuSig2 aggregation) --- + let signer_hex: Vec<&str> = args.signers.split(',').collect(); + let mut musig_pubkeys = Vec::new(); + + for hex in signer_hex { + // let bytes = Vec::from_hex(hex.trim())?; + let pk = musig2::secp256k1::PublicKey::from_str(&hex.trim())?; + musig_pubkeys.push(pk); + } + + if musig_pubkeys.is_empty() { + anyhow::bail!("Must provide at least one signer key"); + } + + // Aggregate MuSig2 key + let mut musig_key_agg_cache = KeyAggContext::new(musig_pubkeys)?; + let agg_pubkey = musig_key_agg_cache.aggregated_pubkey::(); + let (xonly_agg_key, _) = agg_pubkey.x_only_public_key(); + let internal_key = XOnlyPublicKey::from_slice(&xonly_agg_key.serialize())?; + + // --- Governance pubkeys (x-only) --- + let gov_hex: Vec<&str> = args.governance_keys.split(',').collect(); + let mut gov_xonly = Vec::new(); + for hex in gov_hex { + let xonly = PublicKey::from_str(&hex.trim())? + .inner + .x_only_public_key() + .0; + gov_xonly.push(xonly); + } + + let n = gov_xonly.len(); + let m = args.threshold; + + if n == 0 { + anyhow::bail!("Must provide at least one governance key"); + } + if m == 0 || m > n { + anyhow::bail!(format!( + "Invalid threshold: {} (must be between 1 and N={})", + m, n + )); + } + + // --- Build generic M-of-N Taproot Schnorr multisig script --- + let mut builder = Builder::new(); + + for (i, key) in gov_xonly.iter().enumerate() { + if i == 0 { + builder = builder.push_x_only_key(key).push_opcode(OP_CHECKSIG); + } else { + builder = builder.push_x_only_key(key).push_opcode(OP_CHECKSIGADD); + } + } + + let multisig_script = builder + .push_int(m as i64) + .push_opcode(OP_NUMEQUAL) + .into_script(); + + let spend_info = bitcoin::taproot::TaprootBuilder::new() + .add_leaf(0, ScriptBuf::from(multisig_script.clone()))? + .finalize(&secp, internal_key) + .unwrap(); + + let taproot_output_key = spend_info.output_key(); + let net = match args.network.as_str() { + "mainnet" => Network::Bitcoin, + "testnet" => Network::Testnet, + "signet" => Network::Signet, + _ => Network::Regtest, + }; + let taproot_address = Address::p2tr_tweaked(taproot_output_key, net); + + let control_block = spend_info + .control_block(&(multisig_script.clone(), LeafVersion::TapScript)) + .unwrap() + .serialize() + .to_hex_string(bitcoin::hex::Case::Lower); + + // --- Export JSON file --- + let data = ExportData { + aggregated_internal_key: internal_key.to_string(), + governance_script_hex: multisig_script.to_hex_string(), + taproot_output_key: taproot_output_key.to_string(), + merkle_root: spend_info.merkle_root().map(|h| h.to_string()), + taproot_address: taproot_address.to_string(), + control_block, + threshold: m, + total_governance_keys: n, + }; + + let json = serde_json::to_string_pretty(&data)?; + let mut file = File::create(&args.output)?; + file.write_all(json.as_bytes())?; + + println!("Exported wallet info to {}", args.output); + + Ok(()) +} diff --git a/via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs b/via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs new file mode 100644 index 000000000..3accd2981 --- /dev/null +++ b/via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs @@ -0,0 +1,373 @@ +use std::{str::FromStr, sync::Arc, time::Duration}; + +use bitcoin::{ + absolute::LockTime, + blockdata::{opcodes::all::*, script::Builder}, + hashes::Hash, + hex::{Case, DisplayHex, FromHex}, + policy::MAX_STANDARD_TX_WEIGHT, + secp256k1::{self, Keypair, Secp256k1, SecretKey}, + sighash::{Prevouts, SighashCache}, + taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo}, + transaction::Version, + Address, Amount, Network, OutPoint, PrivateKey, PublicKey, ScriptBuf, Sequence, TapLeafHash, + TapNodeHash, TapSighashType, TapTweakHash, Transaction, TxIn, TxOut, Txid, Witness, + XOnlyPublicKey, +}; +use musig2::KeyAggContext; +use tokio::time::sleep; +use via_btc_client::{client::BitcoinClient, traits::BitcoinOps, types::NodeAuth}; +use via_musig2::{ + fee::WithdrawalFeeStrategy, get_signer, get_signer_with_merkle_root, + transaction_builder::TransactionBuilder, verify_signature, Signer, +}; +use zksync_config::configs::via_btc_client::ViaBtcClientConfig; + +const RPC_URL: &str = "http://0.0.0.0:18443"; +const RPC_USERNAME: &str = "rpcuser"; +const RPC_PASSWORD: &str = "rpcpassword"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let secp = Secp256k1::new(); + + // --- MuSig2 setup with 2 keys (unchanged for internal key aggregation) --- + let pk_str_1 = "cQnW8oDqEME4gxJHC4MC9HvJECcF7Ju8oanWdjWLGxDbkfWo7vZa"; + let pk_str_2 = "cVJYEHTzmfdRPoX6fL3vRnZVmqy4D1sWaT5WL9U25oZhQktoeHgo"; + + let private_key_1 = PrivateKey::from_wif(pk_str_1)?; + let private_key_2 = PrivateKey::from_wif(pk_str_2)?; + + let secret_key_1 = SecretKey::from_slice(&private_key_1.inner.secret_bytes())?; + let secret_key_2 = SecretKey::from_slice(&private_key_2.inner.secret_bytes())?; + + let keypair_1 = Keypair::from_secret_key(&secp, &secret_key_1); + let keypair_2 = Keypair::from_secret_key(&secp, &secret_key_2); + + let (internal_key_1, parity_1) = keypair_1.x_only_public_key(); + let (internal_key_2, parity_2) = keypair_2.x_only_public_key(); + + let pubkeys = vec![ + musig2::secp256k1::PublicKey::from_slice(&internal_key_1.public_key(parity_1).serialize())?, + musig2::secp256k1::PublicKey::from_slice(&internal_key_2.public_key(parity_2).serialize())?, + ]; + + let mut musig_key_agg_cache = KeyAggContext::new(pubkeys)?; + let agg_pubkey = musig_key_agg_cache.aggregated_pubkey::(); + + let (xonly_agg_key, _) = agg_pubkey.x_only_public_key(); + let internal_key = XOnlyPublicKey::from_slice(&xonly_agg_key.serialize())?; + + // --- Governance keys (3-of-2 multisig) --- + let gov_pk_str_1 = "cQnW8oDqEME4gxJHC4MC9HvJECcF7Ju8oanWdjWLGxDbkfWo7vZa"; + let gov_pk_str_2 = "cVJYEHTzmfdRPoX6fL3vRnZVmqy4D1sWaT5WL9U25oZhQktoeHgo"; + let gov_pk_str_3 = "cPytijNj4VAnczJD5a21bboiPavYDCLmM9AW6cmjUxrDUYnJXQaf"; + + let gov_private_key_1 = PrivateKey::from_wif(gov_pk_str_1)?; + let gov_private_key_2 = PrivateKey::from_wif(gov_pk_str_2)?; + let gov_private_key_3 = PrivateKey::from_wif(gov_pk_str_3)?; + + // Convert to Schnorr-capable keypairs + let gov_kp_1 = Keypair::from_secret_key( + &secp, + &SecretKey::from_slice(&gov_private_key_1.inner.secret_bytes())?, + ); + let gov_kp_2 = Keypair::from_secret_key( + &secp, + &SecretKey::from_slice(&gov_private_key_2.inner.secret_bytes())?, + ); + let gov_kp_3 = Keypair::from_secret_key( + &secp, + &SecretKey::from_slice(&gov_private_key_3.inner.secret_bytes())?, + ); + + let (gov_x1, _) = gov_kp_1.x_only_public_key(); + let (gov_x2, _) = gov_kp_2.x_only_public_key(); + let (gov_x3, _) = gov_kp_3.x_only_public_key(); + + println!( + "gov_kp_1: {:?}", + &gov_kp_1.public_key().serialize().to_hex_string(Case::Lower) + ); + println!( + "gov_kp_2: {:?}", + &gov_kp_2.public_key().serialize().to_hex_string(Case::Lower) + ); + println!( + "gov_kp_3: {:?}", + &gov_kp_3.public_key().serialize().to_hex_string(Case::Lower) + ); + + // --- Build Taproot-native 2-of-3 Schnorr multisig script --- + let multisig_script = Builder::new() + .push_x_only_key(&gov_x1) + .push_opcode(OP_CHECKSIG) + .push_x_only_key(&gov_x2) + .push_opcode(OP_CHECKSIGADD) + .push_x_only_key(&gov_x3) + .push_opcode(OP_CHECKSIGADD) + .push_int(2) + .push_opcode(OP_NUMEQUAL) + .into_script(); + + let spend_info = TaprootBuilder::new() + .add_leaf(0, ScriptBuf::from(multisig_script.clone())) + .unwrap() + .finalize(&secp, internal_key) + .unwrap(); + + let taproot_output_key = spend_info.output_key(); + let taproot_address = Address::p2tr_tweaked(taproot_output_key, Network::Regtest); + + println!("spend_info.merkle_root(): {:?}", &spend_info.merkle_root()); + + println!("Final Taproot 2-of-3 Schnorr address: {}", taproot_address); + + let pubkeys_str = vec![ + internal_key_1 + .public_key(parity_1) + .serialize() + .to_hex_string(Case::Lower), + internal_key_2 + .public_key(parity_2) + .serialize() + .to_hex_string(Case::Lower), + ]; + + let signer1 = + get_signer_with_merkle_root(pk_str_1, pubkeys_str.clone(), spend_info.merkle_root())?; + let signer2 = + get_signer_with_merkle_root(pk_str_2, pubkeys_str.clone(), spend_info.merkle_root())?; + + // --- RPC setup --- + let auth = NodeAuth::UserPass(RPC_USERNAME.to_string(), RPC_PASSWORD.to_string()); + let config = ViaBtcClientConfig { + network: Network::Regtest.to_string(), + external_apis: vec![], + fee_strategies: vec![], + use_rpc_for_fee_rate: None, + }; + + let btc_client = BitcoinClient::new(RPC_URL, auth, config).unwrap(); + + let utxos = btc_client.fetch_utxos(&taproot_address).await?; + let utxo = utxos[0].clone(); + + let tx_hex = transfer_utxo_from_bridge_address_using_governance_wallet_using_script_path( + btc_client.clone(), + &spend_info, + multisig_script, + gov_kp_1, + gov_kp_2, + gov_x1, + gov_x2, + utxo, + ) + .await?; + + let txid = btc_client.broadcast_signed_transaction(&tx_hex).await?; + println!( + "Broadcasted transfer UTXO to GOV wallet using script path, txid: {:?}", + txid + ); + + sleep(Duration::from_secs(1)).await; + + let tx_hex = + process_withdraw_using_key_hash(btc_client.clone(), signer1, signer2, taproot_address) + .await?; + + let txid = btc_client.broadcast_signed_transaction(&tx_hex).await?; + println!( + "Broadcasted process withdrawal using the script hash, txid: {:?}", + txid + ); + + Ok(()) +} + +async fn process_withdraw_using_key_hash( + btc_client: BitcoinClient, + mut signer1: Signer, + mut signer2: Signer, + taproot_address: Address, +) -> anyhow::Result { + // ------------------------------------------- + // Fetching UTXOs from node + // ------------------------------------------- + let receivers_address = receivers_address(); + + // Create outputs for grouped withdrawals + let outputs: Vec = vec![TxOut { + value: Amount::from_sat(50000000), + script_pubkey: receivers_address.script_pubkey(), + }]; + + let op_return_prefix: &[u8] = b"VIA_PROTOCOL:WITHDRAWAL:"; + let op_return_data = Txid::all_zeros().to_byte_array(); + + let tx_builder = + TransactionBuilder::new(Arc::new(btc_client.clone()), taproot_address.clone())?; + let mut unsigned_tx = tx_builder + .build_transaction_with_op_return( + outputs, + op_return_prefix, + vec![&op_return_data.to_vec()], + Arc::new(WithdrawalFeeStrategy::new()), + None, + None, + MAX_STANDARD_TX_WEIGHT as u64, + ) + .await?[0] + .clone(); + + let messages = tx_builder.get_tr_sighashes(&unsigned_tx)?; + assert_eq!(messages.len(), 1); + + let message1 = messages[0].clone(); + + unsigned_tx.utxos.iter().for_each(|utxo| { + println!( + "{:?}", + utxo.1.script_pubkey == taproot_address.clone().script_pubkey() + ) + }); + + // ------------------------------------------- + // MuSig2 Signing Process + // ------------------------------------------- + + signer1.start_signing_session(message1.clone())?; + signer2.start_signing_session(message1.clone())?; + + let nonce1 = signer1.our_nonce().unwrap(); + let nonce2 = signer2.our_nonce().unwrap(); + + signer1.mark_nonce_submitted(); + signer2.mark_nonce_submitted(); + + signer1.receive_nonce(1, nonce2.clone())?; + signer2.receive_nonce(0, nonce1.clone())?; + + signer1.create_partial_signature()?; + let partial_sig2 = signer2.create_partial_signature()?; + + signer1.mark_partial_sig_submitted(); + signer2.mark_partial_sig_submitted(); + + signer1.receive_partial_signature(1, partial_sig2)?; + + let musig2_signature1 = signer1.create_final_signature()?; + + let mut final_sig_with_hashtype1 = musig2_signature1.serialize().to_vec(); + + let sighash_type = TapSighashType::All; + final_sig_with_hashtype1.push(sighash_type as u8); + println!("final_sig_with_hashtype1 {:?}", &final_sig_with_hashtype1); + + unsigned_tx.tx.input[0].witness = Witness::from(vec![final_sig_with_hashtype1.clone()]); + + let tx_hex = bitcoin::consensus::encode::serialize_hex(&unsigned_tx.tx); + let agg_pub = signer1.aggregated_pubkey(); + + verify_signature(agg_pub.clone(), musig2_signature1, &message1)?; + + Ok(tx_hex) +} + +async fn transfer_utxo_from_bridge_address_using_governance_wallet_using_script_path( + btc_client: BitcoinClient, + spend_info: &TaprootSpendInfo, + multisig_script: ScriptBuf, + gov_kp_1: Keypair, + gov_kp_2: Keypair, + gov_x1: XOnlyPublicKey, + gov_x2: XOnlyPublicKey, + utxo: (OutPoint, TxOut), +) -> anyhow::Result { + let secp = Secp256k1::new(); + + let receivers_address = + Address::from_str("bcrt1q92gkfme6k9dkpagrkwt76etkaq29hvf02w5m38f6shs4ddpw7hzqp347zm")? + .assume_checked(); + + // --- Build spending transaction --- + let inputs: Vec = vec![TxIn { + previous_output: OutPoint { + txid: utxo.0.txid, + vout: utxo.0.vout, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::default(), + }]; + + let outputs: Vec = vec![TxOut { + value: utxo.1.value - Amount::from_sat(500), + script_pubkey: receivers_address.script_pubkey(), + }]; + + let mut spending_tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: inputs, + output: outputs, + }; + + // --- Control block for script path spend --- + let control_block = spend_info + .control_block(&(multisig_script.clone(), LeafVersion::TapScript)) + .unwrap() + .serialize(); + + // --- Sighash --- + let mut sighash_cache = SighashCache::new(&spending_tx); + let prevout = utxo.1.clone(); + let leaf_hash = TapLeafHash::from_script(&multisig_script, LeafVersion::TapScript); + + let sighash = sighash_cache + .taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&[prevout.clone()]), + leaf_hash, + TapSighashType::All, + ) + .expect("sighash"); + + let msg = secp256k1::Message::from_digest_slice(&sighash[..]).unwrap(); + + // --- Schnorr signatures from 2 of 3 governance keys --- + let sig1 = secp.sign_schnorr(&msg, &gov_kp_1); + let sig2 = secp.sign_schnorr(&msg, &gov_kp_2); + + let mut sig1_bytes = sig1.as_ref().to_vec(); + sig1_bytes.push(TapSighashType::All as u8); + + let mut sig2_bytes = sig2.as_ref().to_vec(); + sig2_bytes.push(TapSighashType::All as u8); + + // --- Witness stack: [sig1, sig2, tapscript, control_block] --- + let mut witness = Witness::new(); + witness.push(&[]); // no signature for gov_x3 + witness.push(sig2_bytes); + witness.push(sig1_bytes); + witness.push(multisig_script.as_bytes()); + witness.push(&control_block); + + spending_tx.input[0].witness = witness; + + secp.verify_schnorr(&sig1, &msg, &gov_x1)?; + secp.verify_schnorr(&sig2, &msg, &gov_x2)?; + + let tx_hex = bitcoin::consensus::encode::serialize_hex(&spending_tx); + println!("tx_hex: {:?}", tx_hex); + + Ok(tx_hex) +} + +pub(crate) fn receivers_address() -> Address { + Address::from_str("bc1p0dq0tzg2r780hldthn5mrznmpxsxc0jux5f20fwj0z3wqxxk6fpqm7q0va") + .expect("a valid address") + .require_network(Network::Bitcoin) + .expect("valid address for mainnet") +} From fa6dcfefa0c5e97755cfb6c5b723a43ae3358b00 Mon Sep 17 00:00:00 2001 From: 0xatomFusion <179967211+0xatomFusion@users.noreply.github.com> Date: Sun, 10 Aug 2025 20:52:06 +0100 Subject: [PATCH 6/8] fix: bridge address validation and clean scripts and docs --- core/lib/types/src/via_wallet.rs | 2 +- docs/via_guides/upgrade.md | 11 ++++++++--- .../lib/via_musig2/examples/compute_musig2.rs | 9 +++++++-- .../via_musig2/examples/musig2_with_script_path.rs | 13 +++++-------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/core/lib/types/src/via_wallet.rs b/core/lib/types/src/via_wallet.rs index 501706842..e2201df9c 100644 --- a/core/lib/types/src/via_wallet.rs +++ b/core/lib/types/src/via_wallet.rs @@ -76,7 +76,7 @@ pub struct SystemWallets { impl SystemWallets { pub fn is_valid_bridge_address(&self, bridge_address: Address) -> anyhow::Result<()> { - if !self.verifiers.contains(&bridge_address) { + if self.bridge != bridge_address { anyhow::bail!( "bridge address mismatch, expected one of {:?}, found {}", &self.bridge, diff --git a/docs/via_guides/upgrade.md b/docs/via_guides/upgrade.md index af9171913..e514e1e25 100644 --- a/docs/via_guides/upgrade.md +++ b/docs/via_guides/upgrade.md @@ -250,6 +250,11 @@ VIA_BTC_SENDER_WALLET_ADDRESS=bcrt1qw2mvkvm6alfhe86yf328kgvr7mupdx4vln7kpv ``` 4. Execute an other deposit and withdrawal, the sequencer and verifiers process the batches. + +```sh +via token deposit --amount 10 --receiver-l2-address 0x36615Cf349d7F6344891B1e7CA7C72883F5dc049 --bridge-address bcrt1p3s7m76wp5seprjy4gdxuxrr8pjgd47q5s8lu9vefxmp0my2p4t9qh6s8kq +``` + 5. Done. --- @@ -278,7 +283,7 @@ via multisig create-update-bridge \ --inputTxId \ --inputVout \ --inputAmount \ ---proposalTxid bf8731b79d50b8b4862ae91ee6e3e2beae805ba534dd03daa78de0734a3dc8b8 \ +--proposalTxid \ --fee 500 ``` @@ -323,8 +328,8 @@ VIA_BTC_SENDER_WALLET_ADDRESS=bcrt1q50xmdcwlmt8qhwczxptaq2h5cn3zchcrvqd35v VIA_VERIFIER_PRIVATE_KEY=cVJYEHTzmfdRPoX6fL3vRnZVmqy4D1sWaT5WL9U25oZhQktoeHgo VIA_VERIFIER_WALLET_ADDRESS=bcrt1q50xmdcwlmt8qhwczxptaq2h5cn3zchcrvqd35v VIA_VERIFIER_BRIDGE_ADDRESS_MERKLE_ROOT=2aa187093ce1f9e55ad02aa804480cc01beb9c570781133b768d8cfb12177e25 -VIA_GENESIS_VERIFIERS_PUB_KEYS=025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241,03c2871e18d4fb503ead90461da747b40df5e28da0fd3e067f3731f1a28da60ddf -VIA_GENESIS_COORDINATOR_PUB_KEY=025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241 +VIA_BRIDGE_VERIFIERS_PUB_KEYS=025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241,03c2871e18d4fb503ead90461da747b40df5e28da0fd3e067f3731f1a28da60ddf +VIA_BRIDGE_COORDINATOR_PUB_KEY=025b3c069378f860cc4dae864a491e0cd33cc559b9f82fc856d4dcc74d3d763241 VIA_BRIDGE_BRIDGE_ADDRESS=bcrt1pfk264lnycy2v48h3we2jajyg7kyuvha9yfkd4qmxfrgywz3meyhqhdhmj8 ``` diff --git a/via_verifier/lib/via_musig2/examples/compute_musig2.rs b/via_verifier/lib/via_musig2/examples/compute_musig2.rs index 2334c2962..a9623aa26 100644 --- a/via_verifier/lib/via_musig2/examples/compute_musig2.rs +++ b/via_verifier/lib/via_musig2/examples/compute_musig2.rs @@ -40,7 +40,8 @@ struct Args { } #[derive(Serialize)] -struct ExportData { +pub(crate) struct ExportData { + public_keys: Vec, aggregated_internal_key: String, governance_script_hex: String, taproot_output_key: String, @@ -71,13 +72,16 @@ async fn main() -> anyhow::Result<()> { } // Aggregate MuSig2 key - let mut musig_key_agg_cache = KeyAggContext::new(musig_pubkeys)?; + let musig_key_agg_cache = KeyAggContext::new(musig_pubkeys)?; let agg_pubkey = musig_key_agg_cache.aggregated_pubkey::(); let (xonly_agg_key, _) = agg_pubkey.x_only_public_key(); let internal_key = XOnlyPublicKey::from_slice(&xonly_agg_key.serialize())?; // --- Governance pubkeys (x-only) --- let gov_hex: Vec<&str> = args.governance_keys.split(',').collect(); + + let public_keys = gov_hex.iter().map(|pk| pk.to_string()).collect::>(); + let mut gov_xonly = Vec::new(); for hex in gov_hex { let xonly = PublicKey::from_str(&hex.trim())? @@ -138,6 +142,7 @@ async fn main() -> anyhow::Result<()> { // --- Export JSON file --- let data = ExportData { + public_keys, aggregated_internal_key: internal_key.to_string(), governance_script_hex: multisig_script.to_hex_string(), taproot_output_key: taproot_output_key.to_string(), diff --git a/via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs b/via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs index 3accd2981..a3ee48181 100644 --- a/via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs +++ b/via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs @@ -4,21 +4,20 @@ use bitcoin::{ absolute::LockTime, blockdata::{opcodes::all::*, script::Builder}, hashes::Hash, - hex::{Case, DisplayHex, FromHex}, + hex::{Case, DisplayHex}, policy::MAX_STANDARD_TX_WEIGHT, secp256k1::{self, Keypair, Secp256k1, SecretKey}, sighash::{Prevouts, SighashCache}, taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo}, transaction::Version, - Address, Amount, Network, OutPoint, PrivateKey, PublicKey, ScriptBuf, Sequence, TapLeafHash, - TapNodeHash, TapSighashType, TapTweakHash, Transaction, TxIn, TxOut, Txid, Witness, - XOnlyPublicKey, + Address, Amount, Network, OutPoint, PrivateKey, ScriptBuf, Sequence, TapLeafHash, + TapSighashType, Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey, }; use musig2::KeyAggContext; use tokio::time::sleep; use via_btc_client::{client::BitcoinClient, traits::BitcoinOps, types::NodeAuth}; use via_musig2::{ - fee::WithdrawalFeeStrategy, get_signer, get_signer_with_merkle_root, + fee::WithdrawalFeeStrategy, get_signer_with_merkle_root, transaction_builder::TransactionBuilder, verify_signature, Signer, }; use zksync_config::configs::via_btc_client::ViaBtcClientConfig; @@ -52,7 +51,7 @@ async fn main() -> Result<(), Box> { musig2::secp256k1::PublicKey::from_slice(&internal_key_2.public_key(parity_2).serialize())?, ]; - let mut musig_key_agg_cache = KeyAggContext::new(pubkeys)?; + let musig_key_agg_cache = KeyAggContext::new(pubkeys)?; let agg_pubkey = musig_key_agg_cache.aggregated_pubkey::(); let (xonly_agg_key, _) = agg_pubkey.x_only_public_key(); @@ -154,7 +153,6 @@ async fn main() -> Result<(), Box> { let utxo = utxos[0].clone(); let tx_hex = transfer_utxo_from_bridge_address_using_governance_wallet_using_script_path( - btc_client.clone(), &spend_info, multisig_script, gov_kp_1, @@ -276,7 +274,6 @@ async fn process_withdraw_using_key_hash( } async fn transfer_utxo_from_bridge_address_using_governance_wallet_using_script_path( - btc_client: BitcoinClient, spend_info: &TaprootSpendInfo, multisig_script: ScriptBuf, gov_kp_1: Keypair, From 251b9ca9809884be463335bb5667f2be7eb064cc Mon Sep 17 00:00:00 2001 From: 0xatomFusion <179967211+0xatomFusion@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:08:18 +0100 Subject: [PATCH 7/8] feat: add script to create a gov tx to transfer utxo from bridge --- docs/via_guides/upgrade.md | 67 ++++ via_verifier/lib/via_musig2/Cargo.toml | 4 + .../examples/transfer_utxos_from_bridge.rs | 375 ++++++++++++++++++ 3 files changed, 446 insertions(+) create mode 100644 via_verifier/lib/via_musig2/examples/transfer_utxos_from_bridge.rs diff --git a/docs/via_guides/upgrade.md b/docs/via_guides/upgrade.md index e514e1e25..c095e0333 100644 --- a/docs/via_guides/upgrade.md +++ b/docs/via_guides/upgrade.md @@ -341,3 +341,70 @@ via token deposit --amount 10 --receiver-l2-address 0x36615Cf349d7F6344891B1e7CA 8. Withdraw 1 BTC. 9. The verifier and coordinator process the batch and sequencer finalize the batch. + +## Transfer the UTXOs from the old bridge address to the governance wallet + +1. Get the UTXOs you want to transfer, then create a file `utxos.json`. Each utxo should has a `txid`, `vout` and + `value` + +```json +[ + { + "txid": "1a32c75a5b859ebe799cc00a0d4389633204f5ac607965dba2878a2de627dc8f", + "vout": 1, + "value": 100000000 + } + ... +] +``` + +```sh +curl --user rpcuser:rpcpassword \ + --data-binary '{ + "jsonrpc": "1.0", + "id": "scan_utxo", + "method": "scantxoutset", + "params": [ + "start", + [ + { "desc": "addr(bcrt1pfk264lnycy2v48h3we2jajyg7kyuvha9yfkd4qmxfrgywz3meyhqhdhmj8)", "range": 1000 } + ] + ] + }' \ + -H 'content-type: text/plain;' \ + http://127.0.0.1:18443/ +``` + +2. Create a new tx + +```sh +cargo run \ + --example transfer_utxos_from_bridge -- \ + --from-address bcrt1pfk264lnycy2v48h3we2jajyg7kyuvha9yfkd4qmxfrgywz3meyhqhdhmj8 \ + --to-address bcrt1q92gkfme6k9dkpagrkwt76etkaq29hvf02w5m38f6shs4ddpw7hzqp347zm \ + --action prepare +``` + +2. The signer 1 sign + +```sh +cargo run --example transfer_utxos_from_bridge -- --action sign --private-key cQnW8oDqEME4gxJHC4MC9HvJECcF7Ju8oanWdjWLGxDbkfWo7vZa +``` + +3. The signer 2 sign + +```sh +cargo run --example transfer_utxos_from_bridge -- --action sign --private-key cVJYEHTzmfdRPoX6fL3vRnZVmqy4D1sWaT5WL9U25oZhQktoeHgo +``` + +2. Finalise the tx + +```sh +cargo run --example transfer_utxos_from_bridge -- --action finalize +``` + +2. Broadcast the transaction + +```sh +cargo run --example transfer_utxos_from_bridge -- --action broadcast +``` diff --git a/via_verifier/lib/via_musig2/Cargo.toml b/via_verifier/lib/via_musig2/Cargo.toml index 29cede6a3..5a05e2981 100644 --- a/via_verifier/lib/via_musig2/Cargo.toml +++ b/via_verifier/lib/via_musig2/Cargo.toml @@ -70,3 +70,7 @@ path = "examples/musig2_with_script_path.rs" [[example]] name = "compute_musig2" path = "examples/compute_musig2.rs" + +[[example]] +name = "transfer_utxos_from_bridge" +path = "examples/transfer_utxos_from_bridge.rs" diff --git a/via_verifier/lib/via_musig2/examples/transfer_utxos_from_bridge.rs b/via_verifier/lib/via_musig2/examples/transfer_utxos_from_bridge.rs new file mode 100644 index 000000000..f7a1a8983 --- /dev/null +++ b/via_verifier/lib/via_musig2/examples/transfer_utxos_from_bridge.rs @@ -0,0 +1,375 @@ +use std::{collections::HashMap, fs::File, str::FromStr}; + +use anyhow::{anyhow, Context, Result}; +use bitcoin::{ + absolute::LockTime, + consensus::{self, encode::serialize_hex}, + hashes::Hash, + hex::DisplayHex, + secp256k1::{self, Keypair, Secp256k1, SecretKey}, + sighash::{Prevouts, SighashCache}, + taproot::LeafVersion, + transaction::Version, + Address, Amount, Network, OutPoint, PrivateKey, ScriptBuf, Sequence, TapLeafHash, + TapSighashType, Transaction, TxIn, TxOut, Txid, Witness, +}; +use bitcoincore_rpc::Auth; +use clap::Parser; +use serde::{Deserialize, Serialize}; +use via_btc_client::{client::BitcoinClient, traits::BitcoinOps}; +use zksync_config::configs::via_btc_client::ViaBtcClientConfig; + +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Args { + #[arg(long)] + private_key: Option, + + #[arg(long)] + signers_public_keys: Vec, + + #[arg(long, default_value = "my_wallet.json")] + wallet_path: String, + + #[arg(long, default_value = "utxos.json")] + utxos_path: Option, + + #[arg(long)] + from_address: Option, + + #[arg(long)] + to_address: Option, + + #[arg(long, default_value = "500")] + fee: u64, + + #[arg(long, default_value = "regtest")] + network: String, + + #[arg(long, default_value = "gov_bridge_tx.json")] + output: String, + + #[arg(long, default_value = "prepare")] + action: String, + + #[arg(long, default_value = "http://0.0.0.0:18443")] + rpc_url: String, + + #[arg(long, default_value = "rpcuser")] + rpc_username: String, + + #[arg(long, default_value = "rpcpassword")] + rpc_password: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +enum Action { + Prepare, + Sign, + Finalize, + Broadcast, +} + +impl Action { + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "prepare" => Ok(Action::Prepare), + "sign" => Ok(Action::Sign), + "finalize" => Ok(Action::Finalize), + "broadcast" => Ok(Action::Broadcast), + _ => Err(anyhow!("Invalid action: {s}")), + } + } +} + +#[derive(Deserialize, Serialize, Clone)] +struct UTXO { + txid: String, + vout: u32, + value: u64, +} + +impl UTXO { + fn to_parts(&self, from: &Address) -> (OutPoint, TxOut) { + ( + OutPoint { + txid: Txid::from_str(&self.txid).expect("Invalid txid format"), + vout: self.vout, + }, + TxOut { + value: Amount::from_sat(self.value), + script_pubkey: from.script_pubkey(), + }, + ) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct WalletData { + public_keys: Vec, + aggregated_internal_key: String, + governance_script_hex: String, + taproot_output_key: String, + merkle_root: Option, + taproot_address: String, + control_block: String, + threshold: usize, + total_governance_keys: usize, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct OutputData { + messages: Vec, + signatures: Option>>, + tx: String, + signed_tx: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + match Action::from_str(&args.action)? { + Action::Prepare => do_prepare(&args).await?, + Action::Sign => do_sign(&args)?, + Action::Finalize => do_finalize(&args)?, + Action::Broadcast => do_broadcast(&args).await?, + } + Ok(()) +} + +async fn do_prepare(args: &Args) -> Result<()> { + let wallet: WalletData = read_json(&args.wallet_path)?; + let network = Network::from_str(&args.network)?; + let from_address = Address::from_str( + &args + .from_address + .clone() + .ok_or_else(|| anyhow!("Missing from address"))?, + )? + .require_network(network.clone())?; + let to_address = Address::from_str( + &args + .to_address + .clone() + .ok_or(anyhow!("Missing to address"))?, + )? + .require_network(network.clone())?; + + let utxos: Vec<(OutPoint, TxOut)> = read_json::>( + &args + .utxos_path + .clone() + .ok_or(anyhow!("Missing utxos path"))?, + )? + .into_iter() + .map(|u| u.to_parts(&from_address)) + .collect(); + + let tx = build_tx(utxos.clone(), Amount::from_sat(args.fee), to_address); + + let messages = compute_inputs_sig_hashes( + tx.clone(), + utxos.clone(), + ScriptBuf::from_hex(&wallet.governance_script_hex)?, + ) + .await? + .iter() + .map(|m| hex::encode(m.as_ref())) + .collect(); + + let output_data = OutputData { + messages, + signatures: None, + tx: serialize_hex(&tx), + signed_tx: None, + }; + write_json(&args.output, &output_data) +} + +fn do_sign(args: &Args) -> Result<()> { + let mut output: OutputData = read_json(&args.output)?; + let messages = output + .messages + .iter() + .map(|m| { + let bytes = hex::decode(m)?; + secp256k1::Message::from_digest_slice(&bytes).map_err(|e| anyhow!(e)) + }) + .collect::>>()?; + + let pk_wif = args + .private_key + .clone() + .ok_or_else(|| anyhow!("Missing private key"))?; + let secp = Secp256k1::new(); + let sk = PrivateKey::from_wif(&pk_wif)?; + let keypair = + Keypair::from_secret_key(&secp, &SecretKey::from_slice(&sk.inner.secret_bytes())?); + + let signer_signatures = sign_tx(keypair, messages)? + .iter() + .map(|sig| sig.to_hex_string(bitcoin::hex::Case::Lower)) + .collect::>(); + + output + .signatures + .get_or_insert_with(HashMap::new) + .insert(keypair.public_key().to_string(), signer_signatures); + + write_json(&args.output, &output) +} + +fn do_finalize(args: &Args) -> Result<()> { + let mut output: OutputData = read_json(&args.output)?; + let wallet: WalletData = read_json(&args.wallet_path)?; + let utxos: Vec = read_json( + &args + .utxos_path + .clone() + .ok_or(anyhow!("Missing utxos path"))?, + )?; + + let signatures = output + .signatures + .clone() + .ok_or_else(|| anyhow!("Missing signatures"))?; + + let witnesses = build_witnesses( + args.signers_public_keys.clone(), + utxos.len(), + hex::decode(&wallet.governance_script_hex)?, + hex::decode(&wallet.control_block)?, + signatures, + )?; + + let tx: Transaction = consensus::deserialize(&hex::decode(&output.tx)?)?; + let signed_tx = finalize_tx(tx, witnesses)?; + output.signed_tx = Some(serialize_hex(&signed_tx)); + + write_json(&args.output, &output) +} + +async fn do_broadcast(args: &Args) -> Result<()> { + let output: OutputData = read_json(&args.output)?; + + let auth = Auth::UserPass(args.rpc_username.clone(), args.rpc_password.clone()); + let config = ViaBtcClientConfig { + network: args.network.clone(), + external_apis: vec![], + fee_strategies: vec![], + use_rpc_for_fee_rate: None, + }; + + let btc_client = BitcoinClient::new(&args.rpc_url.clone(), auth, config).unwrap(); + let Some(signed_tx) = output.signed_tx else { + anyhow::bail!("signed signature missing"); + }; + let txid = btc_client.broadcast_signed_transaction(&signed_tx).await?; + + println!("Txid: {:?}", txid.to_string()); + Ok(()) +} + +// ---------- Helpers ---------- + +fn read_json Deserialize<'de>>(path: &str) -> Result { + let file = File::open(path).with_context(|| format!("Opening {path}"))?; + Ok(serde_json::from_reader(file)?) +} + +fn write_json(path: &str, value: &T) -> Result<()> { + let file = File::create(path).with_context(|| format!("Creating {path}"))?; + Ok(serde_json::to_writer_pretty(file, value)?) +} + +fn build_tx(utxos: Vec<(OutPoint, TxOut)>, fee: Amount, to: Address) -> Transaction { + let total_amount = utxos.iter().map(|u| u.1.value).sum::() - fee; + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: utxos + .into_iter() + .map(|(op, _)| TxIn { + previous_output: op, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::default(), + }) + .collect(), + output: vec![TxOut { + value: total_amount, + script_pubkey: to.script_pubkey(), + }], + } +} + +async fn compute_inputs_sig_hashes( + tx: Transaction, + utxos: Vec<(OutPoint, TxOut)>, + multisig_script: ScriptBuf, +) -> Result> { + let leaf_hash = TapLeafHash::from_script(&multisig_script, LeafVersion::TapScript); + let prevouts: Vec<_> = utxos.into_iter().map(|(_, txout)| txout).collect(); + let mut sighash_cache = SighashCache::new(&tx); + + (0..prevouts.len()) + .map(|i| { + let sighash = sighash_cache.taproot_script_spend_signature_hash( + i, + &Prevouts::All(&prevouts), + leaf_hash, + TapSighashType::All, + )?; + Ok(secp256k1::Message::from_digest_slice( + &sighash.as_raw_hash().as_byte_array().to_vec(), + )?) + }) + .collect() +} + +fn sign_tx(kp: Keypair, messages: Vec) -> Result>> { + let secp = Secp256k1::new(); + messages + .into_iter() + .map(|msg| { + let mut sig = secp.sign_schnorr(&msg, &kp).as_ref().to_vec(); + sig.push(TapSighashType::All as u8); + Ok(sig) + }) + .collect() +} + +fn build_witnesses( + signers_public_keys: Vec, + total_utxos: usize, + multisig_script: Vec, + control_block: Vec, + signatures_per_utxo: HashMap>, +) -> anyhow::Result> { + (0..total_utxos) + .map(|utxo_idx| { + let mut witness = Witness::new(); + + for public_key in signers_public_keys.iter().rev() { + let sig = signatures_per_utxo + .get(public_key) + .and_then(|sigs| sigs.get(utxo_idx)) + .map(|s| hex::decode(s)) + .transpose()?; + + witness.push(sig.unwrap_or_default()); + } + + witness.push(&multisig_script); + witness.push(&control_block); + Ok(witness) + }) + .collect::>>() +} + +fn finalize_tx(mut tx: Transaction, witnesses: Vec) -> Result { + for (i, wit) in witnesses.into_iter().enumerate() { + tx.input[i].witness = wit; + } + Ok(tx) +} From ce1113b39e41406957d173179b42f2e73199b960 Mon Sep 17 00:00:00 2001 From: 0xatomFusion <179967211+0xatomFusion@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:44:29 +0100 Subject: [PATCH 8/8] fix: build witness --- docs/via_guides/upgrade.md | 31 ++++++++++++------- .../examples/transfer_utxos_from_bridge.rs | 27 +++++++++------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/docs/via_guides/upgrade.md b/docs/via_guides/upgrade.md index c095e0333..d4cb3097a 100644 --- a/docs/via_guides/upgrade.md +++ b/docs/via_guides/upgrade.md @@ -344,20 +344,18 @@ via token deposit --amount 10 --receiver-l2-address 0x36615Cf349d7F6344891B1e7CA ## Transfer the UTXOs from the old bridge address to the governance wallet -1. Get the UTXOs you want to transfer, then create a file `utxos.json`. Each utxo should has a `txid`, `vout` and - `value` +1. Sent UTXO to the bridge wallet -```json -[ - { - "txid": "1a32c75a5b859ebe799cc00a0d4389633204f5ac607965dba2878a2de627dc8f", - "vout": 1, - "value": 100000000 - } - ... -] +```sh +curl --user rpcuser:rpcpassword \ + --data-binary '{"jsonrpc":"1.0","id":"sendbtc","method":"sendtoaddress","params":["bcrt1pfk264lnycy2v48h3we2jajyg7kyuvha9yfkd4qmxfrgywz3meyhqhdhmj8", 1]}' \ + -H 'content-type: text/plain;' \ + http://127.0.0.1:18443/wallet/Alice ``` +2. List the UTXOs you want to transfer, then create a file `utxos.json`. Each utxo should has a `txid`, `vout` and + `value` + ```sh curl --user rpcuser:rpcpassword \ --data-binary '{ @@ -375,6 +373,17 @@ curl --user rpcuser:rpcpassword \ http://127.0.0.1:18443/ ``` +```json +[ + { + "txid": "", + "vout": 1, + "value": 100000000 + } + ... +] +``` + 2. Create a new tx ```sh diff --git a/via_verifier/lib/via_musig2/examples/transfer_utxos_from_bridge.rs b/via_verifier/lib/via_musig2/examples/transfer_utxos_from_bridge.rs index f7a1a8983..eee97671b 100644 --- a/via_verifier/lib/via_musig2/examples/transfer_utxos_from_bridge.rs +++ b/via_verifier/lib/via_musig2/examples/transfer_utxos_from_bridge.rs @@ -6,7 +6,7 @@ use bitcoin::{ consensus::{self, encode::serialize_hex}, hashes::Hash, hex::DisplayHex, - secp256k1::{self, Keypair, Secp256k1, SecretKey}, + secp256k1::{self, schnorr::Signature, Keypair, Secp256k1, SecretKey}, sighash::{Prevouts, SighashCache}, taproot::LeafVersion, transaction::Version, @@ -25,9 +25,6 @@ struct Args { #[arg(long)] private_key: Option, - #[arg(long)] - signers_public_keys: Vec, - #[arg(long, default_value = "my_wallet.json")] wallet_path: String, @@ -235,7 +232,7 @@ fn do_finalize(args: &Args) -> Result<()> { .ok_or_else(|| anyhow!("Missing signatures"))?; let witnesses = build_witnesses( - args.signers_public_keys.clone(), + wallet.public_keys.clone(), utxos.len(), hex::decode(&wallet.governance_script_hex)?, hex::decode(&wallet.control_block)?, @@ -332,7 +329,10 @@ fn sign_tx(kp: Keypair, messages: Vec) -> Result messages .into_iter() .map(|msg| { - let mut sig = secp.sign_schnorr(&msg, &kp).as_ref().to_vec(); + let signature: Signature = secp.sign_schnorr(&msg, &kp); + secp.verify_schnorr(&signature, &msg, &kp.public_key().x_only_public_key().0)?; + + let mut sig = signature.as_ref().to_vec(); sig.push(TapSighashType::All as u8); Ok(sig) }) @@ -340,7 +340,7 @@ fn sign_tx(kp: Keypair, messages: Vec) -> Result } fn build_witnesses( - signers_public_keys: Vec, + public_keys: Vec, total_utxos: usize, multisig_script: Vec, control_block: Vec, @@ -350,14 +350,17 @@ fn build_witnesses( .map(|utxo_idx| { let mut witness = Witness::new(); - for public_key in signers_public_keys.iter().rev() { - let sig = signatures_per_utxo + for public_key in public_keys.iter().rev() { + if let Some(sig) = signatures_per_utxo .get(public_key) .and_then(|sigs| sigs.get(utxo_idx)) .map(|s| hex::decode(s)) - .transpose()?; - - witness.push(sig.unwrap_or_default()); + .transpose()? + { + witness.push(sig); + } else { + witness.push(&[]); + } } witness.push(&multisig_script);