diff --git a/Cargo.lock b/Cargo.lock index ea611765..61b0e7ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1336,6 +1336,20 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if 1.0.4", + "crossbeam-utils 0.8.21", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -1752,6 +1766,7 @@ dependencies = [ "ethrex-rlp", "hex", "libp2p", + "sha2", "snap", "ssz_types", "tracing", @@ -1770,6 +1785,9 @@ version = "0.1.0" [[package]] name = "ethlambda-storage" version = "0.1.0" +dependencies = [ + "ethlambda-types", +] [[package]] name = "ethlambda-types" @@ -1778,6 +1796,7 @@ dependencies = [ "ethereum-types", "ethereum_ssz", "ethereum_ssz_derive", + "leansig", "serde", "ssz_types", "thiserror 2.0.17", @@ -2995,7 +3014,7 @@ dependencies = [ "serde_arrays", "sha2", "sp1_bls12_381", - "spin", + "spin 0.9.8", ] [[package]] @@ -3019,6 +3038,26 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leansig" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leansig.git?rev=ae12a5feb25d917c42b6466444ebd56ec115a629#ae12a5feb25d917c42b6466444ebd56ec115a629" +dependencies = [ + "dashmap", + "ethereum_ssz", + "num-bigint 0.4.6", + "num-traits", + "p3-baby-bear 0.4.1", + "p3-field 0.4.1", + "p3-koala-bear", + "p3-symmetric 0.4.1", + "rand 0.9.2", + "rayon", + "serde", + "sha3", + "thiserror 2.0.17", +] + [[package]] name = "libc" version = "0.2.178" @@ -4227,24 +4266,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7521838ecab2ddf4f7bc4ceebad06ec02414729598485c1ada516c39900820e8" dependencies = [ "num-bigint 0.4.6", - "p3-field", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", + "p3-field 0.2.3-succinct", + "p3-mds 0.2.3-succinct", + "p3-poseidon2 0.2.3-succinct", + "p3-symmetric 0.2.3-succinct", "rand 0.8.5", "serde", ] +[[package]] +name = "p3-baby-bear" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-challenger", + "p3-field 0.4.1", + "p3-mds 0.4.1", + "p3-monty-31", + "p3-poseidon2 0.4.1", + "p3-symmetric 0.4.1", + "rand 0.9.2", +] + +[[package]] +name = "p3-challenger" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-field 0.4.1", + "p3-maybe-rayon 0.4.1", + "p3-monty-31", + "p3-symmetric 0.4.1", + "p3-util 0.4.1", + "tracing", +] + [[package]] name = "p3-dft" version = "0.2.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46414daedd796f1eefcdc1811c0484e4bced5729486b6eaba9521c572c76761a" dependencies = [ - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", + "p3-field 0.2.3-succinct", + "p3-matrix 0.2.3-succinct", + "p3-maybe-rayon 0.2.3-succinct", + "p3-util 0.2.3-succinct", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "itertools 0.14.0", + "p3-field 0.4.1", + "p3-matrix 0.4.1", + "p3-maybe-rayon 0.4.1", + "p3-util 0.4.1", + "spin 0.10.0", "tracing", ] @@ -4257,11 +4337,39 @@ dependencies = [ "itertools 0.12.1", "num-bigint 0.4.6", "num-traits", - "p3-util", + "p3-util 0.2.3-succinct", "rand 0.8.5", "serde", ] +[[package]] +name = "p3-field" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "itertools 0.14.0", + "num-bigint 0.4.6", + "p3-maybe-rayon 0.4.1", + "p3-util 0.4.1", + "paste", + "rand 0.9.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-koala-bear" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-challenger", + "p3-field 0.4.1", + "p3-monty-31", + "p3-poseidon2 0.4.1", + "p3-symmetric 0.4.1", + "rand 0.9.2", +] + [[package]] name = "p3-matrix" version = "0.2.3-succinct" @@ -4269,20 +4377,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e4de3f373589477cb735ea58e125898ed20935e03664b4614c7fac258b3c42f" dependencies = [ "itertools 0.12.1", - "p3-field", - "p3-maybe-rayon", - "p3-util", + "p3-field 0.2.3-succinct", + "p3-maybe-rayon 0.2.3-succinct", + "p3-util 0.2.3-succinct", "rand 0.8.5", "serde", "tracing", ] +[[package]] +name = "p3-matrix" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "itertools 0.14.0", + "p3-field 0.4.1", + "p3-maybe-rayon 0.4.1", + "p3-util 0.4.1", + "rand 0.9.2", + "serde", + "tracing", + "transpose", +] + [[package]] name = "p3-maybe-rayon" version = "0.2.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3968ad1160310296eb04f91a5f4edfa38fe1d6b2b8cd6b5c64e6f9b7370979e" +[[package]] +name = "p3-maybe-rayon" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" + [[package]] name = "p3-mds" version = "0.2.3-succinct" @@ -4290,14 +4418,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2356b1ed0add6d5dfbf7a338ce534a6fde827374394a52cec16a0840af6e97c9" dependencies = [ "itertools 0.12.1", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-symmetric", - "p3-util", + "p3-dft 0.2.3-succinct", + "p3-field 0.2.3-succinct", + "p3-matrix 0.2.3-succinct", + "p3-symmetric 0.2.3-succinct", + "p3-util 0.2.3-succinct", "rand 0.8.5", ] +[[package]] +name = "p3-mds" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-dft 0.4.1", + "p3-field 0.4.1", + "p3-symmetric 0.4.1", + "p3-util 0.4.1", + "rand 0.9.2", +] + +[[package]] +name = "p3-monty-31" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "itertools 0.14.0", + "num-bigint 0.4.6", + "p3-dft 0.4.1", + "p3-field 0.4.1", + "p3-matrix 0.4.1", + "p3-maybe-rayon 0.4.1", + "p3-mds 0.4.1", + "p3-poseidon2 0.4.1", + "p3-symmetric 0.4.1", + "p3-util 0.4.1", + "paste", + "rand 0.9.2", + "serde", + "spin 0.10.0", + "tracing", + "transpose", +] + [[package]] name = "p3-poseidon2" version = "0.2.3-succinct" @@ -4305,13 +4468,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da1eec7e1b6900581bedd95e76e1ef4975608dd55be9872c9d257a8a9651c3a" dependencies = [ "gcd", - "p3-field", - "p3-mds", - "p3-symmetric", + "p3-field 0.2.3-succinct", + "p3-mds 0.2.3-succinct", + "p3-symmetric 0.2.3-succinct", "rand 0.8.5", "serde", ] +[[package]] +name = "p3-poseidon2" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-field 0.4.1", + "p3-mds 0.4.1", + "p3-symmetric 0.4.1", + "p3-util 0.4.1", + "rand 0.9.2", +] + [[package]] name = "p3-symmetric" version = "0.2.3-succinct" @@ -4319,7 +4494,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb439bea1d822623b41ff4b51e3309e80d13cadf8b86d16ffd5e6efb9fdc360" dependencies = [ "itertools 0.12.1", - "p3-field", + "p3-field 0.2.3-succinct", + "serde", +] + +[[package]] +name = "p3-symmetric" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "itertools 0.14.0", + "p3-field 0.4.1", "serde", ] @@ -4332,6 +4517,14 @@ dependencies = [ "serde", ] +[[package]] +name = "p3-util" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "serde", +] + [[package]] name = "pairing" version = "0.23.0" @@ -5616,10 +5809,10 @@ dependencies = [ "hex", "lazy_static", "num-bigint 0.4.6", - "p3-baby-bear", - "p3-field", - "p3-poseidon2", - "p3-symmetric", + "p3-baby-bear 0.2.3-succinct", + "p3-field 0.2.3-succinct", + "p3-poseidon2 0.2.3-succinct", + "p3-symmetric 0.2.3-succinct", "serde", "sha2", ] @@ -5672,6 +5865,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -5710,6 +5912,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strsim" version = "0.11.1" @@ -6121,6 +6329,16 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "tree_hash" version = "0.12.0" diff --git a/Cargo.toml b/Cargo.toml index 60c86dc5..7e206963 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,9 @@ clap = { version = "4.3", features = ["derive", "env"] } # TODO: switch to ethrex implementation when available ethereum-types = { version = "0.15.1", features = ["serialize"] } +# XMSS signatures +leansig = { git = "https://github.com/leanEthereum/leansig.git", rev = "ae12a5feb25d917c42b6466444ebd56ec115a629" } + # SSZ deps # TODO: roll up our own implementation ethereum_ssz_derive = "0.8.3" diff --git a/crates/common/types/Cargo.toml b/crates/common/types/Cargo.toml index 8391b0c5..556d4395 100644 --- a/crates/common/types/Cargo.toml +++ b/crates/common/types/Cargo.toml @@ -14,6 +14,8 @@ thiserror.workspace = true serde.workspace = true ethereum-types.workspace = true +leansig.workspace = true + ethereum_ssz_derive = "0.10.0" ethereum_ssz = "0.10.0" ssz_types = "0.14.0" diff --git a/crates/common/types/src/attestation.rs b/crates/common/types/src/attestation.rs new file mode 100644 index 00000000..4b66c623 --- /dev/null +++ b/crates/common/types/src/attestation.rs @@ -0,0 +1,44 @@ +use ssz_derive::{Decode, Encode}; +use ssz_types::typenum::U4096; +use tree_hash_derive::TreeHash; + +use crate::{signature::Signature, state::Checkpoint}; + +/// Validator specific attestation wrapping shared attestation data. +#[derive(Debug, Clone, Encode, Decode, TreeHash)] +pub struct Attestation { + /// The index of the validator making the attestation. + pub validator_id: u64, + + /// The attestation data produced by the validator. + pub data: AttestationData, +} + +/// Attestation content describing the validator's observed chain view. +#[derive(Debug, Clone, Encode, Decode, TreeHash)] +pub struct AttestationData { + /// The slot for which the attestation is made. + pub slot: u64, + + /// The checkpoint representing the head block as observed by the validator. + pub head: Checkpoint, + + /// The checkpoint representing the target block as observed by the validator. + pub target: Checkpoint, + + /// The checkpoint representing the source block as observed by the validator. + pub source: Checkpoint, +} + +/// List of validator attestations included in a block. +/// Size limited to [`crate::state::VALIDATOR_REGISTRY_LIMIT`]. +pub type Attestations = ssz_types::VariableList; + +/// Validator attestation bundled with its signature. +#[derive(Clone, Encode, Decode)] +pub struct SignedAttestation { + /// The attestation message signed by the validator. + message: Attestation, + /// Signature aggregation produced by the leanVM (SNARKs in the future). + signature: Signature, +} diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index bd29c673..8d261b7c 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -1,6 +1,55 @@ -use crate::primitives::H256; +use ssz_derive::{Decode, Encode}; +use ssz_types::typenum::{Diff, U488, U3600, U4096}; +use tree_hash_derive::TreeHash; -use crate::state::Slot; +use crate::{ + attestation::{Attestation, Attestations}, + primitives::H256, + signature::{Signature, SignatureSize}, +}; + +/// Envelope carrying a block, an attestation from proposer, and aggregated signatures. +#[derive(Clone, Encode, Decode)] +pub struct SignedBlockWithAttestation { + /// The block plus an attestation from proposer being signed. + pub message: BlockWithAttestation, + + /// Aggregated signature payload for the block. + /// + /// Signatures remain in attestation order followed by the proposer signature + /// over entire message. For devnet 1, however the proposer signature is just + /// over message.proposer_attestation since leanVM is not yet performant enough + /// to aggregate signatures with sufficient throughput. + /// + /// Eventually this field will be replaced by a SNARK (which represents the + /// aggregation of all signatures). + pub signature: BlockSignatures, +} + +// Manual Debug impl because leanSig signatures don't implement Debug. +impl core::fmt::Debug for SignedBlockWithAttestation { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SignedBlockWithAttestation") + .field("message", &self.message) + .field("signature", &"...") + .finish() + } +} + +/// Aggregated signature list included alongside the block. +/// Size limited to [`crate::state::VALIDATOR_REGISTRY_LIMIT`]. +pub type BlockSignatures = + ssz_types::VariableList, U4096>; + +/// Bundle containing a block and the proposer's attestation. +#[derive(Debug, Clone, Encode, Decode, TreeHash)] +pub struct BlockWithAttestation { + /// The proposed block message. + pub block: Block, + + /// The proposer's attestation corresponding to this block. + pub proposer_attestation: Attestation, +} /// The header of a block, containing metadata. /// @@ -10,10 +59,10 @@ use crate::state::Slot; /// /// Headers are smaller than full blocks. They're useful for tracking the chain /// without storing everything. -#[derive(Debug)] +#[derive(Debug, Clone, Encode, Decode, TreeHash)] pub struct BlockHeader { /// The slot in which the block was proposed - pub slot: Slot, + pub slot: u64, /// The index of the validator that proposed the block pub proposer_index: u64, /// The root of the parent block @@ -23,3 +72,31 @@ pub struct BlockHeader { /// The root of the block body pub body_root: H256, } + +/// A complete block including header and body. +#[derive(Debug, Clone, Encode, Decode, TreeHash)] +pub struct Block { + /// The slot in which the block was proposed. + pub slot: u64, + /// The index of the validator that proposed the block. + pub proposer_index: u64, + /// The root of the parent block. + pub parent_root: H256, + /// The root of the state after applying transactions in this block. + pub state_root: H256, + /// The block's payload. + pub body: BlockBody, +} + +/// The body of a block, containing payload data. +/// +/// Currently, the main operation is voting. Validators submit attestations which are +/// packaged into blocks. +#[derive(Debug, Clone, Encode, Decode, TreeHash)] +pub struct BlockBody { + /// Plain validator attestations carried in the block body. + /// + /// Individual signatures live in the aggregated block signature list, so + /// these entries contain only attestation data without per-attestation signatures. + pub attestations: Attestations, +} diff --git a/crates/common/types/src/lib.rs b/crates/common/types/src/lib.rs index c7c712e5..d344562c 100644 --- a/crates/common/types/src/lib.rs +++ b/crates/common/types/src/lib.rs @@ -1,4 +1,6 @@ +pub mod attestation; pub mod block; pub mod genesis; pub mod primitives; +pub mod signature; pub mod state; diff --git a/crates/common/types/src/signature.rs b/crates/common/types/src/signature.rs new file mode 100644 index 00000000..4b91bb62 --- /dev/null +++ b/crates/common/types/src/signature.rs @@ -0,0 +1,10 @@ +use leansig::signature::SignatureScheme; +use ssz_types::typenum::{Diff, U488, U3600}; + +type LeanSignatureScheme = leansig::signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8; + +type LeanSigSignature = ::Signature; + +pub type Signature = LeanSigSignature; + +pub type SignatureSize = Diff; diff --git a/crates/common/types/src/state.rs b/crates/common/types/src/state.rs index 1e8869f9..6b125c5e 100644 --- a/crates/common/types/src/state.rs +++ b/crates/common/types/src/state.rs @@ -4,16 +4,16 @@ use tree_hash_derive::TreeHash; use crate::{block::BlockHeader, genesis::Genesis, primitives::H256}; -#[derive(Debug)] -pub struct Slot(u64); +/// The maximum number of validators that can be in the registry. +pub const VALIDATOR_REGISTRY_LIMIT: u64 = 4096; // 2 ** 12 /// The main consensus state object -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State { /// The chain's configuration parameters config: NetworkConfig, /// The current slot number - slot: Slot, + slot: u64, /// The header of the most recent block latest_block_header: BlockHeader, /// The latest justified checkpoint @@ -36,9 +36,9 @@ impl State { pub fn from_genesis(genesis: &Genesis) -> Self { State { config: genesis.config.clone(), - slot: Slot(0), + slot: 0, latest_block_header: BlockHeader { - slot: Slot(0), + slot: 0, proposer_index: 0, parent_root: H256::ZERO, state_root: H256::ZERO, diff --git a/crates/net/p2p/Cargo.toml b/crates/net/p2p/Cargo.toml index 369147e6..c42b8887 100644 --- a/crates/net/p2p/Cargo.toml +++ b/crates/net/p2p/Cargo.toml @@ -32,5 +32,9 @@ ssz_types = "0.14.0" tree_hash = "0.12.0" tree_hash_derive = "0.12.0" +sha2 = "0.10" + +hex = "0.4" + [dev-dependencies] hex = "0.4" diff --git a/crates/net/p2p/src/gossipsub.rs b/crates/net/p2p/src/gossipsub.rs new file mode 100644 index 00000000..738a37e7 --- /dev/null +++ b/crates/net/p2p/src/gossipsub.rs @@ -0,0 +1,52 @@ +use ethlambda_types::block::SignedBlockWithAttestation; +use libp2p::gossipsub::Event; +use ssz::Decode; +use tracing::{error, info, trace}; + +/// Topic kind for block gossip +pub const BLOCK_TOPIC_KIND: &str = "block"; +/// Topic kind for attestation gossip +pub const ATTESTATION_TOPIC_KIND: &str = "attestation"; + +pub async fn handle_gossipsub_message(event: Event) { + let Event::Message { + propagation_source: _, + message_id: _, + message, + } = event + else { + unreachable!("we already matched on event_loop"); + }; + match message.topic.as_str().split("/").nth(3) { + Some(BLOCK_TOPIC_KIND) => { + let Ok(uncompressed_data) = decompress_message(&message.data) + .inspect_err(|err| error!(%err, "Failed to decompress gossipped block")) + else { + return; + }; + + let Ok(signed_block) = SignedBlockWithAttestation::from_ssz_bytes(&uncompressed_data) + .inspect_err(|err| error!(?err, "Failed to decode gossipped block")) + else { + return; + }; + info!(slot=%signed_block.message.block.slot, "Received new block"); + } + Some(ATTESTATION_TOPIC_KIND) => { + info!( + "Received attestation gossip message of size {} bytes", + message.data.len() + ); + } + _ => { + trace!("Received message on unknown topic: {}", message.topic); + } + } +} + +fn decompress_message(data: &[u8]) -> snap::Result> { + let uncompressed_size = snap::raw::decompress_len(&data)?; + let mut uncompressed_data = vec![0u8; uncompressed_size]; + snap::raw::Decoder::new().decompress(&data, &mut uncompressed_data)?; + Ok(uncompressed_data) +} diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index bde9c565..8590a613 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -6,16 +6,21 @@ use ethrex_rlp::decode::RLPDecode; use libp2p::{ Multiaddr, PeerId, StreamProtocol, futures::StreamExt, - gossipsub::{self, MessageAuthenticity, ValidationMode}, + gossipsub::{MessageAuthenticity, ValidationMode}, identity::{PublicKey, secp256k1}, multiaddr::Protocol, - request_response::{self, Event, Message}, + request_response, swarm::{NetworkBehaviour, SwarmEvent}, }; +use sha2::Digest; use tracing::{info, trace}; -use crate::messages::status::{STATUS_PROTOCOL_V1, Status}; +use crate::{ + gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, + messages::status::{STATUS_PROTOCOL_V1, Status}, +}; +mod gossipsub; mod messages; pub async fn start_p2p(bootnodes: Vec, listening_port: u16) { @@ -35,12 +40,14 @@ pub async fn start_p2p(bootnodes: Vec, listening_port: u16) { // seen_ttl_secs = seconds_per_slot * justification_lookback_slots * 2 .duplicate_cache_time(Duration::from_secs(4 * 3 * 2)) .validation_mode(ValidationMode::Anonymous) + .message_id_fn(compute_message_id) .build() .expect("invalid gossipsub config"); // TODO: setup custom message ID function let gossipsub = libp2p::gossipsub::Behaviour::new(MessageAuthenticity::Anonymous, config) .expect("failed to initiate behaviour"); + let req_resp = request_response::Behaviour::new( vec![( StreamProtocol::new(STATUS_PROTOCOL_V1), @@ -93,6 +100,14 @@ pub async fn start_p2p(bootnodes: Vec, listening_port: u16) { .listen_on(addr) .expect("failed to bind gossipsub listening address"); + let topic_kinds = [BLOCK_TOPIC_KIND, ATTESTATION_TOPIC_KIND]; + let network = "devnet0"; + for topic_kind in topic_kinds { + let topic_str = format!("/leanconsensus/{network}/{topic_kind}/ssz_snappy"); + let topic = libp2p::gossipsub::IdentTopic::new(topic_str); + swarm.behaviour_mut().gossipsub.subscribe(&topic).unwrap(); + } + println!("P2P node started on port {listening_port}"); event_loop(swarm).await; @@ -101,7 +116,7 @@ pub async fn start_p2p(bootnodes: Vec, listening_port: u16) { /// [libp2p Behaviour](libp2p::swarm::NetworkBehaviour) combining Gossipsub and Request-Response Behaviours #[derive(NetworkBehaviour)] struct Behaviour { - gossipsub: gossipsub::Behaviour, + gossipsub: libp2p::gossipsub::Behaviour, req_resp: request_response::Behaviour, } @@ -110,21 +125,16 @@ struct Behaviour { async fn event_loop(mut swarm: libp2p::Swarm) { while let Some(event) = swarm.next().await { match event { - SwarmEvent::Behaviour(BehaviourEvent::ReqResp(message @ Event::Message { .. })) => { + SwarmEvent::Behaviour(BehaviourEvent::ReqResp( + message @ request_response::Event::Message { .. }, + )) => { handle_req_resp_message(&mut swarm, message).await; } - // SwarmEvent::Behaviour(BehaviourEvent::ReqResp(Event::Message { - // peer, - // connection_id, - // message: - // Message::Request { - // request_id, - // request, - // channel, - // }, - // })) => { - // info!(finalized_slot=%request.finalized.slot, head_slot=%request.head.slot, "Received status request from peer {peer}"); - // } + SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( + message @ libp2p::gossipsub::Event::Message { .. }, + )) => { + gossipsub::handle_gossipsub_message(message).await; + } _ => { trace!(?event, "Ignored swarm event"); } @@ -134,9 +144,9 @@ async fn event_loop(mut swarm: libp2p::Swarm) { async fn handle_req_resp_message( swarm: &mut libp2p::Swarm, - event: Event, + event: request_response::Event, ) { - let Event::Message { + let request_response::Event::Message { peer, connection_id: _, message, @@ -145,7 +155,7 @@ async fn handle_req_resp_message( unreachable!("we already matched on event_loop"); }; match message { - Message::Request { + request_response::Message::Request { request_id: _, request, channel, @@ -158,7 +168,7 @@ async fn handle_req_resp_message( .unwrap(); swarm.behaviour_mut().req_resp.send_request(&peer, request); } - Message::Response { + request_response::Message::Response { request_id: _, response, } => { @@ -214,3 +224,24 @@ pub fn parse_validators_file(bootnodes_path: &str) -> Vec { } bootnodes } + +fn compute_message_id(message: &libp2p::gossipsub::Message) -> libp2p::gossipsub::MessageId { + const MESSAGE_DOMAIN_INVALID_SNAPPY: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; + const MESSAGE_DOMAIN_VALID_SNAPPY: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; + + let mut hasher = sha2::Sha256::new(); + let decompressed = snap::raw::Decoder::new().decompress_vec(&message.data); + + let (domain, data) = match decompressed.as_ref() { + Ok(decompressed_data) => (MESSAGE_DOMAIN_VALID_SNAPPY, decompressed_data), + Err(_) => (MESSAGE_DOMAIN_INVALID_SNAPPY, &message.data), + }; + let topic = message.topic.as_str().as_bytes(); + let topic_len = (topic.len() as u64).to_be_bytes(); + hasher.update(&domain); + hasher.update(&topic_len); + hasher.update(topic); + hasher.update(data); + let hash = hasher.finalize(); + libp2p::gossipsub::MessageId(hash[..20].to_vec()) +} diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index 6e52d165..06cfc91a 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -10,3 +10,4 @@ rust-version.workspace = true version.workspace = true [dependencies] +ethlambda-types.workspace = true diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index e69de29b..92b9c89d 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -0,0 +1,85 @@ +use std::collections::HashMap; + +use ethlambda_types::{ + attestation::SignedAttestation, + block::Block, + primitives::H256, + state::{Checkpoint, State}, +}; + +/// Forkchoice store tracking chain state and validator attestations. +/// +/// This is the "local view" that a node uses to run LMD GHOST. It contains: +/// +/// - which blocks and states are known, +/// - which checkpoints are justified and finalized, +/// - which block is currently considered the head, +/// - and, for each validator, their latest attestation that should influence fork choice. +/// +/// The `Store` is updated whenever: +/// - a new block is processed, +/// - an attestation is received (via a block or gossip), +/// - an interval tick occurs (activating new attestations), +/// - or when the head is recomputed. +#[derive(Clone)] +pub struct Store { + /// Current time in intervals since genesis. + time: u64, + + /// Chain configuration parameters. + // config: Config, + + /// Root of the current canonical chain head block. + /// + /// This is the result of running the fork choice algorithm on the current contents of the `Store`. + head: H256, + + /// Root of the current safe target for attestation. + /// + /// This can be used by higher-level logic to restrict which blocks are + /// considered safe to attest to, based on additional safety conditions. + /// + safe_target: H256, + + /// Highest slot justified checkpoint known to the store. + /// + /// LMD GHOST starts from this checkpoint when computing the head. + /// + /// Only descendants of this checkpoint are considered viable. + latest_justified: Checkpoint, + + /// Highest slot finalized checkpoint known to the store. + /// + /// Everything strictly before this checkpoint can be considered immutable. + /// + /// Fork choice will never revert finalized history. + latest_finalized: Checkpoint, + + /// Mapping from block root to Block objects. + /// + /// This is the set of blocks that the node currently knows about. + /// + /// Every block that might participate in fork choice must appear here. + blocks: HashMap, + + /// Mapping from state root to State objects. + /// + /// For each known block, we keep its post-state. + /// + /// These states carry justified and finalized checkpoints that we use to update the + /// `Store`'s latest justified and latest finalized checkpoints. + states: HashMap, + + /// Latest signed attestations by validator that have been processed. + /// + /// - These attestations are "known" and contribute to fork choice weights. + /// - Keyed by validator index to enforce one attestation per validator. + latest_known_attestations: HashMap, + + /// Latest signed attestations by validator that are pending processing. + /// + /// - These attestations are "new" and do not yet contribute to fork choice. + /// - They migrate to `latest_known_attestations` via interval ticks. + /// - Keyed by validator index to enforce one attestation per validator. + latest_new_attestations: HashMap, +}