Skip to content

Commit

Permalink
Persist TX chaining through restarts (#1363)
Browse files Browse the repository at this point in the history
  • Loading branch information
jfldde authored Oct 22, 2024
1 parent b2da620 commit e7d18d2
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 6 deletions.
1 change: 1 addition & 0 deletions bin/citrea/tests/bitcoin_e2e/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod bitcoin_test;
pub mod prover_test;
pub mod sequencer_commitments;
pub mod sequencer_test;
pub mod tx_chain;

pub(super) fn get_citrea_path() -> PathBuf {
std::env::var("CITREA_E2E_TEST_BINARY").map_or_else(
Expand Down
314 changes: 314 additions & 0 deletions bin/citrea/tests/bitcoin_e2e/tx_chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
use async_trait::async_trait;
use bitcoin::{Amount, Transaction};
use bitcoin_da::REVEAL_OUTPUT_AMOUNT;
use bitcoincore_rpc::RpcApi;
use citrea_e2e::bitcoin::FINALITY_DEPTH;
use citrea_e2e::config::TestCaseConfig;
use citrea_e2e::framework::TestFramework;
use citrea_e2e::test_case::{TestCase, TestCaseRunner};
use citrea_e2e::traits::Restart;
use citrea_e2e::Result;

use super::get_citrea_path;

/// Tests sequencer's transaction chaining across multiple batches and sequencer restart
///
/// # Flow
/// 1. Verifies chaining between TX2->TX3 in first batch
/// 2. Verifies cross-batch chaining (TX4->TX3, TX5->TX4) in second batch
/// 3. Restarts sequencer and verifies chaining persists (TX6->TX5, TX7->TX6)
///
/// Each batch should maintain consistent output values:
/// - Commit tx: Output equals reveal tx vsize in bytes * reveal_fee_rate (1 sat/vbyte fee fixed in regtest) + REVEAL_OUTPUT_AMOUNT
/// - Reveal tx: REVEAL_OUTPUT_AMOUNT
///
/// Test for chaining persistence and ordering and chain integrity survive sequencer restarts.
struct TestSequencerTransactionChaining;

impl TestSequencerTransactionChaining {
fn get_reveal_tx_input_value(&self, reveal_tx: &Transaction) -> Amount {
Amount::from_sat(reveal_tx.vsize() as u64 + REVEAL_OUTPUT_AMOUNT)
}
}

#[async_trait]
impl TestCase for TestSequencerTransactionChaining {
async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> {
let sequencer = f.sequencer.as_mut().unwrap();
let da = f.bitcoin_nodes.get(0).expect("DA not running.");

let min_soft_confirmations_per_commitment =
sequencer.min_soft_confirmations_per_commitment();

for _ in 0..min_soft_confirmations_per_commitment {
sequencer.client.send_publish_batch_request().await?;
}

// Wait for blob tx to hit the mempool
da.wait_mempool_len(2, None).await?;

da.generate(1, None).await?;

// Get latest block
let block = da.get_block(&da.get_best_block_hash().await?).await?;
let txs = &block.txdata;

assert_eq!(txs.len(), 3, "Block should contain exactly 3 transactions");

let _coinbase = &txs[0];
let tx2 = &txs[1];
let tx3 = &txs[2];

assert_eq!(
tx3.input[0].previous_output.txid,
tx2.compute_txid(),
"TX3 should reference TX2's output"
);

// Verify output values
assert_eq!(tx2.output[0].value, self.get_reveal_tx_input_value(tx3));
assert_eq!(tx3.output[0].value, Amount::from_sat(REVEAL_OUTPUT_AMOUNT));

// Do another round and make sure second batch is chained from first batch
for _ in 0..min_soft_confirmations_per_commitment {
sequencer.client.send_publish_batch_request().await?;
}

// Wait for blob tx to hit the mempool
da.wait_mempool_len(2, None).await?;

da.generate(1, None).await?;

// Get latest block
let block = da.get_block(&da.get_best_block_hash().await?).await?;
let txs = &block.txdata;

assert_eq!(txs.len(), 3, "Block should contain exactly 3 transactions");

let _coinbase = &txs[0];
let tx4 = &txs[1];
let tx5 = &txs[2];

assert_eq!(
tx4.input[0].previous_output.txid,
tx3.compute_txid(),
"TX4 should reference TX3's output"
);

assert_eq!(
tx5.input[0].previous_output.txid,
tx4.compute_txid(),
"TX5 should reference TX4's output"
);

// Verify output values
assert_eq!(tx4.output[0].value, self.get_reveal_tx_input_value(tx5));
assert_eq!(tx5.output[0].value, Amount::from_sat(REVEAL_OUTPUT_AMOUNT));

sequencer.restart(None).await?;

// Do another round post restart and make sure third batch is chained from second batch
for _ in 0..min_soft_confirmations_per_commitment {
sequencer.client.send_publish_batch_request().await?;
}

// Wait for blob tx to hit the mempool
da.wait_mempool_len(2, None).await?;

da.generate(1, None).await?;

// Get latest block
let block = da.get_block(&da.get_best_block_hash().await?).await?;
let txs = &block.txdata;

assert_eq!(txs.len(), 3, "Block should contain exactly 3 transactions");

let _coinbase = &txs[0];
let tx6 = &txs[1];
let tx7 = &txs[2];

assert_eq!(
tx6.input[0].previous_output.txid,
tx5.compute_txid(),
"TX6 should reference TX5's output"
);

assert_eq!(
tx7.input[0].previous_output.txid,
tx6.compute_txid(),
"TX7 should reference TX6's output"
);

// Verify output values
assert_eq!(tx6.output[0].value, self.get_reveal_tx_input_value(tx7));
assert_eq!(tx7.output[0].value, Amount::from_sat(REVEAL_OUTPUT_AMOUNT));

Ok(())
}
}

#[tokio::test]
async fn test_sequencer_transaction_chaining() -> Result<()> {
TestCaseRunner::new(TestSequencerTransactionChaining)
.set_citrea_path(get_citrea_path())
.run()
.await
}

struct TestProverTransactionChaining;

impl TestProverTransactionChaining {
fn get_reveal_tx_input_value(&self, reveal_tx: &Transaction) -> Amount {
Amount::from_sat(reveal_tx.vsize() as u64 + REVEAL_OUTPUT_AMOUNT)
}
}

#[async_trait]
impl TestCase for TestProverTransactionChaining {
fn test_config() -> TestCaseConfig {
TestCaseConfig {
with_batch_prover: true,
..Default::default()
}
}

async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> {
let sequencer = f.sequencer.as_mut().unwrap();
let batch_prover = f.batch_prover.as_mut().unwrap();
let da = f.bitcoin_nodes.get(0).expect("DA not running.");

let min_soft_confirmations_per_commitment =
sequencer.min_soft_confirmations_per_commitment();

for _ in 0..min_soft_confirmations_per_commitment {
sequencer.client.send_publish_batch_request().await?;
}

// Wait for blob tx to hit the mempool
da.wait_mempool_len(2, None).await?;

da.generate(FINALITY_DEPTH, None).await?;
let finalized_height = da.get_finalized_height().await?;

batch_prover
.wait_for_l1_height(finalized_height, None)
.await?;

da.generate(1, None).await?;
let block_height = da.get_block_count().await?;

// Get block holding prover txs
let block = da
.get_block(&da.get_block_hash(block_height).await?)
.await?;
let txs = &block.txdata;

assert_eq!(txs.len(), 3, "Block should contain exactly 3 transactions");

let _coinbase = &txs[0];
let tx2 = &txs[1];
let tx3 = &txs[2];

assert_eq!(
tx3.input[0].previous_output.txid,
tx2.compute_txid(),
"TX3 should reference TX2's output"
);

// Verify output values
assert_eq!(tx2.output[0].value, self.get_reveal_tx_input_value(tx3));
assert_eq!(tx3.output[0].value, Amount::from_sat(REVEAL_OUTPUT_AMOUNT));

// // Do another round and make sure second batch is chained from first batch
for _ in 0..min_soft_confirmations_per_commitment {
sequencer.client.send_publish_batch_request().await?;
}

// Wait for blob tx to hit the mempool
da.wait_mempool_len(2, None).await?;

da.generate(FINALITY_DEPTH, None).await?;
let finalized_height = da.get_finalized_height().await?;

batch_prover
.wait_for_l1_height(finalized_height, None)
.await?;

da.generate(1, None).await?;
let block_height = da.get_block_count().await?;

// Get block holding prover txs
let block = da
.get_block(&da.get_block_hash(block_height).await?)
.await?;
let txs = &block.txdata;

assert_eq!(txs.len(), 3, "Block should contain exactly 3 transactions");

let _coinbase = &txs[0];
let tx4 = &txs[1];
let tx5 = &txs[2];

assert_eq!(
tx5.input[0].previous_output.txid,
tx4.compute_txid(),
"TX3 should reference TX2's output"
);

// Verify output values
assert_eq!(tx4.output[0].value, self.get_reveal_tx_input_value(tx5));
assert_eq!(tx5.output[0].value, Amount::from_sat(REVEAL_OUTPUT_AMOUNT));

batch_prover.restart(None).await?;

// // Do another round post restart and make sure third batch is chained from second batch
for _ in 0..min_soft_confirmations_per_commitment {
sequencer.client.send_publish_batch_request().await?;
}

// Wait for blob tx to hit the mempool
da.wait_mempool_len(2, None).await?;

da.generate(FINALITY_DEPTH, None).await?;
let finalized_height = da.get_finalized_height().await?;

batch_prover
.wait_for_l1_height(finalized_height, None)
.await?;

da.generate(1, None).await?;
let block_height = da.get_block_count().await?;

// Get block holding prover txs
let block = da
.get_block(&da.get_block_hash(block_height).await?)
.await?;
let txs = &block.txdata;

assert_eq!(txs.len(), 3, "Block should contain exactly 3 transactions");

let _coinbase = &txs[0];
let tx6 = &txs[1];
let tx7 = &txs[2];

assert_eq!(
tx7.input[0].previous_output.txid,
tx6.compute_txid(),
"TX3 should reference TX2's output"
);

// Verify output values
assert_eq!(tx6.output[0].value, self.get_reveal_tx_input_value(tx7));
assert_eq!(tx7.output[0].value, Amount::from_sat(REVEAL_OUTPUT_AMOUNT));

Ok(())
}
}

#[tokio::test]
async fn test_prover_transaction_chaining() -> Result<()> {
TestCaseRunner::new(TestProverTransactionChaining)
.set_citrea_path(get_citrea_path())
.run()
.await
}
2 changes: 1 addition & 1 deletion crates/bitcoin-da/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ pub mod service;
pub mod verifier;

#[cfg(feature = "native")]
const REVEAL_OUTPUT_AMOUNT: u64 = 546;
pub const REVEAL_OUTPUT_AMOUNT: u64 = 546;
10 changes: 5 additions & 5 deletions crates/bitcoin-da/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,17 +278,17 @@ impl BitcoinService {

#[instrument(level = "trace", skip_all, ret)]
async fn get_prev_utxo(&self) -> Result<Option<UTXO>, anyhow::Error> {
let mut pending_utxos = self
let mut previous_utxos = self
.client
.list_unspent(Some(0), Some(0), None, None, None)
.list_unspent(Some(0), None, None, Some(true), None)
.await?;

pending_utxos.retain(|u| u.spendable && u.solvable);
previous_utxos.retain(|u| u.spendable && u.solvable);

// Sorted by ancestor count, the tx with the most ancestors is the latest tx
pending_utxos.sort_unstable_by_key(|utxo| -(utxo.ancestor_count.unwrap_or(0) as i64));
previous_utxos.sort_unstable_by_key(|utxo| -(utxo.ancestor_count.unwrap_or(0) as i64));

Ok(pending_utxos
Ok(previous_utxos
.into_iter()
.find(|u| u.amount >= Amount::from_sat(REVEAL_OUTPUT_AMOUNT))
.map(|u| u.into()))
Expand Down

0 comments on commit e7d18d2

Please sign in to comment.