From 4492c931cc1e3cefe1f98abc1cd765617bc0799d Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Mon, 8 Jan 2024 10:07:09 +0000 Subject: [PATCH] feat(ink): improvements --- ink/core/types.rs | 10 +- ink/proxy/Cargo.toml | 3 + ink/proxy/lib.rs | 52 ++++++---- ink/state/lib.rs | 47 ++++++--- ink/validator/Cargo.toml | 3 + ink/validator/lib.rs | 118 ++++++++++++++++++++--- pallets/hyperdrive/src/ethereum_tests.rs | 2 +- 7 files changed, 179 insertions(+), 56 deletions(-) diff --git a/ink/core/types.rs b/ink/core/types.rs index 1f235657..f4741e6b 100644 --- a/ink/core/types.rs +++ b/ink/core/types.rs @@ -10,7 +10,7 @@ pub enum Version { V1 = 1, } -#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] pub struct IncomingAction { pub id: u64, pub payload: VersionedIncomingActionPayload, @@ -34,25 +34,25 @@ impl PartialOrd for IncomingAction { } } -#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] pub enum VersionedIncomingActionPayload { V1(IncomingActionPayloadV1), } -#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] pub enum IncomingActionPayloadV1 { AssignJobProcessor(AssignProcessorPayloadV1), FinalizeJob(FinalizeJobPayloadV1), Noop, } -#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] pub struct AssignProcessorPayloadV1 { pub job_id: u128, pub processor: [u8; 32], } -#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] pub struct FinalizeJobPayloadV1 { pub job_id: u128, pub unused_reward: u128, diff --git a/ink/proxy/Cargo.toml b/ink/proxy/Cargo.toml index 0305389f..66991904 100755 --- a/ink/proxy/Cargo.toml +++ b/ink/proxy/Cargo.toml @@ -20,6 +20,9 @@ acurast-state-ink = { path = "../state", default-features = false, features = [" acurast-validator-ink = { path = "../validator", default-features = false, features = ["ink-as-dependency"] } acurast-consumer-ink = { path = "../consumer", default-features = false, features = ["ink-as-dependency"] } +[dev-dependencies] +hex-literal = "0.4.1" + [lib] path = "lib.rs" diff --git a/ink/proxy/lib.rs b/ink/proxy/lib.rs index 03017500..6630a6b7 100755 --- a/ink/proxy/lib.rs +++ b/ink/proxy/lib.rs @@ -95,28 +95,9 @@ mod proxy { } fn decode_incoming_action(payload: &Vec) -> Result { - match RawIncomingAction::decode(&mut payload.as_slice()) { + match IncomingAction::decode(&mut payload.as_slice()) { Err(err) => Err(Error::InvalidIncomingAction(format!("{:?}", err))), - Ok(action) => Ok(IncomingAction { - id: action.id, - payload: decode_versioned_incoming_action_payload(action)?, - }), - } - } - - fn decode_versioned_incoming_action_payload( - action: RawIncomingAction, - ) -> Result { - match action.payload_version { - v if v == Version::V1 as u16 => { - let action = IncomingActionPayloadV1::decode(&mut action.payload.as_slice()) - .map_err(|err| { - Error::Verbose(format!("Cannot decode incoming action V1 {:?}", err)) - })?; - - Ok(VersionedIncomingActionPayload::V1(action)) - } - v => Err(Error::UnknownIncomingActionVersion(v)), + Ok(action) => Ok(action), } } @@ -786,6 +767,11 @@ mod proxy { // Views // + #[ink(message)] + pub fn is_action_processed(&self, action_id: u64) -> bool { + self.processed_incoming_actions.contains(action_id) + } + /// The purpose of this method is to generate proofs for outgoing actions #[ink(message)] pub fn generate_proof(&self, from: u64, to: u64) -> Result, Error> { @@ -851,4 +837,28 @@ mod proxy { } } } + + #[cfg(test)] + mod tests { + use hex_literal::hex; + + /// Imports all the definitions from the outer scope so we can use them here. + use super::*; + + #[ink::test] + fn test_action_encoding() { + let encoded_incoming_action = hex!("00000000000000000002"); + + let decoded_incoming_action = + IncomingAction::decode(&mut encoded_incoming_action.as_slice()); + + assert_eq!( + decoded_incoming_action.unwrap(), + IncomingAction { + id: 0, + payload: VersionedIncomingActionPayload::V1(IncomingActionPayloadV1::Noop) + } + ); + } + } } diff --git a/ink/state/lib.rs b/ink/state/lib.rs index d221ddd1..1066f4e7 100755 --- a/ink/state/lib.rs +++ b/ink/state/lib.rs @@ -370,8 +370,11 @@ pub mod state_aggregator { } } - fn fail_if_not_owner(&self) { - assert!(self.config.owner.eq(&self.env().caller()), "NOT_OWNER"); + fn fail_if_not_owner(&self) -> Result<(), Error> { + if self.config.owner.eq(&self.env().caller()) { + return Ok(()); + } + Err(Error::NotAllowed) } fn finalize_snapshot(&mut self, required: bool) { @@ -385,7 +388,6 @@ pub mod state_aggregator { if self.snapshot_start_level + self.config.snapshot_duration < current_block_number { // Finalize snapshot - self.snapshot_counter += 1; // Snapshot previous block level let snapshot_level = current_block_number - 1; @@ -400,15 +402,17 @@ pub mod state_aggregator { snapshot: self.snapshot_counter, level: snapshot_level, }); + + self.snapshot_counter += 1; } else { assert!(!required, "CANNOT_SNAPSHOT"); } } #[ink(message)] - pub fn configure(&mut self, configure: Vec) { + pub fn configure(&mut self, configure: Vec) -> Result<(), Error> { // Only the administrator can configure the contract - self.fail_if_not_owner(); + self.fail_if_not_owner()?; for c in configure { match c { @@ -421,6 +425,8 @@ pub mod state_aggregator { } } } + + Ok(()) } #[ink(message)] @@ -468,8 +474,13 @@ pub mod state_aggregator { } #[ink(message)] - pub fn mmr_size(&self) -> u64 { - self.mmr_size + pub fn next_snapshot(&self) -> u128 { + self.snapshot_counter + } + + #[ink(message)] + pub fn snapshot_level(&self, snapshot: u128) -> u32 { + self.snapshot_level.get(snapshot).expect("UNKNOWN_SNAPSHOT") } #[ink(message)] @@ -478,6 +489,15 @@ pub mod state_aggregator { mmr.get_root(&self.tree).expect("COULD_NOT_GET_ROOT") } + + #[ink(message)] + pub fn can_snapshot(&self) -> bool { + let current_block = Self::env().block_number(); + + let not_empty = self.mmr_size > 0; + + not_empty && (self.snapshot_start_level + self.config.snapshot_duration) < current_block + } } #[cfg(test)] @@ -498,7 +518,6 @@ pub mod state_aggregator { } #[ink::test] - #[should_panic(expected = "NOT_OWNER")] fn test_unauthorized_configure() { let accounts = ink::env::test::default_accounts::(); let admin = accounts.alice; @@ -510,9 +529,11 @@ pub mod state_aggregator { ink::env::test::set_caller::(accounts.bob); // (Panic Expected) : Only the admin can call the configure method - state_aggregator.configure(vec![ConfigureArgument::SetAuthorizedProvider( - data_provider, - )]); + assert!(state_aggregator + .configure(vec![ConfigureArgument::SetAuthorizedProvider( + data_provider, + )]) + .is_err()); } #[ink::test] @@ -531,7 +552,7 @@ pub mod state_aggregator { ink::env::test::set_caller::(admin); // (Panic Expected) : Only the admin can call the configure method - state_aggregator.configure(vec![ConfigureArgument::SetAuthorizedProvider( + let _ = state_aggregator.configure(vec![ConfigureArgument::SetAuthorizedProvider( data_provider, )]); } @@ -547,7 +568,7 @@ pub mod state_aggregator { // Set data provider ink::env::test::set_caller::(admin); - state_aggregator.configure(vec![ConfigureArgument::SetAuthorizedProvider( + let _ = state_aggregator.configure(vec![ConfigureArgument::SetAuthorizedProvider( data_provider, )]); diff --git a/ink/validator/Cargo.toml b/ink/validator/Cargo.toml index e146ca10..d40a91d7 100755 --- a/ink/validator/Cargo.toml +++ b/ink/validator/Cargo.toml @@ -12,6 +12,9 @@ scale-info = { version = "2.6", default-features = false, features = ["derive"], ckb-merkle-mountain-range = { version = "0.6.0", default-features = false } +[dev-dependencies] +hex-literal = "0.4.1" + [lib] path = "lib.rs" diff --git a/ink/validator/lib.rs b/ink/validator/lib.rs index f4c2146c..9580368e 100755 --- a/ink/validator/lib.rs +++ b/ink/validator/lib.rs @@ -55,12 +55,22 @@ pub mod validator { const MAX_VALIDATORS: usize = 50; + #[derive(Decode, Encode, Debug)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum ConfigureArgument { + SetOwner(AccountId), + SetValidators(Vec), + SetMinimumEndorsements(u16), + } + /// Errors returned by the contract's methods. #[derive(Debug, PartialEq, Eq, Encode, Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum Error { ProofInvalid(String), SnapshotUnknown, + SnapshotInvalid, + NotAllowed, } /// A custom type that we can use in our contract storage @@ -68,7 +78,7 @@ pub mod validator { #[derive(Debug)] pub struct Config { /// Multi-sig address allowed to manage the contract - governance_address: AccountId, + owner: AccountId, /// Minimum expected endorsements for a given state root to be considered valid minimum_endorsements: u16, /// Validators @@ -102,7 +112,7 @@ pub mod validator { validators.dedup(); contract.config.validators = validators; - contract.config.governance_address = admin; + contract.config.owner = admin; contract.config.minimum_endorsements = minimum_endorsements; contract } @@ -111,7 +121,7 @@ pub mod validator { pub fn default() -> Self { Self { config: Config { - governance_address: AccountId::from([0x0; 32]), + owner: AccountId::from([0x0; 32]), minimum_endorsements: 0, validators: vec![], }, @@ -122,8 +132,11 @@ pub mod validator { } } - fn fail_if_not_validator(&self, account: &AccountId) { - assert!(self.config.validators.contains(account), "NOT_ALLOWED"); + fn fail_if_not_validator(&self) -> Result<(), Error> { + if self.config.validators.contains(&self.env().caller()) { + return Ok(()); + } + Err(Error::NotAllowed) } fn validate_block_state_root(&self) -> Option<[u8; 32]> { @@ -158,15 +171,44 @@ pub mod validator { Some(selected_candidate) } + fn fail_if_not_owner(&self) -> Result<(), Error> { + if self.config.owner.eq(&self.env().caller()) { + return Ok(()); + } + Err(Error::NotAllowed) + } + + #[ink(message)] + pub fn configure(&mut self, configure: Vec) -> Result<(), Error> { + // Only the administrator can configure the contract + self.fail_if_not_owner()?; + + for c in configure { + match c { + ConfigureArgument::SetOwner(address) => self.config.owner = address, + ConfigureArgument::SetMinimumEndorsements(minimum_endorsements) => { + self.config.minimum_endorsements = minimum_endorsements + } + ConfigureArgument::SetValidators(validators) => { + self.config.validators = validators + } + } + } + + Ok(()) + } + #[ink(message)] - pub fn submit_root(&mut self, snapshot: u128, root: [u8; 32]) { - let caller = Self::env().caller(); + pub fn submit_root(&mut self, snapshot: u128, root: [u8; 32]) -> Result<(), Error> { + let caller = self.env().caller(); // Check if sender is a validator - Self::fail_if_not_validator(self, &caller); + Self::fail_if_not_validator(self)?; // Make sure the snapshots are submitted sequencially - assert!(self.current_snapshot == snapshot, "INVALID_SNAPSHOT"); + if self.current_snapshot != snapshot { + return Err(Error::SnapshotInvalid); + } if !self.snapshot_submissions.contains(caller) { self.snapshot_submissions_accounts.push(caller); @@ -183,6 +225,8 @@ pub mod validator { self.current_snapshot += 1; self.snapshot_submissions = Default::default(); } + + Ok(()) } // @@ -225,17 +269,25 @@ pub mod validator { } } } + + #[ink(message)] + pub fn current_snapshot(&self) -> u128 { + self.current_snapshot + } + + #[ink(message)] + pub fn snapshot_submissions_accounts(&self) -> Vec { + self.snapshot_submissions_accounts.clone() + } } - /// Unit tests in Rust are normally defined within such a `#[cfg(test)]` - /// module and test functions are marked with a `#[test]` attribute. - /// The below code is technically just normal Rust code. #[cfg(test)] mod tests { + use hex_literal::hex; + /// Imports all the definitions from the outer scope so we can use them here. use super::*; - /// We test if the default constructor does its job. #[ink::test] fn test_constructor() { let accounts = ink::env::test::default_accounts::(); @@ -248,13 +300,12 @@ pub mod validator { minimum_endorsements.clone(), validators.clone(), ); - assert_eq!(validator.config.governance_address, admin); + assert_eq!(validator.config.owner, admin); assert_eq!(validator.config.minimum_endorsements, minimum_endorsements); assert_eq!(validator.config.validators, validators); assert_eq!(validator.current_snapshot, 1); } - /// We test a simple use case of our contract. #[ink::test] fn test_submit_root() { let accounts = ink::env::test::default_accounts::(); @@ -268,7 +319,7 @@ pub mod validator { let snapshot_root = [0; 32]; ink::env::test::set_caller::(admin); - validator.submit_root(snapshot_number, snapshot_root); + let _ = validator.submit_root(snapshot_number, snapshot_root); assert_eq!(validator.current_snapshot, 2); assert_eq!(validator.snapshot_submissions_accounts.len(), 1); @@ -276,5 +327,40 @@ pub mod validator { assert_eq!(validator.validate_block_state_root(), Some(snapshot_root)); } + + /// We test a simple use case of our contract. + #[ink::test] + fn test_verify_proof() { + let accounts = ink::env::test::default_accounts::(); + let admin = accounts.alice; + let minimum_endorsements: u16 = 1; + let validators: Vec = vec![admin]; + + let mut validator = Validator::new(admin, minimum_endorsements, validators); + + let snapshot_number = 1; + let snapshot_root = + hex!("f805950edaf6f0ee75cf7ba469c2ea381667f1b75d5bfacf1749500448019049"); + + ink::env::test::set_caller::(admin); + let _ = validator.submit_root(snapshot_number, snapshot_root); + + let proof = MerkleProof { + mmr_size: 3, + proof: vec![hex!( + "79dd2180cc76e44fd7d3b6d1c89b9dfae07800741f7d36837d64bedd7300ed2e" + )], + leaves: vec![ + LeafProof { + leaf_index: 1, + data: hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000ff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000").to_vec() + } + ], + }; + + let proof_validation = validator.verify_proof(snapshot_number, proof); + + assert_eq!(proof_validation, Ok(true)); + } } } diff --git a/pallets/hyperdrive/src/ethereum_tests.rs b/pallets/hyperdrive/src/ethereum_tests.rs index 7c6b8776..60be7bb8 100644 --- a/pallets/hyperdrive/src/ethereum_tests.rs +++ b/pallets/hyperdrive/src/ethereum_tests.rs @@ -3,7 +3,7 @@ use frame_support::assert_ok; use hex_literal::hex; use sp_core::H256; -use sp_runtime::{bounded_vec, AccountId32}; +use sp_runtime::bounded_vec; use std::marker::PhantomData; use crate::chain::ethereum::{