From d2dfc1c4e3debd6db523e08b4545c94fb30cf11f Mon Sep 17 00:00:00 2001 From: theochap <80177219+theochap@users.noreply.github.com> Date: Fri, 18 Aug 2023 19:05:08 +0200 Subject: [PATCH] Optimistic (#650) * Optimistic * Attester incentives module * Reverting typo * Tests attester-incentives * Adding test helper and lint * Lint * Adding tests * Finishing positive test * Refactor tests * Refactor tests * Adding negative test * Valid challenge * Invalid challenge * Adding unbonding tests * Lint fix * Lint * Make lint * Make lint * PR comments + fix tests * Lint * Fixing comments * Refactoring + finishing adding tests * Fix lint * Fix lint * Fixing PR comments * Fixing important comment * Fix nits * Fixing multi-attestation * Fixing nits * Fixing nits * Fixing lints * Fixing all comments PR --- Cargo.lock | 20 + Cargo.toml | 1 + adapters/risc0/src/guest.rs | 12 - adapters/risc0/src/host.rs | 23 - .../src/chain_state/tests.rs | 6 +- .../sov-attester-incentives/Cargo.toml | 37 + .../sov-attester-incentives/README.md | 8 + .../sov-attester-incentives/src/call.rs | 777 ++++++++++++++++++ .../sov-attester-incentives/src/genesis.rs | 64 ++ .../sov-attester-incentives/src/lib.rs | 209 +++++ .../sov-attester-incentives/src/query.rs | 79 ++ .../src/tests/attestation_proccessing.rs | 282 +++++++ .../src/tests/challenger.rs | 344 ++++++++ .../src/tests/helpers.rs | 182 ++++ .../src/tests/invariant.rs | 218 +++++ .../sov-attester-incentives/src/tests/mod.rs | 6 + .../src/tests/unbonding.rs | 165 ++++ .../sov-bank/src/call.rs | 22 +- .../sov-bank/src/lib.rs | 2 +- .../sov-bank/src/query.rs | 16 - .../sov-chain-state/src/call.rs | 11 +- .../sov-chain-state/src/genesis.rs | 3 + .../sov-chain-state/src/lib.rs | 19 +- .../sov-chain-state/src/query.rs | 7 +- .../sov-chain-state/src/tests.rs | 6 +- .../sov-prover-incentives/src/lib.rs | 25 +- module-system/sov-modules-api/src/response.rs | 2 +- module-system/sov-state/src/map.rs | 2 +- module-system/sov-state/src/prover_storage.rs | 16 +- module-system/sov-state/src/storage.rs | 39 +- rollup-interface/Cargo.toml | 4 + .../src/state_machine/mocks/mod.rs | 2 +- .../state_machine/mocks/validity_condition.rs | 23 +- .../src/state_machine/mocks/zk_vm.rs | 8 - rollup-interface/src/state_machine/mod.rs | 2 + .../src/state_machine/optimistic.rs | 46 ++ rollup-interface/src/state_machine/zk/mod.rs | 40 +- 37 files changed, 2597 insertions(+), 131 deletions(-) create mode 100644 module-system/module-implementations/sov-attester-incentives/Cargo.toml create mode 100644 module-system/module-implementations/sov-attester-incentives/README.md create mode 100644 module-system/module-implementations/sov-attester-incentives/src/call.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/genesis.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/lib.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/query.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/tests/attestation_proccessing.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/tests/challenger.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/tests/helpers.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/tests/invariant.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/tests/mod.rs create mode 100644 module-system/module-implementations/sov-attester-incentives/src/tests/unbonding.rs create mode 100644 rollup-interface/src/state_machine/optimistic.rs diff --git a/Cargo.lock b/Cargo.lock index 913eaae4dc..8ef2c39c64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6464,6 +6464,25 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sov-attester-incentives" +version = "0.1.0" +dependencies = [ + "anyhow", + "borsh", + "jmt", + "serde", + "serde_json", + "sov-bank", + "sov-chain-state", + "sov-modules-api", + "sov-modules-macros", + "sov-rollup-interface", + "sov-state", + "tempfile", + "thiserror", +] + [[package]] name = "sov-bank" version = "0.1.0" @@ -6800,6 +6819,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bincode", "borsh", "bytes", "digest 0.10.7", diff --git a/Cargo.toml b/Cargo.toml index e274b95ae4..8232c97f14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "module-system/module-implementations/sov-blob-storage", "module-system/module-implementations/sov-evm", "module-system/module-implementations/sov-prover-incentives", + "module-system/module-implementations/sov-attester-incentives", "module-system/module-implementations/sov-sequencer-registry", "module-system/module-implementations/module-template", "module-system/module-implementations/examples/sov-value-setter", diff --git a/adapters/risc0/src/guest.rs b/adapters/risc0/src/guest.rs index 873aa882e8..acc7e3d8f6 100644 --- a/adapters/risc0/src/guest.rs +++ b/adapters/risc0/src/guest.rs @@ -1,7 +1,6 @@ #[cfg(target_os = "zkvm")] use risc0_zkvm::guest::env; use sov_rollup_interface::zk::{Zkvm, ZkvmGuest}; -use sov_rollup_interface::AddressTrait; use crate::Risc0MethodId; @@ -41,15 +40,4 @@ impl Zkvm for Risc0Guest { // Implement this method once risc0 supports recursion: issue #633 todo!("Implement once risc0 supports recursion: https://github.com/Sovereign-Labs/sovereign-sdk/issues/633") } - - fn verify_and_extract_output< - C: sov_rollup_interface::zk::ValidityCondition, - Add: AddressTrait, - >( - _serialized_proof: &[u8], - _code_commitment: &Self::CodeCommitment, - ) -> Result, Self::Error> { - // Implement this method once risc0 supports recursion: issue https://github.com/Sovereign-Labs/sovereign-sdk/issues/633 - todo!("Implement once risc0 supports recursion: https://github.com/Sovereign-Labs/sovereign-sdk/issues/633") - } } diff --git a/adapters/risc0/src/host.rs b/adapters/risc0/src/host.rs index 9969dbd1ca..0829435bc1 100644 --- a/adapters/risc0/src/host.rs +++ b/adapters/risc0/src/host.rs @@ -6,7 +6,6 @@ use risc0_zkvm::{ Executor, ExecutorEnvBuilder, LocalExecutor, SegmentReceipt, Session, SessionReceipt, }; use sov_rollup_interface::zk::{Zkvm, ZkvmHost}; -use sov_rollup_interface::AddressTrait; #[cfg(feature = "bench")] use zk_cycle_utils::{cycle_count_callback, get_syscall_name, get_syscall_name_cycles}; @@ -79,16 +78,6 @@ impl<'prover> Zkvm for Risc0Host<'prover> { ) -> Result<&'a [u8], Self::Error> { verify_from_slice(serialized_proof, code_commitment) } - - fn verify_and_extract_output< - C: sov_rollup_interface::zk::ValidityCondition, - Add: AddressTrait, - >( - _serialized_proof: &[u8], - _code_commitment: &Self::CodeCommitment, - ) -> Result, Self::Error> { - todo!("Implement once risc0 supports recursion, issue https://github.com/Sovereign-Labs/sovereign-sdk/issues/633") - } } pub struct Risc0Verifier; @@ -104,18 +93,6 @@ impl Zkvm for Risc0Verifier { ) -> Result<&'a [u8], Self::Error> { verify_from_slice(serialized_proof, code_commitment) } - - fn verify_and_extract_output< - C: sov_rollup_interface::zk::ValidityCondition, - Add: AddressTrait, - >( - _serialized_proof: &[u8], - _code_commitment: &Self::CodeCommitment, - ) -> Result, Self::Error> { - // Method to implement: not clear how to deserialize the proof output. - // Issue https://github.com/Sovereign-Labs/sovereign-sdk/issues/621 - todo!("not clear how to deserialize the proof output. Issue https://github.com/Sovereign-Labs/sovereign-sdk/issues/621") - } } fn verify_from_slice<'a>( diff --git a/module-system/module-implementations/integration-tests/src/chain_state/tests.rs b/module-system/module-implementations/integration-tests/src/chain_state/tests.rs index e4dea386fb..99a0925e60 100644 --- a/module-system/module-implementations/integration-tests/src/chain_state/tests.rs +++ b/module-system/module-implementations/integration-tests/src/chain_state/tests.rs @@ -60,7 +60,7 @@ fn test_simple_value_setter_with_chain_state() { let mut working_set = get_working_set(&app_template); // Check the slot height before apply slot - let new_height_storage: u64 = app_template + let new_height_storage = app_template .runtime .chain_state .get_slot_height(&mut working_set); @@ -92,7 +92,7 @@ fn test_simple_value_setter_with_chain_state() { assert_eq!(stored_root, init_root_hash.0, "Root hashes don't match"); // Check the slot height - let new_height_storage: u64 = chain_state_ref.get_slot_height(&mut working_set); + let new_height_storage = chain_state_ref.get_slot_height(&mut working_set); assert_eq!(new_height_storage, 1, "The new height did not update"); @@ -139,7 +139,7 @@ fn test_simple_value_setter_with_chain_state() { assert_eq!(stored_root, init_root_hash.0, "Root hashes don't match"); // Check the slot height - let new_height_storage: u64 = chain_state_ref.get_slot_height(&mut working_set); + let new_height_storage = chain_state_ref.get_slot_height(&mut working_set); assert_eq!(new_height_storage, 2, "The new height did not update"); // Check the tx in progress diff --git a/module-system/module-implementations/sov-attester-incentives/Cargo.toml b/module-system/module-implementations/sov-attester-incentives/Cargo.toml new file mode 100644 index 0000000000..5ee52f2eaf --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "sov-attester-incentives" +description = "A Sovereign SDK module for incentivizing provers" +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } +readme = "README.md" +resolver = "2" + +[dev-dependencies] +sov-rollup-interface = { path = "../../../rollup-interface", version = "0.1", features = ["mocks"] } +sov-modules-api = { path = "../../sov-modules-api", version = "0.1" } +tempfile = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +sov-bank = { path = "../sov-bank", version = "0.1", default-features = false } +sov-chain-state = { path = "../sov-chain-state", version = "0.1", default-features = false } +sov-modules-api = { path = "../../sov-modules-api", version = "0.1", default-features = false } +sov-modules-macros = { path = "../../sov-modules-macros", version = "0.1" } +sov-state = { path = "../../sov-state", version = "0.1", default-features = false } +sov-rollup-interface = { path = "../../../rollup-interface", version = "0.1" } +serde = { workspace = true } +serde_json = { workspace = true, optional = true } +thiserror = { workspace = true } +jmt = { workspace = true } +borsh = { workspace = true, features = ["rc"] } + + +[features] +default = ["native"] +serde = ["dep:serde_json"] +native = ["serde", "sov-modules-api/native"] diff --git a/module-system/module-implementations/sov-attester-incentives/README.md b/module-system/module-implementations/sov-attester-incentives/README.md new file mode 100644 index 0000000000..925eeb735c --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/README.md @@ -0,0 +1,8 @@ +# Attester Incentive module + +**_This module is a placeholder for the logic incentivizing attesters and challengers. This is the full node implementation of the optimistic rollup workflow_** + +This module implements the logic for processing optimistic rollup attestations and challenges. Such +logic is necessary if you want to reward attesters/challengers or do anything else that's "aware" of attestation and challenge generation inside you state transition function. + +This module now implements the complete attestion/challenge verification workflow, as well as the bonding and unbonding processes for attesters and challengers. diff --git a/module-system/module-implementations/sov-attester-incentives/src/call.rs b/module-system/module-implementations/sov-attester-incentives/src/call.rs new file mode 100644 index 0000000000..f9d856ae92 --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/call.rs @@ -0,0 +1,777 @@ +use core::result::Result::Ok; +use std::fmt::Debug; + +use anyhow::Result; +use borsh::{BorshDeserialize, BorshSerialize}; +use sov_bank::{Amount, Coins}; +use sov_chain_state::TransitionHeight; +use sov_modules_api::{CallResponse, Context, Spec}; +use sov_rollup_interface::optimistic::Attestation; +use sov_rollup_interface::zk::{ + StateTransition, ValidityCondition, ValidityConditionChecker, Zkvm, +}; +use sov_state::storage::StorageProof; +use sov_state::{Storage, WorkingSet}; +use thiserror::Error; + +use crate::{AttesterIncentives, UnbondingInfo}; + +/// This enumeration represents the available call messages for interacting with the `AttesterIncentives` module. +#[derive(BorshDeserialize, BorshSerialize, Debug)] +pub enum CallMessage +where + C: Context, +{ + /// Bonds an attester, the parameter is the bond amount + BondAttester(Amount), + /// Start the first phase of the two phase unbonding process + BeginUnbondingAttester, + /// Finish the two phase unbonding + EndUnbondingAttester, + /// Bonds a challenger, the parameter is the bond amount + BondChallenger(Amount), + /// Unbonds a challenger + UnbondChallenger, + /// Proccesses an attestation. + ProcessAttestation(Attestation::Storage as Storage>::Proof>>), + /// Processes a challenge. The challenge is encoded as a [`Vec`]. The second parameter is the transition number + ProcessChallenge(Vec, TransitionHeight), +} + +#[derive(Debug, Error, PartialEq, Eq)] +/// Error type that explains why an user is slashed +pub enum SlashingReason { + #[error("Transition not found")] + /// The specified transition does not exist + TransitionNotFound, + + #[error("The attestation does not contain the right block hash and post state transition")] + /// The specified transition is invalid (block hash, post root hash or validity condition) + TransitionInvalid, + + #[error("The initial hash of the transition is invalid")] + /// The initial hash of the transition is invalid. + InvalidInitialHash, + + #[error("The proof opening raised an error")] + /// The proof verification raised an error + InvalidProofOutputs, + + #[error("No invalid transition to challenge")] + /// No invalid transition to challenge. + NoInvalidTransition, +} + +/// Error raised while processessing the attester incentives +#[derive(Debug, Error, PartialEq, Eq)] +pub enum AttesterIncentiveErrors { + #[error("Attester slashed")] + /// The user was slashed. Reason specified by [`SlashingReason`] + UserSlashed(#[source] SlashingReason), + + #[error("Invalid bonding proof")] + /// The bonding proof was invalid + InvalidBondingProof, + + #[error("The sender key doesn't match the attester key provided in the proof")] + /// The sender key doesn't match the attester key provided in the proof + InvalidSender, + + #[error("Attester is unbonding")] + /// The attester is in the first unbonding phase + AttesterIsUnbonding, + + #[error("User is not trying to unbond at the time of the transaction")] + /// User is not trying to unbond at the time of the transaction + AttesterIsNotUnbonding, + + #[error("First phase unbonding has not been finalized")] + /// The attester is trying to finish the two phase unbonding too soon + UnbondingNotFinalized, + + #[error("The bond is not a 64 bit number")] + /// The bond is not a 64 bit number + InvalidBondFormat, + + #[error("User is not bonded at the time of the transaction")] + /// User is not bonded at the time of the transaction + UserNotBonded, + + #[error("Transition invariant not respected")] + /// Transition invariant not respected + InvalidTransitionInvariant, + + #[error("Error occured when transfered funds")] + /// An error occured when transfered funds + TransferFailure, + + #[error("Error when trying to mint the reward token")] + /// An error occured when trying to mint the reward token + MintFailure, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// A role in the attestation process +pub enum Role { + /// A user who attests to new state transitions + Attester, + /// A user who challenges attestations + Challenger, +} + +impl< + C: sov_modules_api::Context, + Vm: Zkvm, + Cond: ValidityCondition, + Checker: ValidityConditionChecker + BorshDeserialize + BorshSerialize, + > AttesterIncentives +{ + /// This returns the address of the reward token supply + pub fn get_reward_token_supply_address( + &self, + working_set: &mut WorkingSet, + ) -> C::Address { + self.reward_token_supply_address + .get(working_set) + .expect("The reward token supply address should be set at genesis") + } + + /// A helper function that simply slashes an attester and returns a reward value + fn slash_user( + &self, + user: &C::Address, + role: Role, + working_set: &mut WorkingSet, + ) -> u64 { + let bonded_set = match role { + Role::Attester => { + // We have to remove the attester from the unbonding set to prevent him from skiping the first phase + // unbonding if he bonds himself again. + self.unbonding_attesters.remove(user, working_set); + + &self.bonded_attesters + } + Role::Challenger => &self.bonded_challengers, + }; + + // We have to deplete the attester's bonded account, it amounts to removing the attester from the bonded set + let reward = bonded_set.get(user, working_set).unwrap_or_default(); + bonded_set.remove(user, working_set); + + // We raise an event + working_set.add_event("user_slashed", &format!("address {user:?}")); + + reward + } + + fn slash_burn_reward( + &self, + user: &C::Address, + role: Role, + reason: SlashingReason, + working_set: &mut WorkingSet, + ) -> AttesterIncentiveErrors { + self.slash_user(user, role, working_set); + AttesterIncentiveErrors::UserSlashed(reason) + } + + /// A helper function that is used to slash an attester, and put the associated attestation in the slashed pool + fn slash_and_invalidate_attestation( + &self, + attester: &C::Address, + height: TransitionHeight, + reason: SlashingReason, + working_set: &mut WorkingSet, + ) -> AttesterIncentiveErrors { + let reward = self.slash_user(attester, Role::Attester, working_set); + + let curr_reward_value = self + .bad_transition_pool + .get(&height, working_set) + .unwrap_or_default(); + + self.bad_transition_pool + .set(&height, &(curr_reward_value + reward), working_set); + + AttesterIncentiveErrors::UserSlashed(reason) + } + + fn reward_sender( + &self, + context: &C, + amount: u64, + working_set: &mut WorkingSet, + ) -> Result { + let reward_address = self + .reward_token_supply_address + .get(working_set) + .expect("The reward supply address must be set at genesis"); + + let coins = Coins { + token_address: self + .bonding_token_address + .get(working_set) + .expect("Bonding token address must be set"), + amount, + }; + + // Mint tokens and send them + self.bank + .mint( + &coins, + context.sender(), + &C::new(reward_address), + working_set, + ) + .map_err(|_err| AttesterIncentiveErrors::MintFailure)?; + + Ok(CallResponse::default()) + } + + /// A helper function for the `bond_challenger/attester` call. Also used to bond challengers/attesters + /// during genesis when no context is available. + pub(super) fn bond_user_helper( + &self, + bond_amount: u64, + user_address: &C::Address, + role: Role, + working_set: &mut WorkingSet, + ) -> Result { + // If the user is an attester we have to check that he's not trying to unbond + if role == Role::Attester + && self + .unbonding_attesters + .get(user_address, working_set) + .is_some() + { + return Err(AttesterIncentiveErrors::AttesterIsUnbonding); + } + + // Transfer the bond amount from the module's token minting address to the sender. + // On failure, no state is changed + let coins = Coins { + token_address: self + .bonding_token_address + .get(working_set) + .expect("Bonding token address must be set"), + amount: bond_amount, + }; + + self.bank + .transfer_from(user_address, &self.address, coins, working_set) + .map_err(|_err| AttesterIncentiveErrors::TransferFailure)?; + + let (balances, event_key) = match role { + Role::Attester => (&self.bonded_attesters, "bonded_attester"), + Role::Challenger => (&self.bonded_challengers, "bonded_challenger"), + }; + + // Update our record of the total bonded amount for the sender. + // This update is infallible, so no value can be destroyed. + let old_balance = balances.get(user_address, working_set).unwrap_or_default(); + let total_balance = old_balance + bond_amount; + balances.set(user_address, &total_balance, working_set); + + // Emit the bonding event + working_set.add_event( + event_key, + &format!("new_deposit: {bond_amount:?}. total_bond: {total_balance:?}"), + ); + + Ok(CallResponse::default()) + } + + /// Try to unbond the requested amount of coins with context.sender() as the beneficiary. + pub(crate) fn unbond_challenger( + &self, + context: &C, + working_set: &mut WorkingSet, + ) -> Result { + // Get the user's old balance. + if let Some(old_balance) = self.bonded_challengers.get(context.sender(), working_set) { + // Transfer the bond amount from the sender to the module's address. + // On failure, no state is changed + self.reward_sender(context, old_balance, working_set)?; + + // Emit the unbonding event + working_set.add_event( + "unbonded_challenger", + &format!("amount_withdrawn: {old_balance:?}"), + ); + } + + Ok(CallResponse::default()) + } + + /// The attester starts the first phase of the two phase unbonding. We put the current max + /// finalized height with the attester address in the set of unbonding attesters iff the attester + /// is already present in the unbonding set + pub(crate) fn begin_unbond_attester( + &self, + context: &C, + working_set: &mut WorkingSet, + ) -> Result { + // First get the bonded attester + if let Some(bond) = self.bonded_attesters.get(context.sender(), working_set) { + let finalized_height = self + .light_client_finalized_height + .get(working_set) + .expect("Must be set at genesis"); + + // Remove the attester from the bonding set + self.bonded_attesters.remove(context.sender(), working_set); + + // Then add the bonded attester to the unbonding set, with the current finalized height + self.unbonding_attesters.set( + context.sender(), + &UnbondingInfo { + unbonding_initiated_height: finalized_height, + amount: bond, + }, + working_set, + ); + } + + Ok(CallResponse::default()) + } + + pub(crate) fn end_unbond_attester( + &self, + context: &C, + working_set: &mut WorkingSet, + ) -> Result { + // We have to ensure that the attester is unbonding, and that the unbonding transaction + // occurred at least `finality_period` blocks ago to let the attester unbond + if let Some(unbonding_info) = self.unbonding_attesters.get(context.sender(), working_set) { + // These two constants should always be set beforehand, hence we can panic if they're not set + let curr_height = self + .light_client_finalized_height + .get(working_set) + .expect("Should be defined at genesis"); + let finality_period = self + .rollup_finality_period + .get(working_set) + .expect("Should be defined at genesis"); + + if unbonding_info + .unbonding_initiated_height + .saturating_add(finality_period) + > curr_height + { + return Err(AttesterIncentiveErrors::UnbondingNotFinalized); + } + + // Get the user's old balance. + // Transfer the bond amount from the sender to the module's address. + // On failure, no state is changed + self.reward_sender(context, unbonding_info.amount, working_set)?; + + // Update our internal tracking of the total bonded amount for the sender. + self.bonded_attesters.remove(context.sender(), working_set); + self.unbonding_attesters + .remove(context.sender(), working_set); + + // Emit the unbonding event + working_set.add_event("unbonded_challenger", { + let amount = unbonding_info.amount; + &format!("amount_withdrawn: {:?}", amount) + }); + } else { + return Err(AttesterIncentiveErrors::AttesterIsNotUnbonding); + } + Ok(CallResponse::default()) + } + + /// The bonding proof is now a proof that an attester was bonded during the last `finality_period` range. + /// The proof must refer to a valid state of the rollup. The initial root hash must represent a state between + /// the bonding proof one and the current state. + fn check_bonding_proof( + &self, + context: &C, + attestation: &Attestation::Proof>>, + working_set: &mut WorkingSet, + ) -> Result<(), AttesterIncentiveErrors> { + let bonding_root = { + // If we cannot get the transition before the current one, it means that we are trying + // to get the genesis state root + if let Some(transition) = self.chain_state.historical_transitions.get( + &(attestation.proof_of_bond.claimed_transition_num - 1), + working_set, + ) { + transition.post_state_root() + } else { + self.chain_state + .genesis_hash + .get(working_set) + .expect("The genesis hash should be set at genesis") + } + }; + + // This proof checks that the attester was bonded at the given transition num + let bond_opt = working_set + .backing() + .verify_proof( + bonding_root, + attestation.proof_of_bond.proof.clone(), + context.sender(), + &self.bonded_attesters, + ) + .map_err(|_err| AttesterIncentiveErrors::InvalidBondingProof)?; + + let bond = bond_opt.ok_or(AttesterIncentiveErrors::UserNotBonded)?; + let bond: u64 = BorshDeserialize::deserialize(&mut bond.value()) + .map_err(|_err| AttesterIncentiveErrors::InvalidBondFormat)?; + + let minimum_bond = self + .minimum_attester_bond + .get_or_err(working_set) + .expect("The minimum bond should be set at genesis"); + + // We then have to check that the bond was greater than the minimum bond + if bond < minimum_bond { + return Err(AttesterIncentiveErrors::UserNotBonded); + } + + Ok(()) + } + + fn check_transition( + &self, + claimed_transition_height: TransitionHeight, + attester: &C::Address, + attestation: &Attestation::Proof>>, + working_set: &mut WorkingSet, + ) -> Result { + if let Some(curr_tx) = self + .chain_state + .historical_transitions + .get(&claimed_transition_height, working_set) + { + // We first need to compare the initial block hash to the previous post state root + if !curr_tx.compare_hashes(&attestation.da_block_hash, &attestation.post_state_root) { + // Check if the attestation has the same da_block_hash and post_state_root as the actual transition + // that we found in state. If not, slash the attester. + // If so, the attestation is valid, so return Ok + return Err(self.slash_and_invalidate_attestation( + attester, + claimed_transition_height, + SlashingReason::TransitionInvalid, + working_set, + )); + } + Ok(CallResponse::default()) + } else { + // Case where we cannot get the transition from the chain state historical transitions. + Err(self.slash_burn_reward( + attester, + Role::Attester, + SlashingReason::TransitionNotFound, + working_set, + )) + } + } + + fn check_initial_hash( + &self, + claimed_transition_height: TransitionHeight, + attester: &C::Address, + attestation: &Attestation::Proof>>, + working_set: &mut WorkingSet, + ) -> Result { + // Normal state + if let Some(transition) = self + .chain_state + .historical_transitions + .get(&claimed_transition_height.saturating_sub(1), working_set) + { + if transition.post_state_root() != attestation.initial_state_root { + // The initial root hashes don't match, just slash the attester + return Err(self.slash_burn_reward( + attester, + Role::Attester, + SlashingReason::InvalidInitialHash, + working_set, + )); + } + } else { + // Genesis state + // We can assume that the genesis hash is always set, otherwise we need to panic. + // We don't need to prove that the attester was bonded, simply need to check that the current bond is higher than the + // minimal bond and that the attester is not unbonding + + // We add a check here that the claimed transition height is the same as the genesis height. + if self + .chain_state + .genesis_height + .get(working_set) + .expect("Must be set at genesis") + != (claimed_transition_height - 1) + { + return Err(self.slash_burn_reward( + attester, + Role::Attester, + SlashingReason::TransitionNotFound, + working_set, + )); + } + + if self + .chain_state + .get_genesis_hash(working_set) + .expect("The initial hash should be set") + != attestation.initial_state_root + { + // Slash the attester, and burn the fees + return Err(self.slash_burn_reward( + attester, + Role::Attester, + SlashingReason::InvalidInitialHash, + working_set, + )); + } + + // Normal state + } + + Ok(CallResponse::default()) + } + + /// Try to process an attestation, if the attester is bonded + pub(crate) fn process_attestation( + &self, + context: &C, + attestation: Attestation::Proof>>, + working_set: &mut WorkingSet, + ) -> Result { + // We first need to check that the attester is still in the bonding set + if self + .bonded_attesters + .get(context.sender(), working_set) + .is_none() + { + return Err(AttesterIncentiveErrors::UserNotBonded); + } + + // If the bonding proof in the attestation is invalid, light clients will ignore the attestation. In that case, we should too. + self.check_bonding_proof(context, &attestation, working_set)?; + + // We suppose that these values are always defined, otherwise we panic + let last_attested_height = self + .maximum_attested_height + .get(working_set) + .expect("The maximum attested height should be set at genesis"); + let current_finalized_height = self + .light_client_finalized_height + .get(working_set) + .expect("The light client finalized height should be set at genesis"); + let finality = self + .rollup_finality_period + .get(working_set) + .expect("The rollup finality period should be set at genesis"); + + assert!( + current_finalized_height <= last_attested_height, + "The last attested height should always be below the current finalized height." + ); + + // Update the max_attested_height in case the blocks have already been finalized + let new_height_to_attest = last_attested_height + 1; + + // Minimum height at which the proof of bond can be valid + let min_height = new_height_to_attest.saturating_sub(finality); + + // We have to check the following order invariant is respected: + // (height to attest - finality) <= bonding_proof.transition_num <= height to attest + // + // Which with our variable gives: + // min_height <= bonding_proof.transition_num <= new_height_to_attest + // If this invariant is respected, we can be sure that the attester was bonded at new_height_to_attest. + if !(min_height <= attestation.proof_of_bond.claimed_transition_num + && attestation.proof_of_bond.claimed_transition_num <= new_height_to_attest) + { + return Err(AttesterIncentiveErrors::InvalidTransitionInvariant); + } + + // First compare the initial hashes + self.check_initial_hash( + attestation.proof_of_bond.claimed_transition_num, + context.sender(), + &attestation, + working_set, + )?; + + // Then compare the transition + self.check_transition( + attestation.proof_of_bond.claimed_transition_num, + context.sender(), + &attestation, + working_set, + )?; + + working_set.add_event( + "processed_valid_attestation", + &format!("attester: {:?}", context.sender()), + ); + + // Now we have to check whether the claimed_transition_num is the max_attested_height. + // If so update the maximum attested height and reward the sender + if attestation.proof_of_bond.claimed_transition_num == new_height_to_attest { + // Update the maximum attested height + self.maximum_attested_height + .set(&(new_height_to_attest), working_set); + + // Reward the sender + self.reward_sender( + context, + self.minimum_attester_bond + .get(working_set) + .expect("Should be defined at genesis"), + working_set, + )?; + } + + // Then we can optimistically process the transaction + Ok(CallResponse::default()) + } + + fn check_challenge_outputs_against_transition( + &self, + public_outputs: StateTransition, + height: &TransitionHeight, + condition_checker: &mut impl ValidityConditionChecker, + working_set: &mut WorkingSet, + ) -> Result<(), SlashingReason> { + let transition = self + .chain_state + .historical_transitions + .get_or_err(height, working_set) + .map_err(|_| SlashingReason::TransitionInvalid)?; + + let initial_hash = { + if let Some(prev_transition) = self + .chain_state + .historical_transitions + .get(&height.saturating_sub(1), working_set) + { + prev_transition.post_state_root() + } else { + self.chain_state + .genesis_hash + .get(working_set) + .expect("The genesis hash should be set") + } + }; + + if public_outputs.initial_state_root != initial_hash { + return Err(SlashingReason::InvalidInitialHash); + } + + if public_outputs.slot_hash != transition.da_block_hash() { + return Err(SlashingReason::TransitionInvalid); + } + + if public_outputs.validity_condition != *transition.validity_condition() { + return Err(SlashingReason::TransitionInvalid); + } + + // TODO: Should we compare the validity conditions of the public outputs with the ones of the recorded transition? + condition_checker + .check(&public_outputs.validity_condition) + .map_err(|_err| SlashingReason::TransitionInvalid)?; + + Ok(()) + } + + /// Try to process a zk proof, if the challenger is bonded. + pub(crate) fn process_challenge( + &self, + context: &C, + proof: &[u8], + transition_num: &TransitionHeight, + working_set: &mut WorkingSet, + ) -> Result { + // Get the challenger's old balance. + // Revert if they aren't bonded + let old_balance = self + .bonded_challengers + .get_or_err(context.sender(), working_set) + .map_err(|_| AttesterIncentiveErrors::UserNotBonded)?; + + // Check that the challenger has enough balance to process the proof. + let minimum_bond = self + .minimum_challenger_bond + .get(working_set) + .expect("Should be set at genesis"); + + if old_balance < minimum_bond { + return Err(AttesterIncentiveErrors::UserNotBonded); + } + + let code_commitment = self + .commitment_to_allowed_challenge_method + .get(working_set) + .expect("Should be set at genesis") + .commitment; + + // Find the faulty attestation pool and get the associated reward + let attestation_reward: u64 = self + .bad_transition_pool + .get_or_err(transition_num, working_set) + .map_err(|_| { + self.slash_burn_reward( + context.sender(), + Role::Challenger, + SlashingReason::NoInvalidTransition, + working_set, + ) + })?; + + let public_outputs_opt: Result> = + Vm::verify_and_extract_output::(proof, &code_commitment) + .map_err(|e| anyhow::format_err!("{:?}", e)); + + // Don't return an error for invalid proofs - those are expected and shouldn't cause reverts. + match public_outputs_opt { + Ok(public_output) => { + // We get the validity condition checker from the state + let mut validity_checker = self + .validity_cond_checker + .get(working_set) + .expect("Should be defined at genesis"); + + // We have to perform the checks to ensure that the challenge is valid while the attestation isn't. + self.check_challenge_outputs_against_transition( + public_output, + transition_num, + &mut validity_checker, + working_set, + ) + .map_err(|err| { + self.slash_burn_reward(context.sender(), Role::Challenger, err, working_set) + })?; + + // Reward the challenger with half of the attestation reward (avoid DOS) + self.reward_sender(context, attestation_reward / 2, working_set)?; + + // Now remove the bad transition from the pool + self.bad_transition_pool.remove(transition_num, working_set); + + working_set.add_event( + "processed_valid_proof", + &format!("challenger: {:?}", context.sender()), + ); + } + Err(_err) => { + // Slash the challenger + return Err(self.slash_burn_reward( + context.sender(), + Role::Challenger, + SlashingReason::InvalidProofOutputs, + working_set, + )); + } + } + + Ok(CallResponse::default()) + } +} diff --git a/module-system/module-implementations/sov-attester-incentives/src/genesis.rs b/module-system/module-implementations/sov-attester-incentives/src/genesis.rs new file mode 100644 index 0000000000..a62a30cf2f --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/genesis.rs @@ -0,0 +1,64 @@ +use anyhow::Result; +use borsh::{BorshDeserialize, BorshSerialize}; +use sov_rollup_interface::zk::{ValidityCondition, ValidityConditionChecker, Zkvm}; +use sov_state::{Storage, WorkingSet}; + +use crate::call::Role; +use crate::AttesterIncentives; + +impl AttesterIncentives +where + C: sov_modules_api::Context, + Vm: Zkvm, + S: Storage, + P: BorshDeserialize + BorshSerialize, + Cond: ValidityCondition, + Checker: ValidityConditionChecker, +{ + pub(crate) fn init_module( + &self, + config: &::Config, + working_set: &mut WorkingSet, + ) -> Result<()> { + anyhow::ensure!( + !config.initial_attesters.is_empty(), + "At least one prover must be set at genesis!" + ); + + self.minimum_attester_bond + .set(&config.minimum_attester_bond, working_set); + self.minimum_challenger_bond + .set(&config.minimum_challenger_bond, working_set); + + self.commitment_to_allowed_challenge_method.set( + &crate::StoredCodeCommitment { + commitment: config.commitment_to_allowed_challenge_method.clone(), + }, + working_set, + ); + + self.rollup_finality_period + .set(&config.rollup_finality_period, working_set); + + self.bonding_token_address + .set(&config.bonding_token_address, working_set); + + self.reward_token_supply_address + .set(&config.reward_token_supply_address, working_set); + + for (attester, bond) in config.initial_attesters.iter() { + self.bond_user_helper(*bond, attester, Role::Attester, working_set)?; + } + + self.maximum_attested_height + .set(&config.maximum_attested_height, working_set); + + self.light_client_finalized_height + .set(&config.light_client_finalized_height, working_set); + + self.validity_cond_checker + .set(&config.validity_condition_checker, working_set); + + Ok(()) + } +} diff --git a/module-system/module-implementations/sov-attester-incentives/src/lib.rs b/module-system/module-implementations/sov-attester-incentives/src/lib.rs new file mode 100644 index 0000000000..908ca8be2d --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/lib.rs @@ -0,0 +1,209 @@ +#![deny(missing_docs)] +#![doc = include_str!("../README.md")] + +/// Call methods for the module +pub mod call; + +/// Methods used to instantiate the module +pub mod genesis; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "native")] +#[allow(missing_docs)] +pub mod query; + +use std::marker::PhantomData; + +use borsh::{BorshDeserialize, BorshSerialize}; +use call::Role; +use sov_bank::Amount; +use sov_chain_state::TransitionHeight; +use sov_modules_api::{Context, Error}; +use sov_modules_macros::ModuleInfo; +use sov_rollup_interface::zk::{ + StoredCodeCommitment, ValidityCondition, ValidityConditionChecker, Zkvm, +}; +use sov_state::{Storage, WorkingSet}; + +/// Configuration of the attester incentives module +pub struct AttesterIncentivesConfig< + C: Context, + Vm: Zkvm, + Cond: ValidityCondition, + Checker: ValidityConditionChecker, +> { + /// The address of the token to be used for bonding. + pub bonding_token_address: C::Address, + /// The address of the account holding the reward token supply + pub reward_token_supply_address: C::Address, + /// The minimum bond for an attester. + pub minimum_attester_bond: Amount, + /// The minimum bond for a challenger. + pub minimum_challenger_bond: Amount, + /// A code commitment to be used for verifying proofs + pub commitment_to_allowed_challenge_method: Vm::CodeCommitment, + /// A list of initial provers and their bonded amount. + pub initial_attesters: Vec<(C::Address, Amount)>, + /// The finality period of the rollup (constant) in the number of DA layer slots processed. + pub rollup_finality_period: TransitionHeight, + /// The current maximum attested height + pub maximum_attested_height: TransitionHeight, + /// The light client finalized height + pub light_client_finalized_height: TransitionHeight, + /// The validity condition checker used to check validity conditions + pub validity_condition_checker: Checker, + /// Phantom data that contains the validity condition + phantom_data: PhantomData, +} + +/// The information about an attester's unbonding +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug)] +pub struct UnbondingInfo { + /// The height at which an attester started unbonding + pub unbonding_initiated_height: TransitionHeight, + /// The number of tokens that the attester may withdraw + pub amount: Amount, +} + +/// A new module: +/// - Must derive `ModuleInfo` +/// - Must contain `[address]` field +/// - Can contain any number of ` #[state]` or `[module]` fields +#[derive(ModuleInfo)] +pub struct AttesterIncentives< + C: sov_modules_api::Context, + Vm: Zkvm, + Cond: ValidityCondition, + Checker: ValidityConditionChecker, +> { + /// Address of the module. + #[address] + pub address: C::Address, + + /// The amount of time it takes to a light client to be confident + /// that an attested state transition won't be challenged. Measured in + /// number of slots. + #[state] + pub rollup_finality_period: sov_state::StateValue, + + /// The address of the token used for bonding provers + #[state] + pub bonding_token_address: sov_state::StateValue, + + /// The address of the account holding the reward token supply + /// TODO: maybe mint the token before transfering it? The mint method is private in bank + /// so we need a reward address that contains the supply. + #[state] + pub reward_token_supply_address: sov_state::StateValue, + + /// The code commitment to be used for verifying proofs + #[state] + pub commitment_to_allowed_challenge_method: sov_state::StateValue>, + + /// Constant validity condition checker for the module. + #[state] + pub validity_cond_checker: sov_state::StateValue, + + /// The set of bonded attesters and their bonded amount. + #[state] + pub bonded_attesters: sov_state::StateMap, + + /// The set of unbonding attesters, and the unbonding information (ie the + /// height of the chain where they started the unbonding and their associated bond). + #[state] + pub unbonding_attesters: sov_state::StateMap, + + /// The current maximum attestation height + #[state] + pub maximum_attested_height: sov_state::StateValue, + + /// Challengers now challenge a transition and not a specific attestation + /// Mapping from a transition number to the associated reward value. + /// This mapping is populated when the attestations are processed by the rollup + #[state] + pub bad_transition_pool: sov_state::StateMap, + + /// The set of bonded challengers and their bonded amount. + #[state] + pub bonded_challengers: sov_state::StateMap, + + /// The minimum bond for an attester to be eligble + #[state] + pub minimum_attester_bond: sov_state::StateValue, + + /// The minimum bond for an attester to be eligble + #[state] + pub minimum_challenger_bond: sov_state::StateValue, + + /// The height of the most recent block which light clients know to be finalized + #[state] + pub light_client_finalized_height: sov_state::StateValue, + + /// Reference to the Bank module. + #[module] + pub(crate) bank: sov_bank::Bank, + + /// Reference to the chain state module, used to check the initial hashes of the state transition. + #[module] + pub(crate) chain_state: sov_chain_state::ChainState, +} + +impl sov_modules_api::Module + for AttesterIncentives +where + C: sov_modules_api::Context, + Vm: Zkvm, + S: Storage, + P: BorshDeserialize + BorshSerialize, + Cond: ValidityCondition, + Checker: ValidityConditionChecker, +{ + type Context = C; + + type Config = AttesterIncentivesConfig; + + type CallMessage = call::CallMessage; + + fn genesis( + &self, + config: &Self::Config, + working_set: &mut WorkingSet, + ) -> Result<(), Error> { + // The initialization logic + Ok(self.init_module(config, working_set)?) + } + + fn call( + &self, + msg: Self::CallMessage, + context: &Self::Context, + working_set: &mut WorkingSet, + ) -> Result { + match msg { + call::CallMessage::BondAttester(bond_amount) => self + .bond_user_helper(bond_amount, context.sender(), Role::Attester, working_set) + .map_err(|err| err.into()), + call::CallMessage::BeginUnbondingAttester => self + .begin_unbond_attester(context, working_set) + .map_err(|error| error.into()), + + call::CallMessage::EndUnbondingAttester => self + .end_unbond_attester(context, working_set) + .map_err(|error| error.into()), + call::CallMessage::BondChallenger(bond_amount) => self + .bond_user_helper(bond_amount, context.sender(), Role::Challenger, working_set) + .map_err(|err| err.into()), + call::CallMessage::UnbondChallenger => self.unbond_challenger(context, working_set), + call::CallMessage::ProcessAttestation(attestation) => self + .process_attestation(context, attestation, working_set) + .map_err(|error| error.into()), + + call::CallMessage::ProcessChallenge(proof, transition) => self + .process_challenge(context, &proof, &transition, working_set) + .map_err(|error| error.into()), + } + .map_err(|e| e.into()) + } +} diff --git a/module-system/module-implementations/sov-attester-incentives/src/query.rs b/module-system/module-implementations/sov-attester-incentives/src/query.rs new file mode 100644 index 0000000000..4244df30a4 --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/query.rs @@ -0,0 +1,79 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use sov_modules_api::Spec; +use sov_rollup_interface::zk::{ValidityCondition, ValidityConditionChecker, Zkvm}; +use sov_state::storage::{NativeStorage, StorageProof}; +use sov_state::{Storage, WorkingSet}; + +use super::AttesterIncentives; +use crate::call::Role; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct BondAmountResponse { + pub value: u64, +} + +// TODO: implement rpc_gen macro +impl AttesterIncentives +where + C: sov_modules_api::Context, + Vm: Zkvm, + Cond: ValidityCondition, + Checker: ValidityConditionChecker + BorshDeserialize + BorshSerialize, +{ + /// Queries the state of the module. + pub fn get_bond_amount( + &self, + address: C::Address, + role: Role, + working_set: &mut WorkingSet, + ) -> BondAmountResponse { + match role { + Role::Attester => { + BondAmountResponse { + value: self + .bonded_attesters + .get(&address, working_set) + .unwrap_or_default(), // self.value.get(working_set), + } + } + Role::Challenger => { + BondAmountResponse { + value: self + .bonded_challengers + .get(&address, working_set) + .unwrap_or_default(), // self.value.get(working_set), + } + } + } + } + + /// Used by attesters to get a proof that they were bonded before starting to produce attestations. + /// A bonding proof is valid for `max_finality_period` blocks, the attester can only produce transition + /// attestations for this specific amount of time. + pub fn get_bond_proof( + &self, + address: C::Address, + witness: &<::Storage as Storage>::Witness, + working_set: &mut WorkingSet, + ) -> StorageProof<::Proof> + where + C::Storage: NativeStorage, + { + working_set.backing().get_with_proof_from_state_map( + &address, + &self.bonded_attesters, + witness, + ) + } + + /// TODO: Make the unbonding amount queriable: + pub fn get_unbonding_amount( + &self, + _address: C::Address, + _witness: &<::Storage as Storage>::Witness, + _working_set: &mut WorkingSet, + ) -> u64 { + todo!("Make the unbonding amount queriable: https://github.com/Sovereign-Labs/sovereign-sdk/issues/675") + } +} diff --git a/module-system/module-implementations/sov-attester-incentives/src/tests/attestation_proccessing.rs b/module-system/module-implementations/sov-attester-incentives/src/tests/attestation_proccessing.rs new file mode 100644 index 0000000000..7be107a732 --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/tests/attestation_proccessing.rs @@ -0,0 +1,282 @@ +use sov_modules_api::default_context::DefaultContext; +use sov_rollup_interface::optimistic::Attestation; +use sov_state::{ProverStorage, WorkingSet}; + +use crate::call::AttesterIncentiveErrors; +use crate::tests::helpers::{ + execution_simulation, setup, BOND_AMOUNT, INITIAL_BOND_AMOUNT, INIT_HEIGHT, +}; + +/// Start by testing the positive case where the attestations are valid +#[test] +fn test_process_valid_attestation() { + let tmpdir = tempfile::tempdir().unwrap(); + let storage = ProverStorage::with_path(tmpdir.path()).unwrap(); + let mut working_set = WorkingSet::new(storage.clone()); + let (module, token_address, attester_address, _) = setup(&mut working_set); + + // Assert that the attester has the correct bond amount before processing the proof + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + BOND_AMOUNT + ); + + // Simulate the execution of a chain, with the genesis hash and two transitions after. + // Update the chain_state module and the optimistic module accordingly + let (mut exec_vars, mut working_set) = + execution_simulation(3, &module, &storage, attester_address, working_set); + + let context = DefaultContext { + sender: attester_address, + }; + + let transition_2 = exec_vars.pop().unwrap(); + let transition_1 = exec_vars.pop().unwrap(); + let initial_transition = exec_vars.pop().unwrap(); + + // Process a valid attestation for the first transition + { + let attestation = Attestation { + initial_state_root: initial_transition.state_root, + da_block_hash: [1; 32], + post_state_root: transition_1.state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: INIT_HEIGHT + 1, + proof: initial_transition.state_proof, + }, + }; + + module + .process_attestation(&context, attestation, &mut working_set) + .expect("An invalid proof is an error"); + } + + // We can now proceed with the next attestation + { + let attestation = Attestation { + initial_state_root: transition_1.state_root, + da_block_hash: [2; 32], + post_state_root: transition_2.state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: INIT_HEIGHT + 2, + proof: transition_1.state_proof, + }, + }; + + module + .process_attestation(&context, attestation, &mut working_set) + .expect("An invalid proof is an error"); + } + + // Assert that the attester's bond amount has not been burned + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + BOND_AMOUNT + ); + + // Assert that the attester has been awarded the tokens + assert_eq!( + module + .bank + .get_balance_of(attester_address, token_address, &mut working_set) + .unwrap(), + // The attester is bonded at the beginning so he loses BOND_AMOUNT + INITIAL_BOND_AMOUNT - BOND_AMOUNT + 2 * BOND_AMOUNT + ); +} + +#[test] +fn test_burn_on_invalid_attestation() { + let tmpdir = tempfile::tempdir().unwrap(); + let storage = ProverStorage::with_path(tmpdir.path()).unwrap(); + let mut working_set = WorkingSet::new(storage.clone()); + let (module, _token_address, attester_address, _) = setup(&mut working_set); + + // Assert that the prover has the correct bond amount before processing the proof + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + BOND_AMOUNT + ); + + // Simulate the execution of a chain, with the genesis hash and two transitions after. + // Update the chain_state module and the optimistic module accordingly + let (mut exec_vars, mut working_set) = + execution_simulation(3, &module, &storage, attester_address, working_set); + + let transition_2 = exec_vars.pop().unwrap(); + let transition_1 = exec_vars.pop().unwrap(); + let initial_transition = exec_vars.pop().unwrap(); + + let context = DefaultContext { + sender: attester_address, + }; + + // Process an invalid proof for genesis: everything is correct except the storage proof. + // Must simply return an error. Cannot burn the token at this point because we don't know if the + // sender is bonded or not. + { + let attestation = Attestation { + initial_state_root: initial_transition.state_root, + da_block_hash: [1; 32], + post_state_root: transition_1.state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: INIT_HEIGHT + 1, + proof: transition_1.state_proof.clone(), + }, + }; + + let attestation_error = module + .process_attestation(&context, attestation, &mut working_set) + .unwrap_err(); + + assert_eq!( + attestation_error, + AttesterIncentiveErrors::InvalidBondingProof, + "The bonding proof should fail" + ); + } + + // Assert that the prover's bond amount has not been burned + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + BOND_AMOUNT + ); + + // Now proccess a valid attestation for genesis. + { + let attestation = Attestation { + initial_state_root: initial_transition.state_root, + da_block_hash: [1; 32], + post_state_root: transition_1.state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: INIT_HEIGHT + 1, + proof: initial_transition.state_proof, + }, + }; + + module + .process_attestation(&context, attestation, &mut working_set) + .expect("An invalid proof is an error"); + } + + // Then process a new attestation having the wrong initial state root. The attester must be slashed, and the fees burnt + { + let attestation = Attestation { + initial_state_root: initial_transition.state_root, + da_block_hash: [2; 32], + post_state_root: transition_2.state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: INIT_HEIGHT + 2, + proof: transition_1.state_proof.clone(), + }, + }; + + let attestation_error = module + .process_attestation(&context, attestation, &mut working_set) + .unwrap_err(); + + assert_eq!( + attestation_error, + AttesterIncentiveErrors::UserSlashed(crate::call::SlashingReason::InvalidInitialHash) + ) + } + + // Check that the attester's bond has been burnt + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + 0 + ); + + // Check that the attestation is not part of the challengeable set + assert!( + module + .bad_transition_pool + .get(&(INIT_HEIGHT + 2), &mut working_set) + .is_none(), + "The transition should not exist in the pool" + ); + + // Bond the attester once more + module + .bond_user_helper( + BOND_AMOUNT, + &attester_address, + crate::call::Role::Attester, + &mut working_set, + ) + .unwrap(); + + // Process an attestation that has the right bonding proof and initial hash but has a faulty post transition hash. + { + let attestation = Attestation { + initial_state_root: transition_1.state_root, + da_block_hash: [2; 32], + post_state_root: transition_1.state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: INIT_HEIGHT + 2, + proof: transition_1.state_proof, + }, + }; + + let attestation_error = module + .process_attestation(&context, attestation, &mut working_set) + .unwrap_err(); + + assert_eq!( + attestation_error, + AttesterIncentiveErrors::UserSlashed(crate::call::SlashingReason::TransitionInvalid) + ) + } + + // Check that the attester's bond has been burnt + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + 0 + ); + + // The attestation should be part of the challengeable set and its associated value should be the BOND_AMOUNT + assert_eq!( + module + .bad_transition_pool + .get(&(INIT_HEIGHT + 2), &mut working_set) + .unwrap(), + BOND_AMOUNT, + "The transition should not exist in the pool" + ); +} diff --git a/module-system/module-implementations/sov-attester-incentives/src/tests/challenger.rs b/module-system/module-implementations/sov-attester-incentives/src/tests/challenger.rs new file mode 100644 index 0000000000..6f5b7bc016 --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/tests/challenger.rs @@ -0,0 +1,344 @@ +use borsh::BorshSerialize; +use sov_modules_api::default_context::DefaultContext; +use sov_rollup_interface::mocks::{ + MockCodeCommitment, MockProof, MockValidityCond, MockValidityCondChecker, +}; +use sov_rollup_interface::zk::StateTransition; +use sov_state::{ProverStorage, WorkingSet}; + +use crate::call::{AttesterIncentiveErrors, SlashingReason}; +use crate::tests::helpers::{ + commit_get_new_working_set, execution_simulation, setup, BOND_AMOUNT, INITIAL_BOND_AMOUNT, + INIT_HEIGHT, +}; + +/// Test that given an invalid transition, a challenger can successfully challenge it and get rewarded +#[test] +fn test_valid_challenge() { + let tmpdir = tempfile::tempdir().unwrap(); + let storage = ProverStorage::with_path(tmpdir.path()).unwrap(); + let mut working_set = WorkingSet::new(storage.clone()); + let (module, token_address, attester_address, challenger_address) = setup(&mut working_set); + + let working_set = commit_get_new_working_set(&storage, working_set); + + // Simulate the execution of a chain, with the genesis hash and two transitions after. + // Update the chain_state module and the optimistic module accordingly + let (mut exec_vars, mut working_set) = + execution_simulation(3, &module, &storage, attester_address, working_set); + + let _ = exec_vars.pop().unwrap(); + let transition_1 = exec_vars.pop().unwrap(); + let initial_transition = exec_vars.pop().unwrap(); + + module + .bond_user_helper( + BOND_AMOUNT, + &challenger_address, + crate::call::Role::Challenger, + &mut working_set, + ) + .unwrap(); + + // Assert that the challenger has the correct bond amount before processing the proof + assert_eq!( + module + .get_bond_amount( + challenger_address, + crate::call::Role::Challenger, + &mut working_set + ) + .value, + BOND_AMOUNT + ); + + // Set a bad transition to get a reward from + module + .bad_transition_pool + .set(&(INIT_HEIGHT + 1), &BOND_AMOUNT, &mut working_set); + + // Process a correct challenge + let context = DefaultContext { + sender: challenger_address, + }; + + { + let transition = StateTransition { + initial_state_root: initial_transition.state_root, + slot_hash: [1; 32], + final_state_root: transition_1.state_root, + rewarded_address: challenger_address, + validity_condition: MockValidityCond { is_valid: true }, + }; + + let serialized_transition = transition.try_to_vec().unwrap(); + + let commitment = module + .commitment_to_allowed_challenge_method + .get(&mut working_set) + .expect("Should be set at genesis") + .commitment; + + let proof = &MockProof { + program_id: commitment, + is_valid: true, + log: serialized_transition.as_slice(), + } + .encode_to_vec(); + + module + .process_challenge( + &context, + proof.as_slice(), + &(INIT_HEIGHT + 1), + &mut working_set, + ) + .expect("Should not fail"); + + // Check that the challenger was rewarded + assert_eq!( + module + .bank + .get_balance_of(challenger_address, token_address, &mut working_set) + .unwrap(), + INITIAL_BOND_AMOUNT - BOND_AMOUNT + BOND_AMOUNT / 2, + "The challenger should have been rewarded" + ); + + // Check that the challenge set is empty + assert_eq!( + module + .bad_transition_pool + .get(&(INIT_HEIGHT + 1), &mut working_set), + None, + "The transition should have disappeared" + ) + } + + { + // Now try to unbond the challenger + module + .unbond_challenger(&context, &mut working_set) + .expect("The challenger should be able to unbond"); + + // Check the final balance of the challenger + assert_eq!( + module + .bank + .get_balance_of(challenger_address, token_address, &mut working_set) + .unwrap(), + INITIAL_BOND_AMOUNT + BOND_AMOUNT / 2, + "The challenger should have been unbonded" + ) + } +} + +fn invalid_proof_helper( + context: &DefaultContext, + proof: &Vec, + reason: SlashingReason, + challenger_address: sov_modules_api::Address, + module: &crate::AttesterIncentives< + DefaultContext, + sov_rollup_interface::mocks::MockZkvm, + MockValidityCond, + MockValidityCondChecker, + >, + working_set: &mut WorkingSet>, +) { + // Let's bond the challenger and try to publish a false challenge + module + .bond_user_helper( + BOND_AMOUNT, + &challenger_address, + crate::call::Role::Challenger, + working_set, + ) + .expect("Should be able to bond"); + + let err = module + .process_challenge(context, proof.as_slice(), &(INIT_HEIGHT + 1), working_set) + .unwrap_err(); + + // Check the error raised + assert_eq!( + err, + AttesterIncentiveErrors::UserSlashed(reason), + "The challenge processing should fail with an invalid proof error" + ) +} + +#[test] +fn test_invalid_challenge() { + let tmpdir = tempfile::tempdir().unwrap(); + let storage = ProverStorage::with_path(tmpdir.path()).unwrap(); + let mut working_set = WorkingSet::new(storage.clone()); + let (module, _token_address, attester_address, challenger_address) = setup(&mut working_set); + + let working_set = commit_get_new_working_set(&storage, working_set); + + // Simulate the execution of a chain, with the genesis hash and two transitions after. + // Update the chain_state module and the optimistic module accordingly + let (mut exec_vars, mut working_set) = + execution_simulation(3, &module, &storage, attester_address, working_set); + + let _ = exec_vars.pop().unwrap(); + let transition_1 = exec_vars.pop().unwrap(); + let initial_transition = exec_vars.pop().unwrap(); + + // Set a bad transition to get a reward from + module + .bad_transition_pool + .set(&(INIT_HEIGHT + 1), &BOND_AMOUNT, &mut working_set); + + // Process a correct challenge but without a bonded attester + let context = DefaultContext { + sender: challenger_address, + }; + + let transition = StateTransition { + initial_state_root: initial_transition.state_root, + slot_hash: [1; 32], + final_state_root: transition_1.state_root, + rewarded_address: challenger_address, + validity_condition: MockValidityCond { is_valid: true }, + }; + + let serialized_transition = transition.try_to_vec().unwrap(); + + let commitment = module + .commitment_to_allowed_challenge_method + .get(&mut working_set) + .expect("Should be set at genesis") + .commitment; + + { + // A valid proof + let proof = &MockProof { + program_id: commitment.clone(), + is_valid: true, + log: serialized_transition.as_slice(), + } + .encode_to_vec(); + + let err = module + .process_challenge( + &context, + proof.as_slice(), + &(INIT_HEIGHT + 1), + &mut working_set, + ) + .unwrap_err(); + + // Check the error raised + assert_eq!( + err, + AttesterIncentiveErrors::UserNotBonded, + "The challenge processing should fail with an unbonded error" + ) + } + + // Invalid proofs + { + // An invalid proof + let proof = &MockProof { + program_id: commitment.clone(), + is_valid: false, + log: serialized_transition.as_slice(), + } + .encode_to_vec(); + + invalid_proof_helper( + &context, + proof, + SlashingReason::InvalidProofOutputs, + challenger_address, + &module, + &mut working_set, + ); + + // Bad slot hash + let bad_transition = StateTransition { + initial_state_root: initial_transition.state_root, + slot_hash: [2; 32], + final_state_root: transition_1.state_root, + rewarded_address: challenger_address, + validity_condition: MockValidityCond { is_valid: true }, + } + .try_to_vec() + .unwrap(); + + // An invalid proof + let proof = &MockProof { + program_id: commitment, + is_valid: true, + log: bad_transition.as_slice(), + } + .encode_to_vec(); + + invalid_proof_helper( + &context, + proof, + SlashingReason::TransitionInvalid, + challenger_address, + &module, + &mut working_set, + ); + + // Bad validity condition + let bad_transition = StateTransition { + initial_state_root: initial_transition.state_root, + slot_hash: [1; 32], + final_state_root: transition_1.state_root, + rewarded_address: challenger_address, + validity_condition: MockValidityCond { is_valid: false }, + } + .try_to_vec() + .unwrap(); + + // An invalid proof + let proof = &MockProof { + program_id: MockCodeCommitment([0; 32]), + is_valid: true, + log: bad_transition.as_slice(), + } + .encode_to_vec(); + + invalid_proof_helper( + &context, + proof, + SlashingReason::TransitionInvalid, + challenger_address, + &module, + &mut working_set, + ); + + // Bad initial root + let bad_transition = StateTransition { + initial_state_root: transition_1.state_root, + slot_hash: [1; 32], + final_state_root: transition_1.state_root, + rewarded_address: challenger_address, + validity_condition: MockValidityCond { is_valid: true }, + } + .try_to_vec() + .unwrap(); + + // An invalid proof + let proof = &MockProof { + program_id: MockCodeCommitment([0; 32]), + is_valid: true, + log: bad_transition.as_slice(), + } + .encode_to_vec(); + + invalid_proof_helper( + &context, + proof, + SlashingReason::InvalidInitialHash, + challenger_address, + &module, + &mut working_set, + ); + } +} diff --git a/module-system/module-implementations/sov-attester-incentives/src/tests/helpers.rs b/module-system/module-implementations/sov-attester-incentives/src/tests/helpers.rs new file mode 100644 index 0000000000..e6d15cbb37 --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/tests/helpers.rs @@ -0,0 +1,182 @@ +use jmt::proof::SparseMerkleProof; +use sov_bank::{BankConfig, TokenConfig}; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::hooks::SlotHooks; +use sov_modules_api::utils::generate_address; +use sov_modules_api::{Address, Genesis, Spec}; +use sov_rollup_interface::mocks::{ + MockBlock, MockBlockHeader, MockCodeCommitment, MockHash, MockValidityCond, + MockValidityCondChecker, MockZkvm, +}; +use sov_rollup_interface::zk::ValidityConditionChecker; +use sov_state::storage::StorageProof; +use sov_state::{DefaultStorageSpec, ProverStorage, Storage, WorkingSet}; + +use crate::AttesterIncentives; + +type C = DefaultContext; + +pub const TOKEN_NAME: &str = "TEST_TOKEN"; +pub const BOND_AMOUNT: u64 = 1000; +pub const INITIAL_BOND_AMOUNT: u64 = 5 * BOND_AMOUNT; +pub const SALT: u64 = 5; +pub const DEFAULT_ROLLUP_FINALITY: u64 = 3; +pub const INIT_HEIGHT: u64 = 0; + +/// Consumes and commit the existing working set on the underlying storage +/// `storage` must be the underlying storage defined on the working set for this method to work. +pub(crate) fn commit_get_new_working_set( + storage: &ProverStorage, + working_set: WorkingSet<::Storage>, +) -> WorkingSet<::Storage> { + let (reads_writes, witness) = working_set.checkpoint().freeze(); + + storage + .validate_and_commit(reads_writes, &witness) + .expect("Should be able to commit"); + + WorkingSet::new(storage.clone()) +} + +pub(crate) fn create_bank_config_with_token( + token_name: String, + salt: u64, + addresses_count: usize, + initial_balance: u64, +) -> (BankConfig, Vec
) { + let address_and_balances: Vec<(Address, u64)> = (0..addresses_count) + .map(|i| { + let key = format!("key_{}", i); + let addr = generate_address::(&key); + (addr, initial_balance) + }) + .collect(); + + let token_config = TokenConfig { + token_name, + address_and_balances: address_and_balances.clone(), + authorized_minters: vec![address_and_balances.first().unwrap().0], + salt, + }; + + ( + BankConfig { + tokens: vec![token_config], + }, + address_and_balances + .into_iter() + .map(|(addr, _)| addr) + .collect(), + ) +} + +/// Creates a bank config with a token, and a prover incentives module. +/// Returns the prover incentives module and the attester and challenger's addresses. +pub(crate) fn setup( + working_set: &mut WorkingSet<::Storage>, +) -> ( + AttesterIncentives>, + Address, + Address, + Address, +) { + // Initialize bank + let (bank_config, mut addresses) = + create_bank_config_with_token(TOKEN_NAME.to_string(), SALT, 3, INITIAL_BOND_AMOUNT); + let bank = sov_bank::Bank::::default(); + bank.genesis(&bank_config, working_set) + .expect("bank genesis must succeed"); + + let attester_address = addresses.pop().unwrap(); + let challenger_address = addresses.pop().unwrap(); + let reward_supply = addresses.pop().unwrap(); + + let token_address = sov_bank::get_genesis_token_address::(TOKEN_NAME, SALT); + + // Initialize chain state + let chain_state_config = sov_chain_state::ChainStateConfig { + initial_slot_height: INIT_HEIGHT, + }; + + let chain_state = sov_chain_state::ChainState::::default(); + chain_state + .genesis(&chain_state_config, working_set) + .expect("Chain state genesis must succeed"); + + // initialize prover incentives + let module = AttesterIncentives::< + C, + MockZkvm, + MockValidityCond, + MockValidityCondChecker, + >::default(); + let config = crate::AttesterIncentivesConfig { + bonding_token_address: token_address, + reward_token_supply_address: reward_supply, + minimum_attester_bond: BOND_AMOUNT, + minimum_challenger_bond: BOND_AMOUNT, + commitment_to_allowed_challenge_method: MockCodeCommitment([0u8; 32]), + initial_attesters: vec![(attester_address, BOND_AMOUNT)], + rollup_finality_period: DEFAULT_ROLLUP_FINALITY, + maximum_attested_height: INIT_HEIGHT, + light_client_finalized_height: INIT_HEIGHT, + validity_condition_checker: MockValidityCondChecker::::new(), + phantom_data: Default::default(), + }; + + module + .genesis(&config, working_set) + .expect("prover incentives genesis must succeed"); + + (module, token_address, attester_address, challenger_address) +} + +pub(crate) struct ExecutionSimulationVars { + pub state_root: [u8; 32], + pub state_proof: StorageProof::Hasher>>, +} + +/// Generate an execution simulation for a given number of rounds. Returns a list of the successive state roots +/// with associated bonding proofs, as long as the last state root +pub(crate) fn execution_simulation>( + rounds: u8, + module: &AttesterIncentives, + storage: &ProverStorage, + attester_address: ::Address, + mut working_set: WorkingSet<::Storage>, +) -> ( + // Vector of the successive state roots with associated bonding proofs + Vec, + WorkingSet<::Storage>, +) { + let mut ret_exec_vars = Vec::::new(); + + for i in 0..rounds { + // Commit the working set + working_set = commit_get_new_working_set(storage, working_set); + + ret_exec_vars.push(ExecutionSimulationVars { + state_root: storage.get_state_root(&Default::default()).unwrap(), + state_proof: module.get_bond_proof( + attester_address, + &Default::default(), + &mut working_set, + ), + }); + + // Then process the first transaction. Only sets the genesis hash and a transition in progress. + let slot_data = MockBlock { + curr_hash: [i + 1; 32], + header: MockBlockHeader { + prev_hash: MockHash([i; 32]), + }, + height: INIT_HEIGHT + u64::from(i + 1), + validity_cond: MockValidityCond { is_valid: true }, + }; + module + .chain_state + .begin_slot_hook(&slot_data, &mut working_set); + } + + (ret_exec_vars, working_set) +} diff --git a/module-system/module-implementations/sov-attester-incentives/src/tests/invariant.rs b/module-system/module-implementations/sov-attester-incentives/src/tests/invariant.rs new file mode 100644 index 0000000000..2a0ed75a3a --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/tests/invariant.rs @@ -0,0 +1,218 @@ +use sov_modules_api::default_context::DefaultContext; +use sov_rollup_interface::optimistic::Attestation; +use sov_state::{ProverStorage, WorkingSet}; + +use crate::call::AttesterIncentiveErrors; +use crate::tests::helpers::{ + execution_simulation, setup, BOND_AMOUNT, DEFAULT_ROLLUP_FINALITY, INIT_HEIGHT, +}; + +// Test the transition invariant +#[test] +fn test_transition_invariant() { + let tmpdir = tempfile::tempdir().unwrap(); + let storage = ProverStorage::with_path(tmpdir.path()).unwrap(); + let mut working_set = WorkingSet::new(storage.clone()); + let (module, _token_address, attester_address, _) = setup(&mut working_set); + + // Assert that the attester has the correct bond amount before processing the proof + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + BOND_AMOUNT + ); + + // Simulate the execution of a chain, with the genesis hash and two transitions after. + // Update the chain_state module and the optimistic module accordingly + let (exec_vars, mut working_set) = + execution_simulation(20, &module, &storage, attester_address, working_set); + + let context = DefaultContext { + sender: attester_address, + }; + + const NEW_LIGHT_CLIENT_FINALIZED_HEIGHT: u64 = DEFAULT_ROLLUP_FINALITY + INIT_HEIGHT + 1; + + // Update the finalized height and try to prove the INIT_HEIGHT: should fail + module + .light_client_finalized_height + .set(&NEW_LIGHT_CLIENT_FINALIZED_HEIGHT, &mut working_set); + + // Update the initial height + module + .maximum_attested_height + .set(&NEW_LIGHT_CLIENT_FINALIZED_HEIGHT, &mut working_set); + + // Process a valid attestation for the first transition *should fail* + { + let init_height_usize = usize::try_from(INIT_HEIGHT).unwrap(); + let attestation = Attestation { + initial_state_root: exec_vars[init_height_usize].state_root, + da_block_hash: [(init_height_usize + 1).try_into().unwrap(); 32], + post_state_root: exec_vars[init_height_usize + 1].state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: INIT_HEIGHT + 1, + proof: exec_vars[init_height_usize].state_proof.clone(), + }, + }; + + let err = module + .process_attestation(&context, attestation, &mut working_set) + .unwrap_err(); + + assert_eq!( + err, + AttesterIncentiveErrors::InvalidTransitionInvariant, + "Incorrect error raised" + ); + + // The attester should not be slashed + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + BOND_AMOUNT + ); + } + + let new_height = usize::try_from(NEW_LIGHT_CLIENT_FINALIZED_HEIGHT).unwrap(); + + // The attester should be able to process multiple attestations with the same bonding proof + for i in 0..usize::try_from(DEFAULT_ROLLUP_FINALITY + 1).unwrap() { + let old_attestation = Attestation { + initial_state_root: exec_vars[new_height - 1].state_root, + da_block_hash: [(new_height).try_into().unwrap(); 32], + post_state_root: exec_vars[new_height].state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: new_height.try_into().unwrap(), + proof: exec_vars[new_height - 1].state_proof.clone(), + }, + }; + + let new_attestation = Attestation { + initial_state_root: exec_vars[new_height + i - 1].state_root, + da_block_hash: [(new_height + i).try_into().unwrap(); 32], + post_state_root: exec_vars[new_height + i].state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: (new_height + i).try_into().unwrap(), + proof: exec_vars[new_height + i - 1].state_proof.clone(), + }, + }; + + // Testing the transition invariant + // We suppose that these values are always defined, otherwise we panic + let last_height_attested = module + .maximum_attested_height + .get(&mut working_set) + .expect("The maximum attested height should be set at genesis"); + + // Update the max_attested_height in case the blocks have already been finalized + let new_height_to_attest = last_height_attested + 1; + + let min_height = new_height_to_attest.saturating_sub(DEFAULT_ROLLUP_FINALITY); + + // We have to check the following order invariant is respected: + // min_height <= bonding_proof.transition_num <= new_height_to_attest + // If this invariant is respected, we can be sure that the attester was bonded at new_height_to_attest. + let transition_num = old_attestation.proof_of_bond.claimed_transition_num; + + assert!( + min_height <= transition_num, + "The transition number {transition_num} should be above the minimum height {min_height}" + ); + + assert!( + transition_num <= new_height_to_attest, + "The transition number {transition_num} should be below the new max attested height {new_height_to_attest}" + ); + + module + .process_attestation(&context, old_attestation, &mut working_set) + .expect("Should succeed"); + + module + .process_attestation(&context, new_attestation, &mut working_set) + .expect("Should succeed"); + } + + let finality_usize = usize::try_from(DEFAULT_ROLLUP_FINALITY).unwrap(); + + // Now the transition invariant is no longer respected: the transition number is below the minimum height or above the max height + let old_attestation = Attestation { + initial_state_root: exec_vars[new_height].state_root, + da_block_hash: [(new_height + finality_usize + 1).try_into().unwrap(); 32], + post_state_root: exec_vars[new_height + 1].state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: new_height.try_into().unwrap(), + proof: exec_vars[new_height - 1].state_proof.clone(), + }, + }; + + // Testing the transition invariant + // We suppose that these values are always defined, otherwise we panic + let last_height_attested = module + .maximum_attested_height + .get(&mut working_set) + .expect("The maximum attested height should be set at genesis"); + + // Update the max_attested_height in case the blocks have already been finalized + let new_height_to_attest = last_height_attested + 1; + + let min_height = new_height_to_attest.saturating_sub(DEFAULT_ROLLUP_FINALITY); + + let transition_num = old_attestation.proof_of_bond.claimed_transition_num; + + assert!( + min_height > transition_num, + "The transition number {transition_num} should now be below the minimum height {min_height}" + ); + + let err = module + .process_attestation(&context, old_attestation, &mut working_set) + .unwrap_err(); + + assert_eq!( + err, + AttesterIncentiveErrors::InvalidTransitionInvariant, + "The transition invariant is not respected anymore" + ); + + // Now we do the same, except that the proof of bond refers to a transition above the transition to prove + let attestation = Attestation { + initial_state_root: exec_vars[new_height + finality_usize].state_root, + da_block_hash: [(new_height + finality_usize + 1).try_into().unwrap(); 32], + post_state_root: exec_vars[new_height + finality_usize + 1].state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: (new_height + finality_usize + 2).try_into().unwrap(), + proof: exec_vars[new_height + finality_usize + 1] + .state_proof + .clone(), + }, + }; + + let transition_num = attestation.proof_of_bond.claimed_transition_num; + + assert!( + transition_num > new_height_to_attest, + "The transition number {transition_num} should now be below the new height to attest {new_height_to_attest}" + ); + + let err = module + .process_attestation(&context, attestation, &mut working_set) + .unwrap_err(); + + assert_eq!( + err, + AttesterIncentiveErrors::InvalidTransitionInvariant, + "The transition invariant is not respected anymore" + ); +} diff --git a/module-system/module-implementations/sov-attester-incentives/src/tests/mod.rs b/module-system/module-implementations/sov-attester-incentives/src/tests/mod.rs new file mode 100644 index 0000000000..a0a9435ebf --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/tests/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod helpers; + +mod attestation_proccessing; +mod challenger; +mod invariant; +mod unbonding; diff --git a/module-system/module-implementations/sov-attester-incentives/src/tests/unbonding.rs b/module-system/module-implementations/sov-attester-incentives/src/tests/unbonding.rs new file mode 100644 index 0000000000..c5c7c13d2a --- /dev/null +++ b/module-system/module-implementations/sov-attester-incentives/src/tests/unbonding.rs @@ -0,0 +1,165 @@ +use sov_modules_api::default_context::DefaultContext; +use sov_rollup_interface::optimistic::Attestation; +use sov_state::{ProverStorage, WorkingSet}; + +use crate::call::AttesterIncentiveErrors; +use crate::tests::helpers::{ + execution_simulation, setup, BOND_AMOUNT, DEFAULT_ROLLUP_FINALITY, INIT_HEIGHT, +}; + +#[test] +fn test_two_phase_unbonding() { + let tmpdir = tempfile::tempdir().unwrap(); + let storage = ProverStorage::with_path(tmpdir.path()).unwrap(); + let mut working_set = WorkingSet::new(storage.clone()); + let (module, token_address, attester_address, _) = setup(&mut working_set); + + // Assert that the attester has the correct bond amount before processing the proof + assert_eq!( + module + .get_bond_amount( + attester_address, + crate::call::Role::Attester, + &mut working_set + ) + .value, + BOND_AMOUNT + ); + + let context = DefaultContext { + sender: attester_address, + }; + + // Try to skip the first phase of the two phase unbonding. Should fail + { + // Should fail + let err = module + .end_unbond_attester(&context, &mut working_set) + .unwrap_err(); + assert_eq!(err, AttesterIncentiveErrors::AttesterIsNotUnbonding); + } + + // Simulate the execution of a chain, with the genesis hash and two transitions after. + // Update the chain_state module and the optimistic module accordingly + let (mut exec_vars, mut working_set) = + execution_simulation(3, &module, &storage, attester_address, working_set); + + // Start unbonding and then try to prove a transition. User slashed + module + .begin_unbond_attester(&context, &mut working_set) + .expect("Should succeed"); + + let _transition_2 = exec_vars.pop().unwrap(); + let transition_1 = exec_vars.pop().unwrap(); + let initial_transition = exec_vars.pop().unwrap(); + + // Process a valid attestation but get slashed because the attester was trying to unbond. + { + let attestation = Attestation { + initial_state_root: initial_transition.state_root, + da_block_hash: [1; 32], + post_state_root: transition_1.state_root, + proof_of_bond: sov_rollup_interface::optimistic::ProofOfBond { + claimed_transition_num: INIT_HEIGHT + 1, + proof: initial_transition.state_proof, + }, + }; + + let err = module + .process_attestation(&context, attestation, &mut working_set) + .unwrap_err(); + + assert_eq!( + err, + AttesterIncentiveErrors::UserNotBonded, + "The attester should not be bonded" + ); + + // We cannot try to bond either + let err = module + .bond_user_helper( + BOND_AMOUNT, + &attester_address, + crate::call::Role::Attester, + &mut working_set, + ) + .unwrap_err(); + + assert_eq!( + err, + AttesterIncentiveErrors::AttesterIsUnbonding, + "Should raise an AttesterIsUnbonding error" + ); + } + + // Cannot bond again while unbonding + { + let err = module + .bond_user_helper( + BOND_AMOUNT, + &attester_address, + crate::call::Role::Attester, + &mut working_set, + ) + .unwrap_err(); + + assert_eq!( + err, + AttesterIncentiveErrors::AttesterIsUnbonding, + "Should raise that error" + ); + } + + // Now try to complete the two phase unbonding immediately: the second phase should fail because the + // first phase cannot get finalized + { + // Should fail + let err = module + .end_unbond_attester(&context, &mut working_set) + .unwrap_err(); + assert_eq!(err, AttesterIncentiveErrors::UnbondingNotFinalized); + } + + // Now unbond the right way. + { + let initial_account_balance = module + .bank + .get_balance_of(attester_address, token_address, &mut working_set) + .unwrap(); + + // Start unbonding the user: should succeed + module + .begin_unbond_attester(&context, &mut working_set) + .unwrap(); + + let unbonding_info = module + .unbonding_attesters + .get(&attester_address, &mut working_set) + .unwrap(); + + assert_eq!( + unbonding_info.unbonding_initiated_height, INIT_HEIGHT, + "Invalid beginning unbonding height" + ); + + // Wait for the light client to finalize + module + .light_client_finalized_height + .set(&(INIT_HEIGHT + DEFAULT_ROLLUP_FINALITY), &mut working_set); + + // Finish the unbonding: should succeed + module + .end_unbond_attester(&context, &mut working_set) + .unwrap(); + + // Check that the final balance is the same as the initial balance + assert_eq!( + initial_account_balance + BOND_AMOUNT, + module + .bank + .get_balance_of(attester_address, token_address, &mut working_set) + .unwrap(), + "The initial and final account balance don't match" + ); + } +} diff --git a/module-system/module-implementations/sov-bank/src/call.rs b/module-system/module-implementations/sov-bank/src/call.rs index ffaf191f89..377fce707a 100644 --- a/module-system/module-implementations/sov-bank/src/call.rs +++ b/module-system/module-implementations/sov-bank/src/call.rs @@ -139,10 +139,10 @@ impl Bank { /// Mints the `coins` set by the address `minter_address`. If the token address doesn't exist return an error. /// Calls the [`Token::mint`] function and update the `self.tokens` set to store the new minted address. - pub(crate) fn mint( + pub fn mint( &self, - coins: Coins, - minter_address: C::Address, + coins: &Coins, + minter_address: &C::Address, context: &C, working_set: &mut WorkingSet, ) -> Result { @@ -159,7 +159,7 @@ impl Bank { .get_or_err(&coins.token_address, working_set) .with_context(context_logger)?; token - .mint(context.sender(), &minter_address, coins.amount, working_set) + .mint(context.sender(), minter_address, coins.amount, working_set) .with_context(context_logger)?; self.tokens.set(&coins.token_address, &token, working_set); @@ -220,6 +220,20 @@ impl Bank { .with_context(context_logger)?; Ok(CallResponse::default()) } + + /// Helper function used by the rpc method [`balance_of`] to return the balance of the token stored at `token_address` + /// for the user having the address `user_address` from the underlying storage. If the token address doesn't exist, or + /// if the user doesn't have tokens of that type, return `None`. Otherwise, wrap the resulting balance in `Some`. + pub fn get_balance_of( + &self, + user_address: C::Address, + token_address: C::Address, + working_set: &mut WorkingSet, + ) -> Option { + self.tokens + .get(&token_address, working_set) + .and_then(|token| token.balances.get(&user_address, working_set)) + } } /// Creates a new prefix from an already existing prefix `parent_prefix` and a `token_address` diff --git a/module-system/module-implementations/sov-bank/src/lib.rs b/module-system/module-implementations/sov-bank/src/lib.rs index 093b4d2b4c..54db7689ed 100644 --- a/module-system/module-implementations/sov-bank/src/lib.rs +++ b/module-system/module-implementations/sov-bank/src/lib.rs @@ -102,7 +102,7 @@ impl sov_modules_api::Module for Bank { call::CallMessage::Mint { coins, minter_address, - } => Ok(self.mint(coins, minter_address, context, working_set)?), + } => Ok(self.mint(&coins, &minter_address, context, working_set)?), call::CallMessage::Freeze { token_address } => { Ok(self.freeze(token_address, context, working_set)?) diff --git a/module-system/module-implementations/sov-bank/src/query.rs b/module-system/module-implementations/sov-bank/src/query.rs index f9194e17fb..edadefe127 100644 --- a/module-system/module-implementations/sov-bank/src/query.rs +++ b/module-system/module-implementations/sov-bank/src/query.rs @@ -50,19 +50,3 @@ impl Bank { }) } } - -impl Bank { - /// Helper function used by the rpc method [`balance_of`] to return the balance of the token stored at `token_address` - /// for the user having the address `user_address` from the underlying storage. If the token address doesn't exist, or - /// if the user doesn't have tokens of that type, return `None`. Otherwise, wrap the resulting balance in `Some`. - pub fn get_balance_of( - &self, - user_address: C::Address, - token_address: C::Address, - working_set: &mut WorkingSet, - ) -> Option { - self.tokens - .get(&token_address, working_set) - .and_then(|token| token.balances.get(&user_address, working_set)) - } -} diff --git a/module-system/module-implementations/sov-chain-state/src/call.rs b/module-system/module-implementations/sov-chain-state/src/call.rs index 38e3126646..733e9f6bdc 100644 --- a/module-system/module-implementations/sov-chain-state/src/call.rs +++ b/module-system/module-implementations/sov-chain-state/src/call.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use sov_rollup_interface::zk::ValidityCondition; use sov_state::WorkingSet; -use crate::{ChainState, StateTransitionId}; +use crate::{ChainState, StateTransitionId, TransitionHeight}; impl< Ctx: sov_modules_api::Context, @@ -10,18 +10,19 @@ impl< > ChainState { /// Increment the current slot height - pub(crate) fn increment_slot_height(&self, working_set: &mut WorkingSet) { + pub fn increment_slot_height(&self, working_set: &mut WorkingSet) { let current_height = self .slot_height .get(working_set) .expect("Block height must be initialized"); - self.slot_height.set(&(current_height + 1), working_set); + self.slot_height + .set(&(current_height.saturating_add(1)), working_set); } /// Store the previous state transition - pub(crate) fn store_state_transition( + pub fn store_state_transition( &self, - height: u64, + height: TransitionHeight, transition: StateTransitionId, working_set: &mut WorkingSet, ) { diff --git a/module-system/module-implementations/sov-chain-state/src/genesis.rs b/module-system/module-implementations/sov-chain-state/src/genesis.rs index 1262079c1d..ae0dc7bc73 100644 --- a/module-system/module-implementations/sov-chain-state/src/genesis.rs +++ b/module-system/module-implementations/sov-chain-state/src/genesis.rs @@ -10,6 +10,9 @@ impl ChainState { config: &::Config, working_set: &mut WorkingSet, ) -> Result<()> { + self.genesis_height + .set(&config.initial_slot_height, working_set); + self.slot_height .set(&config.initial_slot_height, working_set); Ok(()) diff --git a/module-system/module-implementations/sov-chain-state/src/lib.rs b/module-system/module-implementations/sov-chain-state/src/lib.rs index 0455193d73..008f8f926d 100644 --- a/module-system/module-implementations/sov-chain-state/src/lib.rs +++ b/module-system/module-implementations/sov-chain-state/src/lib.rs @@ -14,7 +14,6 @@ pub mod hooks; pub mod tests; /// The query interface with the module -#[cfg(feature = "native")] pub mod query; use borsh::{BorshDeserialize, BorshSerialize}; @@ -23,6 +22,9 @@ use sov_modules_macros::ModuleInfo; use sov_rollup_interface::zk::{ValidityCondition, ValidityConditionChecker}; use sov_state::WorkingSet; +/// Type alias that contains the height of a given transition +pub type TransitionHeight = u64; + #[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq, Eq)] /// Structure that contains the information needed to represent a single state transition. pub struct StateTransitionId { @@ -64,6 +66,11 @@ impl StateTransitionId { self.da_block_hash } + /// Returns the validity condition associated with the transition + pub fn validity_condition(&self) -> &Cond { + &self.validity_condition + } + /// Checks the validity condition of a state transition pub fn validity_condition_check>( &self, @@ -102,7 +109,7 @@ pub struct ChainState { /// The current block height #[state] - pub slot_height: sov_state::StateValue, + pub slot_height: sov_state::StateValue, /// A record of all previous state transitions which are available to the VM. /// Currently, this includes *all* historical state transitions, but that may change in the future. @@ -110,7 +117,7 @@ pub struct ChainState { /// is stored during transition i+1. This is mainly due to the fact that this structure depends on the /// rollup's root hash which is only stored once the transition has completed. #[state] - pub historical_transitions: sov_state::StateMap>, + pub historical_transitions: sov_state::StateMap>, /// The transition that is currently processed #[state] @@ -120,12 +127,16 @@ pub struct ChainState { /// Set after the first transaction of the rollup is executed, using the `begin_slot` hook. #[state] pub genesis_hash: sov_state::StateValue<[u8; 32]>, + + /// The height of genesis + #[state] + pub genesis_height: sov_state::StateValue, } /// Initial configuration of the chain state pub struct ChainStateConfig { /// Initial slot height - pub initial_slot_height: u64, + pub initial_slot_height: TransitionHeight, } impl sov_modules_api::Module diff --git a/module-system/module-implementations/sov-chain-state/src/query.rs b/module-system/module-implementations/sov-chain-state/src/query.rs index d84b93c2d7..9447b295f1 100644 --- a/module-system/module-implementations/sov-chain-state/src/query.rs +++ b/module-system/module-implementations/sov-chain-state/src/query.rs @@ -2,9 +2,8 @@ use sov_rollup_interface::zk::ValidityCondition; use sov_state::WorkingSet; use super::ChainState; -use crate::{StateTransitionId, TransitionInProgress}; +use crate::{StateTransitionId, TransitionHeight, TransitionInProgress}; -#[derive(serde::Serialize, serde::Deserialize, Debug, Eq, PartialEq)] /// Structure returned by the query methods. pub struct Response { /// Value returned by the queries @@ -14,7 +13,7 @@ pub struct Response { impl ChainState { /// Get the height of the current slot. /// Panics if the slot height is not set - pub fn get_slot_height(&self, working_set: &mut WorkingSet) -> u64 { + pub fn get_slot_height(&self, working_set: &mut WorkingSet) -> TransitionHeight { self.slot_height .get(working_set) .expect("Slot height should be set at initialization") @@ -36,7 +35,7 @@ impl ChainState { /// Returns the completed transition associated with the provided `transition_num`. pub fn get_historical_transitions( &self, - transition_num: u64, + transition_num: TransitionHeight, working_set: &mut WorkingSet, ) -> Option> { self.historical_transitions diff --git a/module-system/module-implementations/sov-chain-state/src/tests.rs b/module-system/module-implementations/sov-chain-state/src/tests.rs index 840c1f1782..424206f2b0 100644 --- a/module-system/module-implementations/sov-chain-state/src/tests.rs +++ b/module-system/module-implementations/sov-chain-state/src/tests.rs @@ -34,7 +34,7 @@ fn test_simple_chain_state() { let mut working_set = WorkingSet::new(storage.clone()); // Check the slot height before any changes to the state. - let initial_height: u64 = chain_state.get_slot_height(&mut working_set); + let initial_height = chain_state.get_slot_height(&mut working_set); assert_eq!( initial_height, INIT_HEIGHT, @@ -60,7 +60,7 @@ fn test_simple_chain_state() { assert_eq!(stored_root, init_root_hash, "Genesis hashes don't match"); // Check that the slot height have been updated - let new_height_storage: u64 = chain_state.get_slot_height(&mut working_set); + let new_height_storage = chain_state.get_slot_height(&mut working_set); assert_eq!( new_height_storage, @@ -100,7 +100,7 @@ fn test_simple_chain_state() { chain_state.begin_slot_hook(&new_slot_data, &mut working_set); // Check that the slot height have been updated correctly - let new_height_storage: u64 = chain_state.get_slot_height(&mut working_set); + let new_height_storage = chain_state.get_slot_height(&mut working_set); assert_eq!( new_height_storage, INIT_HEIGHT + 2, diff --git a/module-system/module-implementations/sov-prover-incentives/src/lib.rs b/module-system/module-implementations/sov-prover-incentives/src/lib.rs index 61d3458e6a..598b65240a 100644 --- a/module-system/module-implementations/sov-prover-incentives/src/lib.rs +++ b/module-system/module-implementations/sov-prover-incentives/src/lib.rs @@ -9,14 +9,13 @@ mod tests; #[cfg(feature = "native")] mod query; -use borsh::{BorshDeserialize, BorshSerialize}; /// The call methods specified in this module pub use call::CallMessage; /// The response type used by RPC queries. #[cfg(feature = "native")] pub use query::Response; use sov_modules_api::{Context, Error, ModuleInfo}; -use sov_rollup_interface::zk::Zkvm; +use sov_rollup_interface::zk::{StoredCodeCommitment, Zkvm}; use sov_state::WorkingSet; /// Configuration of the prover incentives module. Specifies the @@ -34,28 +33,6 @@ pub struct ProverIncentivesConfig { initial_provers: Vec<(C::Address, u64)>, } -/// A wrapper around a code commitment which implements borsh -#[derive(Clone, Debug)] -pub struct StoredCodeCommitment { - commitment: Vm::CodeCommitment, -} - -impl BorshSerialize for StoredCodeCommitment { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - bincode::serialize_into(writer, &self.commitment) - .expect("Serialization to vec is infallible"); - Ok(()) - } -} - -impl BorshDeserialize for StoredCodeCommitment { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let commitment: Vm::CodeCommitment = bincode::deserialize_from(reader) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; - Ok(Self { commitment }) - } -} - /// A new module: /// - Must derive `ModuleInfo` /// - Must contain `[address]` field diff --git a/module-system/sov-modules-api/src/response.rs b/module-system/sov-modules-api/src/response.rs index 1780b9fbbe..19c183784a 100644 --- a/module-system/sov-modules-api/src/response.rs +++ b/module-system/sov-modules-api/src/response.rs @@ -1,3 +1,3 @@ /// Response type for the `Module::call` method. -#[derive(Default)] +#[derive(Default, Debug)] pub struct CallResponse {} diff --git a/module-system/sov-state/src/map.rs b/module-system/sov-state/src/map.rs index 2fae63ac22..c3158e47d0 100644 --- a/module-system/sov-state/src/map.rs +++ b/module-system/sov-state/src/map.rs @@ -19,7 +19,7 @@ where C: StateCodec, { _phantom: (PhantomData, PhantomData), - codec: C, + pub(crate) codec: C, prefix: Prefix, } diff --git a/module-system/sov-state/src/prover_storage.rs b/module-system/sov-state/src/prover_storage.rs index da471bdde8..28fe20839d 100644 --- a/module-system/sov-state/src/prover_storage.rs +++ b/module-system/sov-state/src/prover_storage.rs @@ -7,7 +7,6 @@ use jmt::storage::TreeWriter; use jmt::{JellyfishMerkleTree, KeyHash, RootHash, Version}; use sov_db::state_db::StateDB; -use crate::codec::BorshCodec; use crate::config::Config; use crate::internal_cache::OrderedReadsAndWrites; use crate::storage::{NativeStorage, StorageKey, StorageProof, StorageValue}; @@ -170,9 +169,11 @@ impl Storage for ProverStorage { } impl NativeStorage for ProverStorage { - type ValueWithProof = (Option, Self::Proof); - - fn get_with_proof(&self, key: StorageKey, _witness: &Self::Witness) -> Self::ValueWithProof { + fn get_with_proof( + &self, + key: StorageKey, + _witness: &Self::Witness, + ) -> StorageProof { let merkle = JellyfishMerkleTree::::new(&self.db); let (val_opt, proof) = merkle .get_with_proof( @@ -180,10 +181,11 @@ impl NativeStorage for ProverStorage { self.db.get_next_version() - 1, ) .unwrap(); - ( - val_opt.as_ref().map(|v| StorageValue::new(v, &BorshCodec)), + StorageProof { + key, + value: val_opt.map(StorageValue::from), proof, - ) + } } } diff --git a/module-system/sov-state/src/storage.rs b/module-system/sov-state/src/storage.rs index ed3080a0fe..eaa3b50835 100644 --- a/module-system/sov-state/src/storage.rs +++ b/module-system/sov-state/src/storage.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use std::sync::Arc; +use anyhow::ensure; use borsh::{BorshDeserialize, BorshSerialize}; use hex; use serde::de::DeserializeOwned; @@ -11,7 +12,7 @@ use crate::codec::{StateKeyCodec, StateValueCodec}; use crate::internal_cache::OrderedReadsAndWrites; use crate::utils::AlignedVec; use crate::witness::Witness; -use crate::Prefix; +use crate::{Prefix, StateMap}; // `Key` type for the `Storage` #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, BorshDeserialize, BorshSerialize)] @@ -165,6 +166,24 @@ pub trait Storage: Clone { proof: StorageProof, ) -> Result<(StorageKey, Option), anyhow::Error>; + fn verify_proof + StateValueCodec>( + &self, + state_root: [u8; 32], + proof: StorageProof, + expected_key: &K, + storage_map: &StateMap, + ) -> Result, anyhow::Error> { + let (storage_key, storage_value) = self.open_proof(state_root, proof)?; + + // We have to check that the storage key is the same as the external key + ensure!( + storage_key == StorageKey::new(storage_map.prefix(), expected_key, &storage_map.codec), + "The storage key from the proof doesn't match the expected storage key." + ); + + Ok(storage_value) + } + /// Indicates if storage is empty or not. /// Useful during initialization fn is_empty(&self) -> bool; @@ -191,10 +210,20 @@ impl From<&'static str> for StorageValue { } pub trait NativeStorage: Storage { - /// The object returned by `get_with_proof`. Should contain the returned value and the associated proof - type ValueWithProof; - /// Returns the value corresponding to the key or None if key is absent and a proof to /// get the value. Panics if [`get_with_proof_opt`] returns `None` in place of the proof. - fn get_with_proof(&self, key: StorageKey, witness: &Self::Witness) -> Self::ValueWithProof; + fn get_with_proof(&self, key: StorageKey, witness: &Self::Witness) + -> StorageProof; + + fn get_with_proof_from_state_map + StateValueCodec>( + &self, + key: &K, + state_map: &StateMap, + witness: &Self::Witness, + ) -> StorageProof { + self.get_with_proof( + StorageKey::new(state_map.prefix(), key, &state_map.codec), + witness, + ) + } } diff --git a/rollup-interface/Cargo.toml b/rollup-interface/Cargo.toml index 6c9f752bd9..5ed436c1f7 100644 --- a/rollup-interface/Cargo.toml +++ b/rollup-interface/Cargo.toml @@ -26,6 +26,10 @@ hex = { workspace = true, features = ["serde"] } digest = { workspace = true } sha2 = { workspace = true, optional = true } +# TODO: Replace with serde-compatible borsh implementation when it becomes availabile +# see https://github.com/Sovereign-Labs/sovereign-sdk/issues/215 +bincode = { workspace = true } + anyhow = { workspace = true } # Proptest should be a dev-dependency, but those can't be optional diff --git a/rollup-interface/src/state_machine/mocks/mod.rs b/rollup-interface/src/state_machine/mocks/mod.rs index 5cac31e40c..6f7b250380 100644 --- a/rollup-interface/src/state_machine/mocks/mod.rs +++ b/rollup-interface/src/state_machine/mocks/mod.rs @@ -8,5 +8,5 @@ pub use da::{ MockAddress, MockBatchBuilder, MockBlob, MockBlock, MockBlockHeader, MockDaService, MockDaSpec, MockHash, }; -pub use validity_condition::MockValidityCond; +pub use validity_condition::{MockValidityCond, MockValidityCondChecker}; pub use zk_vm::{MockCodeCommitment, MockProof, MockZkvm}; diff --git a/rollup-interface/src/state_machine/mocks/validity_condition.rs b/rollup-interface/src/state_machine/mocks/validity_condition.rs index efa462a548..250afb5329 100644 --- a/rollup-interface/src/state_machine/mocks/validity_condition.rs +++ b/rollup-interface/src/state_machine/mocks/validity_condition.rs @@ -25,13 +25,13 @@ impl ValidityCondition for MockValidityCond { } } -#[derive(Debug, BorshDeserialize, BorshSerialize)] +#[derive(BorshDeserialize, BorshSerialize, Debug)] /// A mock validity condition checker that always evaluate to cond -pub struct TestValidityCondChecker { - phantom: PhantomData, +pub struct MockValidityCondChecker { + phantom: PhantomData, } -impl ValidityConditionChecker for TestValidityCondChecker { +impl ValidityConditionChecker for MockValidityCondChecker { type Error = Error; fn check(&mut self, condition: &MockValidityCond) -> Result<(), Self::Error> { @@ -42,3 +42,18 @@ impl ValidityConditionChecker for TestValidityCondChecker MockValidityCondChecker { + /// Creates new test validity condition + pub fn new() -> Self { + Self { + phantom: Default::default(), + } + } +} + +impl Default for MockValidityCondChecker { + fn default() -> Self { + Self::new() + } +} diff --git a/rollup-interface/src/state_machine/mocks/zk_vm.rs b/rollup-interface/src/state_machine/mocks/zk_vm.rs index bcea23df81..e066c0fc10 100644 --- a/rollup-interface/src/state_machine/mocks/zk_vm.rs +++ b/rollup-interface/src/state_machine/mocks/zk_vm.rs @@ -5,7 +5,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use crate::zk::{Matches, Zkvm}; -use crate::AddressTrait; /// A mock commitment to a particular zkVM program. #[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] @@ -78,13 +77,6 @@ impl Zkvm for MockZkvm { anyhow::ensure!(proof.is_valid, "Proof is not valid"); Ok(proof.log) } - - fn verify_and_extract_output( - _serialized_proof: &[u8], - _code_commitment: &Self::CodeCommitment, - ) -> Result, Self::Error> { - todo!("Need to specify an output format for the proof logs") - } } #[test] diff --git a/rollup-interface/src/state_machine/mod.rs b/rollup-interface/src/state_machine/mod.rs index 52196cd6fe..170ee09a4a 100644 --- a/rollup-interface/src/state_machine/mod.rs +++ b/rollup-interface/src/state_machine/mod.rs @@ -12,6 +12,8 @@ use serde::Serialize; #[cfg(feature = "mocks")] pub mod mocks; +pub mod optimistic; + /// A marker trait for addresses. pub trait AddressTrait: PartialEq diff --git a/rollup-interface/src/state_machine/optimistic.rs b/rollup-interface/src/state_machine/optimistic.rs new file mode 100644 index 0000000000..84668f7dfe --- /dev/null +++ b/rollup-interface/src/state_machine/optimistic.rs @@ -0,0 +1,46 @@ +//! Utilities for building an optimistic state machine +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use crate::zk::StateTransition; + +/// A proof that the attester was bonded at the transition num `transition_num`. +/// For rollups using the `jmt`, this will be a `jmt::SparseMerkleProof` +#[derive( + Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize, Default, +)] +pub struct ProofOfBond { + /// The transition number for which the proof of bond applies + pub claimed_transition_num: u64, + /// The actual state proof that the attester was bonded + pub proof: StateProof, +} + +/// An attestation that a particular DA layer block transitioned the rollup state to some value +#[derive( + Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize, Default, +)] +pub struct Attestation { + /// The alleged state root before applying the contents of the da block + pub initial_state_root: [u8; 32], + /// The hash of the block in which the transition occurred + pub da_block_hash: [u8; 32], + /// The alleged post-state root + pub post_state_root: [u8; 32], + /// A proof that the attester was bonded at some point in time before the attestation is generated + pub proof_of_bond: ProofOfBond, +} + +/// The contents of a challenge to an attestation, which are contained as a public output of the proof +/// Generic over an address type and a validity condition +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +pub struct ChallengeContents { + /// The rollup address of the originator of this challenge + pub challenger_address: Address, + /// The state transition that was proven + pub state_transition: StateTransition, +} + +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, Serialize, Deserialize)] +/// This struct contains the challenge as a raw blob +pub struct Challenge<'a>(&'a [u8]); diff --git a/rollup-interface/src/state_machine/zk/mod.rs b/rollup-interface/src/state_machine/zk/mod.rs index bb2672415e..f022ffd3a0 100644 --- a/rollup-interface/src/state_machine/zk/mod.rs +++ b/rollup-interface/src/state_machine/zk/mod.rs @@ -32,7 +32,7 @@ pub trait Zkvm { + DeserializeOwned; /// The error type which is returned when a proof fails to verify - type Error: Debug; + type Error: Debug + From; /// Interpret a sequence of a bytes as a proof and attempt to verify it against the code commitment. /// If the proof is valid, return a reference to the public outputs of the proof. @@ -43,10 +43,40 @@ pub trait Zkvm { /// Same as [`verify`], except that instead of returning the output as a serialized array, /// it returns a state transition structure. - fn verify_and_extract_output( + /// TODO: specify a deserializer for the output + fn verify_and_extract_output< + C: ValidityCondition, + Add: AddressTrait + BorshDeserialize + BorshSerialize, + >( serialized_proof: &[u8], code_commitment: &Self::CodeCommitment, - ) -> Result, Self::Error>; + ) -> Result, Self::Error> { + let mut output = Self::verify(serialized_proof, code_commitment)?; + Ok(BorshDeserialize::deserialize_reader(&mut output)?) + } +} + +/// A wrapper around a code commitment which implements borsh serialization +#[derive(Clone, Debug)] +pub struct StoredCodeCommitment { + /// The inner field of the wrapper that contains the code commitment. + pub commitment: Vm::CodeCommitment, +} + +impl BorshSerialize for StoredCodeCommitment { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + bincode::serialize_into(writer, &self.commitment) + .expect("Serialization to vec is infallible"); + Ok(()) + } +} + +impl BorshDeserialize for StoredCodeCommitment { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let commitment: Vm::CodeCommitment = bincode::deserialize_from(reader) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + Ok(Self { commitment }) + } } /// A trait which is accessible from within a zkVM program. @@ -59,7 +89,7 @@ pub trait ZkvmGuest: Zkvm { /// This trait is implemented on the struct/enum which expresses the validity condition pub trait ValidityCondition: - Serialize + DeserializeOwned + BorshDeserialize + BorshSerialize + Debug + Clone + Copy + Serialize + DeserializeOwned + BorshDeserialize + BorshSerialize + Debug + Clone + Copy + PartialEq { /// The error type returned when two [`ValidityCondition`]s cannot be combined. type Error: Into; @@ -73,7 +103,7 @@ pub trait ValidityCondition: /// if and only if the condition `validity_condition` is satisfied. /// /// The period of time covered by a state transition proof may be a single slot, or a range of slots on the DA layer. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq)] pub struct StateTransition { /// The state of the rollup before the transition pub initial_state_root: [u8; 32],