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()
+ );
+}