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

feat: verify a single tx with confirmations, txid, txout multi-proof #3

Merged
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
8 changes: 8 additions & 0 deletions prover/src/dummy_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ impl DummyService {
self.client.clone()
}

pub fn min_height(&self) -> u32 {
self.client.headers_mmr_root.min_height
}

pub fn max_height(&self) -> u32 {
self.client.headers_mmr_root.max_height
}

pub fn generate_header_proof(&self, height: u32) -> Result<Option<core::MmrProof>> {
if height < self.client.headers_mmr_root.min_height
|| self.client.headers_mmr_root.max_height < height
Expand Down
147 changes: 94 additions & 53 deletions prover/src/tests/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ use ckb_bitcoin_spv_verifier::types::{core, packed, prelude::*};

use crate::{tests, utilities, BlockProofGenerator, DummyService};

fn test_spv_client(case_headers: &str, case_txoutproofs: &str, case_blocks: &str) {
fn test_spv_client(
case_headers: &str,
case_txoutproofs: &str,
case_blocks: &str,
verify_tx_range: (u32, u32),
) {
tests::setup();

let headers_path = format!("main-chain/headers/continuous/{case_headers}");
Expand Down Expand Up @@ -61,62 +66,97 @@ fn test_spv_client(case_headers: &str, case_txoutproofs: &str, case_blocks: &str
.map_err(|err| err as i8)
.unwrap();
old_client = new_client;
}

// Verify Tx
let tip_client: packed::SpvClient = service.tip_client().pack();
for bin_file in tests::data::find_bin_files(&txoutproofs_path, "") {
log::trace!("process txoutproof from file {}", bin_file.display());
// Verify Tx in different heights
if verify_tx_range.0 <= height && height <= verify_tx_range.1 {
let tip_client: packed::SpvClient = service.tip_client().pack();
let max_height = service.max_height();

let actual = File::open(&bin_file)
.and_then(|mut file| {
let mut data = Vec::new();
file.read_to_end(&mut data).map(|_| data)
})
.unwrap();
let _: core::MerkleBlock =
utilities::decode_from_slice(&actual).expect("check binary data");

let file_stem = bin_file.file_stem().unwrap().to_str().unwrap();
let (height, tx_index) = if let Some((height_str, indexes_str)) = file_stem.split_once('-')
{
let height: u32 = height_str.parse().unwrap();
let indexes = indexes_str
.split('_')
.filter(|s| !s.is_empty())
.map(|s| {
s.parse()
.map_err(|err| format!("failed to parse \"{s}\" since {err}"))
})
.collect::<Result<Vec<u32>, _>>()
.unwrap();
if indexes.len() > 1 {
log::warn!("TODO with current APIs, only ONE tx is allowed each time");
continue;
}
(height, indexes[0])
} else {
panic!("invalid txoutproof file stem \"{file_stem}\"");
};

let header_proof = service.generate_header_proof(height).unwrap().unwrap();
let tx_proof = packed::TransactionProof::new_builder()
.tx_index(tx_index.pack())
.height(height.pack())
.transaction_proof(core::Bytes::from(actual).pack())
.header_proof(header_proof.pack())
.build();
for bin_file in tests::data::find_bin_files(&txoutproofs_path, "") {
log::trace!("process txoutproof from file {}", bin_file.display());

let block_filename = format!("{height:07}.bin");
let block_file = tests::data::find_bin_file(&blocks_path, &block_filename);
let bpg = BlockProofGenerator::from_bin_file(&block_file).unwrap();
let tx = bpg.get_transaction(tx_index as usize).unwrap();
let tx_bytes = serialize(tx);
let actual = File::open(&bin_file)
.and_then(|mut file| {
let mut data = Vec::new();
file.read_to_end(&mut data).map(|_| data)
})
.unwrap();
let _: core::MerkleBlock =
utilities::decode_from_slice(&actual).expect("check binary data");

let _ = tip_client
.verify_transaction(&tx_bytes, tx_proof.as_reader())
.map_err(|err| err as i8)
.unwrap();
let file_stem = bin_file.file_stem().unwrap().to_str().unwrap();
let (height, tx_index) =
if let Some((height_str, indexes_str)) = file_stem.split_once('-') {
let height: u32 = height_str.parse().unwrap();
let indexes = indexes_str
.split('_')
.filter(|s| !s.is_empty())
.map(|s| {
s.parse()
.map_err(|err| format!("failed to parse \"{s}\" since {err}"))
})
.collect::<Result<Vec<u32>, _>>()
.unwrap();
if indexes.len() > 1 {
log::warn!("TODO with current APIs, only ONE tx is allowed each time");
continue;
}
(height, indexes[0])
} else {
panic!("invalid txoutproof file stem \"{file_stem}\"");
};

let header_proof = service
.generate_header_proof(height)
.unwrap()
.unwrap_or_default();
let tx_proof = packed::TransactionProof::new_builder()
.tx_index(tx_index.pack())
.height(height.pack())
.transaction_proof(core::Bytes::from(actual).pack())
.header_proof(header_proof.pack())
.build();

let block_filename = format!("{height:07}.bin");
let block_file = tests::data::find_bin_file(&blocks_path, &block_filename);
let bpg = BlockProofGenerator::from_bin_file(&block_file).unwrap();
let tx = bpg.get_transaction(tx_index as usize).unwrap();
let txid = tx.txid();
let tx_bytes = serialize(tx);

log::debug!("client-tip {max_height}, tx-height {height}, no confirmations");

let verify_result = tip_client
.verify_transaction_data(&tx_bytes, tx_proof.as_reader(), 0)
.map_err(|err| err as i8);
if height <= max_height {
assert!(verify_result.is_ok());
} else {
assert!(verify_result.is_err());
}

if height + 2 > max_height {
continue;
}

let confirmations = max_height - height;

log::debug!(">>> with confirmations {confirmations}");

let verify_result = tip_client
.verify_transaction(&txid, tx_proof.as_reader(), confirmations - 1)
.map_err(|err| err as i8);
assert!(verify_result.is_ok());
let verify_result = tip_client
.verify_transaction(&txid, tx_proof.as_reader(), confirmations)
.map_err(|err| err as i8);
assert!(verify_result.is_ok());
let verify_result = tip_client
.verify_transaction(&txid, tx_proof.as_reader(), confirmations + 1)
.map_err(|err| err as i8);
assert!(verify_result.is_err());
}
}
}
}

Expand All @@ -126,5 +166,6 @@ fn spv_client_case_1() {
"case-0822528_0830592",
"case-0830000",
"case-0830000_0830000",
(829995, 830005),
);
}
33 changes: 20 additions & 13 deletions verifier/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,52 @@
#[repr(i8)]
pub enum BootstrapError {
// Basic errors.
DecodeHeader = 1,
DecodeHeader = 0x01,
// Check data.
Height = 9,
Height = 0x09,
Pow,
// This is not an error, just make sure the error code is less than 32.
Unreachable = 32,
Unreachable = 0x20,
}

#[repr(i8)]
pub enum UpdateError {
// Basic errors.
DecodeHeader = 1,
DecodeHeader = 0x01,
DecodeTargetAdjustInfo,
// Check headers.
EmptyHeaders = 9,
EmptyHeaders = 0x09,
UncontinuousHeaders,
Difficulty,
Pow,
// Check MMR proof.
Mmr = 17,
Mmr = 0x11,
HeadersMmrProof,
// Check new client.
ClientId = 25,
ClientId = 0x19,
ClientTipBlockHash,
ClientMinimalHeight,
ClientMaximalHeight,
ClientTargetAdjustInfo,
// This is not an error, just make sure the error code is less than 32.
Unreachable = 32,
Unreachable = 0x20,
}

#[repr(i8)]
pub enum VerifyTxError {
// Basic errors.
DecodeTransaction = 1,
DecodeTransaction = 0x01,
DecodeTxOutProof,
// Check
TxOutProof = 9,
HeaderMmrProof,
// Transaction related errors.
TransactionUnconfirmed = 0x09,
TransactionTooOld,
TransactionTooNew,
// Check txout proof.
TxOutProofIsInvalid = 0x11,
TxOutProofInvalidTxIndex,
TxOutProofInvalidTxId,
// Check header mmr proof.
HeaderMmrProof = 0x19,
// This is not an error, just make sure the error code is less than 32.
Unreachable = 32,
Unreachable = 0x20,
}
4 changes: 2 additions & 2 deletions verifier/src/types/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use alloc::fmt;
use alloc::vec::Vec;

pub use bitcoin::{
blockdata::block::Header,
blockdata::transaction::Transaction,
blockdata::{block::Header, transaction::Transaction},
hash_types::Txid,
merkle_tree::MerkleBlock,
pow::{CompactTarget, Target},
};
Expand Down
89 changes: 66 additions & 23 deletions verifier/src/types/extension/packed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use alloc::{vec, vec::Vec};
use bitcoin::{
blockdata::constants::DIFFCHANGE_INTERVAL,
consensus::{deserialize, encode::Error as EncodeError, serialize},
Txid,
};
use molecule::bytes::Bytes;

Expand Down Expand Up @@ -254,55 +253,98 @@ impl packed::SpvClient {

/// Verifies whether a transaction is in the chain or not.
///
/// Do the same checks as `self.verify_transaction(..)`,
/// but require the transaction data as an input argument rather than `Txid`.
///
/// Since the header and the transaction has been recovered from bytes,
/// so this function return them in order to any possible future usages.
/// If you don't need them, just ignore them.
pub fn verify_transaction_data(
&self,
tx: &[u8],
tx_proof: packed::TransactionProofReader,
confirmations: u32,
) -> Result<(core::Header, core::Transaction), VerifyTxError> {
let tx: core::Transaction =
deserialize(tx).map_err(|_| VerifyTxError::DecodeTransaction)?;
let txid = tx.txid();
let header = self.verify_transaction(&txid, tx_proof, confirmations)?;
Ok((header, tx))
}

/// Verifies whether a transaction is in the chain or not.
///
/// Checks:
/// - Check if the transaction is contained in the provided header (via Merkle proof).
/// - In current version, only one transaction could be included in the Merkle proof.
/// - Check if the header is contained in the Bitcoin chain (via MMR proof).
/// - Check the confirmation blocks based on the tip header in current SPV client.
/// - `0` means skip the check of the confirmation blocks.
///
/// Since the header has been recovered from bytes, so this function return it
/// in order to any possible future usages.
/// If you don't need it, just ignore it.
pub fn verify_transaction(
&self,
tx: &[u8],
txid: &core::Txid,
tx_proof: packed::TransactionProofReader,
) -> Result<core::Transaction, VerifyTxError> {
confirmations: u32,
) -> Result<core::Header, VerifyTxError> {
let height: u32 = tx_proof.height().unpack();
let min_height = self.headers_mmr_root().min_height().unpack();
let max_height = self.headers_mmr_root().max_height().unpack();

// Verify Transaction
let (header, tx) = {
let tx: core::Transaction =
deserialize(tx).map_err(|_| VerifyTxError::DecodeTransaction)?;
if min_height > height {
return Err(VerifyTxError::TransactionTooOld);
}
if height > max_height {
return Err(VerifyTxError::TransactionTooNew);
}
if confirmations > 0 && max_height - height < confirmations {
return Err(VerifyTxError::TransactionUnconfirmed);
}

// Verify TxOut proof
let header = {
let merkle_block: core::MerkleBlock =
deserialize(tx_proof.transaction_proof().raw_data())
.map_err(|_| VerifyTxError::DecodeTxOutProof)?;

let mut matches: Vec<Txid> = vec![];
let mut matches: Vec<core::Txid> = vec![];
let mut indexes: Vec<u32> = vec![];

merkle_block
.extract_matches(&mut matches, &mut indexes)
.map_err(|_| VerifyTxError::TxOutProof)?;
.map_err(|_| VerifyTxError::TxOutProofIsInvalid)?;

if matches.len() != 1 || indexes.len() != 1 {
return Err(VerifyTxError::TxOutProof);
if matches.len() != indexes.len() {
return Err(VerifyTxError::TxOutProofIsInvalid);
}

let tx_index: u32 = tx_proof.tx_index().unpack();
let txid = tx.txid();

if txid != matches[0] || tx_index != indexes[0] {
return Err(VerifyTxError::TxOutProof);
}

let header = merkle_block.header;
indexes
.into_iter()
.position(|v| v == tx_index)
.map(|i| matches[i])
.ok_or(VerifyTxError::TxOutProofInvalidTxIndex)
.and_then(|ref id| {
if id == txid {
Ok(())
} else {
Err(VerifyTxError::TxOutProofInvalidTxId)
}
})?;

(header, tx)
merkle_block.header
};

// Verify Header
// Verify Header MMR proof
{
let height: u32 = tx_proof.height().unpack();
let block_hash = header.block_hash();

let min_height = self.headers_mmr_root().min_height().unpack();
let proof: mmr::MMRProof = {
let max_index = self.headers_mmr_root().max_height().unpack() - min_height;
let max_index = max_height - min_height;
let mmr_size = leaf_index_to_mmr_size(u64::from(max_index));
trace!(
"verify MMR proof for header-{height} with \
Expand All @@ -329,6 +371,7 @@ impl packed::SpvClient {
.verify(self.headers_mmr_root(), digests_with_positions)
.map_err(|_| VerifyTxError::HeaderMmrProof)?;
}
Ok(tx)

Ok(header)
}
}
Loading