diff --git a/ink/Cargo.toml b/ink/Cargo.toml new file mode 100644 index 0000000..028d123 --- /dev/null +++ b/ink/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +resolver = "2" +members = [ + "crates/phat_rollup_anchor_ink", + "contracts/test_oracle", +] \ No newline at end of file diff --git a/ink/contracts/test_oracle/Cargo.toml b/ink/contracts/test_oracle/Cargo.toml new file mode 100755 index 0000000..16c130e --- /dev/null +++ b/ink/contracts/test_oracle/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "test_oracle" +version = "0.0.1" +authors = ["GuiGou"] +edition = "2021" + +[dependencies] +ink = { version = "4.2", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } + +openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.1.1", features = ["ownable", "access_control"], default-features = false } + +phat_rollup_anchor_ink = { path = "../../crates/phat_rollup_anchor_ink", default-features = false} + +[dev-dependencies] +ink_e2e = { version = "4.2" } +hex-literal = { version = "0.4.1" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + "openbrush/std", + "phat_rollup_anchor_ink/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/ink/contracts/test_oracle/README.md b/ink/contracts/test_oracle/README.md new file mode 100644 index 0000000..4c08af6 --- /dev/null +++ b/ink/contracts/test_oracle/README.md @@ -0,0 +1,37 @@ +# Test Oracle + +Implements a simple oracle to get/display the price of trading pairs. It uses the crate `phat_rollup_anchor_ink`. +It supports: + - create a trading pair with an id and the token names. The name must match with the API id from CoinGecko. By example: `polkadot`, `astar`, `pha`, `usd`. Only an address granted as `MANAGER` can do it. + - configure the attestor authorized to send the prices. Only an address granted as `MANAGER` can do it. + - send a request to get the price of a given trading pair. Only an address granted as `MANAGER` can do it. + - handle the messages to feed the trading pair. Only an address granted as `ATTESTOR` can do it. + - display the trading pair with this id. + - allow meta transactions to separate the attestor and the payer. + +By default, the contract owner is granted as `MANAGER` but it is not granted as `ATTESTOR`. + +## Build + +To build the contract: + +```bash +cargo contract build +``` + +## Run e2e tests + +Before you can run the test, you have to install a Substrate node with pallet-contracts. By default, `e2e tests` require that you install `substrate-contracts-node`. You do not need to run it in the background since the node is started for each test independently. To install the latest version: +```bash +cargo install contracts-node --git https://github.com/paritytech/substrate-contracts-node.git +``` + +If you want to run any other node with pallet-contracts you need to change `CONTRACTS_NODE` environment variable: +```bash +export CONTRACTS_NODE="YOUR_CONTRACTS_NODE_PATH" +``` + +And finally execute the following command to start e2e tests execution. +```bash +cargo test --features e2e-tests +``` diff --git a/ink/contracts/test_oracle/lib.rs b/ink/contracts/test_oracle/lib.rs new file mode 100755 index 0000000..ff6a2c5 --- /dev/null +++ b/ink/contracts/test_oracle/lib.rs @@ -0,0 +1,957 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![feature(min_specialization)] + +#[openbrush::contract] +pub mod test_oracle { + use ink::codegen::{EmitEvent, Env}; + use ink::prelude::string::String; + use ink::prelude::vec::Vec; + use ink::storage::Mapping; + use openbrush::contracts::access_control::*; + use openbrush::contracts::ownable::*; + use openbrush::traits::Storage; + use scale::{Decode, Encode}; + + use phat_rollup_anchor_ink::impls::{ + kv_store, kv_store::*, message_queue, message_queue::*, meta_transaction, + meta_transaction::*, rollup_anchor, rollup_anchor::*, + }; + + pub type TradingPairId = u32; + + /// Events emitted when a price is received + #[ink(event)] + pub struct PriceReceived { + trading_pair_id: TradingPairId, + price: u128, + } + + /// Events emitted when a error is received + #[ink(event)] + pub struct ErrorReceived { + trading_pair_id: TradingPairId, + err_no: u128, + } + + /// Errors occurred in the contract + #[derive(Encode, Decode, Debug)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum ContractError { + AccessControlError(AccessControlError), + MessageQueueError(MessageQueueError), + MissingTradingPair, + } + /// convertor from MessageQueueError to ContractError + impl From for ContractError { + fn from(error: MessageQueueError) -> Self { + ContractError::MessageQueueError(error) + } + } + /// convertor from MessageQueueError to ContractError + impl From for ContractError { + fn from(error: AccessControlError) -> Self { + ContractError::AccessControlError(error) + } + } + + /// Message to request the price of the trading pair + /// message pushed in the queue by this contract and read by the offchain rollup + #[derive(Encode, Decode)] + struct PriceRequestMessage { + /// id of the pair (use as key in the Mapping) + trading_pair_id: TradingPairId, + /// trading pair like 'polkdatot/usd' + /// Note: it will be better to not save this data in the storage + token0: String, + token1: String, + } + /// Message sent to provide the price of the trading pair + /// response pushed in the queue by the offchain rollup and read by this contract + #[derive(Encode, Decode)] + struct PriceResponseMessage { + /// Type of response + resp_type: u8, + /// id of the pair + trading_pair_id: TradingPairId, + /// price of the trading pair + price: Option, + /// when the price is read + err_no: Option, + } + + /// Type of response when the offchain rollup communicates with this contract + const TYPE_ERROR: u8 = 0; + const TYPE_RESPONSE: u8 = 10; + const TYPE_FEED: u8 = 11; + + /// Data storage + #[derive(Encode, Decode, Default, Eq, PartialEq, Clone, Debug)] + #[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) + )] + pub struct TradingPair { + /// trading pair like 'polkdatot/usd' + /// Note: it will be better to not save this data outside of the storage + token0: String, + token1: String, + /// value of the trading pair + value: u128, + /// number of updates of the value + nb_updates: u16, + /// when the last value has been updated + last_update: u64, + } + + #[ink(storage)] + #[derive(Default, Storage)] + pub struct TestOracle { + #[storage_field] + ownable: ownable::Data, + #[storage_field] + access: access_control::Data, + #[storage_field] + kv_store: kv_store::Data, + #[storage_field] + meta_transaction: meta_transaction::Data, + trading_pairs: Mapping, + } + + impl Ownable for TestOracle {} + impl AccessControl for TestOracle {} + impl KVStore for TestOracle {} + impl MessageQueue for TestOracle {} + impl MetaTxReceiver for TestOracle {} + impl RollupAnchor for TestOracle {} + + impl TestOracle { + #[ink(constructor)] + pub fn new() -> Self { + let mut instance = Self::default(); + let caller = instance.env().caller(); + // set the owner of this contract + instance._init_with_owner(caller); + // set the admin of this contract + instance._init_with_admin(caller); + // grant the role manager to teh given address + instance + .grant_role(MANAGER_ROLE, caller) + .expect("Should grant the role manager"); + instance + } + + #[ink(message)] + #[openbrush::modifiers(access_control::only_role(MANAGER_ROLE))] + pub fn create_trading_pair( + &mut self, + trading_pair_id: TradingPairId, + token0: String, + token1: String, + ) -> Result<(), ContractError> { + // we create a new trading pair or override an existing one + let trading_pair = TradingPair { + token0, + token1, + value: 0, + nb_updates: 0, + last_update: 0, + }; + self.trading_pairs.insert(trading_pair_id, &trading_pair); + Ok(()) + } + + #[ink(message)] + #[openbrush::modifiers(access_control::only_role(MANAGER_ROLE))] + pub fn request_price( + &mut self, + trading_pair_id: TradingPairId, + ) -> Result { + let index = match self.trading_pairs.get(trading_pair_id) { + Some(t) => { + // push the message in the queue + let message = PriceRequestMessage { + trading_pair_id, + token0: t.token0, + token1: t.token1, + }; + self._push_message(&message)? + } + _ => return Err(ContractError::MissingTradingPair), + }; + + Ok(index) + } + + #[ink(message)] + pub fn get_trading_pair(&self, trading_pair_id: TradingPairId) -> Option { + self.trading_pairs.get(trading_pair_id) + } + + #[ink(message)] + pub fn register_attestor( + &mut self, + account_id: AccountId, + ecdsa_public_key: [u8; 33], + ) -> Result<(), RollupAnchorError> { + self.grant_role(ATTESTOR_ROLE, account_id)?; + self.register_ecdsa_public_key(account_id, ecdsa_public_key)?; + Ok(()) + } + + #[ink(message)] + pub fn get_attestor_role(&self) -> RoleType { + ATTESTOR_ROLE + } + + #[ink(message)] + pub fn get_manager_role(&self) -> RoleType { + MANAGER_ROLE + } + } + + impl rollup_anchor::Internal for TestOracle { + fn _on_message_received(&mut self, action: Vec) -> Result<(), RollupAnchorError> { + // parse the response + let message: PriceResponseMessage = + Decode::decode(&mut &action[..]).or(Err(RollupAnchorError::FailedToDecode))?; + + // handle the response + if message.resp_type == TYPE_RESPONSE || message.resp_type == TYPE_FEED { + // we received the price + // register the info + let mut trading_pair = self + .trading_pairs + .get(message.trading_pair_id) + .unwrap_or_default(); + trading_pair.value = message.price.unwrap_or_default(); + trading_pair.nb_updates += 1; + trading_pair.last_update = self.env().block_timestamp(); + self.trading_pairs + .insert(message.trading_pair_id, &trading_pair); + + // emmit te event + self.env().emit_event(PriceReceived { + trading_pair_id: message.trading_pair_id, + price: message.price.unwrap_or_default(), + }); + } else if message.resp_type == TYPE_ERROR { + // we received an error + self.env().emit_event(ErrorReceived { + trading_pair_id: message.trading_pair_id, + err_no: message.err_no.unwrap_or_default(), + }); + } else { + // response type unknown + return Err(RollupAnchorError::UnsupportedAction); + } + + Ok(()) + } + + fn _emit_event_meta_tx_decoded(&self) { + self.env().emit_event(MetaTxDecoded {}); + } + } + + /// Events emitted when a meta transaction is decoded + #[ink(event)] + pub struct MetaTxDecoded {} + + /// Events emitted when a message is pushed in the queue + #[ink(event)] + pub struct MessageQueued { + pub id: u32, + pub data: Vec, + } + + /// Events emitted when a message is proceed + #[ink(event)] + pub struct MessageProcessedTo { + pub id: u32, + } + + impl message_queue::Internal for TestOracle { + fn _emit_event_message_queued(&self, id: u32, data: Vec) { + self.env().emit_event(MessageQueued { id, data }); + } + + fn _emit_event_message_processed_to(&self, id: u32) { + self.env().emit_event(MessageProcessedTo { id }); + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use openbrush::contracts::access_control::accesscontrol_external::AccessControl; + + use ink_e2e::{build_message, PolkadotConfig}; + use phat_rollup_anchor_ink::impls::{ + meta_transaction::metatxreceiver_external::MetaTxReceiver, + rollup_anchor::rollupanchor_external::RollupAnchor, + }; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn test_create_trading_pair(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let trading_pair_id = 10; + + // read the trading pair and check it doesn't exist yet + let get_trading_pair = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.get_trading_pair(trading_pair_id)); + let get_res = client + .call_dry_run(&ink_e2e::bob(), &get_trading_pair, 0, None) + .await; + assert_eq!(None, get_res.return_value()); + + // bob is not granted as manager => it should not be able to create the trading pair + let create_trading_pair = + build_message::(contract_acc_id.clone()).call(|oracle| { + oracle.create_trading_pair( + trading_pair_id, + String::from("polkadot"), + String::from("usd"), + ) + }); + let result = client + .call(&ink_e2e::bob(), create_trading_pair, 0, None) + .await; + assert!( + result.is_err(), + "only manager should not be able to create trading pair" + ); + + // bob is granted as manager + let bob_address = + ink::primitives::AccountId::from(ink_e2e::bob::().account_id().0); + let grant_role = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.grant_role(MANAGER_ROLE, bob_address)); + client + .call(&ink_e2e::alice(), grant_role, 0, None) + .await + .expect("grant bob as attestor failed"); + + // create the trading pair + let create_trading_pair = + build_message::(contract_acc_id.clone()).call(|oracle| { + oracle.create_trading_pair( + trading_pair_id, + String::from("polkadot"), + String::from("usd"), + ) + }); + client + .call(&ink_e2e::bob(), create_trading_pair, 0, None) + .await + .expect("create trading pair failed"); + + // then check if the trading pair exists + let get_trading_pair = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.get_trading_pair(trading_pair_id)); + let get_res = client + .call_dry_run(&ink_e2e::bob(), &get_trading_pair, 0, None) + .await; + let expected_trading_pair = TradingPair { + token0: String::from("polkadot"), + token1: String::from("usd"), + value: 0, + nb_updates: 0, + last_update: 0, + }; + assert_eq!(Some(expected_trading_pair), get_res.return_value()); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_feed_price(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let trading_pair_id = 10; + + // create the trading pair + let create_trading_pair = + build_message::(contract_acc_id.clone()).call(|oracle| { + oracle.create_trading_pair( + trading_pair_id, + String::from("polkadot"), + String::from("usd"), + ) + }); + client + .call(&ink_e2e::alice(), create_trading_pair, 0, None) + .await + .expect("create trading pair failed"); + + // bob is granted as attestor + let bob_address = + ink::primitives::AccountId::from(ink_e2e::bob::().account_id().0); + let grant_role = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.grant_role(ATTESTOR_ROLE, bob_address)); + client + .call(&ink_e2e::alice(), grant_role, 0, None) + .await + .expect("grant bob as attestor failed"); + + // then bob feeds the price + let value: u128 = 150_000_000_000_000_000_000; + let payload = PriceResponseMessage { + resp_type: TYPE_FEED, + trading_pair_id, + price: Some(value), + err_no: None, + }; + let actions = vec![HandleActionInput { + action_type: ACTION_REPLY, + id: None, + action: Some(payload.encode()), + address: None, + }]; + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(vec![], vec![], actions.clone())); + let result = client + .call(&ink_e2e::bob(), rollup_cond_eq, 0, None) + .await + .expect("rollup cond eq failed"); + // events PriceReceived + assert!(result.contains_event("Contracts", "ContractEmitted")); + + // and check if the price is filled + let get_trading_pair = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.get_trading_pair(trading_pair_id)); + let get_res = client + .call_dry_run(&ink_e2e::bob(), &get_trading_pair, 0, None) + .await; + let trading_pair = get_res.return_value().expect("Trading pair not found"); + + assert_eq!(value, trading_pair.value); + assert_eq!(1, trading_pair.nb_updates); + assert_ne!(0, trading_pair.last_update); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_receive_reply(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let trading_pair_id = 10; + + // create the trading pair + let create_trading_pair = + build_message::(contract_acc_id.clone()).call(|oracle| { + oracle.create_trading_pair( + trading_pair_id, + String::from("polkadot"), + String::from("usd"), + ) + }); + client + .call(&ink_e2e::alice(), create_trading_pair, 0, None) + .await + .expect("create trading pair failed"); + + // bob is granted as attestor + let bob_address = + ink::primitives::AccountId::from(ink_e2e::bob::().account_id().0); + let grant_role = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.grant_role(ATTESTOR_ROLE, bob_address)); + client + .call(&ink_e2e::alice(), grant_role, 0, None) + .await + .expect("grant bob as attestor failed"); + + // a price request is sent + let request_price = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.request_price(trading_pair_id)); + let result = client + .call(&ink_e2e::alice(), request_price, 0, None) + .await + .expect("Request price should be sent"); + // event MessageQueued + assert!(result.contains_event("Contracts", "ContractEmitted")); + + let request_id = result.return_value().expect("Request id not found"); + + // then a response is received + let value: u128 = 150_000_000_000_000_000_000; + let payload = PriceResponseMessage { + resp_type: TYPE_RESPONSE, + trading_pair_id, + price: Some(value), + err_no: None, + }; + let actions = vec![ + HandleActionInput { + action_type: ACTION_REPLY, + id: None, + action: Some(payload.encode()), + address: None, + }, + HandleActionInput { + action_type: ACTION_SET_QUEUE_HEAD, + id: Some(request_id + 1), + action: None, + address: None, + }, + ]; + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(vec![], vec![], actions.clone())); + let result = client + .call(&ink_e2e::bob(), rollup_cond_eq, 0, None) + .await + .expect("rollup cond eq should be ok"); + // two events : MessageProcessedTo and PricesRecieved + assert!(result.contains_event("Contracts", "ContractEmitted")); + + // and check if the price is filled + let get_trading_pair = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.get_trading_pair(trading_pair_id)); + let get_res = client + .call_dry_run(&ink_e2e::bob(), &get_trading_pair, 0, None) + .await; + let trading_pair = get_res.return_value().expect("Trading pair not found"); + + assert_eq!(value, trading_pair.value); + assert_eq!(1, trading_pair.nb_updates); + assert_ne!(0, trading_pair.last_update); + + // reply in the future should fail + let actions = vec![ + HandleActionInput { + action_type: ACTION_REPLY, + id: None, + action: Some(payload.encode()), + address: None, + }, + HandleActionInput { + action_type: ACTION_SET_QUEUE_HEAD, + id: Some(request_id + 2), + action: None, + address: None, + }, + ]; + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(vec![], vec![], actions.clone())); + let result = client.call(&ink_e2e::bob(), rollup_cond_eq, 0, None).await; + assert!( + result.is_err(), + "Rollup should fail because we try to pop in the future" + ); + + // reply in the past should fail + let actions = vec![ + HandleActionInput { + action_type: ACTION_REPLY, + id: None, + action: Some(payload.encode()), + address: None, + }, + HandleActionInput { + action_type: ACTION_SET_QUEUE_HEAD, + id: Some(request_id), + action: None, + address: None, + }, + ]; + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(vec![], vec![], actions.clone())); + let result = client.call(&ink_e2e::bob(), rollup_cond_eq, 0, None).await; + assert!( + result.is_err(), + "Rollup should fail because we try to pop in the past" + ); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_receive_error(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let trading_pair_id = 10; + + // create the trading pair + let create_trading_pair = + build_message::(contract_acc_id.clone()).call(|oracle| { + oracle.create_trading_pair( + trading_pair_id, + String::from("polkadot"), + String::from("usd"), + ) + }); + client + .call(&ink_e2e::alice(), create_trading_pair, 0, None) + .await + .expect("create trading pair failed"); + + // bob is granted as attestor + let bob_address = + ink::primitives::AccountId::from(ink_e2e::bob::().account_id().0); + let grant_role = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.grant_role(ATTESTOR_ROLE, bob_address)); + client + .call(&ink_e2e::alice(), grant_role, 0, None) + .await + .expect("grant bob as attestor failed"); + + // a price request is sent + let request_price = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.request_price(trading_pair_id)); + let result = client + .call(&ink_e2e::alice(), request_price, 0, None) + .await + .expect("Request price should be sent"); + // event : MessageQueued + assert!(result.contains_event("Contracts", "ContractEmitted")); + + let request_id = result.return_value().expect("Request id not found"); + + // then a response is received + let payload = PriceResponseMessage { + resp_type: TYPE_ERROR, + trading_pair_id, + price: None, + err_no: Some(12356), + }; + let actions = vec![ + HandleActionInput { + action_type: ACTION_REPLY, + id: None, + action: Some(payload.encode()), + address: None, + }, + HandleActionInput { + action_type: ACTION_SET_QUEUE_HEAD, + id: Some(request_id + 1), + action: None, + address: None, + }, + ]; + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(vec![], vec![], actions.clone())); + let result = client + .call(&ink_e2e::bob(), rollup_cond_eq, 0, None) + .await + .expect("we should proceed error message"); + // two events : MessageProcessedTo and PricesReceived + assert!(result.contains_event("Contracts", "ContractEmitted")); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_bad_attestor(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + // bob is not granted as attestor => it should not be able to send a message + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(vec![], vec![], vec![])); + let result = client.call(&ink_e2e::bob(), rollup_cond_eq, 0, None).await; + assert!( + result.is_err(), + "only attestor should be able to send messages" + ); + + // bob is granted as attestor + let bob_address = + ink::primitives::AccountId::from(ink_e2e::bob::().account_id().0); + let grant_role = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.grant_role(ATTESTOR_ROLE, bob_address)); + client + .call(&ink_e2e::alice(), grant_role, 0, None) + .await + .expect("grant bob as attestor failed"); + + // then bob is abel to send a message + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(vec![], vec![], vec![])); + let result = client + .call(&ink_e2e::bob(), rollup_cond_eq, 0, None) + .await + .expect("rollup cond eq failed"); + // no event + assert!(!result.contains_event("Contracts", "ContractEmitted")); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_bad_messages(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let trading_pair_id = 10; + + // create the trading pair + let create_trading_pair = + build_message::(contract_acc_id.clone()).call(|oracle| { + oracle.create_trading_pair( + trading_pair_id, + String::from("polkadot"), + String::from("usd"), + ) + }); + client + .call(&ink_e2e::alice(), create_trading_pair, 0, None) + .await + .expect("create trading pair failed"); + + // bob is granted as attestor + let bob_address = + ink::primitives::AccountId::from(ink_e2e::bob::().account_id().0); + let grant_role = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.grant_role(ATTESTOR_ROLE, bob_address)); + client + .call(&ink_e2e::alice(), grant_role, 0, None) + .await + .expect("grant bob as attestor failed"); + + let actions = vec![HandleActionInput { + action_type: ACTION_REPLY, + id: None, + action: Some(58u128.encode()), + address: None, + }]; + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(vec![], vec![], actions.clone())); + let result = client.call(&ink_e2e::bob(), rollup_cond_eq, 0, None).await; + assert!( + result.is_err(), + "we should not be able to proceed bad messages" + ); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_optimistic_locking(mut client: ink_e2e::Client) -> E2EResult<()> { + // given + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + // bob is granted as attestor + let bob_address = + ink::primitives::AccountId::from(ink_e2e::bob::().account_id().0); + let grant_role = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.grant_role(ATTESTOR_ROLE, bob_address)); + client + .call(&ink_e2e::alice(), grant_role, 0, None) + .await + .expect("grant bob as attestor failed"); + + // then bob sends a message + // from v0 to v1 => it's ok + let conditions = vec![(123u8.encode(), None)]; + let updates = vec![(123u8.encode(), Some(1u128.encode()))]; + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(conditions.clone(), updates.clone(), vec![])); + let result = client.call(&ink_e2e::bob(), rollup_cond_eq, 0, None).await; + result.expect("This message should be proceed because the condition is met"); + + // test idempotency it should fail because the conditions are not met + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(conditions.clone(), updates.clone(), vec![])); + let result = client.call(&ink_e2e::bob(), rollup_cond_eq, 0, None).await; + assert!( + result.is_err(), + "This message should not be proceed because the condition is not met" + ); + + // from v1 to v2 => it's ok + let conditions = vec![(123u8.encode(), Some(1u128.encode()))]; + let updates = vec![(123u8.encode(), Some(2u128.encode()))]; + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(conditions.clone(), updates.clone(), vec![])); + let result = client.call(&ink_e2e::bob(), rollup_cond_eq, 0, None).await; + result.expect("This message should be proceed because the condition is met"); + + // test idempotency it should fail because the conditions are not met + let rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.rollup_cond_eq(conditions.clone(), updates.clone(), vec![])); + let result = client.call(&ink_e2e::bob(), rollup_cond_eq, 0, None).await; + assert!( + result.is_err(), + "This message should not be proceed because the condition is not met" + ); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_prepare_meta_tx(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::bob(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + // register the ecda public key because I am not able to retrieve if from the account id + // Alice + let from = + ink::primitives::AccountId::from(ink_e2e::alice::().account_id().0); + let ecdsa_public_key: [u8; 33] = hex_literal::hex!( + "037051bed73458951b45ca6376f4096c85bf1a370da94d5336d04867cfaaad019e" + ); + + let register_ecdsa_public_key = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.register_ecdsa_public_key(from, ecdsa_public_key)); + client + .call(&ink_e2e::bob(), register_ecdsa_public_key, 0, None) + .await + .expect("We should be able to register the ecdsa public key"); + + // prepare the meta transaction + let data = u8::encode(&5); + let prepare_meta_tx = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.prepare(from, data.clone())); + let result = client + .call(&ink_e2e::bob(), prepare_meta_tx, 0, None) + .await + .expect("We should be able to prepare the meta tx"); + + let (request, hash) = result + .return_value() + .expect("Expected value when preparing meta tx"); + + assert_eq!(0, request.nonce); + assert_eq!(from, request.from); + assert_eq!(&data, &request.data); + + let expected_hash = hex_literal::hex!( + "17cb4f6eae2f95ba0fbaee9e0e51dc790fe752e7386b72dcd93b9669450c2ccf" + ); + assert_eq!(&expected_hash, &hash.as_ref()); + + Ok(()) + } + + /// + /// Test the meta transactions + /// Charlie is the owner + /// Alice is the attestor + /// Bob is the sender (ie the payer) + /// + #[ink_e2e::test] + async fn test_meta_tx_rollup_cond_eq(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = TestOracleRef::new(); + let contract_acc_id = client + .instantiate("test_oracle", &ink_e2e::charlie(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + // register the ecda public key because I am not able to retrieve if from the account id + // Alice is the attestor + let from = + ink::primitives::AccountId::from(ink_e2e::alice::().account_id().0); + let ecdsa_public_key: [u8; 33] = hex_literal::hex!( + "037051bed73458951b45ca6376f4096c85bf1a370da94d5336d04867cfaaad019e" + ); + + let register_ecdsa_public_key = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.register_ecdsa_public_key(from, ecdsa_public_key)); + client + .call(&ink_e2e::charlie(), register_ecdsa_public_key, 0, None) + .await + .expect("We should be able to register the ecdsa public key"); + + // add the role => it should be succeed + let grant_role = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.grant_role(ATTESTOR_ROLE, from)); + client + .call(&ink_e2e::charlie(), grant_role, 0, None) + .await + .expect("grant the attestor failed"); + + // prepare the meta transaction + let data = RolupCondEqMethodParams::encode(&(vec![], vec![], vec![])); + let prepare_meta_tx = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.prepare(from, data.clone())); + let result = client + .call(&ink_e2e::bob(), prepare_meta_tx, 0, None) + .await + .expect("We should be able to prepare the meta tx"); + + let (request, hash) = result + .return_value() + .expect("Expected value when preparing meta tx"); + + assert_eq!(0, request.nonce); + assert_eq!(from, request.from); + assert_eq!(&data, &request.data); + + let expected_hash = hex_literal::hex!( + "c91f57305dc05a66f1327352d55290a250eb61bba8e3cf8560a4b8e7d172bb54" + ); + assert_eq!(&expected_hash, &hash.as_ref()); + + // signature by Alice of previous hash + let signature : [u8; 65] = hex_literal::hex!("c9a899bc8daa98fd1e819486c57f9ee889d035e8d0e55c04c475ca32bb59389b284d18d785a9db1bdd72ce74baefe6a54c0aa2418b14f7bc96232fa4bf42946600"); + + // do the meta tx + let meta_tx_rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.meta_tx_rollup_cond_eq(request.clone(), signature)); + client + .call(&ink_e2e::bob(), meta_tx_rollup_cond_eq, 0, None) + .await + .expect("meta tx rollup cond eq should not failed"); + + // do it again => it must failed + let meta_tx_rollup_cond_eq = build_message::(contract_acc_id.clone()) + .call(|oracle| oracle.meta_tx_rollup_cond_eq(request.clone(), signature)); + let result = client + .call(&ink_e2e::bob(), meta_tx_rollup_cond_eq, 0, None) + .await; + assert!( + result.is_err(), + "This message should not be proceed because the nonce is obsolete" + ); + + Ok(()) + } + } +} diff --git a/ink/crates/phat_rollup_anchor_ink/Cargo.toml b/ink/crates/phat_rollup_anchor_ink/Cargo.toml new file mode 100644 index 0000000..be32dd3 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "phat_rollup_anchor_ink" +version = "0.0.1" +authors = ["GuiGou"] +edition = "2021" + +[dependencies] +ink = { version = "4.2.0", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } +kv-session = { package = "pink-kv-session", version = "0.2" } + +openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.1.1", features = ["ownable", "access_control"], default-features = false } + +[dev-dependencies] +hex-literal = "0.4.1" + +[lib] +path = "src/lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + "openbrush/std", +] \ No newline at end of file diff --git a/ink/crates/phat_rollup_anchor_ink/README.md b/ink/crates/phat_rollup_anchor_ink/README.md new file mode 100644 index 0000000..3cdee87 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/README.md @@ -0,0 +1,504 @@ +# Phat Rollup Anchor for Ink smart contract + +Library for Ink! smart contract to help you build [Phat Rollup Anchor ](https://github.com/Phala-Network/phat-offchain-rollup/ +)deployed on the Substrate pallet Contracts. +This library uses the [OpenBrush](https://learn.brushfam.io/docs/OpenBrush) library with teh features `ownable` and `access_control` +It provides the following traits and the default implementations for: + - kv_store: key-value store that allows offchain Phat Contracts to perform read/write operations. + - message_queue: Message Queue, enabling a request-response programming model for the smart-contract while ensuring that each request received exactly one response. The default implementation of `message_queue` requires the implementation of the `kv_strore` trait. + - meta_transactions : Allow the offchain Phat Contract to do transactions without paying the gas fee. The fee will be paid by a third party (the relayer). + - rollup_anchor: Use the kv-store and the message queue to allow offchain's rollup transactions. The default implementation of `rollup_anchor` requires the implementation of the traits `kv_strore`, `message_queue`, `meta_transactions` and `access_control::AccessControl`. + + +## Build the crate + +To build the crate: + +```bash +cargo build +``` +## Run the unit tests + +To run the unit tests: + +```bash +cargo test +``` + +## Use this crate in your library + +### Add the dependencies + +The default toml of your project + +```toml +[dependencies] +ink = { version = "4.2.0", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } + +# OpenBrush dependency +openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.1.1", features = ["ownable", "access_control"], default-features = false } + +# Phat Rollup Anchor dependency +phat_rollup_anchor_ink = { path = "phat-rollup-anchor-ink", default-features = false} + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + "openbrush/std", + "phat_rollup_anchor_ink/std", +] +``` + +### Add imports and enable unstable feature + +Use `openbrush::contract` macro instead of `ink::contract`. Import everything from `openbrush::contracts::access_control`, `openbrush::contracts::ownable`, `phat_rollup_anchor_ink::impls::kv_store`, `phat_rollup_anchor_ink::impls::message_queue`, `phat_rollup_anchor_ink::impls::meta_transaction`, `phat_rollup_anchor_ink::impls::rollup_anchor`. + +```rust +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![feature(min_specialization)] + +#[openbrush::contract] +pub mod test_oracle { + + use openbrush::contracts::access_control::*; + use openbrush::contracts::ownable::*; + use openbrush::traits::Storage; + use scale::{Decode, Encode}; + + use phat_rollup_anchor_ink::impls::{ + kv_store, kv_store::*, + message_queue, message_queue::*, + meta_transaction, meta_transaction::*, + rollup_anchor, rollup_anchor::* + }; +... +``` + +### Define storage + +Declare storage struct and declare the fields related to the modules. + +```rust +#[ink(storage)] +#[derive(Default, Storage)] +pub struct TestOracle { + #[storage_field] + ownable: ownable::Data, + #[storage_field] + access: access_control::Data, + #[storage_field] + kv_store: kv_store::Data, + #[storage_field] + meta_transaction: meta_transaction::Data, + ... +} +``` + +### Inherit logic +Inherit implementation of the traits. You can customize (override) methods in this `impl` block. + +```rust +impl Ownable for TestOracle {} +impl AccessControl for TestOracle {} +impl KVStore for TestOracle {} +impl MessageQueue for TestOracle {} +impl MetaTxReceiver for TestOracle {} +impl RollupAnchor for TestOracle {} +``` + +### Define constructor +```rust +impl TestOracle { + #[ink(constructor)] + pub fn new() -> Self { + let mut instance = Self::default(); + let caller = instance.env().caller(); + // set the owner of this contract + instance._init_with_owner(caller); + // set the admin of this contract + instance._init_with_admin(caller); + instance + } +} +``` + +### Internal Traits + +### Internal trait for the message queue +Implement the `message_queue::Internal` trait to emit the events when a message is pushed in the queue and when a message is proceed. +If you don't want to emit the events, you can put an empty block in the methods `_emit_event_message_queued` and `_emit_event_message_processed_to`. + +```rust +/// Events emitted when a message is pushed in the queue +#[ink(event)] +pub struct MessageQueued { + pub id: u32, + pub data: Vec, +} + +/// Events emitted when a message is proceed +#[ink(event)] +pub struct MessageProcessedTo { + pub id: u32, +} + +impl message_queue::Internal for TestOracle { + + fn _emit_event_message_queued(&self, id: u32, data: Vec){ + self.env().emit_event(MessageQueued { id, data }); + } + + fn _emit_event_message_processed_to(&self, id: u32){ + self.env().emit_event(MessageProcessedTo { id }); + } + +} +``` +### Internal trait for the rollup anchor +Implement the `rollup_anchor::Internal` trait to put your business logic when a message is received. +Here an example when the Oracle receives a message with the price feed. + +```rust +impl rollup_anchor::Internal for TestOracle { + fn _on_message_received(&mut self, action: Vec) -> Result<(), RollupAnchorError> { + + // parse the response + let message: PriceResponseMessage = Decode::decode(&mut &action[..]) + .or(Err(RollupAnchorError::FailedToDecode))?; + + // handle the response + if message.resp_type == TYPE_RESPONSE || message.resp_type == TYPE_FEED { // we received the price + // register the info + let mut trading_pair = self.trading_pairs.get(&message.trading_pair_id).unwrap_or_default(); + trading_pair.value = message.price.unwrap_or_default(); + trading_pair.nb_updates += 1; + trading_pair.last_update = self.env().block_timestamp(); + self.trading_pairs.insert(&message.trading_pair_id, &trading_pair); + + // emmit te event + self.env().emit_event( + PriceReceived { + trading_pair_id: message.trading_pair_id, + price: message.price.unwrap_or_default(), + } + ); + + } else if message.resp_type == TYPE_ERROR { // we received an error + self.env().emit_event( + ErrorReceived { + trading_pair_id: message.trading_pair_id, + err_no: message.err_no.unwrap_or_default() + } + ); + } else { + // response type unknown + return Err(RollupAnchorError::UnsupportedAction); + } + + Ok(()) + } + + fn _emit_event_meta_tx_decoded(&self) { + self.env().emit_event(MetaTxDecoded {}); + } +} + +/// Events emitted when a meta transaction is decoded +#[ink(event)] +pub struct MetaTxDecoded {} + +``` + +### Final code +Here the final code of the Price Oracle. + +```rust +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![feature(min_specialization)] + +#[openbrush::contract] +pub mod test_oracle { + use ink::codegen::{EmitEvent, Env}; + use ink::prelude::string::String; + use ink::prelude::vec::Vec; + use ink::storage::Mapping; + use openbrush::contracts::access_control::*; + use openbrush::contracts::ownable::*; + use openbrush::traits::Storage; + use scale::{Decode, Encode}; + + use phat_rollup_anchor_ink::impls::{ + kv_store, kv_store::*, message_queue, message_queue::*, meta_transaction, + meta_transaction::*, rollup_anchor, rollup_anchor::*, + }; + + pub type TradingPairId = u32; + + /// Events emitted when a price is received + #[ink(event)] + pub struct PriceReceived { + trading_pair_id: TradingPairId, + price: u128, + } + + /// Events emitted when a error is received + #[ink(event)] + pub struct ErrorReceived { + trading_pair_id: TradingPairId, + err_no: u128, + } + + /// Errors occurred in the contract + #[derive(Encode, Decode, Debug)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum ContractError { + AccessControlError(AccessControlError), + MessageQueueError(MessageQueueError), + MissingTradingPair, + } + /// convertor from MessageQueueError to ContractError + impl From for ContractError { + fn from(error: MessageQueueError) -> Self { + ContractError::MessageQueueError(error) + } + } + /// convertor from MessageQueueError to ContractError + impl From for ContractError { + fn from(error: AccessControlError) -> Self { + ContractError::AccessControlError(error) + } + } + + /// Message to request the price of the trading pair + /// message pushed in the queue by this contract and read by the offchain rollup + #[derive(Encode, Decode)] + struct PriceRequestMessage { + /// id of the pair (use as key in the Mapping) + trading_pair_id: TradingPairId, + /// trading pair like 'polkdatot/usd' + /// Note: it will be better to not save this data in the storage + token0: String, + token1: String, + } + /// Message sent to provide the price of the trading pair + /// response pushed in the queue by the offchain rollup and read by this contract + #[derive(Encode, Decode)] + struct PriceResponseMessage { + /// Type of response + resp_type: u8, + /// id of the pair + trading_pair_id: TradingPairId, + /// price of the trading pair + price: Option, + /// when the price is read + err_no: Option, + } + + /// Type of response when the offchain rollup communicates with this contract + const TYPE_ERROR: u8 = 0; + const TYPE_RESPONSE: u8 = 10; + const TYPE_FEED: u8 = 11; + + /// Data storage + #[derive(Encode, Decode, Default, Eq, PartialEq, Clone, Debug)] + #[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) + )] + pub struct TradingPair { + /// trading pair like 'polkdatot/usd' + /// Note: it will be better to not save this data outside of the storage + token0: String, + token1: String, + /// value of the trading pair + value: u128, + /// number of updates of the value + nb_updates: u16, + /// when the last value has been updated + last_update: u64, + } + + #[ink(storage)] + #[derive(Default, Storage)] + pub struct TestOracle { + #[storage_field] + ownable: ownable::Data, + #[storage_field] + access: access_control::Data, + #[storage_field] + kv_store: kv_store::Data, + #[storage_field] + meta_transaction: meta_transaction::Data, + trading_pairs: Mapping, + } + + impl Ownable for TestOracle {} + impl AccessControl for TestOracle {} + impl KVStore for TestOracle {} + impl MessageQueue for TestOracle {} + impl MetaTxReceiver for TestOracle {} + impl RollupAnchor for TestOracle {} + + impl TestOracle { + #[ink(constructor)] + pub fn new() -> Self { + let mut instance = Self::default(); + let caller = instance.env().caller(); + // set the owner of this contract + instance._init_with_owner(caller); + // set the admin of this contract + instance._init_with_admin(caller); + // grant the role manager to teh given address + instance + .grant_role(MANAGER_ROLE, caller) + .expect("Should grant the role manager"); + instance + } + + #[ink(message)] + #[openbrush::modifiers(access_control::only_role(MANAGER_ROLE))] + pub fn create_trading_pair( + &mut self, + trading_pair_id: TradingPairId, + token0: String, + token1: String, + ) -> Result<(), ContractError> { + // we create a new trading pair or override an existing one + let trading_pair = TradingPair { + token0, + token1, + value: 0, + nb_updates: 0, + last_update: 0, + }; + self.trading_pairs.insert(trading_pair_id, &trading_pair); + Ok(()) + } + + #[ink(message)] + #[openbrush::modifiers(access_control::only_role(MANAGER_ROLE))] + pub fn request_price( + &mut self, + trading_pair_id: TradingPairId, + ) -> Result { + let index = match self.trading_pairs.get(trading_pair_id) { + Some(t) => { + // push the message in the queue + let message = PriceRequestMessage { + trading_pair_id, + token0: t.token0, + token1: t.token1, + }; + self._push_message(&message)? + } + _ => return Err(ContractError::MissingTradingPair), + }; + + Ok(index) + } + + #[ink(message)] + pub fn get_trading_pair(&self, trading_pair_id: TradingPairId) -> Option { + self.trading_pairs.get(trading_pair_id) + } + + #[ink(message)] + pub fn register_attestor( + &mut self, + account_id: AccountId, + ecdsa_public_key: [u8; 33], + ) -> Result<(), RollupAnchorError> { + self.grant_role(ATTESTOR_ROLE, account_id)?; + self.register_ecdsa_public_key(account_id, ecdsa_public_key)?; + Ok(()) + } + + #[ink(message)] + pub fn get_attestor_role(&self) -> RoleType { + ATTESTOR_ROLE + } + + #[ink(message)] + pub fn get_manager_role(&self) -> RoleType { + MANAGER_ROLE + } + } + + impl rollup_anchor::Internal for TestOracle { + fn _on_message_received(&mut self, action: Vec) -> Result<(), RollupAnchorError> { + // parse the response + let message: PriceResponseMessage = + Decode::decode(&mut &action[..]).or(Err(RollupAnchorError::FailedToDecode))?; + + // handle the response + if message.resp_type == TYPE_RESPONSE || message.resp_type == TYPE_FEED { + // we received the price + // register the info + let mut trading_pair = self + .trading_pairs + .get(message.trading_pair_id) + .unwrap_or_default(); + trading_pair.value = message.price.unwrap_or_default(); + trading_pair.nb_updates += 1; + trading_pair.last_update = self.env().block_timestamp(); + self.trading_pairs + .insert(message.trading_pair_id, &trading_pair); + + // emmit te event + self.env().emit_event(PriceReceived { + trading_pair_id: message.trading_pair_id, + price: message.price.unwrap_or_default(), + }); + } else if message.resp_type == TYPE_ERROR { + // we received an error + self.env().emit_event(ErrorReceived { + trading_pair_id: message.trading_pair_id, + err_no: message.err_no.unwrap_or_default(), + }); + } else { + // response type unknown + return Err(RollupAnchorError::UnsupportedAction); + } + + Ok(()) + } + + fn _emit_event_meta_tx_decoded(&self) { + self.env().emit_event(MetaTxDecoded {}); + } + } + + /// Events emitted when a meta transaction is decoded + #[ink(event)] + pub struct MetaTxDecoded {} + + /// Events emitted when a message is pushed in the queue + #[ink(event)] + pub struct MessageQueued { + pub id: u32, + pub data: Vec, + } + + /// Events emitted when a message is proceed + #[ink(event)] + pub struct MessageProcessedTo { + pub id: u32, + } + + impl message_queue::Internal for TestOracle { + fn _emit_event_message_queued(&self, id: u32, data: Vec) { + self.env().emit_event(MessageQueued { id, data }); + } + + fn _emit_event_message_processed_to(&self, id: u32) { + self.env().emit_event(MessageProcessedTo { id }); + } + } +} +``` \ No newline at end of file diff --git a/ink/crates/phat_rollup_anchor_ink/src/impls/kv_store.rs b/ink/crates/phat_rollup_anchor_ink/src/impls/kv_store.rs new file mode 100644 index 0000000..92ab8a2 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/impls/kv_store.rs @@ -0,0 +1,114 @@ +use kv_session::traits::{Key, Value}; +use openbrush::storage::Mapping; +use openbrush::traits::Storage; + +pub use crate::traits::kv_store::*; + +pub const STORAGE_KEY: u32 = openbrush::storage_unique_key!(Data); + +#[derive(Default, Debug)] +#[openbrush::upgradeable_storage(STORAGE_KEY)] +pub struct Data { + kv_store: Mapping, +} + +impl KVStore for T +where + T: Storage, +{ + default fn get_value(&self, key: Key) -> Option { + self._get_value(&key) + } + + default fn _get_value(&self, key: &Key) -> Option { + self.data::().kv_store.get(key) + } + + default fn _set_value(&mut self, key: &Key, value: Option<&Value>) { + match value { + None => self.data::().kv_store.remove(key), + Some(v) => self.data::().kv_store.insert(key, v), + } + } +} + +#[cfg(test)] +mod tests { + use crate::impls::kv_store::*; + use crate::tests::test_contract::MyContract; + use openbrush::test_utils::accounts; + use scale::Encode; + + #[ink::test] + fn test_get_no_value() { + let accounts = accounts(); + let contract = MyContract::new(accounts.alice); + + let key = b"0x123".to_vec(); + assert_eq!(None, contract._get_value(&key)); + } + + #[ink::test] + fn test_set_encoded_values() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + let key_1 = b"0x123".to_vec(); + let some_value_1 = "0x456".encode(); + contract._set_value(&key_1, Some(&some_value_1)); + + let key_2 = b"0x124".to_vec(); + let some_value_2 = "0x457".encode(); + contract._set_value(&key_2, Some(&some_value_2)); + + match contract._get_value(&key_1) { + Some(v) => assert_eq!(some_value_1, v), + _ => panic!("We should find a value for the key {:?}", key_1), + } + + match contract._get_value(&key_2) { + Some(v) => assert_eq!(some_value_2, v), + _ => panic!("We should find a value for the key {:?}", key_2), + } + + let key_3 = b"0x125".to_vec(); + if let Some(_) = contract._get_value(&key_3) { + panic!("We should not find a value for the key {:?}", key_3); + } + } + + #[ink::test] + fn test_update_same_key() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + let key = b"0x123".to_vec(); + + // update the value + let some_value = "0x456".encode(); + contract._set_value(&key, Some(&some_value)); + match contract._get_value(&key) { + Some(v) => assert_eq!(some_value, v), + _ => panic!("We should find a value for the key {:?}", key), + } + // update the value + let another_value = "0x457".encode(); + contract._set_value(&key, Some(&another_value)); + match contract._get_value(&key) { + Some(v) => assert_eq!(another_value, v), + _ => panic!("We should find a value for the key {:?}", key), + } + // remove the value + contract._set_value(&key, None); + if let Some(_) = contract._get_value(&key) { + panic!("We should not find a value for the key {:?}", key); + } + // update the value + let another_value = "0x458".encode(); + contract._set_value(&key, Some(&another_value)); + match contract._get_value(&key) { + Some(v) => assert_eq!(another_value, v), + _ => panic!("We should find a value for the key {:?}", key), + } + } +} diff --git a/ink/crates/phat_rollup_anchor_ink/src/impls/message_queue.rs b/ink/crates/phat_rollup_anchor_ink/src/impls/message_queue.rs new file mode 100644 index 0000000..0f37605 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/impls/message_queue.rs @@ -0,0 +1,214 @@ +use kv_session::traits::QueueIndex; +use scale::{Decode, Encode}; + +use crate::traits::kv_store::KVStore; +pub use crate::traits::message_queue::{self, *}; + +const QUEUE_PREFIX: &[u8] = b"q/"; +const QUEUE_HEAD_KEY: &[u8] = b"_head"; +const QUEUE_TAIL_KEY: &[u8] = b"_tail"; + +macro_rules! get_key { + ($id:ident) => { + [QUEUE_PREFIX, &$id.encode()].concat() + }; +} + +macro_rules! get_tail_key { + () => { + [QUEUE_PREFIX, QUEUE_TAIL_KEY].concat() + }; +} + +macro_rules! get_head_key { + () => { + [QUEUE_PREFIX, QUEUE_HEAD_KEY].concat() + }; +} + +macro_rules! get_queue_index { + ($kv:ident, $key:ident) => {{ + match $kv._get_value(&$key) { + Some(v) => QueueIndex::decode(&mut v.as_slice()) + .map_err(|_| MessageQueueError::FailedToDecode)?, + _ => 0, + } + }}; +} + +impl MessageQueue for T +where + T: message_queue::Internal, + T: KVStore, +{ + default fn _push_message( + &mut self, + data: &M, + ) -> Result { + let id = self.get_queue_tail()?; + let key = get_key!(id); + let encoded_value = data.encode(); + self._set_value(&key, Some(&encoded_value)); + + self._set_queue_tail(id + 1); + self._emit_event_message_queued(id, data.encode()); + + Ok(id) + } + + fn _get_message(&self, id: QueueIndex) -> Result, MessageQueueError> { + let key = get_key!(id); + match self._get_value(&key) { + Some(v) => { + let message = + M::decode(&mut v.as_slice()).map_err(|_| MessageQueueError::FailedToDecode)?; + Ok(Some(message)) + } + _ => Ok(None), + } + } + + default fn _pop_to(&mut self, target_id: QueueIndex) -> Result<(), MessageQueueError> { + let current_tail_id = self.get_queue_tail()?; + if target_id > current_tail_id { + return Err(MessageQueueError::InvalidPopTarget); + } + + let current_head_id = self.get_queue_head()?; + if target_id < current_head_id { + return Err(MessageQueueError::InvalidPopTarget); + } + + if target_id == current_head_id { + // nothing to do + return Ok(()); + } + + for id in current_head_id..target_id { + let key = get_key!(id); + self._set_value(&key, None); + } + + self._set_queue_head(target_id); + self._emit_event_message_processed_to(target_id); + + Ok(()) + } + + default fn get_queue_tail(&self) -> Result { + let key = get_tail_key!(); + let index = get_queue_index!(self, key); + Ok(index) + } + + default fn get_queue_head(&self) -> Result { + let key = get_head_key!(); + let index = get_queue_index!(self, key); + Ok(index) + } + + default fn _set_queue_tail(&mut self, id: QueueIndex) { + let key = get_tail_key!(); + self._set_value(&key, Some(&id.encode())); + } + + default fn _set_queue_head(&mut self, id: QueueIndex) { + let key = get_head_key!(); + self._set_value(&key, Some(&id.encode())); + } +} + +#[cfg(test)] +mod tests { + + use crate::impls::message_queue::*; + use crate::tests::test_contract::MyContract; + use openbrush::test_utils::accounts; + + #[ink::test] + fn test_push_and_pop_message() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + assert_eq!(0, contract.get_queue_tail().unwrap()); + assert_eq!(0, contract.get_queue_head().unwrap()); + + // push the first message in the queue + let message1 = 123456u128; + let queue_index = contract._push_message(&message1).unwrap(); + assert_eq!(0, queue_index); + assert_eq!(0, contract.get_queue_head().unwrap()); + assert_eq!(1, contract.get_queue_tail().unwrap()); + + // push the second message in the queue + let message2 = 4589u16; + let queue_index = contract._push_message(&message2).unwrap(); + assert_eq!(1, queue_index); + assert_eq!(0, contract.get_queue_head().unwrap()); + assert_eq!(2, contract.get_queue_tail().unwrap()); + + // get the first message + let message_in_queue: Option = contract._get_message(0).unwrap(); + assert_eq!( + message1, + message_in_queue.expect("we expect a message in the queue") + ); + + // get the seconde message + let message_in_queue: Option = contract._get_message(1).unwrap(); + assert_eq!( + message2, + message_in_queue.expect("we expect a message in the queue") + ); + + // pop the two messages + contract._pop_to(2).unwrap(); + assert_eq!(2, contract.get_queue_head().unwrap()); + assert_eq!(2, contract.get_queue_tail().unwrap()); + } + + #[ink::test] + fn test_pop_messages() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + // pop to the future => error + assert_eq!( + Err(MessageQueueError::InvalidPopTarget), + contract._pop_to(2) + ); + + let message = 4589u16; + contract._push_message(&message).unwrap(); + contract._push_message(&message).unwrap(); + contract._push_message(&message).unwrap(); + contract._push_message(&message).unwrap(); + contract._push_message(&message).unwrap(); + + assert_eq!(0, contract.get_queue_head().unwrap()); + assert_eq!(5, contract.get_queue_tail().unwrap()); + + assert_eq!(Ok(()), contract._pop_to(2)); + + assert_eq!(2, contract.get_queue_head().unwrap()); + assert_eq!(5, contract.get_queue_tail().unwrap()); + + // we do nothing + assert_eq!(Ok(()), contract._pop_to(2)); + + assert_eq!(2, contract.get_queue_head().unwrap()); + assert_eq!(5, contract.get_queue_tail().unwrap()); + + // pop to the past => error + assert_eq!( + Err(MessageQueueError::InvalidPopTarget), + contract._pop_to(1) + ); + + // we do nothing + assert_eq!(Ok(()), contract._pop_to(5)); + + assert_eq!(5, contract.get_queue_head().unwrap()); + assert_eq!(5, contract.get_queue_tail().unwrap()); + } +} diff --git a/ink/crates/phat_rollup_anchor_ink/src/impls/meta_transaction.rs b/ink/crates/phat_rollup_anchor_ink/src/impls/meta_transaction.rs new file mode 100644 index 0000000..746abe6 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/impls/meta_transaction.rs @@ -0,0 +1,314 @@ +use ink::env::hash::{Blake2x256, HashOutput}; +use ink::prelude::vec::Vec; +use openbrush::contracts::access_control; +use openbrush::storage::Mapping; +use openbrush::traits::{AccountId, Hash, Storage}; + +pub use crate::traits::meta_transaction::{self, *}; + +pub const STORAGE_KEY: u32 = openbrush::storage_unique_key!(Data); + +type NonceAndEcdsaPk = (Nonce, Vec); + +#[derive(Default, Debug)] +#[openbrush::upgradeable_storage(STORAGE_KEY)] +pub struct Data { + nonces_and_ecdsa_public_key: Mapping, +} + +impl MetaTxReceiver for T +where + T: Storage, + T: Storage, +{ + default fn _get_nonce(&self, from: AccountId) -> Nonce { + self.data::() + .nonces_and_ecdsa_public_key + .get(&from) + .map(|(n, _)| n) + .unwrap_or(0) + } + + default fn get_ecdsa_public_key(&self, from: AccountId) -> [u8; 33] { + match self.data::().nonces_and_ecdsa_public_key.get(&from) { + None => [0; 33], + Some((_, p)) => p.try_into().unwrap_or([0; 33]), + } + } + + #[openbrush::modifiers(access_control::only_role(MANAGER_ROLE))] + default fn register_ecdsa_public_key( + &mut self, + from: AccountId, + ecdsa_public_key: [u8; 33], + ) -> Result<(), MetaTxError> { + match self.data::().nonces_and_ecdsa_public_key.get(&from) { + None => self + .data::() + .nonces_and_ecdsa_public_key + .insert(&from, &(0, ecdsa_public_key.into())), + Some((n, _)) => self + .data::() + .nonces_and_ecdsa_public_key + .insert(&from, &(n, ecdsa_public_key.into())), + } + Ok(()) + } + + default fn prepare( + &self, + from: AccountId, + data: Vec, + ) -> Result<(ForwardRequest, Hash), MetaTxError> { + let nonce = self._get_nonce(from); + + let request = ForwardRequest { from, nonce, data }; + let mut hash = ::Type::default(); + ink::env::hash_encoded::(&request, &mut hash); + + Ok((request, hash.into())) + } + + default fn _verify( + &self, + request: &ForwardRequest, + signature: &[u8; 65], + ) -> Result<(), MetaTxError> { + let (nonce_from, ecdsa_public_key) = match self + .data::() + .nonces_and_ecdsa_public_key + .get(&request.from) + { + Some((n, p)) => (n, p), + _ => return Err(MetaTxError::PublicKeyNotRegistered), + }; + let ecdsa_public_key: [u8; 33] = ecdsa_public_key + .try_into() + .map_err(|_| MetaTxError::PublicKeyNotRegistered)?; + + if request.nonce < nonce_from { + return Err(MetaTxError::NonceTooLow); + } + + let mut hash = ::Type::default(); + ink::env::hash_encoded::(&request, &mut hash); + + // at the moment we can only verify ecdsa signatures + let mut public_key = [0u8; 33]; + ink::env::ecdsa_recover(signature, &hash, &mut public_key) + .map_err(|_| MetaTxError::IncorrectSignature)?; + + if public_key != ecdsa_public_key { + return Err(MetaTxError::PublicKeyNotMatch); + } + Ok(()) + } + + default fn _use_meta_tx( + &mut self, + request: &ForwardRequest, + signature: &[u8; 65], + ) -> Result<(), MetaTxError> { + // verify the signature + self._verify(request, signature)?; + // update the nonce + match self + .data::() + .nonces_and_ecdsa_public_key + .get(&request.from) + { + Some((_, p)) => self + .data::() + .nonces_and_ecdsa_public_key + .insert(&request.from, &(request.nonce + 1, p)), + None => return Err(MetaTxError::PublicKeyNotRegistered), + } + Ok(()) + } +} + +#[macro_export] +macro_rules! use_meta_tx { + ($metaTxReceiver:ident, $request:ident, $signature:ident) => {{ + $metaTxReceiver._use_meta_tx(&$request, &$signature)? + }}; +} + +#[cfg(test)] +mod tests { + use ink::env::debug_println; + use openbrush::test_utils::accounts; + use scale::Encode; + + use crate::impls::meta_transaction::*; + use crate::tests::test_contract::MyContract; + + #[ink::test] + fn test_get_nonce() { + let accounts = accounts(); + let contract = MyContract::new(accounts.bob); + + // no nonce (ie 0) for new account + assert_eq!(0, contract._get_nonce(accounts.bob)); + } + + #[ink::test] + fn test_prepare() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.bob); + + // Alice + let from = AccountId::from(hex_literal::hex!( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + )); + let ecdsa_public_key: [u8; 33] = + hex_literal::hex!("037051bed73458951b45ca6376f4096c85bf1a370da94d5336d04867cfaaad019e"); + + let data = u8::encode(&5); + + // register the ecda public key because I am not able to retrieve if from the account id + contract + .register_ecdsa_public_key(from, ecdsa_public_key) + .expect("Error when registering ecdsa public key"); + + // prepare the meta transaction + let (request, hash) = contract + .prepare(from, data.clone()) + .expect("Error when preparing meta tx"); + + assert_eq!(0, request.nonce); + assert_eq!(from, request.from); + assert_eq!(&data, &request.data); + + debug_println!("code hash: {:02x?}", hash); + let expected_hash = + hex_literal::hex!("17cb4f6eae2f95ba0fbaee9e0e51dc790fe752e7386b72dcd93b9669450c2ccf"); + assert_eq!(&expected_hash, &hash.as_ref()); + } + + #[ink::test] + fn test_verify() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.bob); + + // Alice + let from = AccountId::from(hex_literal::hex!( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + )); + let ecdsa_public_key: [u8; 33] = + hex_literal::hex!("037051bed73458951b45ca6376f4096c85bf1a370da94d5336d04867cfaaad019e"); + + // register the ecda public key because I am not able to retrieve if from the account id + contract + .register_ecdsa_public_key(from, ecdsa_public_key) + .expect("Error when registering ecdsa public key"); + + let nonce: Nonce = 0; + let data = u8::encode(&5); + let request = ForwardRequest { + from, + nonce, + data: data.clone(), + }; + + // signature by Alice of hash : 17cb4f6eae2f95ba0fbaee9e0e51dc790fe752e7386b72dcd93b9669450c2ccf + let signature = hex_literal::hex!("ce68d0383bd8f521a2243415add58ed0aed58c246229f15672ed6f99ba6c6c823a6d5fe7503703423e46206196c499d132533a151e2e7d9754b497a9d3014d9301"); + + // the verification must succeed + assert_eq!(Ok(()), contract._verify(&request, &signature)); + + // incorrect 'from' => the verification must fail + let request = ForwardRequest { + from: accounts.bob, + nonce, + data: data.clone(), + }; + assert_eq!( + Err(MetaTxError::PublicKeyNotRegistered), + contract._verify(&request, &signature) + ); + + // incorrect nonce => the verification must fail + let request = ForwardRequest { + from, + nonce: 1, + data: data.clone(), + }; + assert_eq!( + Err(MetaTxError::PublicKeyNotMatch), + contract._verify(&request, &signature) + ); + + // incorrect data => the verification must fail + let request = ForwardRequest { + from, + nonce, + data: u8::encode(&55), + }; + assert_eq!( + Err(MetaTxError::PublicKeyNotMatch), + contract._verify(&request, &signature) + ); + + // register another ecda public key + let ecdsa_public_key = + hex_literal::hex!("037051bed73458951b45ca6376f4096c85bf1a370da94d5336d04867cfaaad019f"); + contract + .register_ecdsa_public_key(from, ecdsa_public_key) + .expect("Error when registering ecdsa public key"); + // incorrect ecdsa public key => the verification must fail + let request = ForwardRequest { + from, + nonce, + data: data.clone(), + }; + assert_eq!( + Err(MetaTxError::PublicKeyNotMatch), + contract._verify(&request, &signature) + ); + } + + #[ink::test] + fn test_use_meta_tx() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.bob); + + // Alice + let from = AccountId::from(hex_literal::hex!( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + )); + let ecdsa_public_key: [u8; 33] = + hex_literal::hex!("037051bed73458951b45ca6376f4096c85bf1a370da94d5336d04867cfaaad019e"); + + // register the ecda public key + contract + .register_ecdsa_public_key(from, ecdsa_public_key) + .expect("Error when registering ecdsa public key"); + + let nonce: Nonce = 0; + let data = u8::encode(&5); + let request = ForwardRequest { + from, + nonce, + data: data.clone(), + }; + + // signature by Alice + let signature = hex_literal::hex!("ce68d0383bd8f521a2243415add58ed0aed58c246229f15672ed6f99ba6c6c823a6d5fe7503703423e46206196c499d132533a151e2e7d9754b497a9d3014d9301"); + + // the verification must succeed + contract + ._use_meta_tx(&request, &signature) + .expect("Error when using meta tx"); + + // check if the nonce has been updated + assert_eq!(1, contract._get_nonce(from)); + + // test we cannot reuse the same call + // the verification must fail + assert_eq!( + Err(MetaTxError::NonceTooLow), + contract._use_meta_tx(&request, &signature) + ); + } +} diff --git a/ink/crates/phat_rollup_anchor_ink/src/impls/mod.rs b/ink/crates/phat_rollup_anchor_ink/src/impls/mod.rs new file mode 100644 index 0000000..2d70149 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/impls/mod.rs @@ -0,0 +1,4 @@ +pub mod kv_store; +pub mod message_queue; +pub mod meta_transaction; +pub mod rollup_anchor; diff --git a/ink/crates/phat_rollup_anchor_ink/src/impls/rollup_anchor.rs b/ink/crates/phat_rollup_anchor_ink/src/impls/rollup_anchor.rs new file mode 100644 index 0000000..c1114c2 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/impls/rollup_anchor.rs @@ -0,0 +1,382 @@ +use ink::prelude::vec::Vec; +use kv_session::traits::{Key, Value}; +use openbrush::contracts::access_control; +use openbrush::traits::Storage; + +use crate::traits::kv_store; +use crate::traits::message_queue; +use crate::traits::meta_transaction; +pub use crate::traits::rollup_anchor::{self, *}; + +pub type RolupCondEqMethodParams = ( + Vec<(Key, Option)>, + Vec<(Key, Option)>, + Vec, +); +pub type MetatTxRolupCondEqMethodParams = (meta_transaction::ForwardRequest, [u8; 65]); + +impl RollupAnchor for T +where + T: rollup_anchor::Internal, + T: kv_store::KVStore, + T: message_queue::MessageQueue, + T: Storage, + T: access_control::AccessControl, + T: meta_transaction::MetaTxReceiver, +{ + #[openbrush::modifiers(access_control::only_role(ATTESTOR_ROLE))] + default fn rollup_cond_eq( + &mut self, + conditions: Vec<(Key, Option)>, + updates: Vec<(Key, Option)>, + actions: Vec, + ) -> Result { + self._rollup_cond_eq(conditions, updates, actions) + } + + default fn meta_tx_rollup_cond_eq( + &mut self, + request: meta_transaction::ForwardRequest, + signature: [u8; 65], + ) -> Result { + // check the signature + self._use_meta_tx(&request, &signature)?; + + // check the attestor role + if !self.has_role(ATTESTOR_ROLE, request.from) { + return Err(RollupAnchorError::AccessControlError( + access_control::AccessControlError::MissingRole, + )); + } + + // decode the data + let data: RolupCondEqMethodParams = scale::Decode::decode(&mut request.data.as_slice()) + .map_err(|_| RollupAnchorError::FailedToDecode)?; + + // emit the event + self._emit_event_meta_tx_decoded(); + + // call the rollup + self._rollup_cond_eq(data.0, data.1, data.2) + } + + default fn _rollup_cond_eq( + &mut self, + conditions: Vec<(Key, Option)>, + updates: Vec<(Key, Option)>, + actions: Vec, + ) -> Result { + // check the conditions + for cond in conditions { + let key = cond.0; + let current_value = self._get_value(&key); + let expected_value = cond.1; + match (current_value, expected_value) { + (None, None) => {} + (Some(v1), Some(v2)) => { + if v1.ne(&v2) { + // condition is not met + return Err(RollupAnchorError::ConditionNotMet); + } + } + (_, _) => return Err(RollupAnchorError::ConditionNotMet), + } + } + + // apply the updates + for update in updates { + self._set_value(&update.0, update.1.as_ref()); + } + + // apply the actions + for action in actions { + self._handle_action(action)?; + } + + Ok(true) + } + + default fn _handle_action( + &mut self, + input: HandleActionInput, + ) -> Result<(), RollupAnchorError> { + match input.action_type { + ACTION_REPLY => { + self._on_message_received(input.action.ok_or(RollupAnchorError::MissingData)?)? + } + ACTION_SET_QUEUE_HEAD => { + self._pop_to(input.id.ok_or(RollupAnchorError::MissingData)?)? + } + ACTION_GRANT_ATTESTOR => self.grant_role( + ATTESTOR_ROLE, + input.address.ok_or(RollupAnchorError::MissingData)?, + )?, + ACTION_REVOKE_ATTESTOR => self.revoke_role( + ATTESTOR_ROLE, + input.address.ok_or(RollupAnchorError::MissingData)?, + )?, + _ => return Err(RollupAnchorError::UnsupportedAction), + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use openbrush::contracts::access_control::AccessControl; + use openbrush::test_utils::{accounts, change_caller}; + use openbrush::traits::AccountId; + use scale::Encode; + + use crate::impls::message_queue::*; + use crate::impls::meta_transaction::MetaTxError; + use crate::impls::rollup_anchor::*; + use crate::tests::test_contract::MyContract; + use crate::traits::meta_transaction::MetaTxReceiver; + + #[ink::test] + fn test_conditions() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + // no condition, no update, no action => it should work + assert_eq!(contract.rollup_cond_eq(vec![], vec![], vec![]), Ok(true)); + + // test with correct condition + let conditions = vec![(123u8.encode(), None)]; + assert_eq!( + contract.rollup_cond_eq(conditions, vec![], vec![]), + Ok(true) + ); + + // update a value + let updates = vec![(123u8.encode(), Some(456u128.encode()))]; + assert_eq!(contract.rollup_cond_eq(vec![], updates, vec![]), Ok(true)); + + // test with the correct condition + let conditions = vec![(123u8.encode(), Some(456u128.encode()))]; + assert_eq!( + contract.rollup_cond_eq(conditions, vec![], vec![]), + Ok(true) + ); + + // test with incorrect condition (incorrect value) + let conditions = vec![(123u8.encode(), Some(789u128.encode()))]; + assert_eq!( + contract.rollup_cond_eq(conditions, vec![], vec![]), + Err(RollupAnchorError::ConditionNotMet) + ); + + // test with incorrect condition (incorrect value) + let conditions = vec![(123u8.encode(), None)]; + assert_eq!( + contract.rollup_cond_eq(conditions, vec![], vec![]), + Err(RollupAnchorError::ConditionNotMet) + ); + + // test with incorrect condition (incorrect key) + let conditions = vec![ + (123u8.encode(), Some(456u128.encode())), + (124u8.encode(), Some(456u128.encode())), + ]; + assert_eq!( + contract.rollup_cond_eq(conditions, vec![], vec![]), + Err(RollupAnchorError::ConditionNotMet) + ); + } + + #[ink::test] + fn test_actions_missing_data() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + let actions = vec![HandleActionInput { + action_type: ACTION_SET_QUEUE_HEAD, + id: None, // missing data + action: None, + address: None, + }]; + assert_eq!( + contract.rollup_cond_eq(vec![], vec![], actions), + Err(RollupAnchorError::MissingData) + ); + + let actions = vec![HandleActionInput { + action_type: ACTION_REPLY, + id: None, + action: None, // missing data + address: None, + }]; + assert_eq!( + contract.rollup_cond_eq(vec![], vec![], actions), + Err(RollupAnchorError::MissingData) + ); + } + + #[ink::test] + fn test_action_pop_to() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + // no condition, no update, no action + let mut actions = Vec::new(); + actions.push(HandleActionInput { + action_type: ACTION_SET_QUEUE_HEAD, + id: Some(2), + action: None, + address: None, + }); + + assert_eq!( + contract.rollup_cond_eq(vec![], vec![], actions.clone()), + Err(RollupAnchorError::MessageQueueError( + MessageQueueError::InvalidPopTarget + )) + ); + + let message = 4589u16; + contract._push_message(&message).unwrap(); + contract._push_message(&message).unwrap(); + + assert_eq!(contract.rollup_cond_eq(vec![], vec![], actions), Ok(true)); + } + + #[ink::test] + fn test_action_reply() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + let actions = vec![HandleActionInput { + action_type: ACTION_REPLY, + id: Some(2), + action: Some(012u8.encode()), + address: None, + }]; + + assert_eq!(contract.rollup_cond_eq(vec![], vec![], actions), Ok(true)); + } + + #[ink::test] + fn test_grant_role() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + // bob cannot grant the role + change_caller(accounts.bob); + assert_eq!( + Err(access_control::AccessControlError::MissingRole), + contract.grant_role(ATTESTOR_ROLE, accounts.bob) + ); + + // alice, the owner, can do it + change_caller(accounts.alice); + assert_eq!(Ok(()), contract.grant_role(ATTESTOR_ROLE, accounts.bob)); + } + + #[ink::test] + fn test_rollup_cond_eq_role_attestor() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + change_caller(accounts.bob); + + assert_eq!( + Err(RollupAnchorError::AccessControlError( + access_control::AccessControlError::MissingRole + )), + contract.rollup_cond_eq(vec![], vec![], vec![]) + ); + + change_caller(accounts.alice); + contract + .grant_role(ATTESTOR_ROLE, accounts.bob) + .expect("Error when grant the role Attestor"); + + change_caller(accounts.bob); + assert_eq!(Ok(true), contract.rollup_cond_eq(vec![], vec![], vec![])); + } + + #[ink::test] + fn test_meta_tx_rollup_cond_eq() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + // Alice + let from = AccountId::from(hex_literal::hex!( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + )); + let ecdsa_public_key: [u8; 33] = + hex_literal::hex!("037051bed73458951b45ca6376f4096c85bf1a370da94d5336d04867cfaaad019e"); + let data = RolupCondEqMethodParams::encode(&(vec![], vec![], vec![])); + + // register the ecdsa public key + contract + .register_ecdsa_public_key(from, ecdsa_public_key) + .expect("Error when registering ecdsa public key"); + + let (request, hash) = contract + .prepare(from, data) + .expect("Error when preparing meta tx"); + + let expected_hash = + hex_literal::hex!("c91f57305dc05a66f1327352d55290a250eb61bba8e3cf8560a4b8e7d172bb54"); + assert_eq!(&expected_hash, &hash.as_ref()); + + // signature by Alice of previous hash + let signature : [u8; 65] = hex_literal::hex!("c9a899bc8daa98fd1e819486c57f9ee889d035e8d0e55c04c475ca32bb59389b284d18d785a9db1bdd72ce74baefe6a54c0aa2418b14f7bc96232fa4bf42946600"); + + // add the role => it should be succeed + contract + .grant_role(ATTESTOR_ROLE, request.from) + .expect("Error when grant the role Attestor"); + assert_eq!( + Ok(true), + contract.meta_tx_rollup_cond_eq(request.clone(), signature) + ); + + // do it again => it must failed + assert_eq!( + Err(RollupAnchorError::MetaTxError(MetaTxError::NonceTooLow)), + contract.meta_tx_rollup_cond_eq(request.clone(), signature) + ); + } + + #[ink::test] + fn test_meta_tx_rollup_cond_eq_missing_role() { + let accounts = accounts(); + let mut contract = MyContract::new(accounts.alice); + + // Alice + let from = AccountId::from(hex_literal::hex!( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + )); + let ecdsa_public_key: [u8; 33] = + hex_literal::hex!("037051bed73458951b45ca6376f4096c85bf1a370da94d5336d04867cfaaad019e"); + let data = RolupCondEqMethodParams::encode(&(vec![], vec![], vec![])); + + // register the ecdsa public key + contract + .register_ecdsa_public_key(from, ecdsa_public_key) + .expect("Error when registering ecdsa public key"); + + let (request, hash) = contract + .prepare(from, data) + .expect("Error when preparing meta tx"); + + let expected_hash = + hex_literal::hex!("c91f57305dc05a66f1327352d55290a250eb61bba8e3cf8560a4b8e7d172bb54"); + assert_eq!(&expected_hash, &hash.as_ref()); + + // signature by Alice of previous hash + let signature : [u8; 65] = hex_literal::hex!("c9a899bc8daa98fd1e819486c57f9ee889d035e8d0e55c04c475ca32bb59389b284d18d785a9db1bdd72ce74baefe6a54c0aa2418b14f7bc96232fa4bf42946600"); + + // missing role + assert_eq!( + Err(RollupAnchorError::AccessControlError( + access_control::AccessControlError::MissingRole + )), + contract.meta_tx_rollup_cond_eq(request.clone(), signature) + ); + } +} diff --git a/ink/crates/phat_rollup_anchor_ink/src/lib.rs b/ink/crates/phat_rollup_anchor_ink/src/lib.rs new file mode 100644 index 0000000..3810627 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/lib.rs @@ -0,0 +1,10 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(min_specialization)] + +extern crate core; + +pub mod impls; +pub mod traits; + +#[cfg(test)] +pub mod tests; diff --git a/ink/crates/phat_rollup_anchor_ink/src/tests/mod.rs b/ink/crates/phat_rollup_anchor_ink/src/tests/mod.rs new file mode 100644 index 0000000..2d26062 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/tests/mod.rs @@ -0,0 +1,77 @@ +#[openbrush::contract] +pub mod test_contract { + + use crate::impls::kv_store::{self, *}; + use crate::impls::message_queue::{self, *}; + use crate::impls::meta_transaction::{self, *}; + use crate::impls::rollup_anchor::{self, *}; + use ink::env::debug_println; + use openbrush::contracts::access_control::*; + use openbrush::contracts::ownable::*; + use openbrush::traits::Storage; + + #[ink(storage)] + #[derive(Default, Storage)] + pub struct MyContract { + #[storage_field] + ownable: ownable::Data, + #[storage_field] + access: access_control::Data, + #[storage_field] + kv_store: kv_store::Data, + #[storage_field] + meta_transaction: meta_transaction::Data, + } + + impl Ownable for MyContract {} + impl AccessControl for MyContract {} + impl KVStore for MyContract {} + impl MessageQueue for MyContract {} + impl MetaTxReceiver for MyContract {} + impl RollupAnchor for MyContract {} + + impl MyContract { + #[ink(constructor)] + pub fn new(phat_attestor: AccountId) -> Self { + let mut instance = Self::default(); + let caller = instance.env().caller(); + // set the owner of this contract + instance._init_with_owner(caller); + // set the admin of this contract + instance._init_with_admin(caller); + // grant the role manager + instance + .grant_role(MANAGER_ROLE, caller) + .expect("Should grant the role MANAGER_ROLE"); + // grant the role attestor to the given address + instance + .grant_role(ATTESTOR_ROLE, phat_attestor) + .expect("Should grant the role ATTESTOR_ROLE"); + instance + } + } + + impl message_queue::Internal for MyContract { + fn _emit_event_message_queued(&self, id: u32, data: Vec) { + debug_println!( + "Emit event 'message queued {{ id: {:?}, data: {:2x?} }}", + id, + data + ); + } + fn _emit_event_message_processed_to(&self, id: u32) { + debug_println!("Emit event 'message processed to {:?}'", id); + } + } + + impl rollup_anchor::Internal for MyContract { + fn _on_message_received(&mut self, action: Vec) -> Result<(), RollupAnchorError> { + debug_println!("Message received {:?}'", action); + Ok(()) + } + + fn _emit_event_meta_tx_decoded(&self) { + debug_println!("Meta transaction decoded"); + } + } +} diff --git a/ink/crates/phat_rollup_anchor_ink/src/traits/kv_store.rs b/ink/crates/phat_rollup_anchor_ink/src/traits/kv_store.rs new file mode 100644 index 0000000..66cbf29 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/traits/kv_store.rs @@ -0,0 +1,11 @@ +pub use kv_session::traits::{Key, Value}; + +#[openbrush::trait_definition] +pub trait KVStore { + #[ink(message)] + fn get_value(&self, key: Key) -> Option; + + fn _get_value(&self, key: &Key) -> Option; + + fn _set_value(&mut self, key: &Key, value: Option<&Value>); +} diff --git a/ink/crates/phat_rollup_anchor_ink/src/traits/message_queue.rs b/ink/crates/phat_rollup_anchor_ink/src/traits/message_queue.rs new file mode 100644 index 0000000..f991cb1 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/traits/message_queue.rs @@ -0,0 +1,36 @@ +use ink::prelude::vec::Vec; +pub use kv_session::traits::QueueIndex; +use scale::{Decode, Encode}; + +#[derive(Debug, Eq, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum MessageQueueError { + InvalidPopTarget, + FailedToDecode, +} + +#[openbrush::trait_definition] +pub trait MessageQueue { + fn _push_message(&mut self, data: &M) -> Result; + + fn _get_message(&self, id: QueueIndex) -> Result, MessageQueueError>; + + fn _pop_to(&mut self, target_id: QueueIndex) -> Result<(), MessageQueueError>; + + #[ink(message)] + fn get_queue_tail(&self) -> Result; + + #[ink(message)] + fn get_queue_head(&self) -> Result; + + fn _set_queue_tail(&mut self, id: QueueIndex); + + fn _set_queue_head(&mut self, id: QueueIndex); +} + +#[openbrush::trait_definition] +pub trait Internal { + fn _emit_event_message_queued(&self, id: QueueIndex, data: Vec); + + fn _emit_event_message_processed_to(&self, id: QueueIndex); +} diff --git a/ink/crates/phat_rollup_anchor_ink/src/traits/meta_transaction.rs b/ink/crates/phat_rollup_anchor_ink/src/traits/meta_transaction.rs new file mode 100644 index 0000000..e0829a0 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/traits/meta_transaction.rs @@ -0,0 +1,61 @@ +use ink::prelude::vec::Vec; +use openbrush::contracts::access_control::{AccessControlError, RoleType}; +use openbrush::traits::{AccountId, Hash}; +use scale::{Decode, Encode}; + +pub type Nonce = u128; + +pub const MANAGER_ROLE: RoleType = ink::selector_id!("MANAGER_ROLE"); + +#[derive(Debug, Eq, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum MetaTxError { + NonceTooLow, + IncorrectSignature, + PublicKeyNotMatch, + PublicKeyNotRegistered, + AccessControlError(AccessControlError), +} + +/// convertor from AccessControlError to MetaTxError +impl From for MetaTxError { + fn from(error: AccessControlError) -> Self { + MetaTxError::AccessControlError(error) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ForwardRequest { + pub from: AccountId, + pub nonce: Nonce, + pub data: Vec, +} + +pub type PrepareResult = (ForwardRequest, Hash); + +#[openbrush::trait_definition] +pub trait MetaTxReceiver { + fn _get_nonce(&self, from: AccountId) -> Nonce; + + #[ink(message)] + fn get_ecdsa_public_key(&self, from: AccountId) -> [u8; 33]; + + #[ink(message)] + fn register_ecdsa_public_key( + &mut self, + from: AccountId, + ecdsa_public_key: [u8; 33], + ) -> Result<(), MetaTxError>; + + #[ink(message)] + fn prepare(&self, from: AccountId, data: Vec) -> Result; + + fn _verify(&self, request: &ForwardRequest, signature: &[u8; 65]) -> Result<(), MetaTxError>; + + fn _use_meta_tx( + &mut self, + request: &ForwardRequest, + signature: &[u8; 65], + ) -> Result<(), MetaTxError>; +} diff --git a/ink/crates/phat_rollup_anchor_ink/src/traits/mod.rs b/ink/crates/phat_rollup_anchor_ink/src/traits/mod.rs new file mode 100644 index 0000000..2d70149 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/traits/mod.rs @@ -0,0 +1,4 @@ +pub mod kv_store; +pub mod message_queue; +pub mod meta_transaction; +pub mod rollup_anchor; diff --git a/ink/crates/phat_rollup_anchor_ink/src/traits/rollup_anchor.rs b/ink/crates/phat_rollup_anchor_ink/src/traits/rollup_anchor.rs new file mode 100644 index 0000000..5738181 --- /dev/null +++ b/ink/crates/phat_rollup_anchor_ink/src/traits/rollup_anchor.rs @@ -0,0 +1,89 @@ +pub use crate::traits::message_queue::MessageQueueError; +use crate::traits::meta_transaction::{ForwardRequest, MetaTxError}; +use ink::prelude::vec::Vec; +pub use kv_session::traits::{Key, QueueIndex, Value}; +use openbrush::contracts::access_control::{AccessControlError, RoleType}; +use openbrush::traits::AccountId; + +pub const ATTESTOR_ROLE: RoleType = ink::selector_id!("ATTESTOR_ROLE"); + +pub const ACTION_REPLY: u8 = 0; +pub const ACTION_SET_QUEUE_HEAD: u8 = 1; +pub const ACTION_GRANT_ATTESTOR: u8 = 10; +pub const ACTION_REVOKE_ATTESTOR: u8 = 11; + +#[derive(scale::Encode, scale::Decode, Debug, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct HandleActionInput { + pub action_type: u8, + pub action: Option>, + pub address: Option, + pub id: Option, +} + +#[derive(Debug, Eq, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum RollupAnchorError { + FailedToDecode, + UnsupportedAction, + ConditionNotMet, + MissingData, + MessageQueueError(MessageQueueError), + AccessControlError(AccessControlError), + MetaTxError(MetaTxError), +} + +/// convertor from AccessControlError to RollupAnchorError +impl From for RollupAnchorError { + fn from(error: AccessControlError) -> Self { + RollupAnchorError::AccessControlError(error) + } +} + +/// convertor from MessageQueueError to RollupAnchorError +impl From for RollupAnchorError { + fn from(error: MessageQueueError) -> Self { + RollupAnchorError::MessageQueueError(error) + } +} + +/// convertor from MetaTxError to RollupAnchorError +impl From for RollupAnchorError { + fn from(error: MetaTxError) -> Self { + RollupAnchorError::MetaTxError(error) + } +} + +#[openbrush::trait_definition] +pub trait RollupAnchor { + #[ink(message)] + fn rollup_cond_eq( + &mut self, + conditions: Vec<(Key, Option)>, + updates: Vec<(Key, Option)>, + actions: Vec, + ) -> Result; + + fn _rollup_cond_eq( + &mut self, + conditions: Vec<(Key, Option)>, + updates: Vec<(Key, Option)>, + actions: Vec, + ) -> Result; + + #[ink(message)] + fn meta_tx_rollup_cond_eq( + &mut self, + request: ForwardRequest, + signature: [u8; 65], + ) -> Result; + + fn _handle_action(&mut self, input: HandleActionInput) -> Result<(), RollupAnchorError>; +} + +#[openbrush::trait_definition] +pub trait Internal { + fn _on_message_received(&mut self, action: Vec) -> Result<(), RollupAnchorError>; + + fn _emit_event_meta_tx_decoded(&self); +} diff --git a/ink/rust-toolchain.toml b/ink/rust-toolchain.toml new file mode 100644 index 0000000..1ab1e81 --- /dev/null +++ b/ink/rust-toolchain.toml @@ -0,0 +1,14 @@ +[toolchain] +channel = "nightly-2023-02-03" +components = [ + "cargo", + "clippy", + "rust-analyzer", + "rust-src", + "rust-std", + "rustc-dev", + "rustc", + "rustfmt", +] +targets = ["wasm32-unknown-unknown", "wasm32-wasi"] +profile = "minimal"