diff --git a/Cargo.lock b/Cargo.lock index ecaf8da2b21..fc3676c13d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5517,6 +5517,7 @@ dependencies = [ "anyhow", "async-broadcast", "async-trait", + "chrono", "demo-async", "demo-async-init", "demo-delayed-sender-ethexe", diff --git a/ethexe/common/src/consensus.rs b/ethexe/common/src/consensus.rs index e409495237e..9ba74eefa81 100644 --- a/ethexe/common/src/consensus.rs +++ b/ethexe/common/src/consensus.rs @@ -17,7 +17,7 @@ // along with this program. If not, see . use crate::{ - Announce, Digest, HashOf, ToDigest, + Address, Announce, Digest, HashOf, ProtocolTimelines, ToDigest, ecdsa::{ContractSignature, VerifiedData}, gear::BatchCommitment, validators::ValidatorsVec, @@ -43,22 +43,39 @@ pub type VerifiedValidationReply = VerifiedData; // TODO #4553: temporary implementation, should be improved /// Returns block producer for time slot. Next slot is the next validator in the list. -pub const fn block_producer_index(validators_amount: usize, slot: u64) -> usize { +pub const fn block_producer_index_for_slot(validators_amount: usize, slot: u64) -> usize { (slot % validators_amount as u64) as usize } -/// Calculates the producer address for a given slot based on the validators and timestamp. -pub fn block_producer_for( - validators: &ValidatorsVec, - timestamp: u64, - slot_duration: u64, -) -> crate::Address { - let slot = timestamp / slot_duration; - let index = block_producer_index(validators.len(), slot); - validators - .get(index) - .cloned() - .unwrap_or_else(|| unreachable!("index must be valid")) +impl ProtocolTimelines { + /// Calculates the producer address for a given timestamp. + /// + /// # Arguments + /// * `validators` - A non-empty vector of validator addresses. + /// * `timestamp` - The timestamp for which to calculate the block producer. + /// + /// # Panics + /// Panics if timestamp is before genesis. + pub fn block_producer_at(&self, validators: &ValidatorsVec, timestamp: u64) -> Address { + let block_producer_index = self.block_producer_index_at(validators.len(), timestamp); + validators + .get(block_producer_index) + .cloned() + .unwrap_or_else(|| unreachable!("index must be valid")) + } + + /// Calculates the block producer index for a given timestamp. + /// + /// # Arguments + /// * `validators_amount` - The number of validators in the protocol. + /// * `timestamp` - The timestamp for which to calculate the block producer index. + /// + /// # Panics + /// Panics if timestamp is before genesis or if validators_amount is zero. + pub fn block_producer_index_at(&self, validators_amount: usize, timestamp: u64) -> usize { + let slot = self.slot_from_ts(timestamp); + block_producer_index_for_slot(validators_amount, slot) + } } /// Represents a request for validating a batch commitment. @@ -144,7 +161,7 @@ mod tests { let validators_amount = 5; let slot = 7; - let index = block_producer_index(validators_amount, slot); + let index = block_producer_index_for_slot(validators_amount, slot); assert_eq!(index, 2); } @@ -152,16 +169,40 @@ mod tests { #[test] fn block_producer_for_calculates_correct_producer() { let validators = vec![ - crate::Address::from([1; 20]), - crate::Address::from([2; 20]), - crate::Address::from([3; 20]), + Address::from([1; 20]), + Address::from([2; 20]), + Address::from([3; 20]), + ] + .try_into() + .unwrap(); + + let producer = ProtocolTimelines { + slot: 1, + genesis_ts: 0, + ..Default::default() + } + .block_producer_at(&validators, 10); + + assert_eq!(producer, Address::from([2; 20])); + } + + #[test] + fn block_producer_for_calculates_correct_producer_with_genesis_timestamp() { + let validators = vec![ + Address::from([1; 20]), + Address::from([2; 20]), + Address::from([3; 20]), ] .try_into() .unwrap(); - let timestamp = 10; - let producer = block_producer_for(&validators, timestamp, 1); + let producer = ProtocolTimelines { + slot: 2, + genesis_ts: 6, + ..Default::default() + } + .block_producer_at(&validators, 16); - assert_eq!(producer, validators[timestamp as usize % validators.len()]); + assert_eq!(producer, Address::from([3; 20])); } } diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index 2c281138fc1..232b4a21de9 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -254,6 +254,7 @@ pub struct ProtocolTimelines { pub slot: u64, } +// TODO: #5290 remove panics here impl ProtocolTimelines { /// Returns the era index for the given timestamp. Eras starts from 0. /// If given `ts` less than `genesis_ts` function returns `0`; @@ -276,6 +277,13 @@ impl ProtocolTimelines { pub fn era_election_start_ts(&self, era_index: u64) -> u64 { self.era_start_ts(era_index + 1) - self.election } + + #[inline(always)] + pub fn slot_from_ts(&self, ts: u64) -> u64 { + ts.checked_sub(self.genesis_ts) + .expect("timestamp must be >= genesis_ts") + / self.slot + } } /// RemoveFromMailbox key; (msgs sources program (mailbox and queue provider), destination user id) diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 6b38c1a9775..bb007c0e279 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -27,7 +27,7 @@ use crate::{ use anyhow::{Result, anyhow}; use ethexe_common::{ Address, Announce, HashOf, PromisePolicy, ProtocolTimelines, SimpleBlockData, - consensus::{VerifiedAnnounce, VerifiedValidationRequest, block_producer_for}, + consensus::{VerifiedAnnounce, VerifiedValidationRequest}, db::{ConfigStorageRO, OnChainStorageRO}, injected::{Promise, SignedInjectedTransaction}, network::{AnnouncesRequest, AnnouncesResponse}, @@ -42,7 +42,6 @@ use std::{ num::NonZeroUsize, pin::Pin, task::{Context, Poll}, - time::Duration, }; /// Maximum number of pending announces to store @@ -100,7 +99,6 @@ enum State { #[derive(derive_more::Debug)] pub struct ConnectService { db: Database, - slot_duration: Duration, commitment_delay_limit: u32, timelines: ProtocolTimelines, @@ -114,14 +112,12 @@ impl ConnectService { /// /// # Parameters /// - `db`: Database instance. - /// - `slot_duration`: Duration of each slot in the consensus protocol. /// - `commitment_delay_limit`: Maximum allowed delay for announce to be committed. - pub fn new(db: Database, slot_duration: Duration, commitment_delay_limit: u32) -> Self { + pub fn new(db: Database, commitment_delay_limit: u32) -> Self { let timelines = db.config().timelines; Self { db, - slot_duration, commitment_delay_limit, timelines, state: State::WaitingForBlock, @@ -193,11 +189,9 @@ impl ConsensusService for ConnectService { let validators = self.db.validators(block_era).ok_or(anyhow!( "validators not found for synced block({block_hash})" ))?; - let producer = block_producer_for( - &validators, - block.header.timestamp, - self.slot_duration.as_secs(), - ); + let producer = self + .timelines + .block_producer_at(&validators, block.header.timestamp); self.state = State::WaitingForPreparedBlock { block: *block, @@ -383,7 +377,7 @@ mod tests { let db = Database::memory(); let chain = BlockChain::mock((10, validators)).setup(&db); - let mut service = ConnectService::new(db, Duration::from_secs(12), 10); + let mut service = ConnectService::new(db, 10); service .receive_new_chain_head(chain.blocks[10].to_simple()) .unwrap(); diff --git a/ethexe/consensus/src/validator/core.rs b/ethexe/consensus/src/validator/core.rs index c642657c141..601f67f8b66 100644 --- a/ethexe/consensus/src/validator/core.rs +++ b/ethexe/consensus/src/validator/core.rs @@ -37,7 +37,6 @@ use tokio::sync::RwLock; #[derive(derive_more::Debug)] pub struct ValidatorCore { - pub slot_duration: Duration, pub signatures_threshold: u64, pub router_address: Address, pub pub_key: PublicKey, @@ -69,7 +68,6 @@ pub struct ValidatorCore { impl Clone for ValidatorCore { fn clone(&self) -> Self { Self { - slot_duration: self.slot_duration, signatures_threshold: self.signatures_threshold, router_address: self.router_address, pub_key: self.pub_key, diff --git a/ethexe/consensus/src/validator/initial.rs b/ethexe/consensus/src/validator/initial.rs index 692688d3b10..ff91014e114 100644 --- a/ethexe/consensus/src/validator/initial.rs +++ b/ethexe/consensus/src/validator/initial.rs @@ -27,7 +27,6 @@ use anyhow::{Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ SimpleBlockData, - consensus::block_producer_for, db::OnChainStorageRO, network::{AnnouncesRequest, AnnouncesResponse}, }; @@ -228,11 +227,10 @@ impl ValidatorContext { .validators(era_index) .ok_or(anyhow!("validators not found for era {era_index}"))?; - let producer = block_producer_for( - &validators, - block.header.timestamp, - self.core.slot_duration.as_secs(), - ); + let producer = self + .core + .timelines + .block_producer_at(&validators, block.header.timestamp); let my_address = self.core.pub_key.to_address(); if my_address == producer { @@ -287,9 +285,9 @@ mod tests { let (mut ctx, keys, _) = mock_validator_context(); let validators: ValidatorsVec = nonempty![ - ctx.core.pub_key.to_address(), keys[0].to_address(), keys[1].to_address(), + ctx.core.pub_key.to_address(), ] .into(); diff --git a/ethexe/consensus/src/validator/mock.rs b/ethexe/consensus/src/validator/mock.rs index 6bae9f9261a..fa511005998 100644 --- a/ethexe/consensus/src/validator/mock.rs +++ b/ethexe/consensus/src/validator/mock.rs @@ -148,7 +148,7 @@ pub fn mock_validator_context() -> (ValidatorContext, Vec, MockEthere let (signer, _, mut keys) = crate::mock::init_signer_with_keys(10); let ethereum = MockEthereum::default(); let db = Database::memory(); - let timelines = ProtocolTimelines::mock(()); + let timelines = ProtocolTimelines::mock(()).tap_mut(|tl| tl.slot = 1); let limits = BatchLimits::default(); let middleware = MiddlewareWrapper::from_inner(ethereum.clone()); @@ -156,7 +156,6 @@ pub fn mock_validator_context() -> (ValidatorContext, Vec, MockEthere let ctx = ValidatorContext { core: ValidatorCore { - slot_duration: Duration::from_secs(1), signatures_threshold: 1, router_address: 12345.into(), pub_key: keys.pop().unwrap(), diff --git a/ethexe/consensus/src/validator/mod.rs b/ethexe/consensus/src/validator/mod.rs index b4d28ae1267..5385040906d 100644 --- a/ethexe/consensus/src/validator/mod.rs +++ b/ethexe/consensus/src/validator/mod.rs @@ -104,8 +104,6 @@ pub struct ValidatorConfig { /// ECDSA multi-signature threshold // TODO #4637: threshold should be a ratio (and maybe also a block dependent value) pub signatures_threshold: u64, - /// Duration of ethexe slot (only to identify producer for the incoming blocks) - pub slot_duration: Duration, /// Block gas limit for producer to create announces pub block_gas_limit: u64, /// Delay limit for commitment @@ -149,7 +147,6 @@ impl ValidatorService { let ctx = ValidatorContext { core: ValidatorCore { - slot_duration: config.slot_duration, signatures_threshold: config.signatures_threshold, router_address: config.router_address, pub_key: config.pub_key, diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs index c38af2af39c..8ac31bebb31 100644 --- a/ethexe/rpc/src/apis/injected.rs +++ b/ethexe/rpc/src/apis/injected.rs @@ -21,7 +21,6 @@ use anyhow::Result; use dashmap::DashMap; use ethexe_common::{ Address, HashOf, - consensus::block_producer_for, injected::{ AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, SignedPromise, @@ -308,11 +307,7 @@ mod utils { .validators(era) .ok_or_else(|| anyhow::anyhow!("validators not found for era={era}"))?; - Ok(block_producer_for( - &validators, - target_timestamp, - timelines.slot, - )) + Ok(timelines.block_producer_at(&validators, target_timestamp)) } /// Returns the current time since [SystemTime::UNIX_EPOCH]. diff --git a/ethexe/service/Cargo.toml b/ethexe/service/Cargo.toml index 04f3c483d3d..42d5ea0ee3a 100644 --- a/ethexe/service/Cargo.toml +++ b/ethexe/service/Cargo.toml @@ -70,6 +70,7 @@ jsonrpsee = { workspace = true, features = ["client"] } async-broadcast.workspace = true wat.workspace = true tempfile.workspace = true +chrono = "0.4" demo-ping = { workspace = true, features = ["debug", "ethexe"] } demo-value-sender-ethexe = { workspace = true, features = ["debug", "ethexe"] } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 8c061cc63a6..945be93e586 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -329,7 +329,6 @@ impl Service { ValidatorConfig { pub_key, signatures_threshold: threshold, - slot_duration: config.ethereum.block_time, block_gas_limit: config.node.block_gas_limit, // TODO: #4942 commitment_delay_limit is a protocol specific constant // which better to be configurable by router contract @@ -341,11 +340,7 @@ impl Service { }, )?) } else { - Box::pin(ConnectService::new( - db.clone(), - config.ethereum.block_time, - 3, - )) + Box::pin(ConnectService::new(db.clone(), 3)) } }; diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index a434b52d75d..53affcd656a 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -33,7 +33,8 @@ use ethexe_blob_loader::{BlobLoader, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{ Address, COMMITMENT_DELAY_LIMIT, CodeAndId, DEFAULT_BLOCK_GAS_LIMIT, SimpleBlockData, ToDigest, ValidatorsVec, - consensus::{DEFAULT_BATCH_SIZE_LIMIT, DEFAULT_CHAIN_DEEPNESS_THRESHOLD, block_producer_index}, + consensus::{DEFAULT_BATCH_SIZE_LIMIT, DEFAULT_CHAIN_DEEPNESS_THRESHOLD}, + db::ConfigStorageRO, ecdsa::{PrivateKey, PublicKey, SignedData}, events::{ BlockEvent, MirrorEvent, RouterEvent, @@ -636,11 +637,11 @@ impl TestEnv { /// that can produce blocks for the same rpc node, /// then the return may be outdated. pub async fn next_block_producer_index(&self) -> usize { - let timestamp = self.latest_block().await.header.timestamp; - block_producer_index( - self.validators.len(), - (timestamp + self.block_time.as_secs()) / self.block_time.as_secs(), - ) + let timestamp = self.latest_block().await.header.timestamp + self.block_time.as_secs(); + self.db + .config() + .timelines + .block_producer_index_at(self.validators.len(), timestamp) } /// Waits until the next block producer index becomes equal to `index`. @@ -996,7 +997,6 @@ impl Node { ethexe_consensus::ValidatorConfig { pub_key: config.public_key, signatures_threshold: self.threshold, - slot_duration: self.block_time, block_gas_limit: DEFAULT_BLOCK_GAS_LIMIT, commitment_delay_limit: self.commitment_delay_limit, producer_delay: self.block_time / 6, @@ -1010,7 +1010,6 @@ impl Node { } else { Box::pin(ConnectService::new( self.db.clone(), - self.block_time, self.commitment_delay_limit, )) } diff --git a/ethexe/service/tests/injected_tx.rs b/ethexe/service/tests/injected_tx.rs new file mode 100644 index 00000000000..a5ed89a5def --- /dev/null +++ b/ethexe/service/tests/injected_tx.rs @@ -0,0 +1,116 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethexe_common::{ + Address, + injected::{AddressedInjectedTransaction, InjectedTransaction}, +}; +use ethexe_rpc::{BlockClient as _, InjectedClient as _}; +use gprimitives::H256; +use gsigner::secp256k1::{Secp256k1SignerExt as _, Signer}; +use jsonrpsee::ws_client::WsClientBuilder; +use std::{ + str::FromStr as _, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +/// How to run this test: +/// ```bash +/// cargo test -p ethexe-service -- send_injected_tx_join_us --ignored --nocapture --exact +/// ``` +#[tokio::test] +#[ignore = "requires connection to vara.network validator"] +async fn send_injected_tx_join_us() { + const SLOT_DURATION: u64 = 12; + const VARA_ETH_MAINNET_GENESIS_TIMESTAMP: u64 = 1_774_445_351; + const VALIDATOR_RPC_URL: &str = "wss://validator-3-eth.vara.network"; + const DESTINATION: &str = "0x6286a1f8ebbd8b7d2ab75321f3f00b507d5ecc01"; + // SCALE-encoded payload: OneOfUs::JoinUs + const PAYLOAD: &[u8] = "\u{1c}OneOfUs\u{18}JoinUs".as_bytes(); + const END_OF_SLOT_DELAY: u64 = 1; + + let client = WsClientBuilder::new() + .build(VALIDATOR_RPC_URL) + .await + .unwrap(); + + // Get latest block hash as reference_block from ethexe RPC. + let (reference_block, _header) = client.block_header(None).await.unwrap(); + + let signer = Signer::memory(); + let key = signer.generate().unwrap(); + + let tx = InjectedTransaction { + destination: Address::from_str(DESTINATION).unwrap().into(), + payload: PAYLOAD.to_vec().try_into().unwrap(), + value: 0, + reference_block, + salt: H256::random().0.to_vec().try_into().unwrap(), + }; + + let message_id = tx.to_message_id(); + let tx_hash = tx.to_hash(); + println!("Message ID: {message_id:?}"); + println!("Tx hash: {tx_hash:?}"); + + let transaction = AddressedInjectedTransaction { + recipient: Address::default(), + tx: signer.signed_message(key, tx, None).unwrap(), + }; + + let in_slot_position = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + - VARA_ETH_MAINNET_GENESIS_TIMESTAMP) + % SLOT_DURATION; + if in_slot_position < SLOT_DURATION - END_OF_SLOT_DELAY { + let time_to_wait = SLOT_DURATION - in_slot_position - END_OF_SLOT_DELAY; + println!("Waiting for {time_to_wait}s to be close to the end of the slot..."); + tokio::time::sleep(Duration::from_secs(time_to_wait)).await; + } + + println!( + "Sending transaction start({}) ...", + chrono::Utc::now() + .format("%Y-%m-%d %H:%M:%S%.6f") + .to_string() + ); + + let start = Instant::now(); + + let mut subscription = client + .send_transaction_and_watch(transaction) + .await + .unwrap(); + + println!("Waiting for promise (elapsed: {:?}) ...", start.elapsed()); + + let promise = subscription + .next() + .await + .expect("promise from subscription") + .expect("transaction promise") + .into_data(); + + let elapsed = start.elapsed(); + println!( + "Promise received in {:.2}s: {promise:?}", + elapsed.as_secs_f64() + ); +}