Skip to content

Commit

Permalink
Merge pull request #424 from chainbound/lore/fix/proof-verification
Browse files Browse the repository at this point in the history
fix: failing proof verification for blob transaction
  • Loading branch information
thedevbirb authored Nov 27, 2024
2 parents 8a10050 + 08f702c commit 40f54bf
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 155 deletions.
161 changes: 97 additions & 64 deletions bolt-boost/src/proofs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,83 +78,37 @@ pub fn verify_multiproofs(

#[cfg(test)]
mod tests {
use std::fs::File;

use alloy::{
hex::FromHex,
primitives::{Bytes, B256},
primitives::{hex, Bytes, B256},
};
use ssz_rs::{HashTreeRoot, List, PathElement, Prove};

use crate::testutil::*;

/// NOTE: This test is disabled because multiproof support has not landed in ssz-rs main yet.
// #[test]
// fn test_single_multiproof() {
// let (root, transactions) = read_test_transactions();
// println!(
// "Transactions root: {:?}, num transactions: {}",
// root,
// transactions.len()
// );

// // Shoudl be 1073741824, 1048576
// let transactions_list =
// transactions_to_ssz_list::<1073741824, 1048576>(transactions.clone());

// // let index = rand::random::<usize>() % transactions.len();
// let index = 51;

// println!("Index to prove: {index}");

// let root_node = transactions_list.hash_tree_root().unwrap();

// assert_eq!(root_node, root);

// // Generate the path from the transaction indexes
// let path = path_from_indeces(&[index]);

// let start_proof = std::time::Instant::now();
// let (multi_proof, witness) = transactions_list.multi_prove(&[&path]).unwrap();
// println!("Generated multiproof in {:?}", start_proof.elapsed());

// // Root and witness must be the same
// assert_eq!(root, witness);

// let start_verify = std::time::Instant::now();
// assert!(multi_proof.verify(witness).is_ok());
// println!("Verified multiproof in {:?}", start_verify.elapsed());

// // assert!(verify_multiproofs(&[c1_with_data], proofs, root).is_ok());
// }
use crate::{
constraints::ConstraintsCache,
proofs::verify_multiproofs,
testutil::*,
types::{InclusionProofs, SignedConstraints},
};

#[test]
fn test_single_proof() {
let (root, transactions) = read_test_transactions();
println!("Transactions root: {:?}, num transactions: {}", root, transactions.len());

// Shoudl be 1073741824, 1048576
let transactions_list =
transactions_to_ssz_list::<1073741824, 1048576>(transactions.clone());

// let index = rand::random::<usize>() % transactions.len();
let index = 26;

println!("Index to prove: {index}");

// let c1 = ConstraintsMessage {
// validator_index: 0,
// slot: 1,
// top: false,
// transactions: vec![transactions[index].clone()],
// };

// let c1_with_data = ConstraintsWithProofData::try_from(c1).unwrap();

let root_node = transactions_list.hash_tree_root().unwrap();

assert_eq!(root_node, root);

// Generate the path from the transaction indexes
let path = path_from_indeces(&[index]);
let path = path_from_indexes(&[index]);

let start_proof = std::time::Instant::now();
let (proof, witness) = transactions_list.prove(&path).unwrap();
Expand All @@ -166,16 +120,95 @@ mod tests {
let start_verify = std::time::Instant::now();
assert!(proof.verify(witness).is_ok());
println!("Verified proof in {:?}", start_verify.elapsed());

// assert!(verify_multiproofs(&[c1_with_data], proofs, root).is_ok());
}

#[test]
fn test_merkle_multiproof_blob() {
// Proof generated from bolt-builder code for the blob transaction inside
// ./testdata/signed_constraints_with_blob.json
let root =
B256::from(hex!("085f9483581f0302fd8a5a7b03e5aa9f110d4548bd679bedc04764dc9405a700"));

let proof = vec![
hex!("8c0bd07dcc7050700654b730d245db145c92ad92ef6ac81e2361533c66ee9688"),
hex!("ee38e5ba99fa98c9c8963c7e9c59e3128f285454f27daf9549d19c4bb98039fd"),
hex!("af0302f3b715a72dab24a7590f01dc5717c642a39fc5a92bc09518b24e05d56c"),
hex!("c78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c"),
hex!("536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c"),
hex!("9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30"),
hex!("d88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1"),
hex!("87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c"),
hex!("26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193"),
hex!("506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1"),
hex!("ffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b"),
hex!("6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220"),
hex!("b7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f"),
hex!("df6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e"),
hex!("b58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784"),
hex!("d49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb"),
hex!("8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb"),
hex!("8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab"),
hex!("95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4"),
hex!("f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f"),
hex!("0600000000000000000000000000000000000000000000000000000000000000"),
]
.iter()
.map(B256::from)
.collect::<Vec<_>>();

let leaves = [hex!("b4bb948e1cfc750a20fa08d6661d3f0717ca367eec45d81fcf92e8f1ae1fe688")]
.iter()
.map(B256::from)
.collect::<Vec<_>>();

let transaction_hashes =
[hex!("00724d63ef8a791110a66d6e7433d097637aec698f5cf81c44446e1ea5c45a1a")]
.iter()
.map(B256::from)
.collect::<Vec<_>>();

let generalized_indexes = vec![2097152];

let inclusion_proof =
InclusionProofs { transaction_hashes, merkle_hashes: proof, generalized_indexes };

assert!(ssz_rs::multiproofs::verify_merkle_multiproof(
&leaves,
&inclusion_proof.merkle_hashes,
&inclusion_proof.generalized_indexes,
root
)
.is_ok());

let constraints_cache = ConstraintsCache::new();

// We know the inclusion proof is valid, now we start from scratch from a signed constraint
// message

let signed_constraints: Vec<SignedConstraints> = serde_json::from_reader(
File::open("testdata/signed_constraints_with_blob.json").unwrap(),
)
.expect("to read signed constraints");

constraints_cache
.insert(0, signed_constraints[0].message.clone())
.expect("to save constraints");
let constraints_with_proof = constraints_cache.remove(0).expect("to find constraints");

// Sanity check to ensure we're verifying the same transaction
assert_eq!(
constraints_with_proof[0].proof_data[0].0,
inclusion_proof.transaction_hashes[0]
);

assert!(verify_multiproofs(&constraints_with_proof, &inclusion_proof, root).is_ok());
}

/// Testdata from https://github.com/ferranbt/fastssz/blob/455b54c08c81c3a270b6a7160f92ce68408491d4/tests/codetrie_test.go#L195
#[test]
fn test_fastssz_multiproof() {
let root =
B256::from_hex("f1824b0084956084591ff4c91c11bcc94a40be82da280e5171932b967dd146e9")
.unwrap();
B256::from(hex!("f1824b0084956084591ff4c91c11bcc94a40be82da280e5171932b967dd146e9"));

let proof = vec![
"0000000000000000000000000000000000000000000000000000000000000000",
Expand All @@ -197,15 +230,15 @@ mod tests {
.map(|hex| B256::from_hex(hex).unwrap())
.collect::<Vec<_>>();

let indices = vec![10usize, 49usize];
let indexes = vec![10usize, 49usize];

assert!(
ssz_rs::multiproofs::verify_merkle_multiproof(&leaves, &proof, &indices, root).is_ok()
ssz_rs::multiproofs::verify_merkle_multiproof(&leaves, &proof, &indexes, root).is_ok()
);
}

fn path_from_indeces(indeces: &[usize]) -> Vec<PathElement> {
indeces.iter().map(|i| PathElement::from(*i)).collect::<Vec<_>>()
fn path_from_indexes(indexes: &[usize]) -> Vec<PathElement> {
indexes.iter().map(|i| PathElement::from(*i)).collect::<Vec<_>>()
}

fn transactions_to_ssz_list<const B: usize, const N: usize>(
Expand Down
134 changes: 43 additions & 91 deletions bolt-boost/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use alloy::{
consensus::{TxEip4844Variant, TxEnvelope},
eips::eip2718::{Decodable2718, Eip2718Error, Eip2718Result},
primitives::{Bytes, TxHash, B256},
consensus::{Signed, TxEip4844Variant, TxEip4844WithSidecar, TxEnvelope},
eips::eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718},
primitives::{keccak256, Bytes, TxHash, B256},
rpc::types::beacon::{BlsPublicKey, BlsSignature},
signers::k256::sha2::{Digest, Sha256},
};
use alloy_rlp::{BufMut, Encodable};
use axum::http::HeaderMap;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use std::ops::Deref;
use tracing::error;
use tree_hash::TreeHash;

use cb_common::{
constants::COMMIT_BOOST_DOMAIN,
Expand Down Expand Up @@ -99,54 +99,56 @@ impl TryFrom<ConstraintsMessage> for ConstraintsWithProofData {
let transactions = value
.transactions
.iter()
.map(|tx| {
let envelope = TxEnvelope::decode_2718(&mut tx.as_ref())?;
let tx_hash_tree_root = calculate_tx_hash_tree_root(&envelope, tx)?;

Ok((*envelope.tx_hash(), tx_hash_tree_root))
})
.map(calculate_tx_proof_data)
.collect::<Result<Vec<_>, Eip2718Error>>()?;

Ok(Self { message: value, proof_data: transactions })
}
}

/// Calculate the SSZ hash tree root of a transaction, starting from its enveloped form.
/// For type 3 transactions, the hash tree root of the inner transaction is taken (without blobs).
fn calculate_tx_hash_tree_root(
envelope: &TxEnvelope,
raw_tx: &Bytes,
) -> Result<B256, Eip2718Error> {
match envelope {
// For type 3 txs, take the hash tree root of the inner tx (EIP-4844)
TxEnvelope::Eip4844(tx) => match tx.tx() {
TxEip4844Variant::TxEip4844(tx) => {
let mut out = Vec::new();
out.put_u8(0x03);
tx.encode(&mut out);
/// Takes a raw EIP-2718 RLP-encoded transaction and calculates its proof data, consisting of its
/// hash and the hash tree root of the transaction. For type 3 transactions, the hash tree root of
/// the inner transaction is computed without blob sidecar.
fn calculate_tx_proof_data(raw_tx: &Bytes) -> Result<(TxHash, HashTreeRoot), Eip2718Error> {
let Some(is_type_3) = raw_tx.first().map(|type_id| type_id == &0x03) else {
return Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("empty RLP bytes")));
};

Ok(tree_hash::TreeHash::tree_hash_root(&Transaction::<
<DenebSpec as EthSpec>::MaxBytesPerTransaction,
>::from(out)))
}
TxEip4844Variant::TxEip4844WithSidecar(tx) => {
use alloy_rlp::Encodable;
let mut out = Vec::new();
out.put_u8(0x03);
tx.tx.encode(&mut out);
// For blob transactions (type 3), we need to make sure to strip out the blob sidecar when
// calculating both the transaction hash and the hash tree root
if !is_type_3 {
let tx_hash = keccak256(raw_tx);
return Ok((tx_hash, hash_tree_root_raw_tx(raw_tx.to_vec())));
}

Ok(tree_hash::TreeHash::tree_hash_root(&Transaction::<
<DenebSpec as EthSpec>::MaxBytesPerTransaction,
>::from(out)))
}
},
// For other transaction types, take the hash tree root of the whole tx
_ => Ok(tree_hash::TreeHash::tree_hash_root(&Transaction::<
<DenebSpec as EthSpec>::MaxBytesPerTransaction,
>::from(raw_tx.to_vec()))),
let envelope = TxEnvelope::decode_2718(&mut raw_tx.as_ref())?;
let TxEnvelope::Eip4844(signed_tx) = envelope else {
unreachable!("we have already checked it is not a type 3 transaction")
};
let (tx, signature, tx_hash) = signed_tx.into_parts();
match tx {
TxEip4844Variant::TxEip4844(_) => {
// We have the type 3 variant without sidecar, we can safely compute the hash tree root
// of the transaction from the raw RLP bytes.
Ok((tx_hash, hash_tree_root_raw_tx(raw_tx.to_vec())))
}
TxEip4844Variant::TxEip4844WithSidecar(TxEip4844WithSidecar { tx, .. }) => {
// We strip out the sidecar and compute the hash tree root the transaction
let signed = Signed::new_unchecked(tx, signature, tx_hash);
let new_envelope = TxEnvelope::from(signed);
let mut buf = Vec::new();
new_envelope.encode_2718(&mut buf);

Ok((tx_hash, hash_tree_root_raw_tx(buf)))
}
}
}

fn hash_tree_root_raw_tx(raw_tx: Vec<u8>) -> HashTreeRoot {
let tx = Transaction::<<DenebSpec as EthSpec>::MaxBytesPerTransaction>::from(raw_tx);
TreeHash::tree_hash_root(&tx)
}

#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct SignedDelegation {
pub message: DelegationMessage,
Expand Down Expand Up @@ -218,53 +220,3 @@ pub struct RequestConfig {
pub timeout_ms: u64,
pub headers: HeaderMap,
}

#[cfg(test)]
mod tests {
use alloy::{hex::FromHex, primitives::Bytes};

use super::ConstraintsWithProofData;
use crate::types::SignedConstraints;

#[test]
fn decode_constraints_test() {
let raw = r#"{
"message": {
"pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
"slot": 32,
"top": true,
"transactions": [
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4"
]
},
"signature": "0xb8d50ee0d4b269db3d4658c1dac784d273a4160d769e16dce723a9684c390afe5865348416b3bf0f1a4f47098bec9024135d0d95f08bed18eb577a3d8a67f5dc78b13cc62515e280786a73fb267d35dfb7ab46a25ac29bf5bc2fa5b07b3e07a6"
}"#;

let mut c = serde_json::from_str::<SignedConstraints>(raw).unwrap();
let pd = ConstraintsWithProofData::try_from(c.message.clone()).unwrap().proof_data[0];

assert_eq!(
pd.0.to_string(),
"0x385b9f1ba5dbbe419dcbbbbf0840b76b941f3c216d383ec9deb9b1a323ee0cea".to_string()
);

assert_eq!(
pd.1.to_string(),
"0x02e383af0c34516ef38e13391d917d5b61b6f69e17d5234f77cb8cc3a1ae932e".to_string()
);

c.message.transactions[0] = Bytes::from_hex("0x03f9029c01830299f184b2d05e008507aef40a00832dc6c09468d30f47f19c07bccef4ac7fae2dc12fca3e0dc980b90204ef16e845000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000633b68f5d8d3a86593ebb815b4663bcbe0302e31382e302d64657600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004109de8da2a97e37f2e6dc9f7d50a408f9344d7aa1a925ae53daf7fbef43491a571960d76c0cb926190a9da10df7209fb1ba93cd98b1565a3a2368749d505f90c81c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0843b9aca00e1a00141e3a338e30c49ed0501e315bcc45e4edefebed43ab1368a1505461d9cf64901a01e8511e06b17683d89eb57b9869b96b8b611f969f7f56cbc0adc2df7c88a2a07a00910deacf91bba0d74e368d285d311dc5884e7cfe219d85aea5741b2b6e3a2fe").unwrap();

let pd = ConstraintsWithProofData::try_from(c.message).unwrap().proof_data[0];

assert_eq!(
pd.0.to_string(),
"0x15bd881daa1408b33f67fa4bdeb8acfb0a2289d9b4c6f81eef9bb2bb2e52e780".to_string()
);

assert_eq!(
pd.1.to_string(),
"0x0a637924b9f9b28a413b01cb543bcd688850b8964f77576fc71219448f7b4ab9".to_string()
);
}
}
13 changes: 13 additions & 0 deletions bolt-boost/testdata/signed_constraints_with_blob.json

Large diffs are not rendered by default.

0 comments on commit 40f54bf

Please sign in to comment.