diff --git a/examples/demo-nft-module/README.md b/examples/demo-nft-module/README.md index fe3ff4409..4463eeb53 100644 --- a/examples/demo-nft-module/README.md +++ b/examples/demo-nft-module/README.md @@ -463,15 +463,11 @@ Here's an example of how to do it with `AppTemplate` from `sov-default-stf`: fn new(runtime_config: Self::RuntimeConfig) -> Self { let runtime = Runtime::new(); let storage = ZkStorage::with_config(runtime_config).expect("Failed to open zk storage"); - let tx_verifier = DemoAppTxVerifier::new(); - let tx_hooks = DemoAppTxHooks::new(); let app: AppTemplate< ZkDefaultContext, - DemoAppTxVerifier, Runtime, - DemoAppTxHooks, Vm, - > = AppTemplate::new(storage, runtime, tx_verifier, tx_hooks); + > = AppTemplate::new(storage, runtime); Self(app) } ``` diff --git a/examples/demo-stf/README.md b/examples/demo-stf/README.md index 3990dc948..0b8ac8310 100644 --- a/examples/demo-stf/README.md +++ b/examples/demo-stf/README.md @@ -28,97 +28,28 @@ transactions signed by particular private keys. To fill the gap, there's a syste bridges between the two layers of abstraction. The reason the `AppTemplate` is called a "template" is that it's generic. It allows you, the developer, to pass in -several parameters that specify its exact behavior. In order, these four generics are: +several parameters that specify its exact behavior. In order, these generics are: 1. `Context`: a per-transaction struct containing the message's sender. This also provides specs for storage access, so we use different `Context` implementations for Native and ZK execution. In ZK, we read values non-deterministically from hints and check them against a merkle tree, while in native mode we just read values straight from disk. -2. `TxVerifier`: a struct that verifies the signatures on transactions and deserializes them into messages -3. `Runtime`: a collection of modules which make up the rollup's public interface -4. `TxHooks`: a set of functions which are invoked at various points in the transaction lifecycle +2. `Runtime`: a collection of modules which make up the rollup's public interface -To implement your state transition function, you simply need to specify values for each of these four fields. + +To implement your state transition function, you simply need to specify values for each of these fields. So, a typical app definition looks like this: ```rust -pub type MyNativeStf = AppTemplate, MyRuntime, MyTxHooks>; -pub type MyZkStf = AppTemplate, MyRuntime, MyTxHooks>; +pub type MyNativeStf = AppTemplate>; +pub type MyZkStf = AppTemplate>; ``` Note that `DefaultContext` and `ZkDefaultContext` are exported by the `sov_modules_api` crate. In the remainder of this section, we'll walk you through implementing each of the remaining generics. -### Implementing a TxVerifier - -The `TxVerifier` interface is defined in `sov-default-stf`, and has one associated type and one required method: - -```rust -/// TxVerifier encapsulates Transaction verification. -pub trait TxVerifier { - type Transaction; - /// Runs stateless checks against a single RawTx. - fn verify_tx_stateless(&self, raw_tx: RawTx) -> anyhow::Result; -``` - -The semantics of the `TxVerifier` are pretty straightforward - it takes a `RawTx` (a slice of bytes) as an argument, and does -some work to transform it into some output `Transaction` type _without looking at the current rollup state_. This output transaction -type will eventually be fed to the `TxHooks` for _stateful_ verification. - -A typical workflow for a `TxVerifier` is to deserialize the message, and check its signature. As you can see by looking -at the implementation in `tx_verifier_impl.rs`, this is exactly what we do: - -```rust -impl TxVerifier for DemoAppTxVerifier { - // ... - fn verify_tx_stateless(&self, raw_tx: RawTx) -> anyhow::Result { - let mut data = Cursor::new(&raw_tx.data); - let tx = Transaction::::deserialize_reader(&mut data)?; - - // We check signature against runtime_msg and nonce. - let mut hasher = C::Hasher::new(); - hasher.update(&tx.runtime_msg); - hasher.update(&tx.nonce.to_le_bytes()); - - let msg_hash = hasher.finalize(); - - tx.signature.verify(&tx.pub_key, msg_hash)?; - Ok(tx) - } -} -``` - -#### Implementing TxHooks - -Once a transaction has passed stateless verification, it will get fed into the execution pipeline. In this pipeline there are four places -where you can inject custom "hooks" using your `TxHooks` implementation. - -1. At the beginning of the `apply_blob` function, before the blob is deserialized into a group of transactions. This is a good time to - apply per-batch validation logic like ensuring that the sequencer is properly bonded. -2. Immediately before each transaction is dispatched to the runtime. This is a good time to apply stateful transaction verification, like checking - the nonce. -3. Immediately after each transaction is executed. This is a good place to perform any post-execution operations, like incrementing the nonce. -4. At the end of the `apply_blob` function. This is a good place to reward sequencers. - -To use the `AppTemplate`, you need to provide a `TxHooks` implementation which specifies what needs to happen at each of these four -stages. - -Its common for modules that need access to these hooks to export a `Hooks` struct. If you're relying on an unfamiliar module, be sure to check -its documentation to make sure that you know about any hooks that it may rely on. Your `TxHooks` implementation will usually -just be a wrapper which invokes each of these modules hooks. In this demo, we only rely -on two modules which need access to the hooks - `sov-accounts` and `sequencer-registry`, so our `TxHooks` implementation only has two fields. - -```rust -pub struct DemoAppTxHooks { - accounts_hooks: accounts::hooks::Hooks, - sequencer_hooks: sov_sequencer_registry::hooks::Hooks, -} -``` - -You can view the full implementation in `tx_hooks_impl.rs` - -### Implementing Runtime: Pick Your Modules +## Implementing Runtime: Pick Your Modules The final piece of the puzzle is your app's runtime. A runtime is just a list of modules - really, that's it! To add a new module to your app, just add an additional field to the runtime. @@ -143,6 +74,72 @@ initialization code for each module which will get run at your rollup's genesis. allow your runtime to dispatch transactions and queries, and tell it which serialization scheme to use. We recommend borsh, since it's both fast and safe for hashing. +### Implementing Hooks for the Runtime: +The next step is to implement `Hooks` for `MyRuntime`. Hooks are abstractions that allows for the injection of custom logic into the transaction processing pipeline. + +There are two kind of hooks: + +`TxHooks`, which has the following methods: +1. `pre_dispatch_tx_hook`: Invoked immediately before each transaction is processed. This is a good time to apply stateful transaction verification, like checking the nonce. +2. `post_dispatch_tx_hook`: Invoked immediately after each transaction is executed. This is a good place to perform any post-execution operations, like incrementing the nonce. + +`ApplyBlobHooks`, which has the following methods: +1. `begin_blob_hook `Invoked at the beginning of the `apply_blob` function, before the blob is deserialized into a group of transactions. This is a good time to ensure that the sequencer is properly bonded. +2. `end_blob_hook` invoked at the end of the `apply_blob` function. This is a good place to reward sequencers. + +To use the `AppTemplate`, the runtime needs to provide implementation of these hooks which specifies what needs to happen at each of these four stages. + +In this demo, we only rely on two modules which need access to the hooks - `sov-accounts` and `sequencer-registry`. + +The `sov-accounts` module implements `TxHooks` because it needs to check and increment the sender nonce for every transaction. +The `sequencer-registry` implements `ApplyBlobHooks` since it is responsible for managing the sequencer bond. + +The implementation for `MyRuntime` is straightforward because we can leverage the existing hooks provided by `sov-accounts` and `sequencer-registry` and reuse them in our implementation. + +```Rust +impl TxHooks for Runtime { + type Context = C; + + fn pre_dispatch_tx_hook( + &self, + tx: Transaction, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<::Address> { + self.accounts.pre_dispatch_tx_hook(tx, working_set) + } + + fn post_dispatch_tx_hook( + &self, + tx: &Transaction, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + self.accounts.post_dispatch_tx_hook(tx, working_set) + } +} +``` + +```Rust +impl ApplyBlobHooks for Runtime { + type Context = C; + + fn lock_sequencer_bond( + &self, + sequencer: &[u8], + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + self.sequencer.lock_sequencer_bond(sequencer, working_set) + } + + fn reward_sequencer( + &self, + amount: u64, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + self.sequencer.reward_sequencer(amount, working_set) + } +} +``` + That's it - with those three structs implemented, you can plug them into your `AppTemplate` and get a complete State Transition Function! @@ -188,7 +185,7 @@ impl StateTransitionRunner for DemoAppRunner Self { let storage = ProverStorage::with_config(runtime_config.storage) .expect("Failed to open prover storage"); - let app = AppTemplate::new(storage, Runtime::new(), DemoAppTxVerifier::new(), DemoAppTxHooks::new()); + let app = AppTemplate::new(storage, Runtime::new()); Self(app) } // ... @@ -200,7 +197,7 @@ impl StateTransitionRunner for DemoAppRunner Self { let storage = ZkStorage::with_config(runtime_config).expect("Failed to open zk storage"); - let app = AppTemplate::new(storage, Runtime::new(), DemoAppTxVerifier::new(), DemoAppTxHooks::new()); + let app = AppTemplate::new(storage, Runtime::new()); Self(app) } // ... diff --git a/examples/demo-stf/src/app.rs b/examples/demo-stf/src/app.rs index 80a2b075c..69e12be68 100644 --- a/examples/demo-stf/src/app.rs +++ b/examples/demo-stf/src/app.rs @@ -1,8 +1,6 @@ #[cfg(feature = "native")] use crate::runner_config::Config; use crate::runtime::Runtime; -use crate::tx_hooks_impl::DemoAppTxHooks; -use crate::tx_verifier_impl::DemoAppTxVerifier; use sov_default_stf::AppTemplate; pub use sov_default_stf::Batch; use sov_default_stf::SequencerOutcome; @@ -43,7 +41,7 @@ use sov_modules_macros::expose_rpc; #[cfg(feature = "native")] pub type NativeAppRunner = DemoAppRunner; -pub type DemoApp = AppTemplate, Runtime, DemoAppTxHooks, Vm>; +pub type DemoApp = AppTemplate, Vm>; /// Batch receipt type used by the demo app. We export this type so that it's easily accessible to the full node. pub type DemoBatchReceipt = SequencerOutcome; @@ -60,9 +58,7 @@ impl StateTransitionRunner for DemoAppRunner StateTransitionRunner for DemoAppRunner Self { let runtime = Runtime::new(); let storage = ZkStorage::with_config(runtime_config).expect("Failed to open zk storage"); - let tx_verifier = DemoAppTxVerifier::new(); - let tx_hooks = DemoAppTxHooks::new(); - let app: AppTemplate< - ZkDefaultContext, - DemoAppTxVerifier, - Runtime, - DemoAppTxHooks, - Vm, - > = AppTemplate::new(storage, runtime, tx_verifier, tx_hooks); + let app: AppTemplate, Vm> = + AppTemplate::new(storage, runtime); Self(app) } diff --git a/examples/demo-stf/src/bank_cmd/main.rs b/examples/demo-stf/src/bank_cmd/main.rs index 5e3e1758a..32e578657 100644 --- a/examples/demo-stf/src/bank_cmd/main.rs +++ b/examples/demo-stf/src/bank_cmd/main.rs @@ -1,8 +1,9 @@ use anyhow::Context; use borsh::BorshSerialize; use clap::Parser; -use demo_stf::{runtime::Runtime, sign_tx, Transaction}; +use demo_stf::runtime::Runtime; use sov_default_stf::RawTx; +use sov_modules_api::transaction::Transaction; use sov_modules_api::{ default_context::DefaultContext, default_signature::private_key::DefaultPrivateKey, PublicKey, Spec, @@ -103,7 +104,7 @@ impl SerializedTx { let sender_address = sender_priv_key.pub_key().to_address(); let message = Self::serialize_call_message(call_data_path, &sender_address)?; - let sig = sign_tx(&sender_priv_key, &message, nonce); + let sig = Transaction::::sign(&sender_priv_key, &message, nonce); let tx = Transaction::::new(message, sender_priv_key.pub_key(), sig, nonce); Ok(SerializedTx { @@ -289,7 +290,7 @@ mod test { ) .inner; assert!( - matches!(apply_blob_outcome, SequencerOutcome::Rewarded,), + matches!(apply_blob_outcome, SequencerOutcome::Rewarded(0),), "Sequencer execution should have succeeded but failed " ); StateTransitionFunction::::end_slot(demo); diff --git a/examples/demo-stf/src/hooks_impl.rs b/examples/demo-stf/src/hooks_impl.rs new file mode 100644 index 000000000..03b1c3d50 --- /dev/null +++ b/examples/demo-stf/src/hooks_impl.rs @@ -0,0 +1,56 @@ +use crate::runtime::Runtime; +use sov_default_stf::SequencerOutcome; +use sov_modules_api::{ + hooks::{ApplyBlobHooks, TxHooks}, + transaction::Transaction, + Context, Spec, +}; +use sov_state::WorkingSet; + +impl TxHooks for Runtime { + type Context = C; + + fn pre_dispatch_tx_hook( + &self, + tx: Transaction, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<::Address> { + self.accounts.pre_dispatch_tx_hook(tx, working_set) + } + + fn post_dispatch_tx_hook( + &self, + tx: &Transaction, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + self.accounts.post_dispatch_tx_hook(tx, working_set) + } +} + +impl ApplyBlobHooks for Runtime { + type Context = C; + type BlobResult = SequencerOutcome; + + fn begin_blob_hook( + &self, + sequencer: &[u8], + raw_blob: &[u8], + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + self.sequencer + .begin_blob_hook(sequencer, raw_blob, working_set) + } + + fn end_blob_hook( + &self, + result: Self::BlobResult, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + let reward = match result { + SequencerOutcome::Rewarded(r) => r, + SequencerOutcome::Slashed(_) => 0, + SequencerOutcome::Ignored => 0, + }; + self.sequencer.end_blob_hook(reward, working_set) + } +} diff --git a/examples/demo-stf/src/lib.rs b/examples/demo-stf/src/lib.rs index c3dff96e9..3234f6859 100644 --- a/examples/demo-stf/src/lib.rs +++ b/examples/demo-stf/src/lib.rs @@ -1,29 +1,10 @@ pub mod app; #[cfg(feature = "native")] pub mod genesis_config; +pub mod hooks_impl; #[cfg(feature = "native")] pub mod runner_config; pub mod runtime; #[cfg(test)] pub mod tests; -pub mod tx_hooks_impl; -pub mod tx_verifier_impl; - -#[cfg(feature = "native")] -use sov_modules_api::{ - default_context::DefaultContext, - default_signature::{private_key::DefaultPrivateKey, DefaultSignature}, - Hasher, Spec, -}; - pub use sov_state::ArrayWitness; -pub use tx_verifier_impl::Transaction; - -#[cfg(feature = "native")] -pub fn sign_tx(priv_key: &DefaultPrivateKey, message: &[u8], nonce: u64) -> DefaultSignature { - let mut hasher = ::Hasher::new(); - hasher.update(message); - hasher.update(&nonce.to_le_bytes()); - let msg_hash = hasher.finalize(); - priv_key.sign(msg_hash) -} diff --git a/examples/demo-stf/src/tests/data_generation/election_data.rs b/examples/demo-stf/src/tests/data_generation/election_data.rs index e50f4cb70..d0b600451 100644 --- a/examples/demo-stf/src/tests/data_generation/election_data.rs +++ b/examples/demo-stf/src/tests/data_generation/election_data.rs @@ -1,5 +1,3 @@ -use crate::sign_tx; - use super::*; use sov_modules_api::default_context::DefaultContext; use std::rc::Rc; @@ -129,7 +127,7 @@ impl MessageGenerator for ElectionCallMessages { _is_last: bool, ) -> Transaction { let message = Runtime::::encode_election_call(message); - let sig = sign_tx(sender, &message, nonce); + let sig = Transaction::::sign(sender, &message, nonce); Transaction::::new(message, sender.pub_key(), sig, nonce) } } @@ -175,7 +173,7 @@ impl MessageGenerator for InvalidElectionCallMessages { _is_last: bool, ) -> Transaction { let message = Runtime::::encode_election_call(message); - let sig = sign_tx(sender, &message, nonce); + let sig = Transaction::::sign(sender, &message, nonce); Transaction::::new(message, sender.pub_key(), sig, nonce) } } @@ -210,9 +208,9 @@ impl MessageGenerator for BadSigElectionCallMessages { let message = Runtime::::encode_election_call(message); let sig = if is_last { let bad_msg = vec![0; 32]; - sign_tx(sender, &bad_msg, nonce + 1) + Transaction::::sign(sender, &bad_msg, nonce + 1) } else { - sign_tx(sender, &message, nonce) + Transaction::::sign(sender, &message, nonce) }; Transaction::::new(message, sender.pub_key(), sig, nonce) @@ -249,7 +247,7 @@ impl MessageGenerator for BadNonceElectionCallMessages { let nonce = if flag { nonce + 1 } else { nonce }; let message = Runtime::::encode_election_call(message); - let sig = sign_tx(sender, &message, nonce); + let sig = Transaction::::sign(sender, &message, nonce); Transaction::::new(message, sender.pub_key(), sig, nonce) } } @@ -287,7 +285,7 @@ impl MessageGenerator for BadSerializationElectionCallMessages { Runtime::::encode_election_call(message) }; - let sig = sign_tx(sender, &call_data, nonce); + let sig = Transaction::::sign(sender, &call_data, nonce); Transaction::::new(call_data, sender.pub_key(), sig, nonce) } } diff --git a/examples/demo-stf/src/tests/data_generation/mod.rs b/examples/demo-stf/src/tests/data_generation/mod.rs index 5aeb89ac6..00de8740f 100644 --- a/examples/demo-stf/src/tests/data_generation/mod.rs +++ b/examples/demo-stf/src/tests/data_generation/mod.rs @@ -1,9 +1,9 @@ use crate::runtime::Runtime; -use crate::tx_verifier_impl::Transaction; use borsh::BorshSerialize; use sov_default_stf::RawTx; use sov_modules_api::default_context::DefaultContext; use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_modules_api::transaction::Transaction; use sov_modules_api::PublicKey; use std::rc::Rc; diff --git a/examples/demo-stf/src/tests/data_generation/value_setter_data.rs b/examples/demo-stf/src/tests/data_generation/value_setter_data.rs index eb0802f1c..f1ff937fd 100644 --- a/examples/demo-stf/src/tests/data_generation/value_setter_data.rs +++ b/examples/demo-stf/src/tests/data_generation/value_setter_data.rs @@ -1,5 +1,3 @@ -use crate::sign_tx; - use super::*; use sov_modules_api::{ default_context::DefaultContext, default_signature::private_key::DefaultPrivateKey, @@ -49,7 +47,7 @@ impl MessageGenerator for ValueSetterMessages { _is_last: bool, ) -> Transaction { let message = Runtime::::encode_value_setter_call(message); - let sig = sign_tx(sender, &message, nonce); + let sig = Transaction::::sign(sender, &message, nonce); Transaction::::new(message, sender.pub_key(), sig, nonce) } } diff --git a/examples/demo-stf/src/tests/mod.rs b/examples/demo-stf/src/tests/mod.rs index 858318fcf..4cbde1648 100644 --- a/examples/demo-stf/src/tests/mod.rs +++ b/examples/demo-stf/src/tests/mod.rs @@ -14,8 +14,6 @@ use crate::{ DEMO_SEQ_PUB_KEY_STR, }, runtime::{GenesisConfig, Runtime}, - tx_hooks_impl::DemoAppTxHooks, - tx_verifier_impl::DemoAppTxVerifier, }; mod data_generation; @@ -36,9 +34,7 @@ pub fn create_new_demo( ) -> DemoApp { let runtime = Runtime::new(); let storage = ProverStorage::with_path(path).unwrap(); - let tx_hooks = DemoAppTxHooks::new(); - let tx_verifier = DemoAppTxVerifier::new(); - AppTemplate::new(storage, runtime, tx_verifier, tx_hooks) + AppTemplate::new(storage, runtime) } pub fn create_demo_config( diff --git a/examples/demo-stf/src/tests/stf_tests.rs b/examples/demo-stf/src/tests/stf_tests.rs index 52950a46d..ab04fa4f5 100644 --- a/examples/demo-stf/src/tests/stf_tests.rs +++ b/examples/demo-stf/src/tests/stf_tests.rs @@ -41,7 +41,7 @@ pub mod test { ); assert!( - matches!(apply_blob_outcome.inner, SequencerOutcome::Rewarded,), + matches!(apply_blob_outcome.inner, SequencerOutcome::Rewarded(0),), "Sequencer execution should have succeeded but failed " ); @@ -97,7 +97,7 @@ pub mod test { ); assert!( - matches!(apply_blob_outcome.inner, SequencerOutcome::Rewarded,), + matches!(apply_blob_outcome.inner, SequencerOutcome::Rewarded(0),), "Sequencer execution should have succeeded but failed " ); @@ -150,7 +150,7 @@ pub mod test { ) .inner; assert!( - matches!(apply_blob_outcome, SequencerOutcome::Rewarded,), + matches!(apply_blob_outcome, SequencerOutcome::Rewarded(0),), "Sequencer execution should have succeeded but failed " ); } diff --git a/examples/demo-stf/src/tests/tx_revert_tests.rs b/examples/demo-stf/src/tests/tx_revert_tests.rs index 21e8c9997..355fb8bc6 100644 --- a/examples/demo-stf/src/tests/tx_revert_tests.rs +++ b/examples/demo-stf/src/tests/tx_revert_tests.rs @@ -46,7 +46,7 @@ fn test_tx_revert() { ); assert!( - matches!(apply_blob_outcome.inner, SequencerOutcome::Rewarded,), + matches!(apply_blob_outcome.inner, SequencerOutcome::Rewarded(0),), "Unexpected outcome: Batch exeuction should have succeeded" ); diff --git a/examples/demo-stf/src/tx_hooks_impl.rs b/examples/demo-stf/src/tx_hooks_impl.rs deleted file mode 100644 index 7e93cc481..000000000 --- a/examples/demo-stf/src/tx_hooks_impl.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::tx_verifier_impl::Transaction; -use anyhow::Result; -use sov_default_stf::{TxHooks, VerifiedTx}; -use sov_modules_api::{Context, Spec}; -use sov_state::WorkingSet; - -pub struct AppVerifiedTx { - pub(crate) pub_key: C::PublicKey, - pub(crate) sender: C::Address, - pub(crate) runtime_msg: Vec, -} - -impl VerifiedTx for AppVerifiedTx { - type Address = C::Address; - - fn sender(&self) -> &Self::Address { - &self.sender - } - - fn runtime_message(&self) -> &[u8] { - &self.runtime_msg - } -} - -pub struct DemoAppTxHooks { - accounts_hooks: sov_accounts::hooks::Hooks, - sequencer_hooks: sov_sequencer_registry::hooks::Hooks, -} - -impl DemoAppTxHooks { - #[allow(dead_code)] - pub fn new() -> Self { - Self { - accounts_hooks: sov_accounts::hooks::Hooks::::new(), - sequencer_hooks: sov_sequencer_registry::hooks::Hooks::::new(), - } - } -} - -impl TxHooks for DemoAppTxHooks { - type Context = C; - type Transaction = Transaction; - type VerifiedTx = AppVerifiedTx; - - fn pre_dispatch_tx_hook( - &self, - tx: Transaction, - working_set: &mut WorkingSet<::Storage>, - ) -> anyhow::Result { - let addr = self.check_nonce_for_address(tx.nonce, tx.pub_key.clone(), working_set)?; - - Ok(AppVerifiedTx { - pub_key: tx.pub_key, - sender: addr, - runtime_msg: tx.runtime_msg, - }) - } - - fn post_dispatch_tx_hook( - &self, - tx: Self::VerifiedTx, - working_set: &mut WorkingSet<::Storage>, - ) { - self.accounts_hooks - .inc_nonce(&tx.pub_key, working_set) - // At this point we are sure, that the account corresponding to the tx.pub_key is in the db, - // therefore this panic should never happen, we add it for sanity check. - .unwrap_or_else(|e| panic!("Inconsistent nonce {e}")); - } - - fn enter_apply_blob( - &self, - sequencer: &[u8], - working_set: &mut WorkingSet<::Storage>, - ) -> Result<()> { - match self.sequencer_hooks.next_sequencer(working_set) { - Ok(next_sequencer) => { - if next_sequencer != sequencer { - anyhow::bail!("Invalid next sequencer.") - } - } - Err(_) => anyhow::bail!("Sequencer 0x={} not registered. ", hex::encode(sequencer)), - } - - self.sequencer_hooks.lock(working_set) - } - - fn exit_apply_blob( - &self, - amount: u64, - working_set: &mut WorkingSet<::Storage>, - ) -> Result<()> { - self.sequencer_hooks.reward(amount, working_set) - } -} - -impl DemoAppTxHooks { - fn check_nonce_for_address( - &self, - tx_nonce: u64, - tx_pub_key: C::PublicKey, - working_set: &mut WorkingSet, - ) -> anyhow::Result { - let acc = self - .accounts_hooks - .get_or_create_default_account(tx_pub_key, working_set)?; - - let acc_nonce = acc.nonce; - anyhow::ensure!( - acc_nonce == tx_nonce, - "Tx bad nonce, expected: {acc_nonce}, but found: {tx_nonce}", - ); - - Ok(acc.addr) - } -} diff --git a/examples/demo-stf/src/tx_verifier_impl.rs b/examples/demo-stf/src/tx_verifier_impl.rs deleted file mode 100644 index 74879262b..000000000 --- a/examples/demo-stf/src/tx_verifier_impl.rs +++ /dev/null @@ -1,58 +0,0 @@ -use borsh::BorshDeserialize; -use sov_default_stf::{RawTx, TxVerifier}; -use sov_modules_api::{Context, Hasher, Signature}; -use std::{io::Cursor, marker::PhantomData}; - -/// Transaction represents a deserialized RawTx. -#[derive(Debug, PartialEq, Eq, Clone, borsh::BorshDeserialize, borsh::BorshSerialize)] -pub struct Transaction { - pub(crate) signature: C::Signature, - pub(crate) pub_key: C::PublicKey, - pub(crate) runtime_msg: Vec, - pub(crate) nonce: u64, -} - -impl Transaction { - #[allow(dead_code)] - pub fn new(msg: Vec, pub_key: C::PublicKey, signature: C::Signature, nonce: u64) -> Self { - Self { - signature, - runtime_msg: msg, - pub_key, - nonce, - } - } -} - -pub struct DemoAppTxVerifier { - _phantom: PhantomData, -} - -impl DemoAppTxVerifier { - #[allow(dead_code)] - pub fn new() -> Self { - Self { - _phantom: PhantomData, - } - } -} - -impl TxVerifier for DemoAppTxVerifier { - type Transaction = Transaction; - - fn verify_tx_stateless(&self, raw_tx: RawTx) -> anyhow::Result { - let mut data = Cursor::new(&raw_tx.data); - let tx = Transaction::::deserialize_reader(&mut data)?; - - // We check signature against runtime_msg and nonce. - let mut hasher = C::Hasher::new(); - hasher.update(&tx.runtime_msg); - hasher.update(&tx.nonce.to_le_bytes()); - - let msg_hash = hasher.finalize(); - - tx.signature.verify(&tx.pub_key, msg_hash)?; - - Ok(tx) - } -} diff --git a/module-system/module-implementations/sov-accounts/README.md b/module-system/module-implementations/sov-accounts/README.md index 8f8f3ee2d..392c82aa4 100644 --- a/module-system/module-implementations/sov-accounts/README.md +++ b/module-system/module-implementations/sov-accounts/README.md @@ -6,9 +6,7 @@ Account is represented by an `Address` and a `Nonce`. ## Warning -The accounts module exports hooks which must be wired into your state transition function! Be sure to invoke `Hooks::get_or_create_default_account` -in the pre-dispatch transaction hook, and `Hooks::inc_nonce` in the post-dispatch transaction hook. For more details on the -proper usage of these hooks, see the [`demo-stf`](../../../examples/demo-stf/) documentation. +The accounts module implements `TxHooks` which must be wired into your state transition function! Be sure that your `Runtime` implementation for `TxHooks` delegates to the `sov-accounts.` ### The `sov-accounts` module offers the following functionality: diff --git a/module-system/module-implementations/sov-accounts/src/hooks.rs b/module-system/module-implementations/sov-accounts/src/hooks.rs index 574e8fee5..1d9a9ce19 100644 --- a/module-system/module-implementations/sov-accounts/src/hooks.rs +++ b/module-system/module-implementations/sov-accounts/src/hooks.rs @@ -1,40 +1,44 @@ -use crate::{Account, Accounts}; -use anyhow::Result; +use crate::Accounts; +use sov_modules_api::hooks::TxHooks; +use sov_modules_api::transaction::Transaction; use sov_modules_api::Context; -use sov_modules_api::ModuleInfo; -use sov_state::WorkingSet; -pub struct Hooks { - inner: Accounts, -} +use sov_modules_api::Spec; +use sov_state::WorkingSet; -impl Hooks { - pub fn new() -> Self { - Self { - inner: Accounts::new(), - } - } +impl TxHooks for Accounts { + type Context = C; - pub fn get_or_create_default_account( + fn pre_dispatch_tx_hook( &self, - pub_key: C::PublicKey, - working_set: &mut WorkingSet, - ) -> Result> { - match self.inner.accounts.get(&pub_key, working_set) { + tx: Transaction, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<::Address> { + let pub_key = tx.pub_key().clone(); + + let acc = match self.accounts.get(&pub_key, working_set) { Some(acc) => Ok(acc), - None => self.inner.create_default_account(pub_key, working_set), - } + None => self.create_default_account(pub_key, working_set), + }?; + + let tx_nonce = tx.nonce(); + let acc_nonce = acc.nonce; + anyhow::ensure!( + acc_nonce == tx_nonce, + "Tx bad nonce, expected: {acc_nonce}, but found: {tx_nonce}", + ); + + Ok(acc.addr) } - pub fn inc_nonce( + fn post_dispatch_tx_hook( &self, - pub_key: &C::PublicKey, - working_set: &mut WorkingSet, - ) -> Result<()> { - let mut account = self.inner.accounts.get_or_err(pub_key, working_set)?; + tx: &Transaction, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + let mut account = self.accounts.get_or_err(tx.pub_key(), working_set)?; account.nonce += 1; - self.inner.accounts.set(pub_key, account, working_set); - + self.accounts.set(tx.pub_key(), account, working_set); Ok(()) } } diff --git a/module-system/module-implementations/sov-accounts/src/tests.rs b/module-system/module-implementations/sov-accounts/src/tests.rs index 07f27fcbb..b8f7e930b 100644 --- a/module-system/module-implementations/sov-accounts/src/tests.rs +++ b/module-system/module-implementations/sov-accounts/src/tests.rs @@ -1,5 +1,5 @@ use crate::{ - call, hooks, + call, query::{self, Response}, AccountConfig, Accounts, }; @@ -8,7 +8,6 @@ use sov_modules_api::{ AddressBech32, Context, Module, ModuleInfo, PublicKey, Spec, }; use sov_state::{ProverStorage, WorkingSet}; - type C = DefaultContext; #[test] @@ -44,7 +43,6 @@ fn test_config_account() { fn test_update_account() { let native_working_set = &mut WorkingSet::new(ProverStorage::temporary()); let accounts = &mut Accounts::::new(); - let hooks = hooks::Hooks::::new(); let priv_key = DefaultPrivateKey::generate(); @@ -54,8 +52,8 @@ fn test_update_account() { // Test new account creation { - hooks - .get_or_create_default_account(sender.clone(), native_working_set) + accounts + .create_default_account(sender.clone(), native_working_set) .unwrap(); let query_response = accounts.get_account(sender.clone(), native_working_set); @@ -104,20 +102,20 @@ fn test_update_account() { fn test_update_account_fails() { let native_working_set = &mut WorkingSet::new(ProverStorage::temporary()); let accounts = &mut Accounts::::new(); - let hooks = hooks::Hooks::::new(); let sender_1 = DefaultPrivateKey::generate().pub_key(); let sender_context_1 = C::new(sender_1.to_address()); - hooks - .get_or_create_default_account(sender_1, native_working_set) + + accounts + .create_default_account(sender_1, native_working_set) .unwrap(); let priv_key = DefaultPrivateKey::generate(); let sender_2 = priv_key.pub_key(); let sig_2 = priv_key.sign(call::UPDATE_ACCOUNT_MSG); - hooks - .get_or_create_default_account(sender_2.clone(), native_working_set) + accounts + .create_default_account(sender_2.clone(), native_working_set) .unwrap(); // The new public key already exists and the call fails. @@ -134,14 +132,13 @@ fn test_update_account_fails() { fn test_get_acc_after_pub_key_update() { let native_working_set = &mut WorkingSet::new(ProverStorage::temporary()); let accounts = &mut Accounts::::new(); - let hooks = hooks::Hooks::::new(); let sender_1 = DefaultPrivateKey::generate().pub_key(); let sender_1_addr = sender_1.to_address::<::Address>(); let sender_context_1 = C::new(sender_1_addr.clone()); - hooks - .get_or_create_default_account(sender_1, native_working_set) + accounts + .create_default_account(sender_1, native_working_set) .unwrap(); let priv_key = DefaultPrivateKey::generate(); @@ -155,9 +152,11 @@ fn test_get_acc_after_pub_key_update() { ) .unwrap(); - let acc = hooks - .get_or_create_default_account(new_pub_key, native_working_set) + let acc = accounts + .accounts + .get(&new_pub_key, native_working_set) .unwrap(); + assert_eq!(acc.addr, sender_1_addr) } diff --git a/module-system/module-implementations/sov-sequencer-registry/README.md b/module-system/module-implementations/sov-sequencer-registry/README.md index 3af5a5d25..9ead6f07d 100644 --- a/module-system/module-implementations/sov-sequencer-registry/README.md +++ b/module-system/module-implementations/sov-sequencer-registry/README.md @@ -6,10 +6,4 @@ The `sov-sequencer-registry` module is responsible for sequencer registration, s Hooks: -The `sov-sequencer-registry` module does not expose any call messages, and rollup users cannot directly modify the state of the sequencer. Instead, the module provides hooks that can be inserted at various points in the logic of the rollup's state transition function. The module supports the following hooks: - -1. `lock`: Locks the sequencer bond. -1. `next_sequencer`: Returns the next sequencer address. Since currently only a centralized sequencer is supported, this hook always returns the same value, which is the registered sequencer's address. -1. `reward`: Unlocks the sequencer bond, possibly with an additional tip. - -If a sequencer misbehaves, the `reward` hook is never called, and the bond remains locked indefinitely. +The `sov-sequencer-registry` module does not expose any call messages, and rollup users cannot directly modify the state of the sequencer. Instead, the module implements `ApplyBlobHooks` trait. diff --git a/module-system/module-implementations/sov-sequencer-registry/src/hooks.rs b/module-system/module-implementations/sov-sequencer-registry/src/hooks.rs index 73eacccdc..aeedd706e 100644 --- a/module-system/module-implementations/sov-sequencer-registry/src/hooks.rs +++ b/module-system/module-implementations/sov-sequencer-registry/src/hooks.rs @@ -1,50 +1,48 @@ use crate::Sequencer; -use anyhow::Result; -use sov_modules_api::ModuleInfo; +use sov_modules_api::{hooks::ApplyBlobHooks, Context}; use sov_state::WorkingSet; -/// Sequencer hooks description: -/// At the beginning of SDK's `apply_batch` we need to lock some amount of sequencer funds which will be returned -/// along with additional reward upon successful batch execution. If the sequencer is malicious the funds are slashed (remain locked forever). -pub struct Hooks { - inner: Sequencer, -} - -impl Hooks { - pub fn new() -> Self { - Self { - inner: Sequencer::new(), +impl ApplyBlobHooks for Sequencer { + type Context = C; + type BlobResult = u64; + + fn begin_blob_hook( + &self, + sequencer_da: &[u8], + _raw_blob: &[u8], + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + let next_sequencer_da = self.seq_da_address.get_or_err(working_set); + + match next_sequencer_da { + Ok(next_sequencer_da) => { + if next_sequencer_da != sequencer_da { + anyhow::bail!("Invalid next sequencer.") + } + } + Err(_) => anyhow::bail!("Sequencer {:?} not registered. ", sequencer_da), } - } - /// Locks the sequencer coins. - pub fn lock(&self, working_set: &mut WorkingSet) -> Result<()> { - let sequencer = &self.inner.seq_rollup_address.get_or_err(working_set)?; - let locker = &self.inner.address; - let coins = self.inner.coins_to_lock.get_or_err(working_set)?; + let sequencer = &self.seq_rollup_address.get_or_err(working_set)?; + let locker = &self.address; + let coins = self.coins_to_lock.get_or_err(working_set)?; - self.inner - .bank + self.bank .transfer_from(sequencer, locker, coins, working_set)?; Ok(()) } - /// Currently this module supports only centralized sequencer, therefore this method always returns the same DA address. - pub fn next_sequencer(&self, working_set: &mut WorkingSet) -> Result> { - Ok(self.inner.seq_da_address.get_or_err(working_set)?) - } - - /// Unlocks the sequencer coins and awards additional coins (possibly based on transactions fees and used gas). - /// TODO: The `amount` field represents the additional award. As of now, we are not using it because we need to implement - /// the gas and TX fees mechanism first. See: issue number - pub fn reward(&self, _amount: u64, working_set: &mut WorkingSet) -> Result<()> { - let sequencer = &self.inner.seq_rollup_address.get_or_err(working_set)?; - let locker = &self.inner.address; - let coins = self.inner.coins_to_lock.get_or_err(working_set)?; + fn end_blob_hook( + &self, + _result: Self::BlobResult, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()> { + let sequencer = &self.seq_rollup_address.get_or_err(working_set)?; + let locker = &self.address; + let coins = self.coins_to_lock.get_or_err(working_set)?; - self.inner - .bank + self.bank .transfer_from(locker, sequencer, coins, working_set)?; Ok(()) diff --git a/module-system/module-implementations/sov-sequencer-registry/src/tests.rs b/module-system/module-implementations/sov-sequencer-registry/src/tests.rs index 23b588040..0b722cb84 100644 --- a/module-system/module-implementations/sov-sequencer-registry/src/tests.rs +++ b/module-system/module-implementations/sov-sequencer-registry/src/tests.rs @@ -1,9 +1,9 @@ use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::hooks::ApplyBlobHooks; use sov_modules_api::Hasher; use sov_modules_api::{Address, Module, ModuleInfo, Spec}; use sov_state::{ProverStorage, WorkingSet}; -use crate::hooks::Hooks; use crate::query; use crate::{Sequencer, SequencerConfig}; @@ -105,13 +105,6 @@ fn test_sequencer() { let working_set = &mut WorkingSet::new(ProverStorage::temporary()); test_sequencer.geneses(working_set); - let hooks = Hooks::::new(); - - assert_eq!( - SEQUENCER_DA_ADDRESS.to_vec(), - hooks.next_sequencer(working_set).unwrap() - ); - { let resp = test_sequencer.query_balance_via_bank(working_set); assert_eq!(INITIAL_BALANCE, resp.amount.unwrap()); @@ -122,7 +115,10 @@ fn test_sequencer() { // Lock { - hooks.lock(working_set).unwrap(); + test_sequencer + .sequencer + .begin_blob_hook(&SEQUENCER_DA_ADDRESS, &[], working_set) + .unwrap(); let resp = test_sequencer.query_balance_via_bank(working_set); assert_eq!(INITIAL_BALANCE - LOCKED_AMOUNT, resp.amount.unwrap()); @@ -133,7 +129,10 @@ fn test_sequencer() { // Reward { - hooks.reward(0, working_set).unwrap(); + test_sequencer + .sequencer + .end_blob_hook(0, working_set) + .unwrap(); let resp = test_sequencer.query_balance_via_bank(working_set); assert_eq!(INITIAL_BALANCE, resp.amount.unwrap()); diff --git a/module-system/sov-default-stf/README.md b/module-system/sov-default-stf/README.md index dce975d22..b1084fff9 100644 --- a/module-system/sov-default-stf/README.md +++ b/module-system/sov-default-stf/README.md @@ -5,23 +5,22 @@ This crate contains an implementation of a `StateTransitionFunction` called `AppTemplate` that is specifically designed to work with the Module System. The `AppTemplate` relies on a set of traits that, when combined, define the logic for transitioning the rollup state. ```rust -pub struct AppTemplate { +pub struct AppTemplate { pub current_storage: C::Storage, pub runtime: RT, - tx_verifier: V, - tx_hooks: H, working_set: Option>, phantom_vm: PhantomData, } -impl AppTemplate +impl AppTemplate where - RT: DispatchCall + Genesis, - V: TxVerifier, - H: TxHooks, + RT: DispatchCall + + Genesis + + TxHooks + + ApplyBlobHooks, { - pub fn new(storage: C::Storage, runtime: RT, tx_verifier: V, tx_hooks: H) -> Self { + pub fn new(storage: C::Storage, runtime: RT) -> Self { ... } ... @@ -30,8 +29,7 @@ where 1. The `DispatchCall` trait is responsible for decoding serialized messages and forwarding them to the appropriate module. 1. The `Genesis` trait handles the initialization process of the rollup. It sets up the initial state upon the rollup deployment. -1. The `TxVerifier` trait is responsible for validating transactions within the rollup. It ensures that incoming transactions meet the necessary criteria and are valid for execution. -1. The `TxHooks` trait allows for the injection of custom logic into the transaction processing pipeline. It provides a mechanism to execute additional actions or perform specific operations during the transaction processing phase. +1. The `TxHooks` & `ApplyBlobHooks` traits that allow for the injection of custom logic into the transaction processing pipeline. They provide a mechanism to execute additional actions or perform specific operations during the transaction processing phase. ### `Runtime` @@ -51,4 +49,4 @@ pub struct Runtime { The `Runtime` struct acts as the entry point where all the rollup modules are assembled together. The `#[derive]` macro generates the necessary implementations for the `Genesis and DispatchCall` traits from the `sov-module-api` crate. -To obtain an instance of the `StateTransitionFunction`, you can pass a`Runtime`, along with implementations of the `TxVerifier` and `TxHooks` traits, to the `AppTemplate::new(..)` method. This ensures that the implementation of the `StateTransitionFunction` is straightforward and does not require manual integration or complex setup steps. +To obtain an instance of the `StateTransitionFunction`, you can pass a`Runtime`, to the `AppTemplate::new(..)` method. This ensures that the implementation of the `StateTransitionFunction` is straightforward and does not require manual integration or complex setup steps. diff --git a/module-system/sov-default-stf/src/lib.rs b/module-system/sov-default-stf/src/lib.rs index be6fc78e2..4da0a5a78 100644 --- a/module-system/sov-default-stf/src/lib.rs +++ b/module-system/sov-default-stf/src/lib.rs @@ -1,46 +1,45 @@ mod batch; -mod tx_hooks; + mod tx_verifier; use std::marker::PhantomData; pub use batch::Batch; use borsh::BorshDeserialize; +use sov_modules_api::hooks::ApplyBlobHooks; +use sov_modules_api::hooks::TxHooks; use sov_rollup_interface::stf::BatchReceipt; use sov_rollup_interface::stf::TransactionReceipt; use sov_rollup_interface::zk::traits::Zkvm; use sov_rollup_interface::Buf; -use tracing::{debug, error}; -pub use tx_hooks::TxHooks; -pub use tx_hooks::VerifiedTx; -pub use tx_verifier::{RawTx, TxVerifier}; +use tracing::debug; +use tracing::error; +use tx_verifier::verify_txs_stateless; +pub use tx_verifier::RawTx; use sov_modules_api::{Context, DispatchCall, Genesis, Hasher, Spec}; use sov_rollup_interface::{stf::StateTransitionFunction, traits::BatchTrait}; use sov_state::{Storage, WorkingSet}; use std::io::Read; -pub struct AppTemplate { +pub struct AppTemplate { pub current_storage: C::Storage, pub runtime: RT, - tx_verifier: V, - tx_hooks: H, working_set: Option>, phantom_vm: PhantomData, } -impl AppTemplate +impl AppTemplate where - RT: DispatchCall + Genesis, - V: TxVerifier, - H: TxHooks::Transaction>, + RT: DispatchCall + + Genesis + + TxHooks + + ApplyBlobHooks, { - pub fn new(storage: C::Storage, runtime: RT, tx_verifier: V, tx_hooks: H) -> Self { + pub fn new(storage: C::Storage, runtime: RT) -> Self { Self { runtime, current_storage: storage, - tx_verifier, - tx_hooks, working_set: None, phantom_vm: PhantomData, } @@ -64,9 +63,9 @@ where let batch_data_and_hash = BatchDataAndHash::new::(batch); - if let Err(e) = self - .tx_hooks - .enter_apply_blob(sequencer, &mut batch_workspace) + if let Err(e) = + self.runtime + .begin_blob_hook(sequencer, &batch_data_and_hash.data, &mut batch_workspace) { error!( "Error: The transaction was rejected by the 'enter_apply_blob' hook. Skipping batch without slashing the sequencer: {}", @@ -106,10 +105,7 @@ where debug!("Deserialized batch with {} txs", batch.txs.len()); // Run the stateless verification, since it is stateless we don't commit. - let txs = match self - .tx_verifier - .verify_txs_stateless::(batch.take_transactions()) - { + let txs = match verify_txs_stateless(batch.take_transactions()) { Ok(txs) => txs, Err(e) => { // Revert on error @@ -131,7 +127,10 @@ where batch_workspace = batch_workspace.to_revertable(); // Run the stateful verification, possibly modifies the state. - let verified_tx = match self.tx_hooks.pre_dispatch_tx_hook(tx, &mut batch_workspace) { + let sender_address = match self + .runtime + .pre_dispatch_tx_hook(tx.clone(), &mut batch_workspace) + { Ok(verified_tx) => verified_tx, Err(e) => { // Don't revert any state changes made by the pre_dispatch_hook even if it rejects @@ -149,13 +148,14 @@ where } }; - match RT::decode_call(verified_tx.runtime_message()) { + match RT::decode_call(tx.runtime_msg()) { Ok(msg) => { - let ctx = C::new(verified_tx.sender().clone()); + let ctx = C::new(sender_address.clone()); let tx_result = self.runtime.dispatch_call(msg, &mut batch_workspace, &ctx); - self.tx_hooks - .post_dispatch_tx_hook(verified_tx, &mut batch_workspace); + self.runtime + .post_dispatch_tx_hook(&tx, &mut batch_workspace) + .expect("Impossible happened: error in post_dispatch_tx_hook"); let tx_effect = match tx_result { Ok(_) => TxEffect::Successful, @@ -196,15 +196,17 @@ where } // TODO: calculate the amount based of gas and fees - self.tx_hooks - .exit_apply_blob(0, &mut batch_workspace) + + let batch_receipt_contents = SequencerOutcome::Rewarded(0); + self.runtime + .end_blob_hook(batch_receipt_contents, &mut batch_workspace) .expect("Impossible happened: error in exit_apply_batch"); self.working_set = Some(batch_workspace); BatchReceipt { batch_hash: batch_data_and_hash.hash, tx_receipts, - inner: SequencerOutcome::Rewarded, + inner: batch_receipt_contents, } } } @@ -237,7 +239,7 @@ pub enum TxEffect { #[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum SequencerOutcome { - Rewarded, + Rewarded(u64), Slashed(SlashingReason), Ignored, } @@ -249,11 +251,12 @@ pub enum SlashingReason { InvalidTransactionEncoding, } -impl StateTransitionFunction for AppTemplate +impl StateTransitionFunction for AppTemplate where - RT: DispatchCall + Genesis, - V: TxVerifier, - H: TxHooks::Transaction>, + RT: DispatchCall + + Genesis + + TxHooks + + ApplyBlobHooks, { type StateRoot = jmt::RootHash; diff --git a/module-system/sov-default-stf/src/tx_hooks.rs b/module-system/sov-default-stf/src/tx_hooks.rs deleted file mode 100644 index 50f40a64b..000000000 --- a/module-system/sov-default-stf/src/tx_hooks.rs +++ /dev/null @@ -1,45 +0,0 @@ -use anyhow::Result; -use sov_modules_api::{Context, Spec}; -use sov_state::WorkingSet; - -/// Represents a transaction after verification. -pub trait VerifiedTx { - type Address; - fn sender(&self) -> &Self::Address; - fn runtime_message(&self) -> &[u8]; -} - -/// TxHooks allows injecting custom logic into a transaction processing pipeline. -pub trait TxHooks { - type Context: Context; - type Transaction; - type VerifiedTx: VerifiedTx
::Address>; - - /// runs just before a transaction is dispatched to an appropriate module. - fn pre_dispatch_tx_hook( - &self, - tx: Self::Transaction, - working_set: &mut WorkingSet<::Storage>, - ) -> anyhow::Result; - - /// runs after the tx is dispatched to an appropriate module. - fn post_dispatch_tx_hook( - &self, - tx: Self::VerifiedTx, - working_set: &mut WorkingSet<::Storage>, - ); - - /// runs at the beginning of apply_blob. - fn enter_apply_blob( - &self, - sequencer: &[u8], - working_set: &mut WorkingSet<::Storage>, - ) -> Result<()>; - - /// runs at the end of apply_batch. - fn exit_apply_blob( - &self, - amount: u64, - working_set: &mut WorkingSet<::Storage>, - ) -> Result<()>; -} diff --git a/module-system/sov-default-stf/src/tx_verifier.rs b/module-system/sov-default-stf/src/tx_verifier.rs index 456db0642..aa4151549 100644 --- a/module-system/sov-default-stf/src/tx_verifier.rs +++ b/module-system/sov-default-stf/src/tx_verifier.rs @@ -1,8 +1,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use sov_modules_api::{Context, Hasher, Spec}; +use sov_modules_api::{transaction::Transaction, Context, Hasher, Spec}; +use std::io::Cursor; use tracing::debug; - /// RawTx represents a serialized rollup transaction received from the DA. #[derive(Debug, PartialEq, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct RawTx { @@ -17,28 +17,17 @@ impl RawTx { type RawTxHash = [u8; 32]; -/// TxVerifier encapsulates Transaction verification. -pub trait TxVerifier { - type Transaction; - - /// Runs stateless checks against a single RawTx. - fn verify_tx_stateless(&self, raw_tx: RawTx) -> anyhow::Result; - - /// Runs stateless checks against RawTxs. - /// Returns verified transaction and hash of the RawTx. - fn verify_txs_stateless( - &self, - raw_txs: Vec, - ) -> anyhow::Result> { - debug!("Verifying {} transactions", raw_txs.len()); - let mut txs = Vec::with_capacity(raw_txs.len()); - for raw_tx in raw_txs { - let raw_tx_hash = raw_tx.hash::(); - let tx = self.verify_tx_stateless(raw_tx)?; - - txs.push((tx, raw_tx_hash)); - } - - Ok(txs) +pub fn verify_txs_stateless( + raw_txs: Vec, +) -> anyhow::Result, RawTxHash)>> { + let mut txs = Vec::with_capacity(raw_txs.len()); + debug!("Verifying {} transactions", raw_txs.len()); + for raw_tx in raw_txs { + let raw_tx_hash = raw_tx.hash::(); + let mut data = Cursor::new(&raw_tx.data); + let tx = Transaction::::deserialize_reader(&mut data)?; + tx.verify()?; + txs.push((tx, raw_tx_hash)); } + Ok(txs) } diff --git a/module-system/sov-modules-api/src/hooks.rs b/module-system/sov-modules-api/src/hooks.rs new file mode 100644 index 000000000..47a879fbf --- /dev/null +++ b/module-system/sov-modules-api/src/hooks.rs @@ -0,0 +1,44 @@ +use crate::{transaction::Transaction, Context, Spec}; +use sov_state::WorkingSet; + +/// Hooks that execute within the `StateTransitionFunction::apply_blob` function for each processed transaction. +pub trait TxHooks { + type Context: Context; + + /// Runs just before a transaction is dispatched to an appropriate module. + fn pre_dispatch_tx_hook( + &self, + tx: Transaction, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<::Address>; + + /// Runs after the tx is dispatched to an appropriate module. + fn post_dispatch_tx_hook( + &self, + tx: &Transaction, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()>; +} + +/// Hooks related to the Sequencer functionality. +/// In essence, the sequencer locks a bond at the beginning of the `StateTransitionFunction::apply_blob`, +/// and is rewarded once a blob of transactions is processed. +pub trait ApplyBlobHooks { + type Context: Context; + type BlobResult; + + /// Runs at the beginning of apply_blob, locks the sequencer bond. + fn begin_blob_hook( + &self, + sequencer: &[u8], + raw_blob: &[u8], + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()>; + + /// Executes at the end of apply_blob and rewards the sequencer. This method is not invoked if the sequencer has been slashed. + fn end_blob_hook( + &self, + result: Self::BlobResult, + working_set: &mut WorkingSet<::Storage>, + ) -> anyhow::Result<()>; +} diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index 976b84030..6adfc8f97 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -1,31 +1,28 @@ #![feature(associated_type_defaults)] +mod bech32; pub mod default_context; pub mod default_signature; -mod serde_address; - -mod bech32; mod dispatch; mod encode; mod error; +pub mod hooks; mod prefix; mod response; - +mod serde_address; #[cfg(test)] mod tests; +pub mod transaction; pub use crate::bech32::AddressBech32; +use borsh::{BorshDeserialize, BorshSerialize}; +use core::fmt::{self, Debug, Display}; pub use dispatch::{DispatchCall, Genesis}; pub use error::Error; pub use jmt::SimpleHasher as Hasher; - pub use prefix::Prefix; pub use response::CallResponse; - pub use sov_rollup_interface::traits::AddressTrait; use sov_state::{Storage, Witness, WorkingSet}; - -use borsh::{BorshDeserialize, BorshSerialize}; -use core::fmt::{self, Debug, Display}; use thiserror::Error; impl AsRef<[u8]> for Address { diff --git a/module-system/sov-modules-api/src/transaction.rs b/module-system/sov-modules-api/src/transaction.rs new file mode 100644 index 000000000..7fa09f5c0 --- /dev/null +++ b/module-system/sov-modules-api/src/transaction.rs @@ -0,0 +1,71 @@ +#[cfg(feature = "native")] +use crate::default_context::DefaultContext; +#[cfg(feature = "native")] +use crate::default_signature::private_key::DefaultPrivateKey; +#[cfg(feature = "native")] +use crate::default_signature::DefaultSignature; +use crate::Context; +use crate::Hasher; +use crate::Signature; +#[cfg(feature = "native")] +use crate::Spec; + +/// A Transaction object that is compatible with the module-system/sov-default-stf. +#[derive(Debug, PartialEq, Eq, Clone, borsh::BorshDeserialize, borsh::BorshSerialize)] +pub struct Transaction { + signature: C::Signature, + pub_key: C::PublicKey, + runtime_msg: Vec, + nonce: u64, +} + +impl Transaction { + pub fn new(msg: Vec, pub_key: C::PublicKey, signature: C::Signature, nonce: u64) -> Self { + Self { + signature, + runtime_msg: msg, + pub_key, + nonce, + } + } + + pub fn signature(&self) -> &C::Signature { + &self.signature + } + + pub fn pub_key(&self) -> &C::PublicKey { + &self.pub_key + } + + pub fn runtime_msg(&self) -> &[u8] { + &self.runtime_msg + } + + pub fn nonce(&self) -> u64 { + self.nonce + } + + /// Check whether the transaction has been signed correctly. + pub fn verify(&self) -> anyhow::Result<()> { + // We check signature against runtime_msg and nonce. + let mut hasher = C::Hasher::new(); + hasher.update(self.runtime_msg()); + hasher.update(&self.nonce().to_le_bytes()); + let msg_hash = hasher.finalize(); + self.signature().verify(self.pub_key(), msg_hash)?; + + Ok(()) + } +} + +#[cfg(feature = "native")] +impl Transaction { + /// Sign the transaction. + pub fn sign(priv_key: &DefaultPrivateKey, message: &[u8], nonce: u64) -> DefaultSignature { + let mut hasher = ::Hasher::new(); + hasher.update(message); + hasher.update(&nonce.to_le_bytes()); + let msg_hash = hasher.finalize(); + priv_key.sign(msg_hash) + } +} diff --git a/module-system/sov-modules-macros/tests/dispatch/derive_genesis.rs b/module-system/sov-modules-macros/tests/dispatch/derive_genesis.rs index b083bdf1a..b10927a0e 100644 --- a/module-system/sov-modules-macros/tests/dispatch/derive_genesis.rs +++ b/module-system/sov-modules-macros/tests/dispatch/derive_genesis.rs @@ -2,7 +2,7 @@ mod modules; use modules::{first_test_module, second_test_module}; use sov_modules_api::default_context::DefaultContext; -use sov_modules_api::{Context, Module, ModuleInfo}; +use sov_modules_api::{Context, ModuleInfo}; use sov_modules_macros::{DispatchCall, Genesis, MessageCodec}; use sov_state::ProverStorage;