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
132 changes: 42 additions & 90 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,12 +99,7 @@ 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 })
Expand All @@ -113,40 +108,47 @@ impl TryFrom<ConstraintsMessage> for ConstraintsWithProofData {

/// Calculate the SSZ hash tree root of a transaction, starting from its enveloped form.
thedevbirb marked this conversation as resolved.
Show resolved Hide resolved
/// 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);

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);
fn calculate_tx_proof_data(raw_tx: &Bytes) -> Result<(TxHash, HashTreeRoot), Eip2718Error> {
let is_type_3 = raw_tx
.first()
.ok_or(Eip2718Error::RlpError(alloy_rlp::Error::Custom("empty RLP bytes")))?
== &0x03;
thedevbirb marked this conversation as resolved.
Show resolved Hide resolved

// 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 hash tree root the raw
// RLP.
thedevbirb marked this conversation as resolved.
Show resolved Hide resolved
Ok((tx_hash, hash_tree_root_raw_tx(raw_tx.to_vec())))
}
TxEip4844Variant::TxEip4844WithSidecar(TxEip4844WithSidecar { tx, .. }) => {
// We strip out the sidecar and hash tree root the transaction
thedevbirb marked this conversation as resolved.
Show resolved Hide resolved
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.