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

short form da proof #1822

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions crates/bitcoin-da/src/helpers/merkle_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl BitcoinMerkleTree {
pub fn calculate_root_with_merkle_proof(
txid: [u8; 32],
idx: u32,
merkle_proof: Vec<[u8; 32]>,
merkle_proof: &[[u8; 32]],
) -> [u8; 32] {
let mut preimage: [u8; 64] = [0; 64];
let mut combined_hash: [u8; 32] = txid;
Expand Down Expand Up @@ -128,7 +128,7 @@ mod tests {
let root = tree.root();
let idx_path = tree.get_idx_path(0);
let calculated_root =
BitcoinMerkleTree::calculate_root_with_merkle_proof(transactions[0], 0, idx_path);
BitcoinMerkleTree::calculate_root_with_merkle_proof(transactions[0], 0, &idx_path);
assert_eq!(root, calculated_root);
}

Expand Down
32 changes: 32 additions & 0 deletions crates/bitcoin-da/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ use crate::spec::block::BitcoinBlock;
use crate::spec::header::HeaderWrapper;
use crate::spec::header_stream::BitcoinHeaderStream;
use crate::spec::proof::InclusionMultiProof;
use crate::spec::short_proof::BitcoinHeaderShortProof;
use crate::spec::transaction::TransactionWrapper;
use crate::spec::utxo::UTXO;
use crate::spec::{BitcoinSpec, RollupParams};
Expand Down Expand Up @@ -1124,6 +1125,37 @@ impl DaService for BitcoinService {
})
}

fn block_to_short_header_proof(
block: Self::FilteredBlock,
) -> <Self::Spec as DaSpec>::ShortHeaderProof {
let header = block.header;
// Build txid merkle tree

let txids = block
.txdata
.iter()
.map(|tx| tx.compute_txid().as_raw_hash().to_byte_array())
.collect::<Vec<_>>();

let txid_merkle_tree = BitcoinMerkleTree::new(txids);

let txid_merkle_proof = txid_merkle_tree.get_idx_path(0);

let coinbase_tx = block.txdata[0].clone();

// sanity check
assert_eq!(
merkle_tree::BitcoinMerkleTree::calculate_root_with_merkle_proof(
coinbase_tx.compute_txid().as_raw_hash().to_byte_array(),
0,
&txid_merkle_proof
),
header.merkle_root()
);

BitcoinHeaderShortProof::new(header, coinbase_tx, txid_merkle_proof)
}

async fn get_pending_sequencer_commitments(
&self,
sequencer_da_pub_key: &[u8],
Expand Down
6 changes: 3 additions & 3 deletions crates/bitcoin-da/src/spec/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ use super::block_hash::BlockHashWrapper;
Clone, Debug, PartialEq, Eq, Hash, BorshDeserialize, BorshSerialize, Serialize, Deserialize,
)]
pub struct HeaderWrapper {
header: BitcoinHeaderWrapper, // not pub to prevent uses like block.header.header.merkle_root
pub(crate) header: BitcoinHeaderWrapper,
pub tx_count: u32,
pub height: u64,
txs_commitment: [u8; 32],
precomputed_hash: BlockHashWrapper,
pub(crate) txs_commitment: [u8; 32],
pub(crate) precomputed_hash: BlockHashWrapper,
}

impl BlockHeaderTrait for HeaderWrapper {
Expand Down
4 changes: 4 additions & 0 deletions crates/bitcoin-da/src/spec/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use short_proof::BitcoinHeaderShortProof;
use sov_rollup_interface::da::DaSpec;

use self::address::AddressWrapper;
Expand All @@ -17,6 +18,7 @@ pub mod header;
#[cfg(feature = "native")]
pub mod header_stream;
pub mod proof;
pub mod short_proof;
pub mod transaction;
pub mod utxo;

Expand All @@ -42,4 +44,6 @@ impl DaSpec for BitcoinSpec {
type InclusionMultiProof = InclusionMultiProof;

type CompletenessProof = Vec<TransactionWrapper>;

type ShortHeaderProof = BitcoinHeaderShortProof;
}
241 changes: 241 additions & 0 deletions crates/bitcoin-da/src/spec/short_proof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
use sov_rollup_interface::da::{
BlockHeaderTrait, L1UpdateSystemTransactionInfo, ShortHeaderProofVerificationError,
VerifableShortHeaderProof,
};

use super::header::HeaderWrapper;
use super::transaction::TransactionWrapper;
use crate::helpers::{calculate_txid, merkle_tree};
use crate::verifier::WITNESS_COMMITMENT_PREFIX;

#[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Eq, PartialEq, Debug, Clone)]
pub struct BitcoinHeaderShortProof {
pub(crate) header: HeaderWrapper,
pub(crate) coinbase_tx: TransactionWrapper,
pub(crate) coinbase_tx_txid_merkle_proof: Vec<[u8; 32]>,
}

impl BitcoinHeaderShortProof {
pub fn new(
header: HeaderWrapper,
coinbase_tx: TransactionWrapper,
coinbase_tx_txid_merkle_proof: Vec<[u8; 32]>,
) -> Self {
Self {
header,
coinbase_tx,
coinbase_tx_txid_merkle_proof,
}
}
}

impl VerifableShortHeaderProof for BitcoinHeaderShortProof {
fn verify(&self) -> Result<L1UpdateSystemTransactionInfo, ShortHeaderProofVerificationError> {
// First verify that the precomputed (from circuit input) hash actually matches
// the hash of the header
if !self.header.verify_hash() {
return Err(ShortHeaderProofVerificationError::InvalidHeaderHash);
}

// Then check inclusion of coinbase tx to the header
// by calculating txid of coinbase tx
// and comparing self.header.header.merkle_root with reached merkle root
// with the given merkle proof
let claimed_root = merkle_tree::BitcoinMerkleTree::calculate_root_with_merkle_proof(
calculate_txid(&self.coinbase_tx),
0,
&self.coinbase_tx_txid_merkle_proof,
);

if self.header.merkle_root() != claimed_root {
return Err(ShortHeaderProofVerificationError::InvalidCoinbaseMerkleProof);
}

// Then extract the wtxid root from the coinbase tx
// and compare with self.header.txs_comitment()
let commitment_idx = self.coinbase_tx.output.iter().rev().position(|output| {
output
.script_pubkey
.as_bytes()
.starts_with(WITNESS_COMMITMENT_PREFIX)
});

match commitment_idx {
None => {
// If non-segwit block, claimed tx commitment should equal to
// header.merkle_root
if self.header.merkle_root() != Into::<[u8; 32]>::into(self.header.txs_commitment())
{
return Err(ShortHeaderProofVerificationError::WrongTxCommitment {
expected: self.header.merkle_root(),
actual: Into::<[u8; 32]>::into(self.header.txs_commitment()),
});
}
}
Some(idx) => {
// If post-segwit block, extract the commitment from the coinbase tx
// and compare with header.txs_commitment().
let idx = self.coinbase_tx.output.len() - idx - 1; // The index is reversed
let script_pubkey = self.coinbase_tx.output[idx].script_pubkey.as_bytes();
if script_pubkey[6..38] != Into::<[u8; 32]>::into(self.header.txs_commitment()) {
eyusufatik marked this conversation as resolved.
Show resolved Hide resolved
return Err(ShortHeaderProofVerificationError::WrongTxCommitment {
expected: script_pubkey[6..38]
.try_into()
.expect("Must have hash in witness commitment output"),
actual: Into::<[u8; 32]>::into(self.header.txs_commitment()),
});
}
}
}

// Finally return hash, wtxid root and txid proof count
Ok((
// block_hash calculates the hash of the header
self.header.hash().into(),
self.header.txs_commitment().into(),
self.coinbase_tx_txid_merkle_proof.len() as u8,
eyusufatik marked this conversation as resolved.
Show resolved Hide resolved
))
}
}

#[cfg(test)]
mod test {
use std::fs;
use std::ops::Deref;

use bitcoin::hashes::Hash;
use bitcoin::BlockHash;
use hex::FromHex;
use sov_rollup_interface::da::{ShortHeaderProofVerificationError, VerifableShortHeaderProof};
use sov_rollup_interface::services::da::DaService;

use super::BitcoinHeaderShortProof;
use crate::helpers::parsers::parse_hex_transaction;
use crate::service::BitcoinService;
use crate::spec::block::BitcoinBlock;
use crate::spec::block_hash::BlockHashWrapper;
use crate::spec::header::HeaderWrapper;

fn get_proof() -> BitcoinHeaderShortProof {
let block = fs::read("test_data/mainnet/block-882547.bin").unwrap();

let block: bitcoin::Block = bitcoin::consensus::deserialize(block.as_slice()).unwrap();

let block = BitcoinBlock {
header: HeaderWrapper::new(
block.header,
block.txdata.len() as u32,
882547,
<[u8; 32]>::from_hex(
"a4d7206595b921ee04f46e76fda0175dea5ad8d227af75110490d05b6a90df9c",
)
.unwrap(),
),
txdata: block.txdata.into_iter().map(Into::into).collect(),
};

BitcoinService::block_to_short_header_proof(block)
}

#[test]
fn test_correct_short_proof() {
let proof = get_proof();

let (block_hash, tx_commitment, tx_proof_count) =
proof.verify().expect("Proof verification failed");

let mut hash_from_input = <[u8; 32]>::from_hex(
"00000000000000000001a33628ffb58f0705f17815b9b789fe23ad64bfbbeb45",
)
.unwrap();

hash_from_input.reverse();

assert_eq!(block_hash, hash_from_input);
assert_eq!(
tx_commitment,
<[u8; 32]>::from_hex(
"a4d7206595b921ee04f46e76fda0175dea5ad8d227af75110490d05b6a90df9c"
)
.unwrap()
);

assert_eq!(tx_proof_count, 11);
}

#[test]
fn test_incorrect_short_proofs() {
let proof = get_proof();

// malform the merkle proof
{
let mut proof = proof.clone();
proof.coinbase_tx_txid_merkle_proof[3] = [8; 32];

assert_eq!(
proof.verify().unwrap_err(),
ShortHeaderProofVerificationError::InvalidCoinbaseMerkleProof
);
}

// put a different coinbase tx to the proof
{
let mut proof = proof.clone();
proof.coinbase_tx = parse_hex_transaction("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff640374770d2cfabe6d6d7534b66e1756195a543deea74059ce5ecc0ccae15a07faf980a62039cd4ef0ed10000000f09f909f092f4632506f6f6c2f640000000000000000000000000000000000000000000000000000000000000000000000050013172f24000000000722020000000000001976a914c6740a12d0a7d556f89782bf5faf0e12cf25a63988acc7b1c412000000001976a914c85526a428126c00ad071b56341a5a553a5e96a388ac0000000000000000266a24aa21a9ed7d0f77a6a13e7308be6b923d7f6f4b9251f7c47a3d383b8698f3f417e947ed0500000000000000002f6a2d434f52450142fdeae88682a965939fee9b7b2bd5b99694ff64e7ec323813c943336c579e238228a8ebd096a7e50000000000000000126a10455853415401051b0f0e0e0b1f1200130000000000000000266a24486174686e30ea617a55d932e1a85798907987dd308b5ccc6bb92e23587f0e9acba77e8800000000000000002c6a4c2952534b424c4f434b3ae3cf1c4319b02fd3b18d21cf129590786c12e5faf3f481c5e6aac512006e119f0120000000000000000000000000000000000000000000000000000000000000000075609341").unwrap().into();

assert_eq!(
proof.verify().unwrap_err(),
ShortHeaderProofVerificationError::InvalidCoinbaseMerkleProof
);
}

// try to change the wtxid merkle root
{
let mut proof = proof.clone();
let mut tx = proof.coinbase_tx.deref().clone();
// originally df
tx.output[2].script_pubkey.as_mut_bytes()[37] = 0x00;

proof.coinbase_tx = tx.into();

assert_eq!(
proof.verify().unwrap_err(),
ShortHeaderProofVerificationError::InvalidCoinbaseMerkleProof,
);
}

// try to supply wrong wtxid
{
let mut proof = proof.clone();
proof.header.txs_commitment = <[u8; 32]>::from_hex(
"a4d7206595b921ee04f46e76fda0175dea5ad8d227af75110490d05b6a90009c",
)
.unwrap();

assert_eq!(
proof.verify().unwrap_err(),
ShortHeaderProofVerificationError::WrongTxCommitment {
expected: <[u8; 32]>::from_hex(
"a4d7206595b921ee04f46e76fda0175dea5ad8d227af75110490d05b6a90df9c"
)
.unwrap(),
actual: <[u8; 32]>::from_hex(
"a4d7206595b921ee04f46e76fda0175dea5ad8d227af75110490d05b6a90009c"
)
.unwrap()
}
)
}

// try to input wrong `precomputed_hash`
{
let mut proof = proof.clone();
proof.header.precomputed_hash = BlockHashWrapper(BlockHash::from_byte_array([1u8; 32]));

assert_eq!(
proof.verify().unwrap_err(),
ShortHeaderProofVerificationError::InvalidHeaderHash
)
}
}
}
7 changes: 6 additions & 1 deletion crates/bitcoin-da/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ impl DaVerifier for BitcoinVerifier {
match commitment_idx {
// If commitment does not exist
None => {
// TODO: add this here? PR #1822
// if block_header.merkle_root() != block_header.txs_commitment() {
eyusufatik marked this conversation as resolved.
Show resolved Hide resolved
// return Err()
// }

// Relevant txs should be empty if there is no witness data because data is inscribed in the witness
if !blobs.is_empty() {
return Err(ValidationError::InvalidBlock);
Expand Down Expand Up @@ -217,7 +222,7 @@ impl DaVerifier for BitcoinVerifier {
let claimed_root = merkle_tree::BitcoinMerkleTree::calculate_root_with_merkle_proof(
calculate_txid(&inclusion_proof.coinbase_tx),
0,
inclusion_proof.coinbase_merkle_proof,
&inclusion_proof.coinbase_merkle_proof,
);

// Check that the tx root in the block header matches the tx root in the inclusion proof.
Expand Down
Binary file not shown.
6 changes: 6 additions & 0 deletions crates/sovereign-sdk/adapters/mock-da/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,12 @@ impl DaService for MockDaService {
) -> Vec<SequencerCommitment> {
vec![]
}

fn block_to_short_header_proof(
_block: Self::FilteredBlock,
) -> <Self::Spec as DaSpec>::ShortHeaderProof {
unimplemented!()
}
}

fn hash_to_array(bytes: &[u8]) -> [u8; 32] {
Expand Down
Loading
Loading