diff --git a/bin/strata-client/src/extractor.rs b/bin/strata-client/src/extractor.rs index 62205a143..99095fc40 100644 --- a/bin/strata-client/src/extractor.rs +++ b/bin/strata-client/src/extractor.rs @@ -10,8 +10,7 @@ use std::sync::Arc; use bitcoin::{ - consensus::Decodable, hashes::Hash, params::Params, Address, Amount, Network, OutPoint, - TapNodeHash, Transaction, + hashes::Hash, params::Params, Address, Amount, Network, OutPoint, TapNodeHash, Transaction, }; use jsonrpsee::core::RpcResult; use strata_bridge_tx_builder::prelude::{CooperativeWithdrawalInfo, DepositInfo}; @@ -54,22 +53,20 @@ pub(super) async fn extract_deposit_requests( .map_err(RpcServerError::Db)?; let deposit_info_iter = l1_txs.into_iter().filter_map(move |l1_tx| { - let mut raw_tx = l1_tx.tx_data(); - let tx = Transaction::consensus_decode(&mut raw_tx); - if tx.is_err() { - // The client does not benefit from knowing that some transaction stored in the db is - // invalid/corrupted. They should get the rest of the duties even if some of them are - // potentially corrupted. - // The sequencer/full node operator _does_ care about this though. So, we log the - // error instead. - let err = tx.unwrap_err(); - error!(%block_height, ?err, "failed to decode raw tx bytes in L1Database"); - debug!(%block_height, ?err, ?l1_tx, "failed to decode raw tx bytes in L1Database"); - - return None; - } - - let tx = tx.expect("raw tx bytes must be decodable"); + let tx: Transaction = match l1_tx.tx_data().try_into() { + Ok(tx) => tx, + Err(err) => { + // The client does not benefit from knowing that some transaction stored in the db is + // invalid/corrupted. They should get the rest of the duties even if some of them are + // potentially corrupted. + // The sequencer/full node operator _does_ care about this though. So, we log the + // error instead. + error!(%block_height, ?err, "failed to decode raw tx bytes in L1Database"); + debug!(%block_height, ?err, ?l1_tx, "failed to decode raw tx bytes in L1Database"); + + return None; + } + }; let deposit_request_outpoint = OutPoint { txid: tx.compute_txid(), @@ -183,7 +180,6 @@ mod tests { use bitcoin::{ absolute::LockTime, - consensus::Encodable, key::rand::{self, Rng}, opcodes::{OP_FALSE, OP_TRUE}, script::Builder, @@ -199,7 +195,7 @@ mod tests { use strata_primitives::{ bridge::OperatorIdx, buf::Buf32, - l1::{BitcoinAmount, L1BlockManifest, OutputRef, XOnlyPk}, + l1::{BitcoinAmount, L1BlockManifest, OutputRef, RawBitcoinTx, XOnlyPk}, }; use strata_rocksdb::{test_utils::get_rocksdb_tmp_instance, L1Db}; use strata_state::{ @@ -338,7 +334,7 @@ mod tests { l1_db: &Arc, num_blocks: usize, max_txs_per_block: usize, - needle: (Vec, ProtocolOperation), + needle: (RawBitcoinTx, ProtocolOperation), ) -> usize { let mut arb = ArbitraryGenerator::new(); assert!( @@ -399,7 +395,7 @@ mod tests { } /// Create a known transaction that should be present in some block. - fn get_needle() -> (Vec, ProtocolOperation, DepositInfo) { + fn get_needle() -> (RawBitcoinTx, ProtocolOperation, DepositInfo) { let mut arb = ArbitraryGenerator::new(); let network = Network::Regtest; @@ -437,10 +433,6 @@ mod tests { let txid = tx.compute_txid(); let deposit_request_outpoint = OutPoint { txid, vout: 0 }; - let mut raw_tx = vec![]; - tx.consensus_encode(&mut raw_tx) - .expect("should be able to encode tx"); - let total_amount = num_btc * BitcoinAmount::SATS_FACTOR; let protocol_op = ProtocolOperation::DepositRequest(DepositRequestInfo { amt: total_amount, @@ -458,7 +450,7 @@ mod tests { .expect("address must be valid"), ); - (raw_tx, protocol_op, expected_deposit_info) + (tx.into(), protocol_op, expected_deposit_info) } /// Generates a mock transaction either arbitrarily or deterministically. @@ -471,7 +463,7 @@ mod tests { /// generated. The chances of an arbitrarily constructed pair being valid is extremely rare. /// So, you can assume that this flag represents whether the pair is valid (true) or invalid /// (false). - fn generate_mock_tx() -> (Vec, ProtocolOperation, bool) { + fn generate_mock_tx() -> (RawBitcoinTx, ProtocolOperation, bool) { let mut arb = ArbitraryGenerator::new(); let should_be_valid: bool = arb.generate(); @@ -485,7 +477,7 @@ mod tests { (valid_tx, valid_protocol_op, should_be_valid) } - fn generate_invalid_tx(arb: &mut ArbitraryGenerator) -> (Vec, ProtocolOperation) { + fn generate_invalid_tx(arb: &mut ArbitraryGenerator) -> (RawBitcoinTx, ProtocolOperation) { let random_protocol_op: ProtocolOperation = arb.generate(); // true => tx invalid @@ -493,26 +485,16 @@ mod tests { let tx_invalid: bool = OsRng.gen_bool(0.5); if tx_invalid { - let mut random_tx: Vec = arb.generate(); - while random_tx.is_empty() { - random_tx = arb.generate(); - } - - return (random_tx, random_protocol_op); + return (arb.generate(), random_protocol_op); } let (mut valid_tx, _, _) = generate_mock_unsigned_tx(); valid_tx.output[0].script_pubkey = ScriptBuf::from_bytes(arb.generate()); - let mut tx_with_invalid_script_pubkey = vec![]; - valid_tx - .consensus_encode(&mut tx_with_invalid_script_pubkey) - .expect("should be able to encode tx"); - - (tx_with_invalid_script_pubkey, random_protocol_op) + (valid_tx.into(), random_protocol_op) } - fn generate_valid_tx(arb: &mut ArbitraryGenerator) -> (Vec, ProtocolOperation) { + fn generate_valid_tx(arb: &mut ArbitraryGenerator) -> (RawBitcoinTx, ProtocolOperation) { let (tx, spend_info, script_to_spend) = generate_mock_unsigned_tx(); let random_hash = *spend_info @@ -524,10 +506,6 @@ mod tests { .expect("should contain a hash") .as_byte_array(); - let mut raw_tx = vec![]; - tx.consensus_encode(&mut raw_tx) - .expect("should be able to encode transaction"); - let deposit_request_info = DepositRequestInfo { amt: 1_000_000_000, // 10 BTC address: arb.generate(), // random rollup address (this is fine) @@ -536,7 +514,7 @@ mod tests { let deposit_request = ProtocolOperation::DepositRequest(deposit_request_info); - (raw_tx, deposit_request) + (tx.into(), deposit_request) } /// Generate a random chain state with some dispatched deposits. diff --git a/crates/chaintsn/src/transition.rs b/crates/chaintsn/src/transition.rs index 37f12adc2..e8948b8b8 100644 --- a/crates/chaintsn/src/transition.rs +++ b/crates/chaintsn/src/transition.rs @@ -398,7 +398,7 @@ mod tests { .map(|idx| { let record = ArbitraryGenerator::new_with_size(1 << 15).generate(); let proof = ArbitraryGenerator::new_with_size(1 << 12).generate(); - let tx = ArbitraryGenerator::new_with_size(1 << 8).generate(); + let tx = ArbitraryGenerator::new_with_size(1 << 12).generate(); let l1tx = if idx == 1 { let protocol_op = ProtocolOperation::Deposit(DepositInfo { diff --git a/crates/consensus-logic/src/csm/state_tracker.rs b/crates/consensus-logic/src/csm/state_tracker.rs index 2327c07dc..0085c6496 100644 --- a/crates/consensus-logic/src/csm/state_tracker.rs +++ b/crates/consensus-logic/src/csm/state_tracker.rs @@ -200,7 +200,7 @@ mod tests { // prepare the clientState and ClientUpdateOutput for up to 20th index for idx in 0..20 { let mut state = state.clone(); - let l2block: L2Block = ArbitraryGenerator::new().generate(); + let l2block: L2Block = ArbitraryGenerator::new_with_size(1 << 12).generate(); let ss: SyncState = ArbitraryGenerator::new().generate(); let output = ClientUpdateOutput::new( diff --git a/crates/primitives/src/l1/btc.rs b/crates/primitives/src/l1/btc.rs index 3c05cd90f..1c0509d26 100644 --- a/crates/primitives/src/l1/btc.rs +++ b/crates/primitives/src/l1/btc.rs @@ -9,6 +9,7 @@ use arbitrary::{Arbitrary, Unstructured}; use bitcoin::{ absolute::LockTime, address::NetworkUnchecked, + consensus::{deserialize, encode, serialize}, hashes::{sha256d, Hash}, key::{rand, Keypair, Parity, TapTweak}, secp256k1::{SecretKey, XOnlyPublicKey, SECP256K1}, @@ -740,6 +741,99 @@ impl XOnlyPk { } } +/// Represents a raw, byte-encoded Bitcoin transaction with custom [`Arbitrary`] support. +/// Provides conversions (via [`TryFrom`]) to and from [`Transaction`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct RawBitcoinTx(Vec); + +impl RawBitcoinTx { + /// Creates a new `RawBitcoinTx` from a raw byte vector. + pub fn from_raw_bytes(bytes: Vec) -> Self { + RawBitcoinTx(bytes) + } +} + +impl From for RawBitcoinTx { + fn from(value: Transaction) -> Self { + Self(serialize(&value)) + } +} + +impl TryFrom for Transaction { + type Error = encode::Error; + fn try_from(value: RawBitcoinTx) -> Result { + deserialize(&value.0) + } +} + +impl TryFrom<&RawBitcoinTx> for Transaction { + type Error = encode::Error; + fn try_from(value: &RawBitcoinTx) -> Result { + deserialize(&value.0) + } +} + +impl<'a> arbitrary::Arbitrary<'a> for RawBitcoinTx { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + // Random number of inputs and outputs (bounded for simplicity) + let input_count = u.int_in_range::(0..=4)?; + let output_count = u.int_in_range::(0..=4)?; + + // Build random inputs + let mut inputs = Vec::with_capacity(input_count); + for _ in 0..input_count { + // Random 32-byte TXID + let mut txid_bytes = [0u8; 32]; + u.fill_buffer(&mut txid_bytes)?; + + // Random vout + let vout = u32::arbitrary(u)?; + + // Random scriptSig (bounded size) + let script_sig_size = u.int_in_range::(0..=50)?; + let script_sig_bytes = u.bytes(script_sig_size)?; + let script_sig = ScriptBuf::from_bytes(script_sig_bytes.to_vec()); + + inputs.push(TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array(txid_bytes), + vout, + }, + script_sig, + sequence: Sequence::MAX, + witness: Witness::default(), // or generate random witness if desired + }); + } + + // Build random outputs + let mut outputs = Vec::with_capacity(output_count); + for _ in 0..output_count { + // Random value (in satoshis) + let value = Amount::from_sat(u64::arbitrary(u)?); + + // Random scriptPubKey (bounded size) + let script_pubkey_size = u.int_in_range::(0..=50)?; + let script_pubkey_bytes = u.bytes(script_pubkey_size)?; + let script_pubkey = ScriptBuf::from(script_pubkey_bytes.to_vec()); + + outputs.push(TxOut { + value, + script_pubkey, + }); + } + + // Construct the transaction + let tx = Transaction { + version: Version::ONE, + lock_time: LockTime::ZERO, + input: inputs, + output: outputs, + }; + + Ok(tx.into()) + } +} + #[cfg(test)] mod tests { use std::io::Cursor; @@ -752,14 +846,14 @@ mod tests { script::Builder, secp256k1::{Parity, SecretKey, SECP256K1}, taproot::{ControlBlock, LeafVersion, TaprootBuilder, TaprootMerkleBranch}, - Address, Amount, Network, ScriptBuf, TapNodeHash, TxOut, XOnlyPublicKey, + Address, Amount, Network, ScriptBuf, TapNodeHash, Transaction, TxOut, XOnlyPublicKey, }; use rand::{rngs::OsRng, Rng}; use strata_test_utils::ArbitraryGenerator; use super::{ BitcoinAddress, BitcoinAmount, BitcoinTxOut, BitcoinTxid, BorshDeserialize, BorshSerialize, - XOnlyPk, + RawBitcoinTx, XOnlyPk, }; use crate::{ errors::ParseError, @@ -1191,4 +1285,15 @@ mod tests { "original and deserialized txid must be the same" ); } + + #[test] + fn test_bitcoin_tx_arbitrary_generation() { + let mut generator = ArbitraryGenerator::new(); + let raw_tx: RawBitcoinTx = generator.generate(); + let _: Transaction = raw_tx.try_into().expect("should generate valid tx"); + + let raw_tx = RawBitcoinTx::from_raw_bytes(generator.generate()); + let res: Result = raw_tx.try_into(); + assert!(res.is_err()); + } } diff --git a/crates/rocksdb-store/src/l1/db.rs b/crates/rocksdb-store/src/l1/db.rs index fbed391c3..e8e873290 100644 --- a/crates/rocksdb-store/src/l1/db.rs +++ b/crates/rocksdb-store/src/l1/db.rs @@ -236,7 +236,7 @@ mod tests { db: &L1Db, num_txs: usize, ) -> (L1BlockManifest, Vec, CompactMmr) { - let mut arb = ArbitraryGenerator::new(); + let mut arb = ArbitraryGenerator::new_with_size(1 << 12); // TODO maybe tweak this to make it a bit more realistic? let mf: L1BlockManifest = arb.generate(); diff --git a/crates/rocksdb-store/src/l2/db.rs b/crates/rocksdb-store/src/l2/db.rs index 0a70c7579..b69fbc616 100644 --- a/crates/rocksdb-store/src/l2/db.rs +++ b/crates/rocksdb-store/src/l2/db.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use rockbound::{OptimisticTransactionDB, SchemaBatch, SchemaDBOperationsExt}; +use rockbound::{OptimisticTransactionDB, SchemaDBOperationsExt}; use strata_db::{ errors::DbError, traits::{BlockStatus, L2BlockDatabase}, @@ -84,10 +84,7 @@ impl L2BlockDatabase for L2Db { if self.get_block_data(id)?.is_none() { return Ok(()); } - - let mut batch = SchemaBatch::new(); - batch.put::(&id, &status)?; - self.db.write_schemas(batch)?; + self.db.put::(&id, &status)?; Ok(()) } @@ -117,7 +114,7 @@ mod tests { use crate::test_utils::get_rocksdb_tmp_instance; fn get_mock_data() -> L2BlockBundle { - let mut arb = ArbitraryGenerator::new(); + let mut arb = ArbitraryGenerator::new_with_size(1 << 12); let l2_block: L2BlockBundle = arb.generate(); l2_block diff --git a/crates/state/src/l1/tx.rs b/crates/state/src/l1/tx.rs index dba60e5c0..310fb8d07 100644 --- a/crates/state/src/l1/tx.rs +++ b/crates/state/src/l1/tx.rs @@ -1,6 +1,7 @@ use arbitrary::Arbitrary; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use strata_primitives::l1::RawBitcoinTx; use super::L1TxProof; use crate::tx::ProtocolOperation; @@ -12,12 +13,12 @@ use crate::tx::ProtocolOperation; pub struct L1Tx { // TODO: verify if we need L1TxProof or L1WtxProof proof: L1TxProof, - tx: Vec, + tx: RawBitcoinTx, protocol_operation: ProtocolOperation, } impl L1Tx { - pub fn new(proof: L1TxProof, tx: Vec, protocol_operation: ProtocolOperation) -> Self { + pub fn new(proof: L1TxProof, tx: RawBitcoinTx, protocol_operation: ProtocolOperation) -> Self { Self { proof, tx, @@ -29,7 +30,7 @@ impl L1Tx { &self.proof } - pub fn tx_data(&self) -> &[u8] { + pub fn tx_data(&self) -> &RawBitcoinTx { &self.tx } diff --git a/crates/state/src/l1/utils.rs b/crates/state/src/l1/utils.rs index c6bfaa88c..87e2a5855 100644 --- a/crates/state/src/l1/utils.rs +++ b/crates/state/src/l1/utils.rs @@ -1,8 +1,4 @@ -use bitcoin::{ - block::Header, - consensus::{serialize, Encodable}, - Block, -}; +use bitcoin::{block::Header, consensus::Encodable, Block}; use strata_primitives::{buf::Buf32, hash::sha256d}; use crate::{ @@ -46,9 +42,8 @@ pub fn generate_l1_tx(block: &Block, idx: u32, proto_op_data: ProtocolOperation) let tx = &block.txdata[idx as usize]; let proof = L1TxProof::generate(&block.txdata, idx); - let tx = serialize(tx); - L1Tx::new(proof, tx, proto_op_data) + L1Tx::new(proof, tx.clone().into(), proto_op_data) } #[cfg(test)] diff --git a/crates/test-utils/src/l2.rs b/crates/test-utils/src/l2.rs index 5e88a9064..81a793983 100644 --- a/crates/test-utils/src/l2.rs +++ b/crates/test-utils/src/l2.rs @@ -20,7 +20,7 @@ use strata_state::{ use crate::{bitcoin::get_btc_chain, ArbitraryGenerator}; pub fn gen_block(parent: Option<&SignedL2BlockHeader>) -> L2BlockBundle { - let mut arb = ArbitraryGenerator::new(); + let mut arb = ArbitraryGenerator::new_with_size(1 << 12); let header: L2BlockHeader = arb.generate(); let body: L2BlockBody = arb.generate(); let accessory: L2BlockAccessory = arb.generate();