feat: implement network encoding for blob transactions (#4172)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
Dan Cline
2023-08-16 19:10:33 -04:00
committed by GitHub
parent 45db5a6368
commit 40f9576c3a
15 changed files with 745 additions and 248 deletions

View File

@ -1,27 +1,11 @@
//! Implements the `GetPooledTransactions` and `PooledTransactions` message types.
use reth_codecs::{add_arbitrary_tests, derive_arbitrary};
use reth_primitives::{
kzg::{self, Blob, Bytes48, KzgProof, KzgSettings},
TransactionSigned, H256,
};
use reth_rlp::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper};
use reth_codecs::derive_arbitrary;
use reth_primitives::{PooledTransactionsElement, TransactionSigned, H256};
use reth_rlp::{RlpDecodableWrapper, RlpEncodableWrapper};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(any(test, feature = "arbitrary"))]
use proptest::{
arbitrary::{any as proptest_any, ParamsFor},
collection::vec as proptest_vec,
strategy::{BoxedStrategy, Strategy},
};
#[cfg(any(test, feature = "arbitrary"))]
use reth_primitives::{
constants::eip4844::{FIELD_ELEMENTS_PER_BLOB, KZG_TRUSTED_SETUP},
kzg::{KzgCommitment, BYTES_PER_BLOB, BYTES_PER_FIELD_ELEMENT},
};
/// A list of transaction hashes that the peer would like transaction bodies for.
#[derive_arbitrary(rlp)]
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Default)]
@ -47,141 +31,20 @@ where
/// as the request's hashes. Hashes may be skipped, and the client should ensure that each body
/// corresponds to a requested hash. Hashes may need to be re-requested if the bodies are not
/// included in the response.
#[derive_arbitrary(rlp, 10)]
// #[derive_arbitrary(rlp, 10)]
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PooledTransactions(
/// The transaction bodies, each of which should correspond to a requested hash.
pub Vec<TransactionSigned>,
pub Vec<PooledTransactionsElement>,
);
impl From<Vec<TransactionSigned>> for PooledTransactions {
fn from(txs: Vec<TransactionSigned>) -> Self {
PooledTransactions(txs)
PooledTransactions(txs.into_iter().map(Into::into).collect())
}
}
impl From<PooledTransactions> for Vec<TransactionSigned> {
fn from(txs: PooledTransactions) -> Self {
txs.0
}
}
/// A response to [`GetPooledTransactions`] that includes blob data, their commitments, and their
/// corresponding proofs.
///
/// This is defined in [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#networking) as an element
/// of a [PooledTransactions] response.
#[add_arbitrary_tests(rlp, 20)]
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Default)]
pub struct BlobTransaction {
/// The transaction payload.
pub transaction: TransactionSigned,
/// The transaction's blob data.
pub blobs: Vec<Blob>,
/// The transaction's blob commitments.
pub commitments: Vec<Bytes48>,
/// The transaction's blob proofs.
pub proofs: Vec<Bytes48>,
}
impl BlobTransaction {
/// Verifies that the transaction's blob data, commitments, and proofs are all valid.
///
/// Takes as input the [KzgSettings], which should contain the the parameters derived from the
/// KZG trusted setup.
///
/// This ensures that the blob transaction payload has the same number of blob data elements,
/// commitments, and proofs. Each blob data element is verified against its commitment and
/// proof.
///
/// Returns `false` if any blob KZG proof in the response fails to verify.
pub fn validate(&self, proof_settings: &KzgSettings) -> Result<bool, kzg::Error> {
// Verify as a batch
KzgProof::verify_blob_kzg_proof_batch(
self.blobs.as_slice(),
self.commitments.as_slice(),
self.proofs.as_slice(),
proof_settings,
)
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<'a> arbitrary::Arbitrary<'a> for BlobTransaction {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let mut arr = [0u8; BYTES_PER_BLOB];
let blobs: Vec<Blob> = (0..u.int_in_range(1..=16)?)
.map(|_| {
arr = arbitrary::Arbitrary::arbitrary(u).unwrap();
// Ensure that the blob is canonical by ensuring that
// each field element contained in the blob is < BLS_MODULUS
for i in 0..(FIELD_ELEMENTS_PER_BLOB as usize) {
arr[i * BYTES_PER_FIELD_ELEMENT] = 0;
}
Blob::from(arr)
})
.collect();
Ok(generate_blob_transaction(blobs, TransactionSigned::arbitrary(u)?))
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl proptest::arbitrary::Arbitrary for BlobTransaction {
type Parameters = ParamsFor<String>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
(
proptest_vec(proptest_vec(proptest_any::<u8>(), BYTES_PER_BLOB), 1..=5),
proptest_any::<TransactionSigned>(),
)
.prop_map(move |(blobs, tx)| {
let blobs = blobs
.into_iter()
.map(|mut blob| {
let mut arr = [0u8; BYTES_PER_BLOB];
// Ensure that the blob is canonical by ensuring that
// each field element contained in the blob is < BLS_MODULUS
for i in 0..(FIELD_ELEMENTS_PER_BLOB as usize) {
blob[i * BYTES_PER_FIELD_ELEMENT] = 0;
}
arr.copy_from_slice(blob.as_slice());
arr.into()
})
.collect();
generate_blob_transaction(blobs, tx)
})
.boxed()
}
type Strategy = BoxedStrategy<BlobTransaction>;
}
#[cfg(any(test, feature = "arbitrary"))]
fn generate_blob_transaction(blobs: Vec<Blob>, transaction: TransactionSigned) -> BlobTransaction {
let kzg_settings = KZG_TRUSTED_SETUP.clone();
let commitments: Vec<Bytes48> = blobs
.iter()
.map(|blob| KzgCommitment::blob_to_kzg_commitment(blob.clone(), &kzg_settings).unwrap())
.map(|commitment| commitment.to_bytes())
.collect();
let proofs: Vec<Bytes48> = blobs
.iter()
.zip(commitments.iter())
.map(|(blob, commitment)| {
KzgProof::compute_blob_kzg_proof(blob.clone(), *commitment, &kzg_settings).unwrap()
})
.map(|proof| proof.to_bytes())
.collect();
BlobTransaction { transaction, blobs, commitments, proofs }
}
#[cfg(test)]
mod test {
use crate::{message::RequestPair, GetPooledTransactions, PooledTransactions};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
//! Decoding tests for [`PooledTransactions`]
use reth_eth_wire::PooledTransactions;
use reth_primitives::{hex, PooledTransactionsElement};
use reth_rlp::Decodable;
use std::{fs, path::PathBuf};
#[test]
fn decode_pooled_transactions_data() {
let network_data_path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/pooled_transactions_with_blob");
let data = fs::read_to_string(network_data_path).expect("Unable to read file");
let hex_data = hex::decode(data.trim()).unwrap();
let _txs = PooledTransactions::decode(&mut &hex_data[..]).unwrap();
}
#[test]
fn decode_blob_transaction_data() {
let network_data_path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/blob_transaction");
let data = fs::read_to_string(network_data_path).expect("Unable to read file");
let hex_data = hex::decode(data.trim()).unwrap();
let _txs = PooledTransactionsElement::decode(&mut &hex_data[..]).unwrap();
}

View File

@ -12,7 +12,7 @@ use reth_eth_wire::{
};
use reth_interfaces::p2p::error::{RequestError, RequestResult};
use reth_primitives::{
BlockBody, Bytes, Header, PeerId, ReceiptWithBloom, TransactionSigned, H256,
BlockBody, Bytes, Header, PeerId, PooledTransactionsElement, ReceiptWithBloom, H256,
};
use std::{
fmt,
@ -199,7 +199,7 @@ impl PeerResponse {
pub enum PeerResponseResult {
BlockHeaders(RequestResult<Vec<Header>>),
BlockBodies(RequestResult<Vec<BlockBody>>),
PooledTransactions(RequestResult<Vec<TransactionSigned>>),
PooledTransactions(RequestResult<Vec<PooledTransactionsElement>>),
NodeData(RequestResult<Vec<Bytes>>),
Receipts(RequestResult<Vec<Vec<ReceiptWithBloom>>>),
}

View File

@ -193,7 +193,8 @@ where
// we sent a response at which point we assume that the peer is aware of the transaction
peer.transactions.extend(transactions.iter().map(|tx| tx.hash()));
let resp = PooledTransactions(transactions);
// TODO: remove this! this will be different when we introduce the blobpool
let resp = PooledTransactions(transactions.into_iter().map(Into::into).collect());
let _ = response.send(Ok(resp));
}
}
@ -579,7 +580,11 @@ where
{
match result {
Ok(Ok(txs)) => {
this.import_transactions(peer_id, txs.0, TransactionSource::Response);
// convert all transactions to the inner transaction type, ignoring any
// sidecars
// TODO: remove this! this will be different when we introduce the blobpool
let transactions = txs.0.into_iter().map(|tx| tx.into_transaction()).collect();
this.import_transactions(peer_id, transactions, TransactionSource::Response)
}
Ok(Err(req_err)) => {
this.on_request_error(peer_id, req_err);