From 94445cbbdec902e2e9cc1fde3045a937520bab7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Mon, 22 Sep 2025 14:35:21 +0200 Subject: [PATCH 1/5] add Recover contract --- examples/solidity/bindings/src/lib.rs | 1 + examples/solidity/bindings/src/recover.rs | 487 ++++++++++++++++++ .../src/test_mint_balance_precompile.rs | 6 +- examples/solidity/src/Recover.sol | 5 + .../src/TestMintBalancePrecompile.sol | 2 +- 5 files changed, 497 insertions(+), 4 deletions(-) create mode 100644 examples/solidity/bindings/src/recover.rs create mode 100644 examples/solidity/src/Recover.sol diff --git a/examples/solidity/bindings/src/lib.rs b/examples/solidity/bindings/src/lib.rs index d98739bd..86feb762 100644 --- a/examples/solidity/bindings/src/lib.rs +++ b/examples/solidity/bindings/src/lib.rs @@ -7,6 +7,7 @@ pub mod r#auction; pub mod r#fast_types; pub mod r#pi2; pub mod r#ranked_feed; +pub mod r#recover; pub mod r#test_mint_balance_precompile; pub mod r#time; pub mod r#voting; diff --git a/examples/solidity/bindings/src/recover.rs b/examples/solidity/bindings/src/recover.rs new file mode 100644 index 00000000..6a424f6d --- /dev/null +++ b/examples/solidity/bindings/src/recover.rs @@ -0,0 +1,487 @@ +/** + +Generated by the following Solidity interface... +```solidity +interface Recover { + function recover(bytes memory fromTx) external; +} +``` + +...which was generated by the following JSON ABI: +```json +[ + { + "type": "function", + "name": "recover", + "inputs": [ + { + "name": "fromTx", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + } +] +```*/ +#[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style, + clippy::empty_structs_with_brackets +)] +pub mod Recover { + use super::*; + use alloy::sol_types as alloy_sol_types; + /// The creation / init bytecode of the contract. + /// + /// ```text + ///0x6080604052348015600e575f5ffd5b5060e98061001b5f395ff3fe6080604052348015600e575f5ffd5b50600436106026575f3560e01c8063a4a1edb114602a575b5f5ffd5b60406004803603810190603c919060a6565b6042565b005b5050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f840112606c57606b604e565b5b8235905067ffffffffffffffff81111560865760856052565b5b602083019150836001820283011115609f57609e6056565b5b9250929050565b5f5f6020838503121560b95760b86046565b5b5f83013567ffffffffffffffff81111560d35760d2604a565b5b60dd85828601605a565b9250925050925092905056 + /// ``` + #[rustfmt::skip] + #[allow(clippy::all)] + pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static( + b"`\x80`@R4\x80\x15`\x0EW__\xFD[P`\xE9\x80a\0\x1B_9_\xF3\xFE`\x80`@R4\x80\x15`\x0EW__\xFD[P`\x046\x10`&W_5`\xE0\x1C\x80c\xA4\xA1\xED\xB1\x14`*W[__\xFD[`@`\x04\x806\x03\x81\x01\x90`<\x91\x90`\xA6V[`BV[\0[PPV[__\xFD[__\xFD[__\xFD[__\xFD[__\xFD[__\x83`\x1F\x84\x01\x12`lW`k`NV[[\x825\x90Pg\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x81\x11\x15`\x86W`\x85`RV[[` \x83\x01\x91P\x83`\x01\x82\x02\x83\x01\x11\x15`\x9FW`\x9E`VV[[\x92P\x92\x90PV[__` \x83\x85\x03\x12\x15`\xB9W`\xB8`FV[[_\x83\x015g\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x81\x11\x15`\xD3W`\xD2`JV[[`\xDD\x85\x82\x86\x01`ZV[\x92P\x92PP\x92P\x92\x90PV", + ); + /// The runtime bytecode of the contract, as deployed on the network. + /// + /// ```text + ///0x6080604052348015600e575f5ffd5b50600436106026575f3560e01c8063a4a1edb114602a575b5f5ffd5b60406004803603810190603c919060a6565b6042565b005b5050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f840112606c57606b604e565b5b8235905067ffffffffffffffff81111560865760856052565b5b602083019150836001820283011115609f57609e6056565b5b9250929050565b5f5f6020838503121560b95760b86046565b5b5f83013567ffffffffffffffff81111560d35760d2604a565b5b60dd85828601605a565b9250925050925092905056 + /// ``` + #[rustfmt::skip] + #[allow(clippy::all)] + pub static DEPLOYED_BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static( + b"`\x80`@R4\x80\x15`\x0EW__\xFD[P`\x046\x10`&W_5`\xE0\x1C\x80c\xA4\xA1\xED\xB1\x14`*W[__\xFD[`@`\x04\x806\x03\x81\x01\x90`<\x91\x90`\xA6V[`BV[\0[PPV[__\xFD[__\xFD[__\xFD[__\xFD[__\xFD[__\x83`\x1F\x84\x01\x12`lW`k`NV[[\x825\x90Pg\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x81\x11\x15`\x86W`\x85`RV[[` \x83\x01\x91P\x83`\x01\x82\x02\x83\x01\x11\x15`\x9FW`\x9E`VV[[\x92P\x92\x90PV[__` \x83\x85\x03\x12\x15`\xB9W`\xB8`FV[[_\x83\x015g\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x81\x11\x15`\xD3W`\xD2`JV[[`\xDD\x85\x82\x86\x01`ZV[\x92P\x92PP\x92P\x92\x90PV", + ); + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `recover(bytes)` and selector `0xa4a1edb1`. +```solidity +function recover(bytes memory fromTx) external; +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct recoverCall { + #[allow(missing_docs)] + pub fromTx: alloy::sol_types::private::Bytes, + } + ///Container type for the return parameters of the [`recover(bytes)`](recoverCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct recoverReturn {} + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Bytes,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy::sol_types::private::Bytes,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: recoverCall) -> Self { + (value.fromTx,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for recoverCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { fromTx: tuple.0 } + } + } + } + { + #[doc(hidden)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: recoverReturn) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for recoverReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self {} + } + } + } + impl recoverReturn { + fn _tokenize( + &self, + ) -> ::ReturnToken<'_> { + () + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for recoverCall { + type Parameters<'a> = (alloy::sol_types::sol_data::Bytes,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = recoverReturn; + type ReturnTuple<'a> = (); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "recover(bytes)"; + const SELECTOR: [u8; 4] = [164u8, 161u8, 237u8, 177u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self.fromTx, + ), + ) + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + recoverReturn::_tokenize(ret) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(Into::into) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(Into::into) + } + } + }; + ///Container for all the [`Recover`](self) function calls. + #[derive(serde::Serialize, serde::Deserialize)] + #[derive()] + pub enum RecoverCalls { + #[allow(missing_docs)] + recover(recoverCall), + } + #[automatically_derived] + impl RecoverCalls { + /// All the selectors of this enum. + /// + /// Note that the selectors might not be in the same order as the variants. + /// No guarantees are made about the order of the selectors. + /// + /// Prefer using `SolInterface` methods instead. + pub const SELECTORS: &'static [[u8; 4usize]] = &[[164u8, 161u8, 237u8, 177u8]]; + } + #[automatically_derived] + impl alloy_sol_types::SolInterface for RecoverCalls { + const NAME: &'static str = "RecoverCalls"; + const MIN_DATA_LENGTH: usize = 64usize; + const COUNT: usize = 1usize; + #[inline] + fn selector(&self) -> [u8; 4] { + match self { + Self::recover(_) => ::SELECTOR, + } + } + #[inline] + fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> { + Self::SELECTORS.get(i).copied() + } + #[inline] + fn valid_selector(selector: [u8; 4]) -> bool { + Self::SELECTORS.binary_search(&selector).is_ok() + } + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw( + selector: [u8; 4], + data: &[u8], + ) -> alloy_sol_types::Result { + static DECODE_SHIMS: &[fn(&[u8]) -> alloy_sol_types::Result] = &[ + { + fn recover(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(RecoverCalls::recover) + } + recover + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err( + alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + ), + ); + }; + DECODE_SHIMS[idx](data) + } + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw_validate( + selector: [u8; 4], + data: &[u8], + ) -> alloy_sol_types::Result { + static DECODE_VALIDATE_SHIMS: &[fn( + &[u8], + ) -> alloy_sol_types::Result] = &[ + { + fn recover(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(RecoverCalls::recover) + } + recover + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err( + alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + ), + ); + }; + DECODE_VALIDATE_SHIMS[idx](data) + } + #[inline] + fn abi_encoded_size(&self) -> usize { + match self { + Self::recover(inner) => { + ::abi_encoded_size(inner) + } + } + } + #[inline] + fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec) { + match self { + Self::recover(inner) => { + ::abi_encode_raw(inner, out) + } + } + } + } + use alloy::contract as alloy_contract; + /**Creates a new wrapper around an on-chain [`Recover`](self) contract instance. + +See the [wrapper's documentation](`RecoverInstance`) for more details.*/ + #[inline] + pub const fn new< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >(address: alloy_sol_types::private::Address, provider: P) -> RecoverInstance { + RecoverInstance::::new(address, provider) + } + /**Deploys this contract using the given `provider` and constructor arguments, if any. + +Returns a new instance of the contract, if the deployment was successful. + +For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/ + #[inline] + pub fn deploy< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + provider: P, + ) -> impl ::core::future::Future< + Output = alloy_contract::Result>, + > { + RecoverInstance::::deploy(provider) + } + /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` +and constructor arguments, if any. + +This is a simple wrapper around creating a `RawCallBuilder` with the data set to +the bytecode concatenated with the constructor's ABI-encoded arguments.*/ + #[inline] + pub fn deploy_builder< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >(provider: P) -> alloy_contract::RawCallBuilder { + RecoverInstance::::deploy_builder(provider) + } + /**A [`Recover`](self) instance. + +Contains type-safe methods for interacting with an on-chain instance of the +[`Recover`](self) contract located at a given `address`, using a given +provider `P`. + +If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!) +documentation on how to provide it), the `deploy` and `deploy_builder` methods can +be used to deploy a new instance of the contract. + +See the [module-level documentation](self) for all the available methods.*/ + #[derive(Clone)] + pub struct RecoverInstance { + address: alloy_sol_types::private::Address, + provider: P, + _network: ::core::marker::PhantomData, + } + #[automatically_derived] + impl ::core::fmt::Debug for RecoverInstance { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple("RecoverInstance").field(&self.address).finish() + } + } + /// Instantiation and getters/setters. + #[automatically_derived] + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > RecoverInstance { + /**Creates a new wrapper around an on-chain [`Recover`](self) contract instance. + +See the [wrapper's documentation](`RecoverInstance`) for more details.*/ + #[inline] + pub const fn new( + address: alloy_sol_types::private::Address, + provider: P, + ) -> Self { + Self { + address, + provider, + _network: ::core::marker::PhantomData, + } + } + /**Deploys this contract using the given `provider` and constructor arguments, if any. + +Returns a new instance of the contract, if the deployment was successful. + +For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/ + #[inline] + pub async fn deploy( + provider: P, + ) -> alloy_contract::Result> { + let call_builder = Self::deploy_builder(provider); + let contract_address = call_builder.deploy().await?; + Ok(Self::new(contract_address, call_builder.provider)) + } + /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` +and constructor arguments, if any. + +This is a simple wrapper around creating a `RawCallBuilder` with the data set to +the bytecode concatenated with the constructor's ABI-encoded arguments.*/ + #[inline] + pub fn deploy_builder(provider: P) -> alloy_contract::RawCallBuilder { + alloy_contract::RawCallBuilder::new_raw_deploy( + provider, + ::core::clone::Clone::clone(&BYTECODE), + ) + } + /// Returns a reference to the address. + #[inline] + pub const fn address(&self) -> &alloy_sol_types::private::Address { + &self.address + } + /// Sets the address. + #[inline] + pub fn set_address(&mut self, address: alloy_sol_types::private::Address) { + self.address = address; + } + /// Sets the address and returns `self`. + pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self { + self.set_address(address); + self + } + /// Returns a reference to the provider. + #[inline] + pub const fn provider(&self) -> &P { + &self.provider + } + } + impl RecoverInstance<&P, N> { + /// Clones the provider and returns a new instance with the cloned provider. + #[inline] + pub fn with_cloned_provider(self) -> RecoverInstance { + RecoverInstance { + address: self.address, + provider: ::core::clone::Clone::clone(&self.provider), + _network: ::core::marker::PhantomData, + } + } + } + /// Function calls. + #[automatically_derived] + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > RecoverInstance { + /// Creates a new call builder using this contract instance's provider and address. + /// + /// Note that the call can be any function call, not just those defined in this + /// contract. Prefer using the other methods for building type-safe contract calls. + pub fn call_builder( + &self, + call: &C, + ) -> alloy_contract::SolCallBuilder<&P, C, N> { + alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call) + } + ///Creates a new call builder for the [`recover`] function. + pub fn recover( + &self, + fromTx: alloy::sol_types::private::Bytes, + ) -> alloy_contract::SolCallBuilder<&P, recoverCall, N> { + self.call_builder(&recoverCall { fromTx }) + } + } + /// Event filters. + #[automatically_derived] + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > RecoverInstance { + /// Creates a new event filter using this contract instance's provider and address. + /// + /// Note that the type can be any event, not just those defined in this contract. + /// Prefer using the other methods for building type-safe event filters. + pub fn event_filter( + &self, + ) -> alloy_contract::Event<&P, E, N> { + alloy_contract::Event::new_sol(&self.provider, &self.address) + } + } +} diff --git a/examples/solidity/bindings/src/test_mint_balance_precompile.rs b/examples/solidity/bindings/src/test_mint_balance_precompile.rs index 8e451dd2..1decfc06 100644 --- a/examples/solidity/bindings/src/test_mint_balance_precompile.rs +++ b/examples/solidity/bindings/src/test_mint_balance_precompile.rs @@ -3,7 +3,7 @@ Generated by the following Solidity interface... ```solidity interface TestMintBalancePrecompile { - function mint(uint256 value) external; + function mint(uint256 value) external view; } ``` @@ -21,7 +21,7 @@ interface TestMintBalancePrecompile { } ], "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "view" } ] ```*/ @@ -59,7 +59,7 @@ pub mod TestMintBalancePrecompile { #[derive(Default, Debug, PartialEq, Eq, Hash)] /**Function with signature `mint(uint256)` and selector `0xa0712d68`. ```solidity -function mint(uint256 value) external; +function mint(uint256 value) external view; ```*/ #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] #[derive(Clone)] diff --git a/examples/solidity/src/Recover.sol b/examples/solidity/src/Recover.sol new file mode 100644 index 00000000..9b5dee44 --- /dev/null +++ b/examples/solidity/src/Recover.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.26; + +contract Recover { + function recover(bytes calldata fromTx) public {} +} diff --git a/examples/solidity/src/TestMintBalancePrecompile.sol b/examples/solidity/src/TestMintBalancePrecompile.sol index 0a839eb6..9ed46bd6 100644 --- a/examples/solidity/src/TestMintBalancePrecompile.sol +++ b/examples/solidity/src/TestMintBalancePrecompile.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; contract TestMintBalancePrecompile { address constant PRECOMPILE = address(uint160(uint256(keccak256("POD_MINT_BALANCE")))); - function mint(uint256 value) public { + function mint(uint256 value) public view { (bool success,) = PRECOMPILE.staticcall(abi.encode(value)); require(success, "Precompile call failed"); } From 090b3971f3473256ca5de9d7360321874fc060b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 24 Sep 2025 12:49:10 +0200 Subject: [PATCH 2/5] add cert quorum to Committee --- rust-sdk/src/network/mod.rs | 2 ++ types/src/consensus/committee.rs | 38 ++++++++++++++++++++++++-------- types/src/ledger/log.rs | 21 ++++++++++-------- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/rust-sdk/src/network/mod.rs b/rust-sdk/src/network/mod.rs index ec4be015..6505048c 100644 --- a/rust-sdk/src/network/mod.rs +++ b/rust-sdk/src/network/mod.rs @@ -344,6 +344,7 @@ impl PodReceiptResponse { .iter() .map(|a| a.signature) .collect(), + committee.quorum_size, )?; committee.verify_aggregate_attestation( @@ -354,6 +355,7 @@ impl PodReceiptResponse { .iter() .map(|a| a.signature) .collect(), + committee.quorum_size, )?; Ok(()) diff --git a/types/src/consensus/committee.rs b/types/src/consensus/committee.rs index d6fec677..368de2e1 100644 --- a/types/src/consensus/committee.rs +++ b/types/src/consensus/committee.rs @@ -1,6 +1,7 @@ use super::{Attestation, Certificate}; use crate::cryptography::hash::{Hash, Hashable}; use alloy_primitives::{Address, Signature}; +use anyhow::ensure; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -17,16 +18,32 @@ pub enum CommitteeError { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Committee { pub validators: BTreeSet
, + pub cert_quorum: usize, pub quorum_size: usize, } impl Committee { - pub fn new(validators: impl IntoIterator, quorum_size: usize) -> Self { - let validator_set = validators.into_iter().collect(); - Committee { - validators: validator_set, + pub fn new( + validators: impl IntoIterator, + cert_quorum: usize, + quorum_size: usize, + ) -> anyhow::Result { + ensure!(quorum_size > 0, "quorum_size must be greater than 0"); + ensure!(cert_quorum > 0, "cert_quorum must be greater than 0"); + ensure!( + cert_quorum <= quorum_size, + "cert_quorum must be less than or equal to quorum_size" + ); + let validators: BTreeSet
= validators.into_iter().collect(); + ensure!( + quorum_size <= validators.len(), + "quorum_size must be less than or equal to the number of validators" + ); + Ok(Committee { + validators, quorum_size, - } + cert_quorum, + }) } pub fn size(&self) -> usize { @@ -66,11 +83,12 @@ impl Committee { &self, digest: Hash, signatures: &Vec, + quorum_size: usize, ) -> Result<(), CommitteeError> { - if signatures.len() < self.quorum_size { + if signatures.len() < quorum_size { return Err(CommitteeError::InsufficientQuorum { got: signatures.len(), - required: self.quorum_size, + required: quorum_size, }); } @@ -95,10 +113,10 @@ impl Committee { } // Verify we have enough unique valid signatures from committee members - if recovered_signers.len() < self.quorum_size { + if recovered_signers.len() < quorum_size { return Err(CommitteeError::InsufficientQuorum { got: recovered_signers.len(), - required: self.quorum_size, + required: quorum_size, }); } @@ -108,10 +126,12 @@ impl Committee { pub fn verify_certificate( &self, certificate: &Certificate, + quorum_size: usize, ) -> Result<(), CommitteeError> { self.verify_aggregate_attestation( certificate.certified.hash_custom(), &certificate.signatures, + quorum_size, ) } } diff --git a/types/src/ledger/log.rs b/types/src/ledger/log.rs index 4b561cf2..316d7ee8 100644 --- a/types/src/ledger/log.rs +++ b/types/src/ledger/log.rs @@ -81,15 +81,18 @@ impl VerifiableLog { }) } pub fn verify(&self, committee: &Committee) -> Result<(), CommitteeError> { - committee.verify_certificate(&Certificate { - signatures: self - .pod_metadata - .attestations - .iter() - .map(|att| att.signature) - .collect(), - certified: self.pod_metadata.receipt.clone(), - }) + committee.verify_certificate( + &Certificate { + signatures: self + .pod_metadata + .attestations + .iter() + .map(|att| att.signature) + .collect(), + certified: self.pod_metadata.receipt.clone(), + }, + committee.quorum_size, + ) } pub fn confirmation_time(&self) -> Timestamp { let num_attestations = self.pod_metadata.attestations.len(); From 13adf12a235067322180c30a37a5bea168b16b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 24 Sep 2025 12:49:42 +0200 Subject: [PATCH 3/5] add bot attestation --- types/src/consensus/attestation.rs | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/types/src/consensus/attestation.rs b/types/src/consensus/attestation.rs index fd924b7f..629c895c 100644 --- a/types/src/consensus/attestation.rs +++ b/types/src/consensus/attestation.rs @@ -133,17 +133,24 @@ impl From>> for TimestampedHeadlessAttestation { mod sol { alloy_sol_types::sol! { - #[derive(Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + #[derive(Copy, Debug, std::hash::Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] struct AttestedTx { bytes32 tx_hash; bool success; uint64 committee_epoch; } + + #[derive(Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + struct AttestedBot { + uint64 nonce; + address signer; + } } } -pub use sol::AttestedTx; +pub use sol::{AttestedBot, AttestedTx}; impl AttestedTx { pub fn success(tx_hash: Hash, committee_epoch: u64) -> Self { @@ -153,6 +160,14 @@ impl AttestedTx { committee_epoch, } } + + pub fn failure(tx_hash: Hash, committee_epoch: u64) -> Self { + Self { + tx_hash, + success: false, + committee_epoch, + } + } } impl Hashable for AttestedTx { @@ -175,3 +190,19 @@ impl Merkleizable for AttestedTx { ); } } + +impl AttestedBot { + pub fn new(nonce: u64, signer: Address) -> Self { + Self { nonce, signer } + } +} + +impl Hashable for AttestedBot { + fn hash_custom(&self) -> Hash { + self.eip712_signing_hash(&alloy_sol_types::eip712_domain! { + name: "attest_bot", + version: "1", + chain_id: 0x50d, + }) + } +} From 50b8fbb1c76253e3aaad09e73bdeaed1b58d9123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 26 Sep 2025 10:31:43 +0200 Subject: [PATCH 4/5] add PodTransactionExt trait --- types/src/ledger/transaction.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/types/src/ledger/transaction.rs b/types/src/ledger/transaction.rs index 07c53cf2..337fa0de 100644 --- a/types/src/ledger/transaction.rs +++ b/types/src/ledger/transaction.rs @@ -1,5 +1,5 @@ use alloy_consensus::{SignableTransaction, TxEip1559}; -use alloy_primitives::Address; +use alloy_primitives::{Address, U256}; use alloy_sol_types::SolValue; use crate::cryptography::{ @@ -9,6 +9,25 @@ use crate::cryptography::{ pub type Transaction = TxEip1559; +pub trait PodTransactionExt { + /// TX gas cost (max_fee_per_gas * gas_limit) + fn cost_from_gas(&self) -> Option; + + /// Total cost of the transaction (gas cost + value) + fn total_cost(&self) -> Option; +} + +impl PodTransactionExt for Transaction { + fn cost_from_gas(&self) -> Option { + self.max_fee_per_gas.checked_mul(self.gas_limit as u128) + } + + fn total_cost(&self) -> Option { + self.cost_from_gas() + .and_then(|cost| U256::from(cost).checked_add(self.value)) + } +} + impl Merkleizable for Transaction { fn append_leaves(&self, builder: &mut MerkleBuilder) { builder.add_field("to", self.to.to().unwrap_or(&Address::ZERO).hash_custom()); From 0c7a47236148ae3ea9c6fb05110d7f6df3e42aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Tue, 30 Sep 2025 13:11:22 +0200 Subject: [PATCH 5/5] specialized methods to verify different threshold certs --- types/src/consensus/committee.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/types/src/consensus/committee.rs b/types/src/consensus/committee.rs index 368de2e1..5453cf59 100644 --- a/types/src/consensus/committee.rs +++ b/types/src/consensus/committee.rs @@ -134,4 +134,18 @@ impl Committee { quorum_size, ) } + + pub fn verify_high_quorum_certificate( + &self, + certificate: &Certificate, + ) -> Result<(), CommitteeError> { + self.verify_certificate(certificate, self.quorum_size) + } + + pub fn verify_low_quorum_certificate( + &self, + certificate: &Certificate, + ) -> Result<(), CommitteeError> { + self.verify_certificate(certificate, self.cert_quorum) + } }