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

fix: failing proof verification for blob transaction #424

Merged
merged 8 commits into from
Nov 27, 2024
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());
thedevbirb marked this conversation as resolved.
Show resolved Hide resolved

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)
thedevbirb marked this conversation as resolved.
Show resolved Hide resolved
.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);
thedevbirb marked this conversation as resolved.
Show resolved Hide resolved

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.