Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create RawBitcoinTx wrapper type for Transaction #605

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: RawBitcoinTx
prajwolrg committed Jan 15, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 670e5399b48d8005017c25a3ed818424562329f6
72 changes: 25 additions & 47 deletions bin/strata-client/src/extractor.rs
Original file line number Diff line number Diff line change
@@ -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 @@
.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() {
delbonis marked this conversation as resolved.
Show resolved Hide resolved
// 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");

Check warning on line 65 in bin/strata-client/src/extractor.rs

Codecov / codecov/patch

bin/strata-client/src/extractor.rs#L58-L65

Added lines #L58 - L65 were not covered by tests

return None;

Check warning on line 67 in bin/strata-client/src/extractor.rs

Codecov / codecov/patch

bin/strata-client/src/extractor.rs#L67

Added line #L67 was not covered by tests
}
};

let deposit_request_outpoint = OutPoint {
txid: tx.compute_txid(),
@@ -183,7 +180,6 @@

use bitcoin::{
absolute::LockTime,
consensus::Encodable,
key::rand::{self, Rng},
opcodes::{OP_FALSE, OP_TRUE},
script::Builder,
@@ -199,7 +195,7 @@
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 @@
l1_db: &Arc<Store>,
num_blocks: usize,
max_txs_per_block: usize,
needle: (Vec<u8>, ProtocolOperation),
needle: (RawBitcoinTx, ProtocolOperation),
) -> usize {
let mut arb = ArbitraryGenerator::new();
assert!(
@@ -399,7 +395,7 @@
}

/// Create a known transaction that should be present in some block.
fn get_needle() -> (Vec<u8>, ProtocolOperation, DepositInfo) {
fn get_needle() -> (RawBitcoinTx, ProtocolOperation, DepositInfo) {
let mut arb = ArbitraryGenerator::new();
let network = Network::Regtest;

@@ -437,10 +433,6 @@
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 @@
.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 @@
/// 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<u8>, ProtocolOperation, bool) {
fn generate_mock_tx() -> (RawBitcoinTx, ProtocolOperation, bool) {
let mut arb = ArbitraryGenerator::new();

let should_be_valid: bool = arb.generate();
@@ -485,34 +477,24 @@
(valid_tx, valid_protocol_op, should_be_valid)
}

fn generate_invalid_tx(arb: &mut ArbitraryGenerator) -> (Vec<u8>, ProtocolOperation) {
fn generate_invalid_tx(arb: &mut ArbitraryGenerator) -> (RawBitcoinTx, ProtocolOperation) {
let random_protocol_op: ProtocolOperation = arb.generate();

// true => tx invalid
// false => script_pubkey in tx output invalid
let tx_invalid: bool = OsRng.gen_bool(0.5);

if tx_invalid {
let mut random_tx: Vec<u8> = 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<u8>, 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 @@
.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 @@

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.
2 changes: 1 addition & 1 deletion crates/chaintsn/src/transition.rs
Original file line number Diff line number Diff line change
@@ -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 {
2 changes: 1 addition & 1 deletion crates/consensus-logic/src/csm/state_tracker.rs
Original file line number Diff line number Diff line change
@@ -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(
98 changes: 96 additions & 2 deletions crates/primitives/src/l1/btc.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
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,92 @@
}
}

impl From<Transaction> for RawBitcoinTx {
fn from(value: Transaction) -> Self {
Self(serialize(&value))
}
}

impl TryFrom<RawBitcoinTx> for Transaction {
type Error = encode::Error;
fn try_from(value: RawBitcoinTx) -> Result<Self, Self::Error> {
deserialize(&value.0)
}
}

impl TryFrom<&RawBitcoinTx> for Transaction {
type Error = encode::Error;
fn try_from(value: &RawBitcoinTx) -> Result<Self, Self::Error> {
deserialize(&value.0)
}
}
prajwolrg marked this conversation as resolved.
Show resolved Hide resolved

impl<'a> arbitrary::Arbitrary<'a> for RawBitcoinTx {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
// Random number of inputs and outputs (bounded for simplicity)
let input_count = u.int_in_range::<usize>(0..=4)?;
let output_count = u.int_in_range::<usize>(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::<usize>(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::<usize>(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())
}
}

/// 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)]

Check warning on line 827 in crates/primitives/src/l1/btc.rs

Codecov / codecov/patch

crates/primitives/src/l1/btc.rs#L827

Added line #L827 was not covered by tests
pub struct RawBitcoinTx(Vec<u8>);
prajwolrg marked this conversation as resolved.
Show resolved Hide resolved

#[cfg(test)]
mod tests {
use std::io::Cursor;
@@ -752,14 +839,14 @@
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 +1278,11 @@
"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");
}
}
2 changes: 1 addition & 1 deletion crates/rocksdb-store/src/l1/db.rs
Original file line number Diff line number Diff line change
@@ -236,7 +236,7 @@ mod tests {
db: &L1Db,
num_txs: usize,
) -> (L1BlockManifest, Vec<L1Tx>, 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();
9 changes: 3 additions & 6 deletions crates/rocksdb-store/src/l2/db.rs
Original file line number Diff line number Diff line change
@@ -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::<L2BlockStatusSchema>(&id, &status)?;
self.db.write_schemas(batch)?;
self.db.put::<L2BlockStatusSchema>(&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
7 changes: 4 additions & 3 deletions crates/state/src/l1/tx.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
tx: RawBitcoinTx,
protocol_operation: ProtocolOperation,
}

impl L1Tx {
pub fn new(proof: L1TxProof, tx: Vec<u8>, 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
}

Loading