Skip to content
Open
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions ethexe/common/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
//! Common db types and traits.

use crate::{
Announce, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines,
Address, Announce, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines,
Schedule, SimpleBlockData, ValidatorsVec,
events::BlockEvent,
gear::StateTransition,
injected::{InjectedTransaction, SignedInjectedTransaction},
injected::{InjectedTransaction, Promise, SignedInjectedTransaction},
};
use alloc::{
collections::{BTreeSet, VecDeque},
Expand All @@ -34,6 +34,7 @@ use gear_core::{
ids::{ActorId, CodeId},
};
use gprimitives::H256;
use gsigner::secp256k1::Signature;
use parity_scale_codec::{Decode, Encode};

/// Ethexe metadata associated with an on-chain block.
Expand Down Expand Up @@ -134,11 +135,25 @@ pub trait InjectedStorageRO {
&self,
hash: HashOf<InjectedTransaction>,
) -> Option<SignedInjectedTransaction>;

/// Returns the promise by its transaction hash.
fn promise(&self, hash: HashOf<InjectedTransaction>) -> Option<Promise>;

fn promise_signature(&self, hash: HashOf<InjectedTransaction>) -> Option<(Signature, Address)>;
}

#[auto_impl::auto_impl(&)]
pub trait InjectedStorageRW: InjectedStorageRO {
fn set_injected_transaction(&self, tx: SignedInjectedTransaction);

fn set_promise(&self, promise: Promise);

fn set_promise_signature(
&self,
hash: HashOf<InjectedTransaction>,
signature: Signature,
address: Address,
);
}

#[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Eq, Hash)]
Expand Down
99 changes: 89 additions & 10 deletions ethexe/common/src/injected.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use core::hash::Hash;
use gear_core::rpc::ReplyInfo;
use gprimitives::{ActorId, H256, MessageId};
use parity_scale_codec::{Decode, Encode};
use sha3::{Digest, Keccak256};
use sha3::{Digest as _, Keccak256};
use sp_core::Bytes;

/// Recent block hashes window size used to check transaction mortality.
Expand Down Expand Up @@ -133,26 +133,72 @@ pub struct Promise {
/// It will be shared among other validators as a proof of promise.
pub type SignedPromise = SignedMessage<Promise>;

impl ToDigest for Promise {
fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
let Self { tx_hash, reply } = self;
/// A wrapper on top of [`CompactPromiseHashes`].
///
/// [`CompactPromiseHashes`] is a lightweight version of [`SignedPromise`], that is
/// needed to reduce the amount of data transferred in network between validators.
pub type CompactSignedPromise = SignedMessage<CompactPromiseHashes>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For naming compatibility I think better to use SignedCompactPromise (using Signed as prefix)


hasher.update(tx_hash.inner());
impl Promise {
/// Calculates the `blake2b` hash from promise's reply.
pub fn reply_hash(&self) -> HashOf<ReplyInfo> {
let ReplyInfo {
payload,
code,
value,
} = reply;
} = &self.reply;

let bytes = [
payload.as_ref(),
code.to_bytes().as_ref(),
value.to_be_bytes().as_ref(),
]
.concat();
unsafe { HashOf::new(gear_core::utils::hash(&bytes).into()) }
}
}

hasher.update(payload);
hasher.update(code.to_bytes());
hasher.update(value.to_be_bytes());
impl ToDigest for Promise {
fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
// The hash of `Promise` equals to hash of `CompactPromiseHashes`.
CompactPromiseHashes::from(self).update_hasher(hasher);
}
}

/// The hashes of [`Promise`].
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct CompactPromiseHashes {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub struct CompactPromiseHashes {
pub struct CompactPromise {

pub tx_hash: HashOf<InjectedTransaction>,
pub reply_hash: HashOf<ReplyInfo>,
}

impl From<&Promise> for CompactPromiseHashes {
fn from(promise: &Promise) -> Self {
Self {
tx_hash: promise.tx_hash,
reply_hash: promise.reply_hash(),
}
}
}

#[cfg(test)]
impl ToDigest for CompactPromiseHashes {
fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
let Self {
tx_hash,
reply_hash,
} = self;

hasher.update(tx_hash.inner());
hasher.update(reply_hash.inner());
}
}

#[cfg(all(test, feature = "mock"))]
mod tests {
use gsigner::PrivateKey;

use super::*;
use crate::mock::Mock;

#[test]
fn signed_message_and_injected_transactions() {
Expand Down Expand Up @@ -191,4 +237,37 @@ mod tests {
signed_tx.address()
);
}

#[test]
fn promise_hashes_digest_equal_to_promise_digest() {
let promise = {
let mut promise = Promise::mock(());
promise.reply.value = 123;
promise.reply.payload = vec![1u8, 2u8, 42u8, 66u8];
promise
};
let promise_digest = promise.to_digest();
let promise_hashes = CompactPromiseHashes::from(&promise);

let promise_hashes_digest = promise_hashes.to_digest();
assert_eq!(promise_digest, promise_hashes_digest);
}

#[test]
fn compact_signature_valid_for_promise() {
let pk = PrivateKey::random();

let promise = Promise::mock(());
let promise_hashes = CompactPromiseHashes::from(&promise);
let compact_signed_promise = CompactSignedPromise::create(pk, promise_hashes).unwrap();

let (signature, address) = (
*compact_signed_promise.signature(),
compact_signed_promise.address(),
);

let signed_promise = SignedMessage::try_from_parts(promise.clone(), signature, address)
.expect("SignedMessage<Promise> was correctly constructed from CompactSignedPromise");
assert_eq!(signed_promise.into_data(), promise);
}
}
27 changes: 25 additions & 2 deletions ethexe/common/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ use crate::{
ecdsa::{PrivateKey, SignedMessage},
events::BlockEvent,
gear::{BatchCommitment, ChainCommitment, CodeCommitment, Message, StateTransition},
injected::{AddressedInjectedTransaction, InjectedTransaction},
injected::{AddressedInjectedTransaction, InjectedTransaction, Promise},
};
use alloc::{collections::BTreeMap, vec};
use gear_core::code::{CodeMetadata, InstrumentedCode};
use gear_core::{
code::{CodeMetadata, InstrumentedCode},
message::{ReplyCode, SuccessReplyReason},
rpc::ReplyInfo,
};
use gprimitives::{CodeId, H256};
use itertools::Itertools;
use std::collections::{BTreeSet, VecDeque};
Expand Down Expand Up @@ -650,3 +654,22 @@ impl Mock<HashOf<Announce>> for ComputedAnnounce {
}
}
}

impl Mock<HashOf<InjectedTransaction>> for Promise {
fn mock(tx_hash: HashOf<InjectedTransaction>) -> Self {
Self {
tx_hash,
reply: ReplyInfo {
payload: vec![],
value: 0,
code: ReplyCode::Success(SuccessReplyReason::Manual),
},
}
}
}

impl Mock for Promise {
fn mock(_args: ()) -> Self {
Promise::mock(HashOf::zero())
}
}
6 changes: 5 additions & 1 deletion ethexe/compute/src/compute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use ethexe_common::{
Announce, ComputedAnnounce, HashOf, SimpleBlockData,
db::{
AnnounceStorageRO, AnnounceStorageRW, BlockMetaStorageRO, CodesStorageRW,
LatestDataStorageRO, LatestDataStorageRW, OnChainStorageRO,
InjectedStorageRW, LatestDataStorageRO, LatestDataStorageRW, OnChainStorageRO,
},
events::BlockEvent,
};
Expand Down Expand Up @@ -168,6 +168,10 @@ impl<P: ProcessorExt> ComputeSubService<P> {
})
.ok_or(ComputeError::LatestDataNotFound)?;

promises.clone().into_iter().for_each(|promise| {
db.set_promise(promise);
});
Comment on lines +171 to +173
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move this to RPC? as I can see it's completely useless except if it's RPC. So, better to avoid useless cross services guaranties (in this case compute guaranties for RPC that promises are set in database)


Ok(ComputedAnnounce {
announce_hash,
promises,
Expand Down
4 changes: 2 additions & 2 deletions ethexe/consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use anyhow::Result;
use ethexe_common::{
Announce, ComputedAnnounce, Digest, HashOf, SimpleBlockData,
consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest},
injected::{SignedInjectedTransaction, SignedPromise},
injected::{CompactSignedPromise, SignedInjectedTransaction},
network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage},
};
use futures::{Stream, stream::FusedStream};
Expand Down Expand Up @@ -124,5 +124,5 @@ pub enum ConsensusEvent {
Warning(String),
/// Promises for [`ethexe_common::injected::InjectedTransaction`]s execution in some announce.
#[from]
Promises(Vec<SignedPromise>),
Promises(Vec<CompactSignedPromise>),
}
18 changes: 18 additions & 0 deletions ethexe/consensus/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use ethexe_common::{
AggregatedPublicKey, BatchCommitment, ChainCommitment, CodeCommitment, RewardsCommitment,
StateTransition, ValidatorsCommitment,
},
injected::{CompactPromiseHashes, CompactSignedPromise, Promise},
};
use gprimitives::{CodeId, H256, U256};
use gsigner::secp256k1::{Secp256k1SignerExt, Signer};
Expand Down Expand Up @@ -431,6 +432,23 @@ pub fn sort_transitions_by_value_to_receive(transitions: &mut [StateTransition])
});
}

/// A helper function that signs a bundle of promises to distribute signed promises in network.
pub fn sign_announce_promises(
signer: &Signer,
public_key: PublicKey,
promises: Vec<Promise>,
) -> Result<Vec<CompactSignedPromise>> {
promises
.into_iter()
.map(|promise| {
let promise_hashes = CompactPromiseHashes::from(&promise);
signer
.signed_message(public_key, promise_hashes, None)
.map_err(Into::into)
})
.collect::<Result<_, _>>()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
13 changes: 3 additions & 10 deletions ethexe/consensus/src/validator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ use anyhow::{Result, anyhow};
pub use core::BatchCommitter;
use derive_more::{Debug, From};
use ethexe_common::{
Address, ComputedAnnounce, SimpleBlockData, ToDigest,
Address, ComputedAnnounce, SimpleBlockData,
consensus::{VerifiedAnnounce, VerifiedValidationRequest},
db::OnChainStorageRO,
ecdsa::{PublicKey, SignedMessage},
ecdsa::PublicKey,
injected::SignedInjectedTransaction,
network::AnnouncesResponse,
};
Expand All @@ -69,7 +69,7 @@ use futures::{
stream::{FusedStream, FuturesUnordered},
};
use gprimitives::H256;
use gsigner::secp256k1::{Secp256k1SignerExt, Signer};
use gsigner::secp256k1::Signer;
use initial::Initial;
use std::{
collections::VecDeque,
Expand Down Expand Up @@ -547,11 +547,4 @@ impl ValidatorContext {
pub fn pending(&mut self, event: impl Into<PendingEvent>) {
self.pending_events.push_front(event.into());
}

pub fn sign_message<T: Sized + ToDigest>(&self, data: T) -> Result<SignedMessage<T>> {
Ok(self
.core
.signer
.signed_message(self.core.pub_key, data, None)?)
}
}
15 changes: 5 additions & 10 deletions ethexe/consensus/src/validator/producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ use super::{
use crate::{
ConsensusEvent,
announces::{self, DBAnnouncesExt},
utils::sign_announce_promises,
validator::DefaultProcessing,
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use derive_more::{Debug, Display};
use ethexe_common::{
Announce, ComputedAnnounce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO,
Expand Down Expand Up @@ -82,15 +83,9 @@ impl StateHandler for Producer {
if *expected == computed_data.announce_hash =>
{
if !computed_data.promises.is_empty() {
let signed_promises = computed_data
.promises
.into_iter()
.map(|promise| {
self.ctx
.sign_message(promise)
.context("producer: failed to sign promise")
})
.collect::<Result<_, _>>()?;
let (signer, public_key) = (&self.ctx.core.signer, self.ctx.core.pub_key);
let signed_promises =
sign_announce_promises(signer, public_key, computed_data.promises)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not important, for me is much more clear without sign_announce_promises - the logic is too specific and trivial to move it to separate function


self.ctx.output(ConsensusEvent::Promises(signed_promises));
}
Expand Down
1 change: 1 addition & 0 deletions ethexe/db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ethexe-common = { workspace = true, features = ["std"] }
ethexe-runtime-common = { workspace = true, features = ["std"] }
gear-core = { workspace = true, features = ["std"] }
gprimitives = { workspace = true, features = ["std"] }
gsigner = {workspace = true, features = ["secp256k1", "codec"]}

anyhow.workspace = true
dashmap.workspace = true
Expand Down
Loading