From 1d9c9d2c57d1a8636fed432a99e8883ec5eaade3 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 18 Oct 2022 12:22:20 +0200 Subject: [PATCH 01/47] feat: remove nft --- apps/src/bin/anoma-client/cli.rs | 6 - apps/src/bin/anoma/cli.rs | 2 - apps/src/lib/cli.rs | 122 ------- apps/src/lib/client/tx.rs | 74 ---- shared/src/types/mod.rs | 1 - shared/src/types/nft.rs | 369 -------------------- shared/src/types/transaction/mod.rs | 2 - shared/src/types/transaction/nft.rs | 49 --- tx_prelude/src/lib.rs | 1 - tx_prelude/src/nft.rs | 89 ----- vp_prelude/src/lib.rs | 1 - vp_prelude/src/nft.rs | 116 ------- wasm/checksums.json | 3 - wasm/wasm_source/Cargo.toml | 3 - wasm/wasm_source/Makefile | 3 - wasm/wasm_source/src/lib.rs | 6 - wasm/wasm_source/src/tx_init_nft.rs | 16 - wasm/wasm_source/src/tx_mint_nft.rs | 15 - wasm/wasm_source/src/vp_nft.rs | 500 ---------------------------- wasm/wasm_source/src/vp_user.rs | 10 - 20 files changed, 1388 deletions(-) delete mode 100644 shared/src/types/nft.rs delete mode 100644 shared/src/types/transaction/nft.rs delete mode 100644 tx_prelude/src/nft.rs delete mode 100644 vp_prelude/src/nft.rs delete mode 100644 wasm/wasm_source/src/tx_init_nft.rs delete mode 100644 wasm/wasm_source/src/tx_mint_nft.rs delete mode 100644 wasm/wasm_source/src/vp_nft.rs diff --git a/apps/src/bin/anoma-client/cli.rs b/apps/src/bin/anoma-client/cli.rs index b87cdb5c66..9734e16e72 100644 --- a/apps/src/bin/anoma-client/cli.rs +++ b/apps/src/bin/anoma-client/cli.rs @@ -27,12 +27,6 @@ pub async fn main() -> Result<()> { Sub::TxInitValidator(TxInitValidator(args)) => { tx::submit_init_validator(ctx, args).await; } - Sub::TxInitNft(TxInitNft(args)) => { - tx::submit_init_nft(ctx, args).await; - } - Sub::TxMintNft(TxMintNft(args)) => { - tx::submit_mint_nft(ctx, args).await; - } Sub::TxInitProposal(TxInitProposal(args)) => { tx::submit_init_proposal(ctx, args).await; } diff --git a/apps/src/bin/anoma/cli.rs b/apps/src/bin/anoma/cli.rs index ccde0c3618..3737f63a8b 100644 --- a/apps/src/bin/anoma/cli.rs +++ b/apps/src/bin/anoma/cli.rs @@ -46,8 +46,6 @@ fn handle_command(cmd: cli::cmds::Anoma, raw_sub_cmd: String) -> Result<()> { | cli::cmds::Anoma::TxCustom(_) | cli::cmds::Anoma::TxTransfer(_) | cli::cmds::Anoma::TxUpdateVp(_) - | cli::cmds::Anoma::TxInitNft(_) - | cli::cmds::Anoma::TxMintNft(_) | cli::cmds::Anoma::TxInitProposal(_) | cli::cmds::Anoma::TxVoteProposal(_) => { handle_subcommand("namadac", sub_args) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 4534d5bc0d..bc52a00bc1 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -47,8 +47,6 @@ pub mod cmds { TxCustom(TxCustom), TxTransfer(TxTransfer), TxUpdateVp(TxUpdateVp), - TxInitNft(TxInitNft), - TxMintNft(TxMintNft), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), } @@ -62,8 +60,6 @@ pub mod cmds { .subcommand(TxCustom::def()) .subcommand(TxTransfer::def()) .subcommand(TxUpdateVp::def()) - .subcommand(TxInitNft::def()) - .subcommand(TxMintNft::def()) .subcommand(TxInitProposal::def()) .subcommand(TxVoteProposal::def()) } @@ -76,8 +72,6 @@ pub mod cmds { let tx_custom = SubCmd::parse(matches).map(Self::TxCustom); let tx_transfer = SubCmd::parse(matches).map(Self::TxTransfer); let tx_update_vp = SubCmd::parse(matches).map(Self::TxUpdateVp); - let tx_nft_create = SubCmd::parse(matches).map(Self::TxInitNft); - let tx_nft_mint = SubCmd::parse(matches).map(Self::TxMintNft); let tx_init_proposal = SubCmd::parse(matches).map(Self::TxInitProposal); let tx_vote_proposal = @@ -88,8 +82,6 @@ pub mod cmds { .or(tx_custom) .or(tx_transfer) .or(tx_update_vp) - .or(tx_nft_create) - .or(tx_nft_mint) .or(tx_init_proposal) .or(tx_vote_proposal) } @@ -155,9 +147,6 @@ pub mod cmds { .subcommand(TxUpdateVp::def().display_order(1)) .subcommand(TxInitAccount::def().display_order(1)) .subcommand(TxInitValidator::def().display_order(1)) - // Nft transactions - .subcommand(TxInitNft::def().display_order(1)) - .subcommand(TxMintNft::def().display_order(1)) // Proposal transactions .subcommand(TxInitProposal::def().display_order(1)) .subcommand(TxVoteProposal::def().display_order(1)) @@ -188,8 +177,6 @@ pub mod cmds { let tx_init_account = Self::parse_with_ctx(matches, TxInitAccount); let tx_init_validator = Self::parse_with_ctx(matches, TxInitValidator); - let tx_nft_create = Self::parse_with_ctx(matches, TxInitNft); - let tx_nft_mint = Self::parse_with_ctx(matches, TxMintNft); let tx_init_proposal = Self::parse_with_ctx(matches, TxInitProposal); let tx_vote_proposal = @@ -216,8 +203,6 @@ pub mod cmds { .or(tx_update_vp) .or(tx_init_account) .or(tx_init_validator) - .or(tx_nft_create) - .or(tx_nft_mint) .or(tx_init_proposal) .or(tx_vote_proposal) .or(bond) @@ -275,8 +260,6 @@ pub mod cmds { TxUpdateVp(TxUpdateVp), TxInitAccount(TxInitAccount), TxInitValidator(TxInitValidator), - TxInitNft(TxInitNft), - TxMintNft(TxMintNft), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), Bond(Bond), @@ -1034,50 +1017,6 @@ pub mod cmds { } } - #[derive(Clone, Debug)] - pub struct TxInitNft(pub args::NftCreate); - - impl SubCmd for TxInitNft { - const CMD: &'static str = "init-nft"; - - fn parse(matches: &ArgMatches) -> Option - where - Self: Sized, - { - matches - .subcommand_matches(Self::CMD) - .map(|matches| TxInitNft(args::NftCreate::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about("Create a new NFT.") - .add_args::() - } - } - - #[derive(Clone, Debug)] - pub struct TxMintNft(pub args::NftMint); - - impl SubCmd for TxMintNft { - const CMD: &'static str = "mint-nft"; - - fn parse(matches: &ArgMatches) -> Option - where - Self: Sized, - { - matches - .subcommand_matches(Self::CMD) - .map(|matches| TxMintNft(args::NftMint::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about("Mint new NFT tokens.") - .add_args::() - } - } - #[derive(Clone, Debug)] pub struct TxInitProposal(pub args::InitProposal); @@ -1316,7 +1255,6 @@ pub mod args { const LOCALHOST: ArgFlag = flag("localhost"); const MODE: ArgOpt = arg_opt("mode"); const NET_ADDRESS: Arg = arg("net-address"); - const NFT_ADDRESS: Arg
= arg("nft-address"); const OWNER: ArgOpt = arg_opt("owner"); const PROPOSAL_OFFLINE: ArgFlag = flag("offline"); const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); @@ -1989,66 +1927,6 @@ pub mod args { } } - // Transaction to create a new nft - #[derive(Clone, Debug)] - pub struct NftCreate { - /// Common tx argumentsips - pub tx: Tx, - /// Path to the nft file description - pub nft_data: PathBuf, - } - - impl Args for NftCreate { - fn parse(matches: &ArgMatches) -> Self { - let tx = Tx::parse(matches); - let data_path = DATA_PATH.parse(matches); - - Self { - tx, - nft_data: data_path, - } - } - - fn def(app: App) -> App { - app.add_args::() - .arg(DATA_PATH.def().about("The path nft description file.")) - } - } - - #[derive(Clone, Debug)] - pub struct NftMint { - /// Common tx arguments - pub tx: Tx, - /// The nft address - pub nft_address: Address, - /// The nft token description - pub nft_data: PathBuf, - } - - impl Args for NftMint { - fn parse(matches: &ArgMatches) -> Self { - let tx = Tx::parse(matches); - let nft_address = NFT_ADDRESS.parse(matches); - let data_path = DATA_PATH.parse(matches); - - Self { - tx, - nft_address, - nft_data: data_path, - } - } - - fn def(app: App) -> App { - app.add_args::() - .arg(NFT_ADDRESS.def().about("The nft address.")) - .arg( - DATA_PATH.def().about( - "The data path file that describes the nft tokens.", - ), - ) - } - } - /// Query token balance(s) #[derive(Clone, Debug)] pub struct QueryBalance { diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 7a4d542536..b6349c67b4 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -16,12 +16,10 @@ use namada::types::governance::{ OfflineProposal, OfflineVote, Proposal, ProposalVote, }; use namada::types::key::*; -use namada::types::nft::{self, Nft, NftToken}; use namada::types::storage::Epoch; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, }; -use namada::types::transaction::nft::{CreateNft, MintNft}; use namada::types::transaction::{pos, InitAccount, InitValidator, UpdateVp}; use namada::types::{address, storage, token}; use namada::{ledger, vm}; @@ -49,13 +47,10 @@ const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm"; const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm"; const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; -const TX_INIT_NFT: &str = "tx_init_nft.wasm"; -const TX_MINT_NFT: &str = "tx_mint_nft.wasm"; const VP_USER_WASM: &str = "vp_user.wasm"; const TX_BOND_WASM: &str = "tx_bond.wasm"; const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; -const VP_NFT: &str = "vp_nft.wasm"; const ENV_VAR_ANOMA_TENDERMINT_WEBSOCKET_TIMEOUT: &str = "ANOMA_TENDERMINT_WEBSOCKET_TIMEOUT"; @@ -465,75 +460,6 @@ pub async fn submit_transfer(ctx: Context, args: args::TxTransfer) { process_tx(ctx, &args.tx, tx, Some(&args.source)).await; } -pub async fn submit_init_nft(ctx: Context, args: args::NftCreate) { - let file = File::open(&args.nft_data).expect("File must exist."); - let nft: Nft = serde_json::from_reader(file) - .expect("Couldn't deserialize nft data file"); - - let vp_code = match &nft.vp_path { - Some(path) => { - std::fs::read(path).expect("Expected a file at given code path") - } - None => ctx.read_wasm(VP_NFT), - }; - - let signer = Some(WalletAddress::new(nft.creator.clone().to_string())); - - let data = CreateNft { - tag: nft.tag.to_string(), - creator: nft.creator, - vp_code, - keys: nft.keys, - opt_keys: nft.opt_keys, - tokens: nft.tokens, - }; - - let data = data.try_to_vec().expect( - "Encoding transfer data to initialize a new account shouldn't fail", - ); - - let tx_code = ctx.read_wasm(TX_INIT_NFT); - - let tx = Tx::new(tx_code, Some(data)); - process_tx(ctx, &args.tx, tx, signer.as_ref()).await; -} - -pub async fn submit_mint_nft(ctx: Context, args: args::NftMint) { - let file = File::open(&args.nft_data).expect("File must exist."); - let nft_tokens: Vec = - serde_json::from_reader(file).expect("JSON was not well-formatted"); - - let nft_creator_key = nft::get_creator_key(&args.nft_address); - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - let nft_creator_address = - match rpc::query_storage_value::
(&client, &nft_creator_key) - .await - { - Some(addr) => addr, - None => { - eprintln!("No creator key found for {}", &args.nft_address); - safe_exit(1); - } - }; - - let signer = Some(WalletAddress::new(nft_creator_address.to_string())); - - let data = MintNft { - address: args.nft_address, - creator: nft_creator_address, - tokens: nft_tokens, - }; - - let data = data.try_to_vec().expect( - "Encoding transfer data to initialize a new account shouldn't fail", - ); - - let tx_code = ctx.read_wasm(TX_MINT_NFT); - - let tx = Tx::new(tx_code, Some(data)); - process_tx(ctx, &args.tx, tx, signer.as_ref()).await; -} - pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { let file = File::open(&args.proposal_data).expect("File must exist."); let proposal: Proposal = diff --git a/shared/src/types/mod.rs b/shared/src/types/mod.rs index 240fbdb70c..b8895c095d 100644 --- a/shared/src/types/mod.rs +++ b/shared/src/types/mod.rs @@ -8,7 +8,6 @@ pub mod hash; pub mod ibc; pub mod internal; pub mod key; -pub mod nft; pub mod storage; pub mod time; pub mod token; diff --git a/shared/src/types/nft.rs b/shared/src/types/nft.rs deleted file mode 100644 index ebfff7d5ce..0000000000 --- a/shared/src/types/nft.rs +++ /dev/null @@ -1,369 +0,0 @@ -//! Nft types -use std::fmt; - -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; - -use super::address::Address; -use super::storage::{DbKeySeg, Key, KeySeg}; - -const NFT_KEY: &str = "nft"; -const TAG_KEY: &str = "tag"; -const CREATOR_KEY: &str = "creator"; -const KEYS: &str = "keys"; -const OPTIONAL_KEYS: &str = "optional_keys"; -const METADATA_KEY: &str = "metadata"; -const APPROVALS_KEY: &str = "approvals"; -const BURNT_KEY: &str = "burnt"; -const IDS_KEY: &str = "ids"; -const CURRENT_OWNER_KEY: &str = "current_owner"; -const PAST_OWNERS_KEY: &str = "past_owners"; -const VALUE_KEY: &str = "value"; -const OPTIONAL_VALUE: &str = "optional_value"; - -#[derive( - Debug, - Clone, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, - Eq, - PartialEq, - Hash, - PartialOrd, -)] -/// Nft Version tag -pub enum NftTag { - /// Tag v1 - V1, -} - -impl Default for NftTag { - fn default() -> Self { - Self::V1 - } -} - -impl fmt::Display for NftTag { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - NftTag::V1 => write!(f, "v1"), - } - } -} -#[derive( - Debug, - Clone, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, - Eq, - PartialEq, - Hash, - PartialOrd, -)] -/// The definition of an NFT -pub struct Nft { - #[serde(default)] - /// Nft version - pub tag: NftTag, - /// The source address - pub creator: Address, - /// The path to a file containing the validity predicate associated with - /// the NFT - pub vp_path: Option, - /// Mandatory NFT fields - pub keys: Vec, - #[serde(default = "default_opt_keys")] - /// Optional NFT fields - pub opt_keys: Vec, - /// The list of tokens - pub tokens: Vec, -} - -impl fmt::Display for Nft { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "tag: {}, creator: {}, tokens: {:?}", - self.tag, self.creator, self.tokens - ) - } -} - -#[derive( - Debug, - Clone, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, - Eq, - PartialEq, - Hash, - PartialOrd, -)] -/// The definition of an NFT token -pub struct NftToken { - /// The token id - pub id: u64, - /// The URI containing metadata - pub metadata: String, - /// Current owner - pub current_owner: Option
, - /// Past owners - #[serde(default = "default_past_owners")] - pub past_owners: Vec
, - /// Approved addresses - pub approvals: Vec
, - /// Mandatory fields values - pub values: Vec, - #[serde(default = "default_opt_values")] - /// Optionals fields values - pub opt_values: Vec, - #[serde(default = "default_burnt")] - /// Is token burnt - pub burnt: bool, -} - -impl fmt::Display for NftToken { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "id: {}, metadata: {}, values: {:?}", - self.id, self.metadata, self.values - ) - } -} - -fn default_opt_keys() -> Vec { - Vec::new() -} - -fn default_past_owners() -> Vec
{ - Vec::new() -} - -fn default_opt_values() -> Vec { - Vec::new() -} - -fn default_burnt() -> bool { - false -} - -/// Get the nft prefix -pub fn _nft_prefix(address: &Address) -> Key { - Key::from(address.to_db_key()) - .push(&NFT_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft token prefix -pub fn _nft_token_prefix(address: &Address, token_id: &str) -> Key { - _nft_prefix(address) - .push(&IDS_KEY.to_owned()) - .expect("Cannot obtain a storage key") - .push(&token_id.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft owner storage key -pub fn get_tag_key(address: &Address) -> Key { - _nft_prefix(address) - .push(&TAG_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft owner storage key -pub fn get_creator_key(address: &Address) -> Key { - _nft_prefix(address) - .push(&CREATOR_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft keys storage key -pub fn get_keys_key(address: &Address) -> Key { - _nft_prefix(address) - .push(&KEYS.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft keys storage key -pub fn get_optional_keys_key(address: &Address) -> Key { - _nft_prefix(address) - .push(&OPTIONAL_KEYS.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft metadata storage key -pub fn get_token_metadata_key(address: &Address, nft_id: &str) -> Key { - _nft_token_prefix(address, nft_id) - .push(&METADATA_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft current_owner storage key -pub fn get_token_current_owner_key(address: &Address, nft_id: &str) -> Key { - _nft_token_prefix(address, nft_id) - .push(&CURRENT_OWNER_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft current_owner storage key -pub fn get_token_past_owners_key(address: &Address, nft_id: &str) -> Key { - _nft_token_prefix(address, nft_id) - .push(&PAST_OWNERS_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft value storage key -pub fn get_token_value_key(address: &Address, nft_id: &str) -> Key { - _nft_token_prefix(address, nft_id) - .push(&VALUE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft optional value storage key -pub fn get_token_optional_value_key(address: &Address, nft_id: &str) -> Key { - _nft_token_prefix(address, nft_id) - .push(&OPTIONAL_VALUE.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft burnt storage key -pub fn get_token_burnt_key(address: &Address, nft_id: &str) -> Key { - _nft_token_prefix(address, nft_id) - .push(&BURNT_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the nft approval storage key -pub fn get_token_approval_key(address: &Address, nft_id: &str) -> Key { - _nft_token_prefix(address, nft_id) - .push(&APPROVALS_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Check that nft is created by a specific creator address -pub fn is_nft_creator_key(key: &Key, address: &Address) -> Option
{ - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(nft_addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(creator_key), - ] if nft_addr == address - && prefix == NFT_KEY - && creator_key == CREATOR_KEY => - { - Some(nft_addr.to_owned()) - } - _ => None, - } -} - -/// Check that a particular key is a approval storage key -pub fn is_nft_approval_key( - key: &Key, - address: &Address, -) -> Option<(Address, String)> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(nft_addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(ids_key), - DbKeySeg::StringSeg(token_id_key), - DbKeySeg::StringSeg(approval_key), - ] if nft_addr == address - && prefix == NFT_KEY - && ids_key == IDS_KEY - && approval_key == APPROVALS_KEY => - { - Some((nft_addr.to_owned(), token_id_key.to_owned())) - } - _ => None, - } -} - -/// Check that a particular key is a metadata storage key -pub fn is_nft_metadata_key( - key: &Key, - address: &Address, -) -> Option<(Address, String)> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(nft_addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(ids_key), - DbKeySeg::StringSeg(token_id_key), - DbKeySeg::StringSeg(metadata_key), - ] if nft_addr == address - && prefix == NFT_KEY - && ids_key == IDS_KEY - && metadata_key == METADATA_KEY => - { - Some((nft_addr.to_owned(), token_id_key.to_owned())) - } - _ => None, - } -} - -/// Check that a particular key is a current_owner storage key -pub fn is_nft_current_owner_key( - key: &Key, - address: &Address, -) -> Option<(Address, String)> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(nft_addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(ids_key), - DbKeySeg::StringSeg(token_id_key), - DbKeySeg::StringSeg(current_owner_key), - ] if nft_addr == address - && prefix == NFT_KEY - && ids_key == IDS_KEY - && current_owner_key == CURRENT_OWNER_KEY => - { - Some((nft_addr.to_owned(), token_id_key.to_owned())) - } - _ => None, - } -} - -/// Check that a particular key is a past_owners storage key -pub fn is_nft_past_owners_key( - key: &Key, - address: &Address, -) -> Option<(Address, String)> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(nft_addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(ids_key), - DbKeySeg::StringSeg(token_id_key), - DbKeySeg::StringSeg(past_owners_key), - ] if nft_addr == address - && prefix == NFT_KEY - && ids_key == IDS_KEY - && past_owners_key == PAST_OWNERS_KEY => - { - Some((nft_addr.to_owned(), token_id_key.to_owned())) - } - _ => None, - } -} - -/// Check that a key points to a nft storage key -pub fn is_nft_key(key: &Key) -> Option<&Address> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(nft_addr), - DbKeySeg::StringSeg(prefix), - .., - ] if prefix == NFT_KEY => Some(nft_addr), - _ => None, - } -} diff --git a/shared/src/types/transaction/mod.rs b/shared/src/types/transaction/mod.rs index a7d5ee864b..3ec4bbd0e3 100644 --- a/shared/src/types/transaction/mod.rs +++ b/shared/src/types/transaction/mod.rs @@ -7,8 +7,6 @@ pub mod decrypted; pub mod encrypted; /// txs to manage governance pub mod governance; -/// txs to manage nfts -pub mod nft; pub mod pos; /// transaction protocols made by validators pub mod protocol; diff --git a/shared/src/types/transaction/nft.rs b/shared/src/types/transaction/nft.rs deleted file mode 100644 index 10d5ffa43f..0000000000 --- a/shared/src/types/transaction/nft.rs +++ /dev/null @@ -1,49 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; - -use crate::types::address::Address; -use crate::types::nft::NftToken; - -/// A tx data type to create a new NFT -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, -)] -pub struct CreateNft { - /// Nft version - pub tag: String, - /// The creator address - pub creator: Address, - /// The nft vp code - pub vp_code: Vec, - /// The nft keys - pub keys: Vec, - /// The nft optional keys - pub opt_keys: Vec, - /// The nft tokens descriptions - pub tokens: Vec, -} - -/// A tx data type to mint nft tokens -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, -)] -pub struct MintNft { - /// The nft address - pub address: Address, - /// The creator address - pub creator: Address, - /// The nft tokens - pub tokens: Vec, -} diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 730adb3155..b512472a80 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -8,7 +8,6 @@ pub mod governance; pub mod ibc; -pub mod nft; pub mod proof_of_stake; pub mod token; diff --git a/tx_prelude/src/nft.rs b/tx_prelude/src/nft.rs deleted file mode 100644 index 4ed179fe27..0000000000 --- a/tx_prelude/src/nft.rs +++ /dev/null @@ -1,89 +0,0 @@ -use namada::types::address::Address; -use namada::types::nft; -use namada::types::nft::NftToken; -use namada::types::transaction::nft::{CreateNft, MintNft}; - -use super::*; - -/// Initialize a new NFT token address. -pub fn init_nft(ctx: &mut Ctx, nft: CreateNft) -> EnvResult
{ - let address = ctx.init_account(&nft.vp_code)?; - - // write tag - let tag_key = nft::get_tag_key(&address); - ctx.write(&tag_key, &nft.tag)?; - - // write creator - let creator_key = nft::get_creator_key(&address); - ctx.write(&creator_key, &nft.creator)?; - - // write keys - let keys_key = nft::get_keys_key(&address); - ctx.write(&keys_key, &nft.keys)?; - - // write optional keys - let optional_keys_key = nft::get_optional_keys_key(&address); - ctx.write(&optional_keys_key, nft.opt_keys)?; - - // mint tokens - aux_mint_token(ctx, &address, &nft.creator, nft.tokens, &nft.creator)?; - - ctx.insert_verifier(&nft.creator)?; - - Ok(address) -} - -pub fn mint_tokens(ctx: &mut Ctx, nft: MintNft) -> TxResult { - aux_mint_token(ctx, &nft.address, &nft.creator, nft.tokens, &nft.creator) -} - -fn aux_mint_token( - ctx: &mut Ctx, - nft_address: &Address, - creator_address: &Address, - tokens: Vec, - verifier: &Address, -) -> TxResult { - for token in tokens { - // write token metadata - let metadata_key = - nft::get_token_metadata_key(nft_address, &token.id.to_string()); - ctx.write(&metadata_key, &token.metadata)?; - - // write current owner token as creator - let current_owner_key = nft::get_token_current_owner_key( - nft_address, - &token.id.to_string(), - ); - ctx.write( - ¤t_owner_key, - &token - .current_owner - .unwrap_or_else(|| creator_address.clone()), - )?; - - // write value key - let value_key = - nft::get_token_value_key(nft_address, &token.id.to_string()); - ctx.write(&value_key, &token.values)?; - - // write optional value keys - let optional_value_key = nft::get_token_optional_value_key( - nft_address, - &token.id.to_string(), - ); - ctx.write(&optional_value_key, &token.opt_values)?; - - // write approval addresses - let approval_key = - nft::get_token_approval_key(nft_address, &token.id.to_string()); - ctx.write(&approval_key, &token.approvals)?; - - // write burnt propriety - let burnt_key = - nft::get_token_burnt_key(nft_address, &token.id.to_string()); - ctx.write(&burnt_key, token.burnt)?; - } - ctx.insert_verifier(verifier)?; - Ok(()) -} diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index e6618bc5de..6bffb07d7e 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -7,7 +7,6 @@ #![deny(rustdoc::private_intra_doc_links)] pub mod key; -pub mod nft; pub mod token; // used in the VP input diff --git a/vp_prelude/src/nft.rs b/vp_prelude/src/nft.rs deleted file mode 100644 index 1d5d019169..0000000000 --- a/vp_prelude/src/nft.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! NFT validity predicate - -use std::collections::BTreeSet; - -use namada::ledger::native_vp::VpEnv; -use namada::types::address::Address; -pub use namada::types::nft::*; -use namada::types::storage::Key; - -use super::{accept, reject, Ctx, EnvResult, VpResult}; - -enum KeyType { - Metadata(Address, String), - Approval(Address, String), - CurrentOwner(Address, String), - Creator(Address), - PastOwners(Address, String), - Unknown, -} - -pub fn vp( - ctx: &Ctx, - _tx_da_ta: Vec, - nft_address: &Address, - keys_changed: &BTreeSet, - verifiers: &BTreeSet
, -) -> VpResult { - for key in keys_changed { - match get_key_type(key, nft_address) { - KeyType::Creator(_creator_addr) => { - super::log_string("creator cannot be changed."); - return reject(); - } - KeyType::Approval(nft_address, token_id) => { - super::log_string(format!( - "nft vp, checking approvals with token id: {}", - token_id - )); - - if !(is_creator(ctx, &nft_address, verifiers)? - || is_approved( - ctx, - &nft_address, - token_id.as_ref(), - verifiers, - )?) - { - return reject(); - } - } - KeyType::Metadata(nft_address, token_id) => { - super::log_string(format!( - "nft vp, checking if metadata changed: {}", - token_id - )); - if !is_creator(ctx, &nft_address, verifiers)? { - return reject(); - } - } - _ => { - if !is_creator(ctx, nft_address, verifiers)? { - return reject(); - } - } - } - } - accept() -} - -fn is_approved( - ctx: &Ctx, - nft_address: &Address, - nft_token_id: &str, - verifiers: &BTreeSet
, -) -> EnvResult { - let approvals_key = get_token_approval_key(nft_address, nft_token_id); - let approval_addresses: Vec
= - ctx.read_pre(&approvals_key)?.unwrap_or_default(); - return Ok(approval_addresses - .iter() - .any(|addr| verifiers.contains(addr))); -} - -fn is_creator( - ctx: &Ctx, - nft_address: &Address, - verifiers: &BTreeSet
, -) -> EnvResult { - let creator_key = get_creator_key(nft_address); - let creator_address: Address = ctx.read_pre(&creator_key)?.unwrap(); - Ok(verifiers.contains(&creator_address)) -} - -fn get_key_type(key: &Key, nft_address: &Address) -> KeyType { - let is_creator_key = is_nft_creator_key(key, nft_address); - let is_metadata_key = is_nft_metadata_key(key, nft_address); - let is_approval_key = is_nft_approval_key(key, nft_address); - let is_current_owner_key = is_nft_current_owner_key(key, nft_address); - let is_past_owner_key = is_nft_past_owners_key(key, nft_address); - if let Some(nft_address) = is_creator_key { - return KeyType::Creator(nft_address); - } - if let Some((nft_address, token_id)) = is_metadata_key { - return KeyType::Metadata(nft_address, token_id); - } - if let Some((nft_address, token_id)) = is_approval_key { - return KeyType::Approval(nft_address, token_id); - } - if let Some((nft_address, token_id)) = is_current_owner_key { - return KeyType::CurrentOwner(nft_address, token_id); - } - if let Some((nft_address, token_id)) = is_past_owner_key { - return KeyType::PastOwners(nft_address, token_id); - } - KeyType::Unknown -} diff --git a/wasm/checksums.json b/wasm/checksums.json index 8ddbe1dc85..be2d172bc9 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -2,16 +2,13 @@ "tx_bond.wasm": "tx_bond.865260ca3ca9ed4c611cc5cbc8b4d864232f447f06c59afa0b7c1c63c3fa897c.wasm", "tx_ibc.wasm": "tx_ibc.1360fdf276c4591d937aaac3bb40ddb573abeba382651474ae23138aac65c3e5.wasm", "tx_init_account.wasm": "tx_init_account.72d9e1daa43998ce617eafba1547b34f26b128f0fb6e02cfdbc85ecf1f345fd4.wasm", - "tx_init_nft.wasm": "tx_init_nft.22a886305fda24438dbf3fc0f864ee32db4a398bf748edd2fddba1fc4679bc35.wasm", "tx_init_proposal.wasm": "tx_init_proposal.649bde547179ebee5449f5954425cd266c5063fae97f98f06de327387e8898ba.wasm", "tx_init_validator.wasm": "tx_init_validator.d40077c1bf1263e1e8e42f54cd893473c5d9c1f395e5d30f44a34d98d9b8dde4.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.78b3a43c5c5b4362cb3023c8e7e25755447e9022952306a62f2d8aab6e99dbee.wasm", "tx_transfer.wasm": "tx_transfer.4c06f6af1f008fccdd42fefdc8015adea9fa60f802603a8be149ec2c6421656e.wasm", "tx_unbond.wasm": "tx_unbond.c3d54895e1a271c86838d54d821d52b5bf5764928811cff30767a4445ebbf653.wasm", "tx_update_vp.wasm": "tx_update_vp.3b709f301e55cb970ec1df98c85c2486561c3243ab1c191d3e078ee345b8b93a.wasm", "tx_vote_proposal.wasm": "tx_vote_proposal.671b46a77d03d10ca4e406f2bbc48adba0e541272a2963fd88d3a8c60e888fcd.wasm", "tx_withdraw.wasm": "tx_withdraw.39e0012e110b19c7000400d11adc355bd8e89605b3c9ca10017c4766eed0ad69.wasm", - "vp_nft.wasm": "vp_nft.b23df820ae3faa13c9ed73121886b712fa0c613b7f43567f528cda05ef75fab6.wasm", "vp_testnet_faucet.wasm": "vp_testnet_faucet.45c740dcfa6e803d55a56cb51b02f2086147dfef3e955c0e8c3f18bf93ae0227.wasm", "vp_token.wasm": "vp_token.74bca3c9999640c39bbd1c1364b65ffe8086e2d3ed124413251295d527326b57.wasm", "vp_user.wasm": "vp_user.1c75ea1e55eb1244f023a525f9e20f0d34e52aebf0bd008dcf122fdc13bdf16a.wasm" diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 5291d81390..b36d2e9978 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -16,16 +16,13 @@ tx_bond = ["namada_tx_prelude"] tx_from_intent = ["namada_tx_prelude"] tx_ibc = ["namada_tx_prelude"] tx_init_account = ["namada_tx_prelude"] -tx_init_nft = ["namada_tx_prelude"] tx_init_proposal = ["namada_tx_prelude"] tx_init_validator = ["namada_tx_prelude"] -tx_mint_nft = ["namada_tx_prelude"] tx_transfer = ["namada_tx_prelude"] tx_unbond = ["namada_tx_prelude"] tx_update_vp = ["namada_tx_prelude"] tx_vote_proposal = ["namada_tx_prelude"] tx_withdraw = ["namada_tx_prelude"] -vp_nft = ["namada_vp_prelude"] vp_testnet_faucet = ["namada_vp_prelude", "once_cell"] vp_token = ["namada_vp_prelude"] vp_user = ["namada_vp_prelude", "once_cell", "rust_decimal"] diff --git a/wasm/wasm_source/Makefile b/wasm/wasm_source/Makefile index 48d4675ea6..ce4655c39a 100644 --- a/wasm/wasm_source/Makefile +++ b/wasm/wasm_source/Makefile @@ -8,16 +8,13 @@ nightly := $(shell cat ../../rust-nightly-version) wasms := tx_bond wasms += tx_ibc wasms += tx_init_account -wasms += tx_init_nft wasms += tx_init_validator wasms += tx_init_proposal -wasms += tx_mint_nft wasms += tx_vote_proposal wasms += tx_transfer wasms += tx_unbond wasms += tx_update_vp wasms += tx_withdraw -wasms += vp_nft wasms += vp_testnet_faucet wasms += vp_token wasms += vp_user diff --git a/wasm/wasm_source/src/lib.rs b/wasm/wasm_source/src/lib.rs index 8929992754..9075c60153 100644 --- a/wasm/wasm_source/src/lib.rs +++ b/wasm/wasm_source/src/lib.rs @@ -4,14 +4,10 @@ pub mod tx_bond; pub mod tx_ibc; #[cfg(feature = "tx_init_account")] pub mod tx_init_account; -#[cfg(feature = "tx_init_nft")] -pub mod tx_init_nft; #[cfg(feature = "tx_init_proposal")] pub mod tx_init_proposal; #[cfg(feature = "tx_init_validator")] pub mod tx_init_validator; -#[cfg(feature = "tx_mint_nft")] -pub mod tx_mint_nft; #[cfg(feature = "tx_transfer")] pub mod tx_transfer; #[cfg(feature = "tx_unbond")] @@ -22,8 +18,6 @@ pub mod tx_update_vp; pub mod tx_vote_proposal; #[cfg(feature = "tx_withdraw")] pub mod tx_withdraw; -#[cfg(feature = "vp_nft")] -pub mod vp_nft; #[cfg(feature = "vp_testnet_faucet")] pub mod vp_testnet_faucet; #[cfg(feature = "vp_token")] diff --git a/wasm/wasm_source/src/tx_init_nft.rs b/wasm/wasm_source/src/tx_init_nft.rs deleted file mode 100644 index de67dfbb53..0000000000 --- a/wasm/wasm_source/src/tx_init_nft.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! A tx to initialize a new NFT account. - -use namada_tx_prelude::*; - -#[transaction] -fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]) - .wrap_err("failed to decode SignedTxData")?; - let data = signed.data.ok_or_err_msg("Missing data")?; - let tx_data = transaction::nft::CreateNft::try_from_slice(&data[..]) - .wrap_err("failed to decode CreateNft")?; - log_string("apply_tx called to create a new NFT"); - - let _address = nft::init_nft(ctx, tx_data)?; - Ok(()) -} diff --git a/wasm/wasm_source/src/tx_mint_nft.rs b/wasm/wasm_source/src/tx_mint_nft.rs deleted file mode 100644 index d3ab17e7ad..0000000000 --- a/wasm/wasm_source/src/tx_mint_nft.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! A tx to mint new NFT token(s). - -use namada_tx_prelude::*; - -#[transaction] -fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { - let signed = SignedTxData::try_from_slice(&tx_data[..]) - .wrap_err("failed to decode SignedTxData")?; - let data = signed.data.ok_or_err_msg("Missing data")?; - let tx_data = transaction::nft::MintNft::try_from_slice(&data[..]) - .wrap_err("failed to decode MintNft")?; - log_string("apply_tx called to mint a new NFT tokens"); - - nft::mint_tokens(ctx, tx_data) -} diff --git a/wasm/wasm_source/src/vp_nft.rs b/wasm/wasm_source/src/vp_nft.rs deleted file mode 100644 index 77a9df8306..0000000000 --- a/wasm/wasm_source/src/vp_nft.rs +++ /dev/null @@ -1,500 +0,0 @@ -//! A VP for a nft. - -use namada_vp_prelude::*; - -#[validity_predicate] -fn validate_tx( - ctx: &Ctx, - tx_data: Vec, - addr: Address, - keys_changed: BTreeSet, - verifiers: BTreeSet
, -) -> VpResult { - log_string(format!( - "validate_tx called with token addr: {}, key_changed: {:#?}, \ - verifiers: {:?}", - addr, keys_changed, verifiers - )); - - if !is_valid_tx(ctx, &tx_data)? { - return reject(); - } - - let vp_check = keys_changed.iter().all(|key| { - if key.is_validity_predicate().is_some() { - match ctx.read_bytes_post(key) { - Ok(Some(vp)) => { - matches!(is_vp_whitelisted(ctx, &vp), Ok(true)) - } - _ => false, - } - } else { - true - } - }); - - Ok(vp_check && nft::vp(ctx, tx_data, &addr, &keys_changed, &verifiers)?) -} - -#[cfg(test)] -mod tests { - use namada::types::nft::{self, NftToken}; - use namada::types::transaction::nft::{CreateNft, MintNft}; - use namada_tests::log::test; - use namada_tests::tx::{self, tx_host_env, TestTxEnv}; - use namada_tests::vp::*; - use namada_tx_prelude::{StorageWrite, TxEnv}; - - use super::*; - - const VP_ALWAYS_TRUE_WASM: &str = - "../../wasm_for_tests/vp_always_true.wasm"; - - /// Test that no-op transaction (i.e. no storage modifications) accepted. - #[test] - fn test_no_op_transaction() { - let mut tx_env = TestTxEnv::default(); - - let nft_creator = address::testing::established_address_2(); - tx_env.spawn_accounts([&nft_creator]); - - // just a dummy vp, its not used during testing - let vp_code = - std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - - tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft( - tx::ctx(), - CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.write_log.commit_tx(); - - vp_host_env::init_from_tx(nft_address.clone(), tx_env, |address| { - // Apply transfer in a transaction - tx::ctx().insert_verifier(address).unwrap() - }); - - let vp_env = vp_host_env::take(); - let tx_data: Vec = vec![]; - let keys_changed: BTreeSet = - vp_env.all_touched_storage_keys(); - let verifiers: BTreeSet
= vp_env.get_verifiers(); - vp_host_env::set(vp_env); - assert!( - validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) - .unwrap() - ); - } - - /// Test that you can create an nft without tokens - #[test] - fn test_mint_no_tokens() { - let mut tx_env = TestTxEnv::default(); - - let nft_creator = address::testing::established_address_2(); - tx_env.spawn_accounts([&nft_creator]); - - // just a dummy vp, its not used during testing - let vp_code = - std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - - tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft( - tx::ctx(), - CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.write_log.commit_tx(); - - vp_host_env::init_from_tx(nft_address.clone(), tx_env, |address| { - // Apply transfer in a transaction - tx_host_env::nft::mint_tokens( - tx::ctx(), - MintNft { - address: nft_address.clone(), - tokens: vec![], - creator: nft_creator.clone(), - }, - ) - .unwrap(); - tx::ctx().insert_verifier(address).unwrap() - }); - - let vp_env = vp_host_env::take(); - let tx_data: Vec = vec![]; - let keys_changed: BTreeSet = - vp_env.all_touched_storage_keys(); - let verifiers: BTreeSet
= vp_env.get_verifiers(); - vp_host_env::set(vp_env); - - assert!( - validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) - .unwrap() - ); - } - - /// Test that you can create an nft with tokens - #[test] - fn test_mint_tokens() { - let mut tx_env = TestTxEnv::default(); - - let nft_creator = address::testing::established_address_2(); - let nft_token_owner = address::testing::established_address_1(); - tx_env.spawn_accounts([&nft_creator, &nft_token_owner]); - - // just a dummy vp, its not used during testing - let vp_code = - std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - - tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft( - tx::ctx(), - CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.commit_tx_and_block(); - - vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { - // Apply transfer in a transaction - tx_host_env::nft::mint_tokens( - tx::ctx(), - MintNft { - address: nft_address.clone(), - creator: nft_creator.clone(), - tokens: vec![NftToken { - id: 1, - values: vec![], - opt_values: vec![], - metadata: "".to_string(), - approvals: vec![], - current_owner: Some(nft_token_owner.clone()), - past_owners: vec![], - burnt: false, - }], - }, - ) - .unwrap(); - }); - - let vp_env = vp_host_env::take(); - let tx_data: Vec = vec![]; - let keys_changed: BTreeSet = - vp_env.all_touched_storage_keys(); - let verifiers: BTreeSet
= vp_env.get_verifiers(); - vp_host_env::set(vp_env); - - assert!( - validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) - .unwrap() - ); - } - - /// Test that only owner can mint new tokens - #[test] - fn test_mint_tokens_wrong_owner() { - let mut tx_env = TestTxEnv::default(); - - let nft_creator = address::testing::established_address_2(); - let nft_token_owner = address::testing::established_address_1(); - tx_env.spawn_accounts([&nft_creator, &nft_token_owner]); - - // just a dummy vp, its not used during testing - let vp_code = - std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - - tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft( - tx::ctx(), - CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.commit_tx_and_block(); - - vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { - // Apply transfer in a transaction - tx_host_env::nft::mint_tokens( - tx::ctx(), - MintNft { - address: nft_address.clone(), - creator: nft_token_owner.clone(), - tokens: vec![NftToken { - id: 1, - values: vec![], - opt_values: vec![], - metadata: "".to_string(), - approvals: vec![], - current_owner: Some(nft_token_owner.clone()), - past_owners: vec![], - burnt: false, - }], - }, - ) - .unwrap(); - }); - - let vp_env = vp_host_env::take(); - let tx_data: Vec = vec![]; - let keys_changed: BTreeSet = - vp_env.all_touched_storage_keys(); - let verifiers: BTreeSet
= vp_env.get_verifiers(); - vp_host_env::set(vp_env); - - assert!( - !validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) - .unwrap() - ); - } - - /// Test that an approval can add another approval - #[test] - fn test_mint_tokens_with_approvals_authorized() { - let mut tx_env = TestTxEnv::default(); - - let nft_creator = address::testing::established_address_2(); - let nft_token_owner = address::testing::established_address_1(); - let nft_token_approval = address::testing::established_address_3(); - let nft_token_approval_2 = address::testing::established_address_4(); - tx_env.spawn_accounts([ - &nft_creator, - &nft_token_owner, - &nft_token_approval, - &nft_token_approval_2, - ]); - - // just a dummy vp, its not used during testing - let vp_code = - std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - - tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft( - tx::ctx(), - CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.commit_tx_and_block(); - - tx_host_env::set(tx_env); - tx_host_env::nft::mint_tokens( - tx::ctx(), - MintNft { - address: nft_address.clone(), - creator: nft_creator.clone(), - tokens: vec![NftToken { - id: 1, - values: vec![], - opt_values: vec![], - metadata: "".to_string(), - approvals: vec![nft_token_approval.clone()], - current_owner: None, - past_owners: vec![], - burnt: false, - }], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.commit_tx_and_block(); - - vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { - let approval_key = nft::get_token_approval_key(&nft_address, "1"); - tx::ctx() - .write( - &approval_key, - [&nft_token_approval_2, &nft_token_approval], - ) - .unwrap(); - tx::ctx().insert_verifier(&nft_token_approval).unwrap(); - }); - - let vp_env = vp_host_env::take(); - let tx_data: Vec = vec![]; - let keys_changed: BTreeSet = - vp_env.all_touched_storage_keys(); - let verifiers: BTreeSet
= vp_env.get_verifiers(); - vp_host_env::set(vp_env); - - assert!( - validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) - .unwrap() - ); - } - - /// Test that an approval can add another approval - #[test] - fn test_mint_tokens_with_approvals_not_authorized() { - let mut tx_env = TestTxEnv::default(); - - let nft_creator = address::testing::established_address_2(); - let nft_token_owner = address::testing::established_address_1(); - let nft_token_approval = address::testing::established_address_3(); - let nft_token_approval_2 = address::testing::established_address_4(); - tx_env.spawn_accounts([ - &nft_creator, - &nft_token_owner, - &nft_token_approval, - &nft_token_approval_2, - ]); - - // just a dummy vp, its not used during testing - let vp_code = - std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - - tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft( - tx::ctx(), - CreateNft { - tag: "v1".to_string(), - creator: nft_creator.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.commit_tx_and_block(); - - tx_host_env::set(tx_env); - tx_host_env::nft::mint_tokens( - tx::ctx(), - MintNft { - address: nft_address.clone(), - creator: nft_creator.clone(), - tokens: vec![NftToken { - id: 1, - values: vec![], - opt_values: vec![], - metadata: "".to_string(), - approvals: vec![nft_token_approval.clone()], - current_owner: None, - past_owners: vec![], - burnt: false, - }], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.commit_tx_and_block(); - - vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { - let approval_key = nft::get_token_approval_key(&nft_address, "1"); - tx::ctx() - .write( - &approval_key, - [&nft_token_approval_2, &nft_token_approval], - ) - .unwrap(); - tx::ctx().insert_verifier(&nft_token_approval_2).unwrap(); - }); - - let vp_env = vp_host_env::take(); - let tx_data: Vec = vec![]; - let keys_changed: BTreeSet = - vp_env.all_touched_storage_keys(); - let verifiers: BTreeSet
= vp_env.get_verifiers(); - vp_host_env::set(vp_env); - - assert!( - !validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) - .unwrap() - ); - } - - /// Test nft address cannot be changed - #[test] - fn test_cant_change_owner() { - let mut tx_env = TestTxEnv::default(); - - let nft_owner = address::testing::established_address_2(); - let another_address = address::testing::established_address_1(); - tx_env.spawn_accounts([&nft_owner, &another_address]); - - // just a dummy vp, its not used during testing - let vp_code = - std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); - - tx_host_env::set(tx_env); - let nft_address = tx_host_env::nft::init_nft( - tx::ctx(), - CreateNft { - tag: "v1".to_string(), - creator: nft_owner.clone(), - vp_code, - keys: vec![], - opt_keys: vec![], - tokens: vec![], - }, - ) - .unwrap(); - - let mut tx_env = tx_host_env::take(); - tx_env.commit_tx_and_block(); - - vp_host_env::init_from_tx(nft_address.clone(), tx_env, |_| { - let creator_key = nft::get_creator_key(&nft_address); - tx::ctx().write(&creator_key, &another_address).unwrap(); - }); - - let vp_env = vp_host_env::take(); - let tx_data: Vec = vec![]; - let keys_changed: BTreeSet = - vp_env.all_touched_storage_keys(); - let verifiers: BTreeSet
= vp_env.get_verifiers(); - vp_host_env::set(vp_env); - - assert!( - !validate_tx(&CTX, tx_data, nft_address, keys_changed, verifiers) - .unwrap() - ); - } -} diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 256dc6bb17..be0a215261 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -15,7 +15,6 @@ use once_cell::unsync::Lazy; enum KeyType<'a> { Token(&'a Address), PoS, - Nft(&'a Address), Vp(&'a Address), GovernanceVote(&'a Address), Unknown, @@ -31,8 +30,6 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { Self::Token(address) } else if proof_of_stake::is_pos_key(key) { Self::PoS - } else if let Some(address) = nft::is_nft_key(key) { - Self::Nft(address) } else if gov_storage::is_vote_key(key) { let voter_address = gov_storage::get_voter_address(key); if let Some(address) = voter_address { @@ -141,13 +138,6 @@ fn validate_tx( ); valid } - KeyType::Nft(owner) => { - if owner == &addr { - *valid_sig - } else { - true - } - } KeyType::GovernanceVote(voter) => { if voter == &addr { *valid_sig From d1521f41f559b05fdde14894d1b0ef454bc90618 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Oct 2022 10:48:57 +0000 Subject: [PATCH 02/47] [ci] wasm checksums update --- wasm/checksums.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index be2d172bc9..e82e144d60 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,15 +1,15 @@ { - "tx_bond.wasm": "tx_bond.865260ca3ca9ed4c611cc5cbc8b4d864232f447f06c59afa0b7c1c63c3fa897c.wasm", - "tx_ibc.wasm": "tx_ibc.1360fdf276c4591d937aaac3bb40ddb573abeba382651474ae23138aac65c3e5.wasm", - "tx_init_account.wasm": "tx_init_account.72d9e1daa43998ce617eafba1547b34f26b128f0fb6e02cfdbc85ecf1f345fd4.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.649bde547179ebee5449f5954425cd266c5063fae97f98f06de327387e8898ba.wasm", - "tx_init_validator.wasm": "tx_init_validator.d40077c1bf1263e1e8e42f54cd893473c5d9c1f395e5d30f44a34d98d9b8dde4.wasm", - "tx_transfer.wasm": "tx_transfer.4c06f6af1f008fccdd42fefdc8015adea9fa60f802603a8be149ec2c6421656e.wasm", - "tx_unbond.wasm": "tx_unbond.c3d54895e1a271c86838d54d821d52b5bf5764928811cff30767a4445ebbf653.wasm", - "tx_update_vp.wasm": "tx_update_vp.3b709f301e55cb970ec1df98c85c2486561c3243ab1c191d3e078ee345b8b93a.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.671b46a77d03d10ca4e406f2bbc48adba0e541272a2963fd88d3a8c60e888fcd.wasm", - "tx_withdraw.wasm": "tx_withdraw.39e0012e110b19c7000400d11adc355bd8e89605b3c9ca10017c4766eed0ad69.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.45c740dcfa6e803d55a56cb51b02f2086147dfef3e955c0e8c3f18bf93ae0227.wasm", - "vp_token.wasm": "vp_token.74bca3c9999640c39bbd1c1364b65ffe8086e2d3ed124413251295d527326b57.wasm", - "vp_user.wasm": "vp_user.1c75ea1e55eb1244f023a525f9e20f0d34e52aebf0bd008dcf122fdc13bdf16a.wasm" + "tx_bond.wasm": "tx_bond.3e251d09c015897dca068d5fbcdcc34dc70c60663359ff72f6e19b9a2f0ff7ec.wasm", + "tx_ibc.wasm": "tx_ibc.08895322db0684712206366edbbf998b8f904a9253869879e0f248d7de94c74e.wasm", + "tx_init_account.wasm": "tx_init_account.7ac886f38b0b498cb891ae50c049b9552fa8979134e72781d249012309565d03.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.9c4e8f53c74c3c6a26d7f82d94eff36448967c0b0605b28e487570f15298b6f1.wasm", + "tx_init_validator.wasm": "tx_init_validator.a65b6998f9f2b12be49f48fd423d8251e1002667dbe74daccc00ea51d10a3935.wasm", + "tx_transfer.wasm": "tx_transfer.610453bdd812fdcfe93f288e4ec8a80f5f461caca6e02213f7a4d6e54b0a1a6a.wasm", + "tx_unbond.wasm": "tx_unbond.5c7d9de88950c14aea8ea4a90fdbfd5a3a809af20b34dee2be905ff51b0a40f4.wasm", + "tx_update_vp.wasm": "tx_update_vp.30fb6b5ac031756c9096347002806eea7793b3e66297e2786708164745db28dd.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.f0f12f8eac13bf5e74df8cb8540a6a461f009b36f46d3f6f50ddfef77776a17e.wasm", + "tx_withdraw.wasm": "tx_withdraw.ae59222f5c869220653cff791b71e04f82023843d1035594db35612d9bf67f2c.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.01f23a5e4affbc7b3255919b88454c893b74058631959e2b89d0fc573abd8d32.wasm", + "vp_token.wasm": "vp_token.bfd458dfd47d582e107f00598f2c182ebf26efb0fb35e6e964a0fbe3383be83c.wasm", + "vp_user.wasm": "vp_user.27d9c692889250027966ab39845002c7f29f5f5e892a7b3f9cc048afceb9877c.wasm" } \ No newline at end of file From 84323786a7b3aabff07f4dbb68fc2b8aad30006b Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 17 Oct 2022 10:52:11 +0200 Subject: [PATCH 03/47] [feat]: Added the recovery id to secp256k signatures --- shared/src/types/key/secp256k1.rs | 79 +++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/shared/src/types/key/secp256k1.rs b/shared/src/types/key/secp256k1.rs index 889b4de258..f22299f028 100644 --- a/shared/src/types/key/secp256k1.rs +++ b/shared/src/types/key/secp256k1.rs @@ -8,6 +8,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use data_encoding::HEXLOWER; +use libsecp256k1::RecoveryId; #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; use serde::de::{Error, SeqAccess, Visitor}; @@ -253,7 +254,7 @@ impl RefTo for SecretKey { /// Secp256k1 signature #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Signature(pub libsecp256k1::Signature); +pub struct Signature(pub libsecp256k1::Signature, pub RecoveryId); impl super::Signature for Signature { const TYPE: SchemeType = SigScheme::TYPE; @@ -291,6 +292,7 @@ impl Serialize for Signature { for elem in &arr[..] { seq.serialize_element(elem)?; } + seq.serialize_element(&self.1.serialize())?; seq.end() } } @@ -303,7 +305,7 @@ impl<'de> Deserialize<'de> for Signature { struct ByteArrayVisitor; impl<'de> Visitor<'de> for ByteArrayVisitor { - type Value = [u8; libsecp256k1::util::SIGNATURE_SIZE]; + type Value = [u8; libsecp256k1::util::SIGNATURE_SIZE + 1]; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str(&format!( @@ -312,13 +314,13 @@ impl<'de> Deserialize<'de> for Signature { )) } - fn visit_seq(self, mut seq: A) -> Result<[u8; 64], A::Error> + fn visit_seq(self, mut seq: A) -> Result<[u8; 65], A::Error> where A: SeqAccess<'de>, { - let mut arr = [0u8; libsecp256k1::util::SIGNATURE_SIZE]; + let mut arr = [0u8; libsecp256k1::util::SIGNATURE_SIZE + 1]; #[allow(clippy::needless_range_loop)] - for i in 0..libsecp256k1::util::SIGNATURE_SIZE { + for i in 0..libsecp256k1::util::SIGNATURE_SIZE + 1 { arr[i] = seq .next_element()? .ok_or_else(|| Error::invalid_length(i, &self))?; @@ -328,23 +330,34 @@ impl<'de> Deserialize<'de> for Signature { } let arr_res = deserializer.deserialize_tuple( - libsecp256k1::util::SIGNATURE_SIZE, + libsecp256k1::util::SIGNATURE_SIZE + 1, ByteArrayVisitor, )?; - let sig = libsecp256k1::Signature::parse_standard(&arr_res) + let sig_array: [u8; 64] = arr_res[..64].try_into().unwrap(); + let sig = libsecp256k1::Signature::parse_standard(&sig_array) .map_err(D::Error::custom); - Ok(Signature(sig.unwrap())) + Ok(Signature( + sig.unwrap(), + RecoveryId::parse(arr_res[64]).map_err(Error::custom)?, + )) } } impl BorshDeserialize for Signature { fn deserialize(buf: &mut &[u8]) -> std::io::Result { // deserialize the bytes first + let (sig_bytes, recovery_id) = BorshDeserialize::deserialize(buf)?; + Ok(Signature( - libsecp256k1::Signature::parse_standard( - &(BorshDeserialize::deserialize(buf)?), - ) - .map_err(|e| { + libsecp256k1::Signature::parse_standard(&sig_bytes).map_err( + |e| { + std::io::Error::new( + ErrorKind::InvalidInput, + format!("Error decoding secp256k1 signature: {}", e), + ) + }, + )?, + RecoveryId::parse(recovery_id).map_err(|e| { std::io::Error::new( ErrorKind::InvalidInput, format!("Error decoding secp256k1 signature: {}", e), @@ -356,7 +369,10 @@ impl BorshDeserialize for Signature { impl BorshSerialize for Signature { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - BorshSerialize::serialize(&self.0.serialize(), writer) + BorshSerialize::serialize( + &(self.0.serialize(), self.1.serialize()), + writer, + ) } } @@ -392,6 +408,28 @@ impl PartialOrd for Signature { } } +impl TryFrom<&[u8; 65]> for Signature { + type Error = ParseSignatureError; + + fn try_from(sig: &[u8; 65]) -> Result { + let sig_bytes = sig[..64].try_into().unwrap(); + let recovery_id = RecoveryId::parse(sig[64]).map_err(|err| { + ParseSignatureError::InvalidEncoding(std::io::Error::new( + ErrorKind::Other, + err, + )) + })?; + libsecp256k1::Signature::parse_standard(&sig_bytes) + .map(|sig| Self(sig, recovery_id)) + .map_err(|err| { + ParseSignatureError::InvalidEncoding(std::io::Error::new( + ErrorKind::Other, + err, + )) + }) + } +} + /// An implementation of the Secp256k1 signature scheme #[derive( Debug, @@ -426,20 +464,21 @@ impl super::SigScheme for SigScheme { /// Sign the data with a key fn sign(keypair: &SecretKey, data: impl AsRef<[u8]>) -> Self::Signature { - #[cfg(not(any(test, features = "secp256k1-sign-verify")))] + #[cfg(not(any(test, feature = "secp256k1-sign-verify")))] { // to avoid `unused-variables` warn let _ = (keypair, data); panic!("\"secp256k1-sign-verify\" feature must be enabled"); } - #[cfg(any(test, features = "secp256k1-sign-verify"))] + #[cfg(any(test, feature = "secp256k1-sign-verify"))] { use sha2::{Digest, Sha256}; let hash = Sha256::digest(data.as_ref()); let message = libsecp256k1::Message::parse_slice(hash.as_ref()) .expect("Message encoding should not fail"); - Signature(libsecp256k1::sign(&message, &keypair.0).0) + let (sig, recovery_id) = libsecp256k1::sign(&message, &keypair.0); + Signature(sig, recovery_id) } } @@ -448,14 +487,14 @@ impl super::SigScheme for SigScheme { data: &T, sig: &Self::Signature, ) -> Result<(), VerifySigError> { - #[cfg(not(any(test, features = "secp256k1-sign-verify")))] + #[cfg(not(any(test, feature = "secp256k1-sign-verify")))] { // to avoid `unused-variables` warn let _ = (pk, data, sig); panic!("\"secp256k1-sign-verify\" feature must be enabled"); } - #[cfg(any(test, features = "secp256k1-sign-verify"))] + #[cfg(any(test, feature = "secp256k1-sign-verify"))] { use sha2::{Digest, Sha256}; let bytes = &data @@ -481,14 +520,14 @@ impl super::SigScheme for SigScheme { data: &[u8], sig: &Self::Signature, ) -> Result<(), VerifySigError> { - #[cfg(not(any(test, features = "secp256k1-sign-verify")))] + #[cfg(not(any(test, feature = "secp256k1-sign-verify")))] { // to avoid `unused-variables` warn let _ = (pk, data, sig); panic!("\"secp256k1-sign-verify\" feature must be enabled"); } - #[cfg(any(test, features = "secp256k1-sign-verify"))] + #[cfg(any(test, feature = "secp256k1-sign-verify"))] { use sha2::{Digest, Sha256}; let hash = Sha256::digest(data); From 381d960dec1e0ec6a232ea7ef696e8fc55af009d Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 17 Oct 2022 14:50:36 +0200 Subject: [PATCH 04/47] [fix]: Fixed the borsh schema for libsecp256k signatures --- shared/src/types/key/secp256k1.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/shared/src/types/key/secp256k1.rs b/shared/src/types/key/secp256k1.rs index f22299f028..d901e46d25 100644 --- a/shared/src/types/key/secp256k1.rs +++ b/shared/src/types/key/secp256k1.rs @@ -383,10 +383,16 @@ impl BorshSchema for Signature { borsh::schema::Definition, >, ) { - // Encoded as `[u8; SIGNATURE_SIZE]` - let elements = "u8".into(); - let length = libsecp256k1::util::SIGNATURE_SIZE as u32; - let definition = borsh::schema::Definition::Array { elements, length }; + // Encoded as `([u8; SIGNATURE_SIZE], u8)` + let signature = + <[u8; libsecp256k1::util::SIGNATURE_SIZE]>::declaration(); + <[u8; libsecp256k1::util::SIGNATURE_SIZE]>::add_definitions_recursively( + definitions, + ); + let recovery = "u8".into(); + let definition = borsh::schema::Definition::Tuple { + elements: vec![signature, recovery], + }; definitions.insert(Self::declaration(), definition); } From e19de0133a687dae3f620a1588e03b137ee510c7 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Tue, 18 Oct 2022 08:46:32 -0400 Subject: [PATCH 05/47] wasm: update checksums.json --- wasm/checksums.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 8ddbe1dc85..0346a5f924 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,18 +1,18 @@ { - "tx_bond.wasm": "tx_bond.865260ca3ca9ed4c611cc5cbc8b4d864232f447f06c59afa0b7c1c63c3fa897c.wasm", - "tx_ibc.wasm": "tx_ibc.1360fdf276c4591d937aaac3bb40ddb573abeba382651474ae23138aac65c3e5.wasm", - "tx_init_account.wasm": "tx_init_account.72d9e1daa43998ce617eafba1547b34f26b128f0fb6e02cfdbc85ecf1f345fd4.wasm", - "tx_init_nft.wasm": "tx_init_nft.22a886305fda24438dbf3fc0f864ee32db4a398bf748edd2fddba1fc4679bc35.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.649bde547179ebee5449f5954425cd266c5063fae97f98f06de327387e8898ba.wasm", - "tx_init_validator.wasm": "tx_init_validator.d40077c1bf1263e1e8e42f54cd893473c5d9c1f395e5d30f44a34d98d9b8dde4.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.78b3a43c5c5b4362cb3023c8e7e25755447e9022952306a62f2d8aab6e99dbee.wasm", - "tx_transfer.wasm": "tx_transfer.4c06f6af1f008fccdd42fefdc8015adea9fa60f802603a8be149ec2c6421656e.wasm", - "tx_unbond.wasm": "tx_unbond.c3d54895e1a271c86838d54d821d52b5bf5764928811cff30767a4445ebbf653.wasm", - "tx_update_vp.wasm": "tx_update_vp.3b709f301e55cb970ec1df98c85c2486561c3243ab1c191d3e078ee345b8b93a.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.671b46a77d03d10ca4e406f2bbc48adba0e541272a2963fd88d3a8c60e888fcd.wasm", - "tx_withdraw.wasm": "tx_withdraw.39e0012e110b19c7000400d11adc355bd8e89605b3c9ca10017c4766eed0ad69.wasm", - "vp_nft.wasm": "vp_nft.b23df820ae3faa13c9ed73121886b712fa0c613b7f43567f528cda05ef75fab6.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.45c740dcfa6e803d55a56cb51b02f2086147dfef3e955c0e8c3f18bf93ae0227.wasm", - "vp_token.wasm": "vp_token.74bca3c9999640c39bbd1c1364b65ffe8086e2d3ed124413251295d527326b57.wasm", - "vp_user.wasm": "vp_user.1c75ea1e55eb1244f023a525f9e20f0d34e52aebf0bd008dcf122fdc13bdf16a.wasm" + "tx_bond.wasm": "tx_bond.3923a43d94f9f0e6f599eea444b1e8da2f9e0dec9d91b17b1053ec25b8b27737.wasm", + "tx_ibc.wasm": "tx_ibc.a378f3a686ade80e2ff6cbe7b03f8c9dd5181daa5373cf528629233357168e09.wasm", + "tx_init_account.wasm": "tx_init_account.71a92f9d1e0536f770adf098fc56c2a192c2859230bd95d1b62c04ce95a0b7e6.wasm", + "tx_init_nft.wasm": "tx_init_nft.e9cebc207875226d7efe3ff480b7faf4777d0fdabd84be3d66183d16048b778c.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.7522b1ca3375fa8a046a7614a85c7159d389a1ead690e0f04fd725c48873bad8.wasm", + "tx_init_validator.wasm": "tx_init_validator.18f98312917073d012db8d074f68df844255a517716a94f5d7666309afd7105d.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.ea18cc8c175c9f67967fc273cf50b3190ad218aa928e5c6a072aa3944dd0d142.wasm", + "tx_transfer.wasm": "tx_transfer.5190bf12cace454f634c8d0f61574a31b9c2bff188107987fc3451c641b47d08.wasm", + "tx_unbond.wasm": "tx_unbond.3d053ba14f2e782a33f88c171ccbfaaf59e446c2de40f5c95dfdd7d41f165b8b.wasm", + "tx_update_vp.wasm": "tx_update_vp.838540296e54c77fe8ae25e7b8d1d0a71cfee8e2a5638118d30a40271c4c7764.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.4a8a05c36801eb617d4f6b4f356cd67e43b47ca0cf8e06d249c25b021eb3df41.wasm", + "tx_withdraw.wasm": "tx_withdraw.396dc8ec19baf320751e339e86ee361562e2f233535113c8e8453275a2d4c9dc.wasm", + "vp_nft.wasm": "vp_nft.15d1e95307e4efa29c3723aee6149550ec7a81704c82d31a5f8f2dbe132f6921.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.63337e8bbbd6673baec234b4d4a19e8e44788db7bf74fa55baad520494538229.wasm", + "vp_token.wasm": "vp_token.96eecb4464674c785b5d51d831ee8ee6d7a4e79c843c4889e426025f8abf395a.wasm", + "vp_user.wasm": "vp_user.519cd4d07b57e8237d1bb22e62b374f53b4f4e91e574d807061ec37a8b2897b0.wasm" } \ No newline at end of file From f787667bd91a8e1d0de0d3029ff08a1b3d875317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 20 Oct 2022 15:22:18 +0200 Subject: [PATCH 06/47] client: add a block query to print hash, height and time of a block --- apps/src/bin/anoma-client/cli.rs | 3 +++ apps/src/lib/cli.rs | 23 +++++++++++++++++++++++ apps/src/lib/client/rpc.rs | 15 +++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/apps/src/bin/anoma-client/cli.rs b/apps/src/bin/anoma-client/cli.rs index b87cdb5c66..cadc215bd4 100644 --- a/apps/src/bin/anoma-client/cli.rs +++ b/apps/src/bin/anoma-client/cli.rs @@ -52,6 +52,9 @@ pub async fn main() -> Result<()> { Sub::QueryEpoch(QueryEpoch(args)) => { rpc::query_epoch(args).await; } + Sub::QueryBlock(QueryBlock(args)) => { + rpc::query_block(args).await; + } Sub::QueryBalance(QueryBalance(args)) => { rpc::query_balance(ctx, args).await; } diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 8e4c7f78c9..f31d84b526 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -167,6 +167,7 @@ pub mod cmds { .subcommand(Withdraw::def().display_order(2)) // Queries .subcommand(QueryEpoch::def().display_order(3)) + .subcommand(QueryBlock::def().display_order(3)) .subcommand(QueryBalance::def().display_order(3)) .subcommand(QueryBonds::def().display_order(3)) .subcommand(QueryVotingPower::def().display_order(3)) @@ -198,6 +199,7 @@ pub mod cmds { let unbond = Self::parse_with_ctx(matches, Unbond); let withdraw = Self::parse_with_ctx(matches, Withdraw); let query_epoch = Self::parse_with_ctx(matches, QueryEpoch); + let query_block = Self::parse_with_ctx(matches, QueryBlock); let query_balance = Self::parse_with_ctx(matches, QueryBalance); let query_bonds = Self::parse_with_ctx(matches, QueryBonds); let query_voting_power = @@ -224,6 +226,7 @@ pub mod cmds { .or(unbond) .or(withdraw) .or(query_epoch) + .or(query_block) .or(query_balance) .or(query_bonds) .or(query_voting_power) @@ -283,6 +286,7 @@ pub mod cmds { Unbond(Unbond), Withdraw(Withdraw), QueryEpoch(QueryEpoch), + QueryBlock(QueryBlock), QueryBalance(QueryBalance), QueryBonds(QueryBonds), QueryVotingPower(QueryVotingPower), @@ -936,6 +940,25 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct QueryBlock(pub args::Query); + + impl SubCmd for QueryBlock { + const CMD: &'static str = "block"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| QueryBlock(args::Query::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Query the last committed block.") + .add_args::() + } + } + #[derive(Clone, Debug)] pub struct QueryBalance(pub args::QueryBalance); diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 6c1e3fb5f3..7eb580d212 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -72,6 +72,21 @@ pub async fn query_epoch(args: args::Query) -> Epoch { cli::safe_exit(1) } +/// Query the last committed block +pub async fn query_block( + args: args::Query, +) -> tendermint_rpc::endpoint::block::Response { + let client = HttpClient::new(args.ledger_address).unwrap(); + let response = client.latest_block().await.unwrap(); + println!( + "Last committed block ID: {}, height: {}, time: {}", + response.block_id, + response.block.header.height, + response.block.header.time + ); + response +} + /// Query the raw bytes of given storage key pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { let client = HttpClient::new(args.query.ledger_address).unwrap(); From ca3608dd528443e6e3c93932aab9559dcb786a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 20 Oct 2022 15:26:48 +0200 Subject: [PATCH 07/47] changelog: add #658 --- .changelog/unreleased/features/658-add-client-block-query.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/658-add-client-block-query.md diff --git a/.changelog/unreleased/features/658-add-client-block-query.md b/.changelog/unreleased/features/658-add-client-block-query.md new file mode 100644 index 0000000000..29584d3a0f --- /dev/null +++ b/.changelog/unreleased/features/658-add-client-block-query.md @@ -0,0 +1,2 @@ +- Client: Add a command to query the last committed block's hash, height and + timestamp. ([#658](https://github.com/anoma/namada/issues/658)) From ea2d533065106118931eec9f740ca9477c7a65ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 20 Oct 2022 15:35:01 +0200 Subject: [PATCH 08/47] test/e2e/helpers: add a helper to query and parse block height --- tests/src/e2e/helpers.rs | 60 ++++++++++++++++++++++++++++++++++- tests/src/e2e/ledger_tests.rs | 51 +++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 705c822760..d0dd60a688 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::process::Command; use std::str::FromStr; +use std::time::{Duration, Instant}; use std::{env, time}; use color_eyre::eyre::Result; @@ -14,7 +15,7 @@ use namada::types::key::*; use namada::types::storage::Epoch; use namada_apps::config::{Config, TendermintMode}; -use super::setup::{Test, ENV_VAR_DEBUG, ENV_VAR_USE_PREBUILT_BINARIES}; +use super::setup::{sleep, Test, ENV_VAR_DEBUG, ENV_VAR_USE_PREBUILT_BINARIES}; use crate::e2e::setup::{Bin, Who, APPS_PACKAGE}; use crate::run; @@ -148,6 +149,63 @@ pub fn get_epoch(test: &Test, ledger_address: &str) -> Result { Ok(Epoch(epoch)) } +/// Get the last committed block height. +pub fn get_height(test: &Test, ledger_address: &str) -> Result { + let mut find = run!( + test, + Bin::Client, + &["block", "--ledger-address", ledger_address], + Some(10) + )?; + let (unread, matched) = find.exp_regex("Last committed block ID: .*")?; + // Expected `matched` string is e.g.: + // + // ``` + // Last committed block F10B5E77F972F68CA051D289474B6E75574B446BF713A7B7B71D7ECFC61A3B21, height: 4, time: 2022-10-20T10:52:28.828745Z + // ``` + let height_str = strip_trailing_newline(&matched) + .trim() + // Find the height part ... + .split_once("height: ") + .unwrap() + // ... take what's after it ... + .1 + // ... find the next comma ... + .rsplit_once(',') + .unwrap() + // ... and take what's before it. + .0; + u64::from_str(height_str).map_err(|e| { + eyre!(format!( + "Height parsing failed from {} trimmed from {}, Error: \ + {}\n\nUnread output: {}", + height_str, matched, e, unread + )) + }) +} + +/// Sleep until the given height is reached or panic when time out is reached +/// before the height +pub fn wait_for_block_height( + test: &Test, + ledger_address: &str, + height: u64, + timeout_secs: u64, +) -> Result<()> { + let start = Instant::now(); + let loop_timeout = Duration::new(timeout_secs, 0); + loop { + let current = get_height(test, ledger_address)?; + if current >= height { + break Ok(()); + } + if Instant::now().duration_since(start) > loop_timeout { + panic!("Timed out waiting for height {height}, current {current}"); + } + sleep(1); + } +} + pub fn generate_bin_command(bin_name: &str, manifest_path: &Path) -> Command { let use_prebuilt_binaries = match env::var(ENV_VAR_USE_PREBUILT_BINARIES) { Ok(var) => var.to_ascii_lowercase() != "false", diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index a998844764..c3e94b9238 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -23,6 +23,7 @@ use namada_apps::config::genesis::genesis_config::{ use serde_json::json; use setup::constants::*; +use super::helpers::{get_height, wait_for_block_height}; use super::setup::get_all_wasms_hashes; use crate::e2e::helpers::{ find_address, find_voting_power, get_actor_rpc, get_epoch, @@ -123,6 +124,16 @@ fn test_node_connectivity() -> Result<()> { let _bg_validator_0 = validator_0.background(); let _bg_validator_1 = validator_1.background(); + let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); + let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); + let non_validator_rpc = get_actor_rpc(&test, &Who::NonValidator); + + // Find the block height on the validator + let after_tx_height = get_height(&test, &validator_0_rpc)?; + + // Wait for the non-validator to be synced to at least the same height + wait_for_block_height(&test, &non_validator_rpc, after_tx_height, 10)?; + let query_balance_args = |ledger_rpc| { vec![ "balance", @@ -134,10 +145,6 @@ fn test_node_connectivity() -> Result<()> { ledger_rpc, ] }; - - let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); - let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); - let non_validator_rpc = get_actor_rpc(&test, &Who::NonValidator); for ledger_rpc in &[validator_0_rpc, validator_1_rpc, non_validator_rpc] { let mut client = run!(test, Bin::Client, query_balance_args(ledger_rpc), Some(40))?; @@ -1848,7 +1855,7 @@ fn test_genesis_validators() -> Result<()> { let bg_validator_0 = validator_0.background(); let bg_validator_1 = validator_1.background(); - let bg_non_validator = non_validator.background(); + let _bg_non_validator = non_validator.background(); // 4. Submit a valid token transfer tx let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); @@ -1879,12 +1886,42 @@ fn test_genesis_validators() -> Result<()> { // 3. Check that all the nodes processed the tx with the same result let mut validator_0 = bg_validator_0.foreground(); let mut validator_1 = bg_validator_1.foreground(); - let mut non_validator = bg_non_validator.foreground(); let expected_result = "all VPs accepted transaction"; + // We cannot check this on non-validator node as it might sync without + // applying the tx itself, but its state should be the same, checked below. validator_0.exp_string(expected_result)?; validator_1.exp_string(expected_result)?; - non_validator.exp_string(expected_result)?; + let _bg_validator_0 = validator_0.background(); + let _bg_validator_1 = validator_1.background(); + + let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); + let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); + let non_validator_rpc = get_actor_rpc(&test, &Who::NonValidator); + + // Find the block height on the validator + let after_tx_height = get_height(&test, &validator_0_rpc)?; + + // Wait for the non-validator to be synced to at least the same height + wait_for_block_height(&test, &non_validator_rpc, after_tx_height, 10)?; + + let query_balance_args = |ledger_rpc| { + vec![ + "balance", + "--owner", + validator_1_alias, + "--token", + XAN, + "--ledger-address", + ledger_rpc, + ] + }; + for ledger_rpc in &[validator_0_rpc, validator_1_rpc, non_validator_rpc] { + let mut client = + run!(test, Bin::Client, query_balance_args(ledger_rpc), Some(40))?; + client.exp_string("XAN: 1000000000010.1")?; + client.assert_success(); + } Ok(()) } From 83e83315a5eaec302d08ed5db242d70625cfe45f Mon Sep 17 00:00:00 2001 From: Abuzer Rafey Date: Mon, 15 Aug 2022 03:04:50 -0400 Subject: [PATCH 09/47] Modify specs --- documentation/specs/src/SUMMARY.md | 5 +- documentation/specs/src/base-ledger.md | 2 +- .../specs/src/base-ledger/core-concepts.md | 15 +++++ .../specs/src/base-ledger/execution.md | 15 +++-- .../specs/src/base-ledger/governance.md | 65 ++++++++++++++----- documentation/specs/src/economics.md | 2 +- .../specs/src/economics/proof-of-stake.md | 4 +- .../proof-of-stake/bonding-mechanism.md | 22 ++----- .../proof-of-stake/cubic-slashing.md | 14 +++- .../proof-of-stake/reward-distribution.md | 11 ---- .../src/economics/public-goods-funding.md | 2 +- documentation/specs/src/further-reading.md | 14 +++- .../src/interoperability/ethereum-bridge.md | 6 +- .../specs/src/interoperability/ibc.md | 12 ++-- documentation/specs/src/introduction.md | 41 +++++------- documentation/specs/src/masp.md | 3 +- documentation/specs/src/masp/asset-type.md | 8 +-- documentation/specs/src/masp/burn-and-mint.md | 2 +- .../specs/src/masp/ledger-integration.md | 64 +++++++++--------- documentation/specs/src/masp/trusted-setup.md | 3 +- .../user-interfaces/external-integrations.md | 2 +- .../user-interfaces/web-explorer-interface.md | 10 +-- .../user-interfaces/web-wallet-interface.md | 10 ++- .../specs/src/user-interfaces/web-wallet.md | 2 +- 24 files changed, 182 insertions(+), 152 deletions(-) create mode 100644 documentation/specs/src/base-ledger/core-concepts.md diff --git a/documentation/specs/src/SUMMARY.md b/documentation/specs/src/SUMMARY.md index 65e2de1ddf..49891b44e9 100644 --- a/documentation/specs/src/SUMMARY.md +++ b/documentation/specs/src/SUMMARY.md @@ -2,12 +2,9 @@ - [Introduction](./introduction.md) - [Base ledger](./base-ledger.md) - - [Consensus](./base-ledger/consensus.md) + - [Core Concepts](./base-ledger/core-concepts.md) - [Execution](./base-ledger/execution.md) - [Governance](./base-ledger/governance.md) - - [Default account](./base-ledger/default-account.md) - - [Multisignature account](./base-ledger/multisignature.md) - - [Fungible token](./base-ledger/fungible-token.md) - [Multi-asset shielded pool](./masp.md) - [Ledger integration](./masp/ledger-integration.md) - [Asset type](./masp/asset-type.md) diff --git a/documentation/specs/src/base-ledger.md b/documentation/specs/src/base-ledger.md index b345208e58..a848fa4eac 100644 --- a/documentation/specs/src/base-ledger.md +++ b/documentation/specs/src/base-ledger.md @@ -1,3 +1,3 @@ ## Base ledger -The base ledger of Namada includes a [consensus system](./base-ledger/consensus.md), validity predicate-based [execution system](./base-ledger/execution.md), and signalling-based [governance mechanism](./base-ledger/governance.md). Namada's ledger also includes proof-of-stake, slashing, fees, and inflation funding for staking rewards, shielded pool incentives, and public goods -- these are specified in the [economics section](./economics.md). \ No newline at end of file +The base ledger of Namada includes a [consensus system](./base-ledger/core-concepts.md), validity predicate-based [execution system](./base-ledger/execution.md), and signalling-based [governance mechanism](./base-ledger/governance.md). Namada's ledger also includes proof-of-stake, slashing, fees, and inflation funding for staking rewards, shielded pool incentives, and public goods -- these are specified in the [economics section](./economics.md) \ No newline at end of file diff --git a/documentation/specs/src/base-ledger/core-concepts.md b/documentation/specs/src/base-ledger/core-concepts.md new file mode 100644 index 0000000000..61214acb95 --- /dev/null +++ b/documentation/specs/src/base-ledger/core-concepts.md @@ -0,0 +1,15 @@ +# Consensus + +Namada uses [Tendermint Go](https://github.com/tendermint/tendermint) through the [tendermint-rs](https://github.com/heliaxdev/tendermint-rs) bindings in order to provide peer-to-peer transaction gossip, BFT consensus, and state machine replication for Namada's custom state machine. + +## Default account + +The default account validity predicate authorises transactions on the basis of a cryptographic signature. + +## Fungible token + +The fungible token validity predicate authorises token balance changes on the basis of conservation-of-supply and approval-by-sender. + +## k-of-n multisignature + +The k-of-n multisignature validity predicate authorises transactions on the basis of k out of n parties approving them. \ No newline at end of file diff --git a/documentation/specs/src/base-ledger/execution.md b/documentation/specs/src/base-ledger/execution.md index 3395ffb858..24b5fb992b 100644 --- a/documentation/specs/src/base-ledger/execution.md +++ b/documentation/specs/src/base-ledger/execution.md @@ -2,17 +2,20 @@ The Namada ledger execution system is based on an initial version of the [Anoma protocol](https://specs.anoma.net). The system implements a generic computational substrate with WASM-based transactions and validity predicate verification architecture, on top of which specific features of Namada such as IBC, proof-of-stake, and the MASP are built. +## Validity predicates + +Conceptually, a validity predicate (VP) is a function from the transaction's data and the storage state prior and posterior to a transaction execution returning a boolean value. A transaction may modify any data in the accounts' dynamic storage sub-space. Upon transaction execution, the VPs associated with the accounts whose storage has been modified are invoked to verify the transaction. If any of them reject the transaction, all of its storage modifications are discarded. ## Namada ledger -The Namada ledger is built on top of [Tendermint](https://docs.tendermint.com/master/spec/)'s [ABCI](https://docs.tendermint.com/master/spec/abci/) interface with a slight deviation from the ABCI convention: in Namada, the transactions are currently *not* being executed in ABCI's `DeliverTx` method, but rather in the `EndBlock` method. The reason for this is to prepare for future DKG and threshold decryption integration, which has not yet been fully finished and hence is out-of-scope for the initial release version of Namada. +The Namada ledger is built on top of [Tendermint](https://docs.tendermint.com/master/spec/)'s [ABCI](https://docs.tendermint.com/master/spec/abci/) interface with a slight deviation from the ABCI convention: in Namada, the transactions are currently *not* being executed in ABCI's [`DeliverTx` method](https://docs.tendermint.com/master/spec/abci/abci.html), but rather in the [`EndBlock` method](https://docs.tendermint.com/master/spec/abci/abci.html). The reason for this is to prepare for future DKG and threshold decryption integration. The ledger features an account-based system (in which UTXO-based systems such as the MASP can be internally implemented as specific accounts), where each account has a unique address and a dynamic key-value storage sub-space. Every account in Namada is associated with exactly one validity predicate. Fungible tokens, for example, are accounts, whose rules are governed by their validity predicates. Many of the base ledger subsystems specified here are themselves just special Namada accounts too (e.g. PoS, IBC and MASP). -Interaction with the Namada ledger are made possible via transactions (note [transaction whitelist](#transaction-and-validity-predicate-whitelist)). Please refer to the [protocol section](https://docs.anoma.network/master/specs/ledger.html#the-protocol) that specifies the transaction execution model. In Namada, transactions are allowed to perform arbitrary modifications to the storage of any account, but the transaction will be accepted and state changes applied only if all the validity predicates that were triggered by the transaction accept it. That is, the accounts whose storage sub-spaces were touched by the transaction and/or an account that was explicitly elected by the transaction as the verifier will all have their validity predicates verifying the transaction. A transaction can add any number of additional verifiers, but cannot remove the ones determined by the protocol. For example, a transparent fungible token transfer would typically trigger 3 validity predicates - those of the token, source and target addresses. +Interaction with the Namada ledger are made possible via transactions (note transaction whitelist below). In Namada, transactions are allowed to perform arbitrary modifications to the storage of any account, but the transaction will be accepted and state changes applied only if all the validity predicates that were triggered by the transaction accept it. That is, the accounts whose storage sub-spaces were touched by the transaction and/or an account that was explicitly elected by the transaction as the verifier will all have their validity predicates verifying the transaction. A transaction can add any number of additional verifiers, but cannot remove the ones determined by the protocol. For example, a transparent fungible token transfer would typically trigger 3 validity predicates - those of the token, source and target addresses. ## Supported validity predicates -Conceptually, a VP is a function from the transaction's data and the storage state prior and posterior to a transaction execution returning a boolean value. A transaction may modify any data in the accounts' dynamic storage sub-space. Upon transaction execution, the VPs associated with the accounts whose storage has been modified are invoked to verify the transaction. If any of them reject the transaction, all of its storage modifications are discarded. While the execution model is fully programmable, for Namada only a selected subset of provided validity predicates and transactions will be permitted through pre-defined whitelists configured at network launch. +While the execution model is fully programmable, for Namada only a selected subset of provided validity predicates and transactions will be permitted through pre-defined whitelists configured at network launch. There are some native VPs for internal transparent addresses that are built into the ledger. All the other VPs are implemented as WASM programs. One can build a custom VP using the [VP template](https://github.com/anoma/anoma/tree/master/wasm/vp_template) or use one of the pre-defined VPs. @@ -24,7 +27,7 @@ Supported validity predicates for Namada: - SlashFund (see [spec](./governance.md#SlashFundAddress)) - Protocol parameters - WASM - - Fungible token (see [spec](./fungible-token.md)) + - Fungible token (see [spec](./core-concepts.md)) - MASP (see [spec](../masp.md)) - - Implicit account VP (see [spec](./default-account.md)) - - k-of-n multisignature VP (see [spec](./multisignature.md)) + - Implicit account VP (see [spec](./core-concepts.md)) + - k-of-n multisignature VP (see [spec](./core-concepts.md)) diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index 40c7281af9..7372a00539 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -1,11 +1,23 @@ # Namada Governance +Before describing Namada governance, it is useful to define the concepts of validators, delegators, and NAM. + +Namada's economic model is based around a single native token, NAM, which is controlled by the protocol. + +A Namada validator is an account with a public consensus key, which may participate in producing blocks and governance activities. A validator may not also be a delegator. + +A Namada delegator is an account that delegates some tokens to a validator. A delegator may not also be a validator. + Namada introduces a governance mechanism to propose and apply protocol changes with or without the need for a hard fork. Anyone holding some `NAM` will be able to propose some changes to which delegators and validators will cast their `yay` or `nay` votes. Governance on Namada supports both `signaling` and `voting` mechanisms. The difference between the the two is that the former is needed when the changes require a hard fork. In cases where the chain is not able to produce blocks anymore, Namada relies on [off chain](#off-chain-protocol) signaling to agree on a common move. +Further informtion regarding delegators, validators, and NAM is contained in the [economics section](../economics.md). + ## On-chain protocol ### Governance Address + Governance adds 2 internal addresses: + - `GovernanceAddress` - `SlashFundAddress` @@ -13,6 +25,7 @@ The first internal address contains all the proposals under its address space. The second internal address holds the funds of rejected proposals. ### Governance storage + Each proposal will be stored in a sub-key under the internal proposal address. The storage keys involved are: ``` @@ -26,6 +39,8 @@ Each proposal will be stored in a sub-key under the internal proposal address. T /\$GovernanceAddress/proposal/epoch/\$id: u64 ``` +An epoch is a range of blocks or time that is defined by the base ledger and made available to the PoS system. This document assumes that epochs are identified by consecutive natural numbers. All the data relevant to PoS are [associated with epochs](../economics/proof-of-stake/bonding-mechanism.md/#epoched-data). + - `Author` address field will be used to credit the locked funds if the proposal is approved. - `/\$GovernanceAddress/proposal/\$epoch/\$id` is used for easing the ledger governance execution. `\$epoch` refers to the same value as the on specific in the `grace_epoch` field. - The `content` value should follow a standard format. We leverage a similar format to what is described in the [BIP2](https://github.com/bitcoin/bips/blob/master/bip-0002.mediawiki#bip-format-and-structure) document: @@ -74,14 +89,16 @@ This is to leverage the `NAM` VP to check that the funds were correctly locked. The governance subkey, `/\$GovernanceAddress/proposal/\$id/funds` will be used after the tally step to know the exact amount of tokens to refund or move to Treasury. ### GovernanceAddress VP -Just like Pos, also governance has his own storage space. The `GovernanceAddress` validity predicate task is to check the integrity and correctness of new proposals. A proposal, to be correct, must satisfy the following: + +Just like PoS, also governance has its own storage space. The `GovernanceAddress` validity predicate task is to check the integrity and correctness of new proposals. A proposal, to be correct, must satisfy the following: + - Mandatory storage writes are: - - counter - - author - - funds - - voting_start epoch - - voting_end epoch - - grace_epoch + - counter + - author + - funds + - voting_start epoch + - voting_end epoch + - grace_epoch - Lock some funds >= `min_proposal_fund` - Contains a unique ID - Contains a start, end and grace Epoch @@ -89,8 +106,8 @@ Just like Pos, also governance has his own storage space. The `GovernanceAddress - Should contain a text describing the proposal with length < `max_proposal_content_size` characters. - Vote can be done only by a delegator or validator - Validator can vote only in the initial 2/3 of the whole proposal duration (`end_epoch` - `start_epoch`) -- Due to the previous requirement, the following must be true,`(EndEpoch - StartEpoch) % 3 == 0` -- If defined, `proposalCode` should be the wasm bytecode representation of +- Due to the previous requirement, the following must be true,`(EndEpoch - StartEpoch) % 3 == 0` +- If defined, `proposalCode` should be the wasm bytecode representation of the changes. This code is triggered in case the proposal has a position outcome. - The difference between `grace_epoch` and `end_epoch` should be of at least `min_proposal_grace_epochs` @@ -100,6 +117,7 @@ If `proposal_code` is `Empty` or `None` , the proposal upgrade will need to be It is possible to check the actual implementation [here](https://github.com/anoma/namada/blob/master/shared/src/ledger/governance/mod.rs#L69). Example of `proposalCode` could be: + - storage writes to change some protocol parameter - storage writes to restore a slash - storage writes to change a non-native vp @@ -140,19 +158,20 @@ Vote transaction creates or modifies the following storage key: /\$GovernanceAddress/proposal/\$id/vote/\$delegation_address/\$voter_address: Enum(yay|nay) ``` -The storage key will only be created if the transaction is signed either by -a validator or a delegator. +The storage key will only be created if the transaction is signed either by +a validator or a delegator. Validators will be able to vote only for 2/3 of the total voting period, while delegators can vote until the end of the voting period. -If a delegator votes opposite to its validator, this will *override* the +If a delegator votes opposite to its validator, this will *override* the corresponding vote of this validator (e.g. if a delegator has a voting power of 200 and votes opposite to the delegator holding these tokens, than 200 will be subtracted from the voting power of the involved validator). As a small form of space/gas optimization, if a delegator votes accordingly to its validator, the vote will not actually be submitted to the chain. This logic is applied only if the following conditions are satisfied: - The transaction is not being forced -- The vote is submitted in the last third of the voting period (the one exclusive to delegators). This second condition is necessary to prevent a validator from changing its vote after a delegator vote has been submitted, effectively stealing the delegator's vote. +- The vote is submitted in the last third of the voting period (the one exclusive to delegators). This second condition is necessary to prevent a validator from changing its vote after a delegator vote has been submitted, effectively stealing the delegator's vote. ### Tally + At the beginning of each new epoch (and only then), in the `FinalizeBlock` event, tallying will occur for all the proposals ending at this epoch (specified via the `grace_epoch` field of the proposal). The proposal has a positive outcome if 2/3 of the staked `NAM` total is voting `yay`. Tallying is computed with the following rules: @@ -166,33 +185,39 @@ All the computation above must be made at the epoch specified in the `start_epoc It is possible to check the actual implementation [here](https://github.com/anoma/namada/blob/master/shared/src/ledger/governance/utils.rs#L68). ### Refund and Proposal Execution mechanism -Together with tallying, in the first block at the beginning of each epoch, in the `FinalizeBlock` event, the protocol will manage the execution of accepted proposals and refunding. For each ended proposal with a positive outcome, it will refund the locked funds from `GovernanceAddress` to the proposal author address (specified in the proposal `author` field). For each proposal that has been rejected, instead, the locked funds will be moved to the `SlashFundAddress`. Moreover, if the proposal had a positive outcome and `proposal_code` is defined, these changes will be executed right away. + +Together with tallying, in the first block at the beginning of each epoch, in the `FinalizeBlock` event, the protocol will manage the execution of accepted proposals and refunding. A successful proposal will refund the locked funds from `GovernanceAddress` to the proposal author address (specified in the proposal `author` field). A failed proposal will move locked funds to `SlashFundAddress`. Moreover, if the proposal had a positive outcome and `proposal_code` is defined, these changes will be executed right away. To summarize the execution of governance in the `FinalizeBlock` event: If the proposal outcome is positive and current epoch is equal to the proposal `grace_epoch`, in the `FinalizeBlock` event: + - transfer the locked funds to the proposal `author` - execute any changes specified by `proposal_code` In case the proposal was rejected or if any error, in the `FinalizeBlock` event: -- transfer the locked funds to `SlashFundAddress` -The result is then signaled by creating and inserting a [`Tendermint Event`](https://github.com/tendermint/tendermint/blob/ab0835463f1f89dcadf83f9492e98d85583b0e71/docs/spec/abci/abci.md#events. +- transfer the locked funds to `SlashFundAddress` +The result is then signaled by creating and inserting a [`Tendermint Event`](https://github.com/tendermint/tendermint/blob/ab0835463f1f89dcadf83f9492e98d85583b0e71/docs/spec/abci/abci.md#events). ## SlashFundAddress + Funds locked in `SlashFundAddress` address should be spendable only by proposals. ### SlashFundAddress storage + ``` /\$SlashFundAddress/?: Vec ``` The funds will be stored under: + ``` /\$NAMAddress/balance/\$SlashFundAddress: u64 ``` ### SlashFundAddress VP + The slash_fund validity predicate will approve a transfer only if the transfer has been made by the protocol (by checking the existence of `/\$GovernanceAddress/pending/\$proposal_id` storage key) It is possible to check the actual implementation [here](https://github.com/anoma/namada/blob/main/shared/src/ledger/slash_fund/mod.rs#L70). @@ -200,8 +225,10 @@ It is possible to check the actual implementation [here](https://github.com/anom ## Off-chain protocol ### Create proposal -A CLI command to create a signed JSON representation of the proposal. The + +A CLI command to create a signed JSON representation of the proposal. The JSON will have the following structure: + ``` { content: Base64>, @@ -216,8 +243,9 @@ The signature is produced over the hash of the concatenation of: `content`, `aut ### Create vote -A CLI command to create a signed JSON representation of a vote. The JSON +A CLI command to create a signed JSON representation of a vote. The JSON will have the following structure: + ``` { proposalHash: Base64>, @@ -230,6 +258,7 @@ will have the following structure: The proposalHash is produced over the concatenation of: `content`, `author`, `votingStart`, `votingEnd`, `voter` and `vote`. ### Tally + Same mechanism as [on chain](#tally) tally but instead of reading the data from storage it will require a list of serialized json votes. ## Interfaces diff --git a/documentation/specs/src/economics.md b/documentation/specs/src/economics.md index bf7346b63a..f18c7d7023 100644 --- a/documentation/specs/src/economics.md +++ b/documentation/specs/src/economics.md @@ -1,3 +1,3 @@ ## Economics -Namada's economic model is based around a single native token, NAM, which is controlled by the protocol. Users pay transaction fees in NAM and other tokens (see [fee system](./economics/fee-system.md)), so demand for NAM can be expected to track demand for block space. On the supply side, the protocol mints NAM at a fixed maximum per-annum rate based on a fraction of the current supply (see [inflation system](./economics/inflation-system.md)), which is directed to three areas of protocol subsidy: [proof-of-stake](./economics/proof-of-stake.md), [shielded pool incentives](./economics/shielded-pool-incentives.md), and [public-goods funding](./economics/public-goods-funding.md). Inflation rates for these three areas are adjusted independently (the first two on PD controllers and the third based on funding decisions) and excess tokens are slowly burned. \ No newline at end of file +Namada users pay transaction fees in NAM and other tokens (see [fee system](./economics/fee-system.md) and [governance](./base-ledger/governance.md)), so demand for NAM can be expected to track demand for block space. On the supply side, the protocol mints NAM at a fixed maximum per-annum rate based on a fraction of the current supply (see [inflation system](./economics/inflation-system.md)), which is directed to three areas of protocol subsidy: [proof-of-stake](./economics/proof-of-stake.md), [shielded pool incentives](./economics/shielded-pool-incentives.md), and [public-goods funding](./economics/public-goods-funding.md). Inflation rates for these three areas are adjusted independently (the first two on PD controllers and the third based on funding decisions) and excess tokens are slowly burned. \ No newline at end of file diff --git a/documentation/specs/src/economics/proof-of-stake.md b/documentation/specs/src/economics/proof-of-stake.md index 808f02e585..3102616e65 100644 --- a/documentation/specs/src/economics/proof-of-stake.md +++ b/documentation/specs/src/economics/proof-of-stake.md @@ -4,7 +4,7 @@ This section of the specification describes the proof-of-stake mechanism of Nama This section is split into three subcomponents: the [bonding mechanism](./proof-of-stake/bonding-mechanism.md), [reward distribution](./proof-of-stake/reward-distribution.md), and [cubic slashing](./proof-of-stake/cubic-slashing.md). -## Introduction +## Context Blockchain systems rely on economic security (directly or indirectly) to prevent @@ -13,7 +13,7 @@ for actors to behave according to protocol. The aim is that economic incentives promote correct long-term operation of the system and economic punishments discourage diverging from correct protocol execution either by mistake or -with the intent of carrying out attacks. Many PoS blockcains rely on the 1/3 Byzantine rule, where they make the assumption the adversary cannot control more 2/3 of the total stake or 2/3 of the actors. +with the intent of carrying out attacks. Many PoS blockchains rely on the 1/3 Byzantine rule, where they make the assumption the adversary cannot control more 2/3 of the total stake or 2/3 of the actors. ## Goals of Rewards and Slashing: Liveness and Security diff --git a/documentation/specs/src/economics/proof-of-stake/bonding-mechanism.md b/documentation/specs/src/economics/proof-of-stake/bonding-mechanism.md index ab0449e026..349e766def 100644 --- a/documentation/specs/src/economics/proof-of-stake/bonding-mechanism.md +++ b/documentation/specs/src/economics/proof-of-stake/bonding-mechanism.md @@ -1,9 +1,5 @@ # Bonding mechanism -## Epoch - -An epoch is a range of blocks or time that is defined by the base ledger and made available to the PoS system. This document assumes that epochs are identified by consecutive natural numbers. All the data relevant to PoS are [associated with epochs](#epoched-data). - ### Epoched data Epoched data is data associated with a specific epoch that is set in advance. @@ -16,16 +12,11 @@ The data relevant to the PoS system in the ledger's state are epoched. Each data Changes to the epoched data do not take effect immediately. Instead, changes in epoch `n` are queued to take effect in the epoch `n + pipeline_length` for most cases and `n + unboding_length` for [unbonding](#unbond) actions. Should the same validator's data or same bonds (i.e. with the same identity) be updated more than once in the same epoch, the later update overrides the previously queued-up update. For bonds, the token amounts are added up. Once the epoch `n` has ended, the queued-up updates for epoch `n + pipeline_length` are final and the values become immutable. -## Entities - -- [Validator](#validator): An account with a public consensus key, which may participate in producing blocks and governance activities. A validator may not also be a delegator. -- [Delegator](#delegator): An account that delegates some tokens to a validator. A delegator may not also be a validator. - Additionally, any account may submit evidence for [a slashable misbehaviour](#slashing). ### Validator -A validator must have a public consensus key. Additionally, it may also specify optional metadata fields (TBA). +A validator must have a public consensus key. A validator may be in one of the following states: - *inactive*: @@ -115,7 +106,7 @@ Once an offence has been reported: - [cubic slashing](./cubic-slashing.md): escalated slashing -Instead of absolute values, validators' total bonded token amounts and bonds' and unbonds' token amounts are stored as their deltas (i.e. the change of quantity from a previous epoch) to allow distinguishing changes for different epoch, which is essential for determining whether tokens should be slashed. However, because slashes for a fault that occurred in epoch `n` may only be applied before the beginning of epoch `n + unbonding_length`, in epoch `m` we can sum all the deltas of total bonded token amounts and bonds and unbond with the same source and validator for epoch equal or less than `m - unboding_length` into a single total bonded token amount, single bond and single unbond record. This is to keep the total number of total bonded token amounts for a unique validator and bonds and unbonds for a unique pair of source and validator bound to a maximum number (equal to `unbonding_length`). +Instead of absolute values, validators' total bonded token amounts and bonds' and unbonds' token amounts are stored as their deltas (i.e. the change of quantity from a previous epoch) to allow distinguishing changes for different epoch, which is essential for determining whether tokens should be slashed. Slashes for a fault that occurred in epoch `n` may only be applied before the beginning of epoch `n + unbonding_length`. For this reason, in epoch `m` we can sum all the deltas of total bonded token amounts and bonds and unbond with the same source and validator for epoch equal or less than `m - unboding_length` into a single total bonded token amount, single bond and single unbond record. This is to keep the total number of total bonded token amounts for a unique validator and bonds and unbonds for a unique pair of source and validator bound to a maximum number (equal to `unbonding_length`). To disincentivize validators misbehaviour in the PoS system a validator may be slashed for any fault that it has done. An evidence of misbehaviour may be submitted by any account for a fault that occurred in epoch `n` anytime before the beginning of epoch `n + unbonding_length`. @@ -123,6 +114,11 @@ A valid evidence reduces the validator's total bonded token amount by the slash The invariant is that the sum of amounts that may be withdrawn from a misbehaving validator must always add up to the total bonded token amount. +## Initialization + +An initial validator set with self-bonded token amounts must be given on system initialization. + +This set is used to pre-compute epochs in the genesis block from epoch `0` to epoch `pipeline_length - 1`. ## System parameters @@ -296,8 +292,4 @@ struct Slash { } ``` -## Initialization - -An initial validator set with self-bonded token amounts must be given on system initialization. -This set is used to pre-compute epochs in the genesis block from epoch `0` to epoch `pipeline_length - 1`. diff --git a/documentation/specs/src/economics/proof-of-stake/cubic-slashing.md b/documentation/specs/src/economics/proof-of-stake/cubic-slashing.md index 4d4ac07523..d63347308e 100644 --- a/documentation/specs/src/economics/proof-of-stake/cubic-slashing.md +++ b/documentation/specs/src/economics/proof-of-stake/cubic-slashing.md @@ -31,4 +31,16 @@ calculateSlashRate slashes = Validator can later submit a transaction to unjail themselves after a configurable period. When the transaction is applied and accepted, the validator updates its state to "candidate" and is added back to the validator set starting at the epoch at pipeline offset (active or inactive, depending on its voting power). -At present, funds slashed are sent to the governance treasury. In the future we could potentially reward the slash discoverer with part of the slash, for which some sort of commit-reveal mechanism will be required to prevent front-running. +At present, funds slashed are sent to the governance treasury. + +## Slashes + +Slashes should lead to punishment for delegators who were contributing voting power to the validator at the height of the infraction, _as if_ the delegations were iterated over and slashed individually. + +This can be implemented as a negative inflation rate for a particular block. + +Instant redelegation is not supported. Redelegations must wait the unbonding period. + + \ No newline at end of file diff --git a/documentation/specs/src/economics/proof-of-stake/reward-distribution.md b/documentation/specs/src/economics/proof-of-stake/reward-distribution.md index 70f662f97a..730e423e76 100644 --- a/documentation/specs/src/economics/proof-of-stake/reward-distribution.md +++ b/documentation/specs/src/economics/proof-of-stake/reward-distribution.md @@ -152,16 +152,5 @@ The commission rate $c_V(e)$ is the same for all delegations to a validator $V$ While rewards are given out at the end of every epoch, voting power is only updated after the pipeline offset. According to the [proof-of-stake system](bonding-mechanism.md#epoched-data), at the current epoch `e`, the validator sets an only be updated for epoch `e + pipeline_offset`, and it should remain unchanged from epoch `e` to `e + pipeline_offset - 1`. Updating voting power in the current epoch would violate this rule. -## Slashes - -Slashes should lead to punishment for delegators who were contributing voting power to the validator at the height of the infraction, _as if_ the delegations were iterated over and slashed individually. - -This can be implemented as a negative inflation rate for a particular block. - -Instant redelegation is not supported. Redelegations must wait the unbonding period. - - diff --git a/documentation/specs/src/economics/public-goods-funding.md b/documentation/specs/src/economics/public-goods-funding.md index 96e82dfc93..bfb779b1fa 100644 --- a/documentation/specs/src/economics/public-goods-funding.md +++ b/documentation/specs/src/economics/public-goods-funding.md @@ -10,7 +10,7 @@ There is a lot of existing research into public-goods funding to which justice c Namada instantiates a dual proactive/retroactive public-goods funding model, stewarded by a public-goods council elected by limited liquid democracy. -This proposal requires the following protocol components: +This requires the following protocol components: - Limited liquid democracy / targeted delegation: Namada's current voting mechanism is altered to add targeted delegation. By default, each delegator delegates their vote in governance to their validator, but they can set an alternative governance delegate who can instead vote on their behalf (but whose vote can be overridden as usual). Validators can also set governance delegates, in which case those delegates can vote on their behalf, and on the behalf of all delegators to that validator who do not override the vote, unless the validator overrides the vote. This is a limited form of liquid democracy which could be extended in the future. - Funding council: bi-annually (every six months), Namada governance elects a public goods funding council by stake-weighted approval vote (see below). Public goods funding councils run as groups. The public goods funding council decides according to internal decision-making procedures (practically probably limited to a k-of-n multisignature) how to allocate continuous funding and retroactive funding during their term. Namada genesis includes an initial funding council, and the next election will occur six months after launch. - Continuous funding: Namada prints an amount of inflation fixed on a percentage basis dedicated to continuous funding. Each quarter, the public goods funding council selects recipients and amounts (which in total must receive all of the funds, although they could burn some) and submits this list to the protocol. Inflation is distributed continuously by the protocol to these recipients during that quarter. diff --git a/documentation/specs/src/further-reading.md b/documentation/specs/src/further-reading.md index 971007c530..d92add791f 100644 --- a/documentation/specs/src/further-reading.md +++ b/documentation/specs/src/further-reading.md @@ -1,3 +1,15 @@ ## Further reading -Thanks for reading! You can find further information about the state of Namada at [namada.net](https://namada.net). \ No newline at end of file +Thanks for reading! You can find further information about the project below: + +[The state of Namada](https://namada.net). +[The state of Anoma](https://anoma.net/) +[Anoma source code](https://github.com/anoma/anoma) +[Namada source code](https://github.com/anoma/namada) +[Anoma Community](https://anoma.net/community) +[Heliax](https://heliax.dev/) +[Anoma Medium page](https://medium.com/anomanetwork) +[Namada Docs](https://docs.namada.net/) +[Anoma Discord](https://discord.com/invite/anoma) +[Namada Twitter](https://twitter.com/namadanetwork) +[Anoma Twitter](https://twitter.com/anomanetwork) \ No newline at end of file diff --git a/documentation/specs/src/interoperability/ethereum-bridge.md b/documentation/specs/src/interoperability/ethereum-bridge.md index cfc5ce6841..c6d52e3447 100644 --- a/documentation/specs/src/interoperability/ethereum-bridge.md +++ b/documentation/specs/src/interoperability/ethereum-bridge.md @@ -320,7 +320,7 @@ struct MintWrappedNam { ``` If a user wishes to mint a wrapped Namada token on Ethereum, they must submit a transaction on Namada that: -- stores `MintWrappedNam` on chain somewhere - TBD +- stores `MintWrappedNam` on chain somewhere - sends the correct amount of Namada token to `#EthBridgeEscrow` Just as in redeeming Ethereum assets above, it is incumbent on the end user to @@ -385,7 +385,7 @@ appropriate state change, emit logs, etc. ## Starting the bridge Before the bridge can start running, some storage may need to be initialized in -Namada. TBD. +Namada. ## Resources which may be helpful: - [Gravity Bridge Solidity contracts](https://github.com/Gravity-Bridge/Gravity-Bridge/tree/main/solidity) @@ -393,5 +393,3 @@ Namada. TBD. - [Rainbow Bridge contracts](https://github.com/aurora-is-near/rainbow-bridge/tree/master/contracts) - [IBC in Solidity](https://github.com/hyperledger-labs/yui-ibc-solidity) -Operational notes: -1. We will bundle the Ethereum full node with the `namada` daemon executable. diff --git a/documentation/specs/src/interoperability/ibc.md b/documentation/specs/src/interoperability/ibc.md index 198266dd93..0848243d33 100644 --- a/documentation/specs/src/interoperability/ibc.md +++ b/documentation/specs/src/interoperability/ibc.md @@ -6,29 +6,27 @@ ## IBC transaction An IBC transaction [`tx_ibc.wasm`](https://github.com/anoma/anoma/blob/fd4b7ab36929f47369ae82c82966891cb0ccc625/wasm/wasm_source/src/lib.rs#L224-L233) is provided. We have to set an IBC message to the transaction data corresponding to execute an IBC operation. -The transaction decodes the data to an IBC message and handles IBC-related data, e.g. it makes a new connection ID and writes a new connection end for `MsgConnectionOpenTry`. The operations are implemented in [`IbcActions`](https://docs.anoma.network/master/rustdoc/anoma/ledger/ibc/handler/trait.IbcActions.html).The transaction doesn't check the validity for the state changes. IBC validity predicate is in charge of the validity. +The transaction decodes the data to an IBC message and handles IBC-related data, e.g. it makes a new connection ID and writes a new connection end for `MsgConnectionOpenTry`. The operations are implemented in [`IbcActions`](https://github.com/anoma/anoma/blob/50b5e77f04a9afc036656353335bd232fcdba8a7/vm_env/src/ibc.rs).The transaction doesn't check the validity for the state changes. IBC validity predicate is in charge of the validity. ## IBC validity predicate -[IBC validity predicate](https://docs.anoma.network/master/rustdoc/anoma/ledger/ibc/vp/struct.Ibc.html#impl-NativeVp) checks if an IBC-related transaction satisfies IBC protocol. When an IBC-related transaction is executed, i.e. a transaction changes the state of the key that contains [`InternalAddress::Ibc`](https://docs.anoma.network/master/rustdoc/anoma/types/address/enum.InternalAddress.html#variant.Ibc), IBC validity predicate (one of the native validity predicates) is executed. For example, if an IBC connection end is created in the transaction, IBC validity predicate validates the creation. If the creation with `MsgConnectionOpenTry` is invalid, e.g. the counterpart connection end doesn't exist, the validity predicate makes the transaction fail. +[IBC validity predicate](https://docs.anoma.network/master/rustdoc/anoma/ledger/ibc/vp/struct.Ibc.html#impl-NativeVp) checks if an IBC-related transaction satisfies IBC protocol. When an IBC-related transaction is executed, i.e. a transaction changes the state of the key that contains [`InternalAddress::Ibc`](https://github.com/anoma/anoma/blob/50b5e77f04a9afc036656353335bd232fcdba8a7/shared/src/types/address.rs), IBC validity predicate (one of the native validity predicates) is executed. For example, if an IBC connection end is created in the transaction, IBC validity predicate validates the creation. If the creation with `MsgConnectionOpenTry` is invalid, e.g. the counterpart connection end doesn't exist, the validity predicate makes the transaction fail. ## Fungible Token Transfer The transfer of fungible tokens over an IBC channel on separate chains is defined in [ICS20](https://github.com/cosmos/ibc/blob/master/spec/app/ics-020-fungible-token-transfer/README.md). -In Anoma, the sending tokens is triggered by a transaction having [MsgTransfer](https://github.com/informalsystems/ibc-rs/blob/0a952b295dbcf67bcabb79ce57ce92c9c8d7e5c6/modules/src/applications/ics20_fungible_token_transfer/msgs/transfer.rs#L20-L37) as transaction data. A packet including [`FungibleTokenPacketData`](https://docs.anoma.network/master/rustdoc/anoma/types/ibc/data/struct.FungibleTokenPacketData.html) is made from the message in the transaction execution. +In Anoma, the sending tokens is triggered by a transaction having [MsgTransfer](https://github.com/informalsystems/ibc-rs/blob/0a952b295dbcf67bcabb79ce57ce92c9c8d7e5c6/modules/src/applications/ics20_fungible_token_transfer/msgs/transfer.rs#L20-L37) as transaction data. A packet including [`FungibleTokenPacketData`](https://github.com/anoma/anoma/blob/50b5e77f04a9afc036656353335bd232fcdba8a7/shared/src/types/ibc/data.rs) is made from the message in the transaction execution. Anoma chain receives the tokens by a transaction having [MsgRecvPacket](https://github.com/informalsystems/ibc-rs/blob/0a952b295dbcf67bcabb79ce57ce92c9c8d7e5c6/modules/src/core/ics04_channel/msgs/recv_packet.rs#L19-L23) which has the packet including `FungibleTokenPacketData`. The sending and receiving tokens in a transaction are validated by not only -IBC validity predicate but also [IBC token validity predicate](https://docs. -anoma.network/master/rustdoc/anoma/ledger/ibc/vp/struct.IbcToken. -html#impl-NativeVp). IBC validity predicate validates if sending and receiving the packet is proper. IBC token validity predicate is also one of the native validity predicates and checks if the token transfer is valid. If the transfer is not valid, e.g. an unexpected amount is minted, the validity predicate makes the transaction fail. +IBC validity predicate but also [IBC token validity predicate](https://github.com/anoma/anoma/blob/50b5e77f04a9afc036656353335bd232fcdba8a7/shared/src/ledger/ibc/vp/token.rs). IBC validity predicate validates if sending and receiving the packet is proper. IBC token validity predicate is also one of the native validity predicates and checks if the token transfer is valid. If the transfer is not valid, e.g. an unexpected amount is minted, the validity predicate makes the transaction fail. A transaction escrowing/unescrowing a token changes the escrow account's balance of the token. The key is `{token_addr}/balance/{escrow_addr}`. A transaction burning a token changes the burn account's balance of the token. The key is `{token_addr}/balance/BURN_ADDR`. A transaction minting a token changes the mint account's balance of the token. The key is `{token_addr} -/balance/MINT_ADDR`. `{escrow_addr}`, `{BURN_ADDR}`, and `{MINT_ADDR}` are addresses of [`InternalAddress`](https://docs.anoma.network/master/rustdoc/anoma/types/address/enum.InternalAddress.html). When these addresses are included in the changed keys after transaction execution, IBC token validity predicate is executed. +/balance/MINT_ADDR`. `{escrow_addr}`, `{BURN_ADDR}`, and `{MINT_ADDR}` are addresses of [`InternalAddress`](https://github.com/anoma/anoma/blob/50b5e77f04a9afc036656353335bd232fcdba8a7/shared/src/types/address.rs). When these addresses are included in the changed keys after transaction execution, IBC token validity predicate is executed. ## IBC message diff --git a/documentation/specs/src/introduction.md b/documentation/specs/src/introduction.md index 6f4971e049..0d4f83fd33 100644 --- a/documentation/specs/src/introduction.md +++ b/documentation/specs/src/introduction.md @@ -2,31 +2,28 @@ Welcome to the Namada specifications! +## What is Namada? + Namada is a sovereign proof-of-stake blockchain, using Tendermint BFT consensus, that enables multi-asset private transfers for any native or non-native asset -using a multi-asset shielded pool derived from the Sapling circuit. Namada features -full IBC protocol support, a natively integrated Ethereum bridge, a modern proof-of-stake -system with automatic reward compounding and cubic slashing, a stake-weighted governance -signalling mechanism, and a proactive/retroactive public goods funding system. -Users of shielded transfers are rewarded for their contributions -to the privacy set in the form of native protocol tokens. A multi-asset shielded transfer wallet -is provided in order to facilitate safe and private user interaction with the protocol. +using a [multi-asset shielded pool](https://research.metastate.dev/multi-asset_shielded_pool/) derived from the [Sapling circuit](https://z.cash/upgrade/sapling/). Namada features full IBC protocol support, a natively integrated Ethereum bridge, a modern proof-of-stakesystem with automatic reward compounding and cubic slashing, a stake-weighted governance signalling mechanism, and a proactive/retroactive public goods funding system. Users of shielded transfers are rewarded for their contributions to the privacy set in the form of native protocol tokens. A multi-asset shielded transfer wallet is provided in order to facilitate safe and private user interaction with the protocol. + +You can learn more about Namada [here](https://medium.com/anomanetwork/introducing-namada-shielded-transfers-with-any-assets-dce2e579384c). +### What is Anoma? -### How does Namada relate to Anoma? +The Anoma protocol is designed to facilitate the operation of networked fractal instances, which intercommunicate but can utilise varied state machines and security models. +A fractal instance is an instance of the Anoma consensus and execution protocols operated by a set of networked validators. Anoma’s fractal instance architecture is an attempt to build a platform which is architecturally homogeneous and with a heterogeneous security model. Thus, different fractal instances may specialise in different tasks and serve different communities. Privacy should be default and inherent in the systems we use for transacting. -Namada is the first fractal instance launched as part of the Anoma ecosystem. +### How does Namada relate to Anoma? -The Anoma protocol is designed to facilitate the operation of networked fractal instances, -which intercommunicate but can utilise varied state machines and security models. Different -fractal instances may specialise in different tasks and serve different communities. The Namada -instance will be the first such fractal instance, and it will be focused exclusively on the use-case of private asset transfers. +The Namada instance will be the first such fractal instance, and it will be focused exclusively on the use-case of private asset transfers. Namada is a helpful stepping stone to finalise, test, and launch a protocol version that is simpler than the full +Anoma protocol but still encapsulates a unified and useful set of features. ### Raison d'être -Safe and user-friendly multi-asset privacy doesn't yet exist in the blockchain ecosystem. -Up until now users have had the choice of either a sovereign chain that reissues assets (e.g. Zcash) -or a privacy preserving solution built on an existing smart contract chain (e.g. Tornado Cash on -Ethereum). Both have large trade-offs: in the former case, users don't have +Privacy should be default and inherent in the systems we use for transacting. Yet safe and user-friendly multi-asset privacy doesn't yet exist in the blockchain ecosystem. +Up until now users have had the choice of either a sovereign chain that reissues assets (e.g. [Zcash](https://z.cash/)) +or a privacy preserving solution built on an existing smart contract chain. Both have large trade-offs: in the former case, users don't have assets that they actually want to transact with, and in the latter case, the restrictions of existing platforms mean that users leak a ton of metadata and the protocols are expensive and clunky to use. @@ -36,15 +33,7 @@ and fungible or non-fungible assets (such as ERC20 tokens) sent over a custom Et reduces transfer costs and streamlines UX as much as possible. Once assets are on Namada, shielded transfers are cheap and all assets contribute to the same anonymity set. -Namada is also a helpful stepping stone to finalise, test, -and launch a protocol version that is simpler than the full -Anoma protocol but still encapsulates a unified and useful -set of features. There are reasons to expect that it may -make sense for a fractal instance focused exclusively on -shielded transfers to exist in the long-term, as it can -provide throughput and user-friendliness guarantees which -are more difficult to provide with a more general platform. -Namada is designed to be such an instance. +Users on Namada can earn rewards, retain privacy of assets, and contribute to the overall privacy set. ### Layout of this specification diff --git a/documentation/specs/src/masp.md b/documentation/specs/src/masp.md index 4e1fadc087..d1a87e9b8b 100644 --- a/documentation/specs/src/masp.md +++ b/documentation/specs/src/masp.md @@ -7,4 +7,5 @@ See the following documents: - [Ledger integration](./masp/ledger-integration.md) - [Asset type schema](./masp/asset-type.md) - [Burn and mint](./masp/burn-and-mint.md) -- [Convert circuit](./masp/convert-circuit.md) \ No newline at end of file +- [Convert circuit](./masp/convert-circuit.md) +- [Shielded pool incentives](./economics/shielded-pool-incentives.md) \ No newline at end of file diff --git a/documentation/specs/src/masp/asset-type.md b/documentation/specs/src/masp/asset-type.md index 7504c7ef0f..bc6c701698 100644 --- a/documentation/specs/src/masp/asset-type.md +++ b/documentation/specs/src/masp/asset-type.md @@ -2,22 +2,22 @@ MASP notes carry balances that are some positive integer amount of an asset type. Per both the MASP specification and the implementation, the -asset *identifier* is an 32-byte Blake2s hash of an arbitrary asset +asset *identifier* is an 32-byte [Blake2s hash](https://www.blake2.net/) of an arbitrary asset *name* string, although the full 32-byte space is not used because the identifier must itself hash to an elliptic curve point (currently guaranteed by incrementing a nonce until the hash is a curve point). The final curve point is the asset *type* proper, used in computations. The following is a schema for the arbitrary asset name string intended -to support various uses; at least fungible tokens and NFTs, but possibly -others. +to support various uses - currently fungible tokens and NFTs, but possibly +others in future. The asset name string is built up from a number of segments, joined by a separator. We use `/` as the separator. Segments may be one of the following: -- **Controlling address** segment: an Anoma address which controls the +- **Controlling address** segment: a Namada address which controls the asset. For example, this is the fungible token address for a fungible token. This segment must be present, and must be first; it should in theory be an error to transparently transact in assets of this type diff --git a/documentation/specs/src/masp/burn-and-mint.md b/documentation/specs/src/masp/burn-and-mint.md index f66b8e28eb..2c9bb6e3b5 100644 --- a/documentation/specs/src/masp/burn-and-mint.md +++ b/documentation/specs/src/masp/burn-and-mint.md @@ -62,7 +62,7 @@ It is also critical not to allow cycles. For example, if $\{(A_1, -1), (A_2, 2)\ It may theoretically be possible to implement similar mechanisms with only the existing Spend and Output circuits. For example, a Merkle tree of many Notes could be created with asset generator $[-1] vb_1 + vb_2$ and many different values, allowing anyone to Spend these public Notes, which will only balance if proper amounts of asset type 1 are Spent and asset type 2 are Output. -However, the Nullifier integrity check of the Spend circuit reveals the nullifier of each of these Notes, which removes the privacy of the conversion as the public nullifier is linkable to the allowed conversion. In addition, each Note has a fixed value, preventing arbitrary value conversions. +However, the Nullifier integrity check of the Spend circuit reveals the nullifier of each of these Notes. This removes the privacy of the conversion as the public nullifier is linkable to the allowed conversion. In addition, each Note has a fixed value, preventing arbitrary value conversions. ## Conclusion diff --git a/documentation/specs/src/masp/ledger-integration.md b/documentation/specs/src/masp/ledger-integration.md index 0eed1d7d80..0f4f0cabd8 100644 --- a/documentation/specs/src/masp/ledger-integration.md +++ b/documentation/specs/src/masp/ledger-integration.md @@ -4,32 +4,31 @@ The overall aim of this integration is to have the ability to provide a multi-asset shielded pool following the MASP spec as an account on the -current Anoma blockchain implementation. +current Namada blockchain implementation. -## Shielded pool VP +## Shielded pool validity predicate (VP) -The shielded value pool can be an Anoma "established account" with a +The shielded value pool can be an Namada established account with a validity predicate which handles the verification of shielded transactions. Similarly to zcash, the asset balance of the shielded pool itself is transparent - that is, from the transparent perspective, the MASP is just an account holding assets. The shielded pool VP has the following functions: -- Accept only valid transactions involving assets moving in or out of +- Accepts only valid transactions involving assets moving in or out of the pool. -- Accept valid shielded-to-shielded transactions, which don't move - assets from the perspective of transparent Anoma. -- Publish the note commitment and nullifier reveal Merkle trees. +- Accepts valid shielded-to-shielded transactions, which don't move + assets from the perspective of transparent Namada. +- Publishes the note commitment and nullifier reveal Merkle trees. To make this possible, the host environment needs to provide verification primitives to VPs. One possibility is to provide a single -high-level "verify transaction output descriptions and proofs" -operation, but another is to provide cryptographic functions in the host +high-level operation to verify transaction output descriptions and proofs, but another is to provide cryptographic functions in the host environment and implement the verifier as part of the VP. -The shielded pool needs the ability to update the commitment and -nullifier Merkle trees as it receives transactions. This may possibly be -achievable via the temporary storage mechanism added for IBC, with the +In future, the shielded pool will be able to update the commitment and +nullifier Merkle trees as it receives transactions. This could likely be +achieved via the temporary storage mechanism added for IBC, with the trees finalized with each block. The input to the VP is the following set of state changes: @@ -37,7 +36,7 @@ The input to the VP is the following set of state changes: - updates to the shielded pool's asset balances - new encrypted notes - updated note and nullifier tree states (partial, because we only have - the last block's anchor?) + the last block's anchor) and the following data which is ancillary from the ledger's perspective: @@ -71,7 +70,7 @@ struct OutputDescription { c_enc: [u8; ENC_CIPHERTEXT_SIZE], // Encrypted note key recovery ciphertext c_out: [u8; OUT_CIPHERTEXT_SIZE], - // Zero-knowledge proof of the new encrypted note's location (?) + // Zero-knowledge proof of the new encrypted note's location zkproof: Proof, } ``` @@ -79,7 +78,7 @@ struct OutputDescription { Given these inputs: The VP must verify the proofs for all spend and output descriptions -(`bellman::groth16`), as well as the signature for spend notes. +([`bellman::groth16`](https://docs.rs/bellman/latest/bellman/groth16/index.html)), as well as the signature for spend notes. Encrypted notes from output descriptions must be published in the storage so that holders of the viewing key can view them; however, the @@ -89,15 +88,14 @@ Nullifiers and commitments must be appended to their respective Merkle trees in the VP's storage as well, which is a transaction-level rather than a block-level state update. -Additionally to the individual spend and output description +In addition to the individual spend and output description verifications, the final transparent asset value change described in the -transaction must equal the pool asset value change, and as an additional -sanity check, the pool's balance of any asset may not end up negative -(this may already be impossible?). (needs more input) +transaction must equal the pool asset value change. As an additional +sanity check, the pool's balance of any asset may not end up negative. NB: Shielded-to-shielded transactions in an asset do not, from the ledger's perspective, transact in that asset; therefore, the asset's own -VP is not run as described above, and cannot be because the shielded +VP cannot run as described above because the shielded pool is asset-hiding. ## Client capabilities @@ -116,7 +114,7 @@ if any, and proof data computed offline by the client. The client and wallet must be extended to support the shielded pool and the cryptographic operations needed to interact with it. From the -perspective of the transparent Anoma protocol, a shielded transaction is +perspective of the transparent Namada protocol, a shielded transaction is just a data write to the MASP storage, unless it moves value in or out of the pool. The client needs the capability to create notes, transactions, and proofs of transactions, but it has the advantage of @@ -148,7 +146,7 @@ For cryptographic details and further information, see Note that this structure is required only by the client; the VP only handles commitments to this data. -Diversifiers are selected (randomly?) by the client and used to +Diversifiers are selected by the client and used to diversify addresses and their associated keys. `v` and `t` identify the asset type and value. Asset identifiers are derived from asset names, which are arbitrary strings (in this case, token/other asset VP @@ -217,7 +215,7 @@ struct TxOut { ``` Note that in contrast to Sapling's UTXO based approach, our transparent inputs/outputs are based on the account model used -in the rest of Anoma. +in the rest of Namada. # Shielded Transfer Specification ## Transfer Format @@ -242,17 +240,17 @@ pub struct Transfer { ``` ## Conditions Below, the conditions necessary for a valid shielded or unshielded transfer are outlined: -* A shielded component equal to `None` indicates a transparent Anoma transaction -* Otherwise the shielded component must have the form `Some(x)` where `x` has the transaction encoding specified in the [Multi-Asset Shielded Pool Specication](https://raw.githubusercontent.com/anoma/masp/main/docs/multi-asset-shielded-pool.pdf) +* A shielded component equal to `None` indicates a transparent Namada transaction +* Otherwise the shielded component must have the form `Some(x)` where `x` has the transaction encoding specified in the [Multi-Asset Shielded Pool Specs]() * Hence for a shielded transaction to be valid: - * the `Transfer` must satisfy the usual conditions for Anoma ledger transfers (i.e. sufficient funds, ...) as enforced by token and account validity predicates - * the `Transaction` must satisfy the conditions specified in the [Multi-Asset Shielded Pool Specication](https://raw.githubusercontent.com/anoma/masp/main/docs/multi-asset-shielded-pool.pdf) - * the `Transaction` and `Transfer` together must additionaly satisfy the below boundary conditions intended to ensure consistency between the MASP validity predicate ledger and Anoma ledger + * the `Transfer` must satisfy the usual conditions for Namada ledger transfers (i.e. sufficient funds, ...) as enforced by token and account validity predicates + * the `Transaction` must satisfy the conditions specified in the [Multi-Asset Shielded Pool Specification](https://github.com/anoma/masp/blob/main/docs/multi-asset-shielded-pool.pdf) + * the `Transaction` and `Transfer` together must additionally satisfy the below boundary conditions intended to ensure consistency between the MASP validity predicate ledger and Namada ledger * A key equal to `None` indicates an unpinned shielded transaction; one that can only be found by scanning and trial-decrypting the entire shielded pool * Otherwise the key must have the form `Some(x)` where `x` is a `String` such that there exists no prior accepted transaction with the same key ### Boundary Conditions -Below, the conditions necessary to maintain consistency between the MASP validity predicate ledger and Anoma ledger are outlined: +Below, the conditions necessary to maintain consistency between the MASP validity predicate ledger and Namada ledger are outlined: * If the target address is the MASP validity predicate, then no transparent outputs are permitted in the shielded transaction * If the target address is not the MASP validity predicate, then: * there must be exactly one transparent output in the shielded transaction and: @@ -287,7 +285,7 @@ Below are miscellaneous remarks on the capabilities and limitations of the curre * This key must not be reused, this is in order to avoid revealing that multiple transactions are going to the same entity ## Multi-Asset Shielded Pool Specification Differences from Zcash Protocol Specification -The [Multi-Asset Shielded Pool Specication](https://media.githubusercontent.com/media/anoma/masp/main/docs/multi-asset-shielded-pool.pdf) referenced above is in turn an extension to the [Zcash Protocol Specification](https://zips.z.cash/protocol/protocol.pdf). Below, the changes from the Zcash Protocol Specification assumed to have been integrated into the Multi-Asset Shielded Pool Specification are listed: +The [Multi-Asset Shielded Pool Specification](https://github.com/anoma/masp/blob/main/docs/multi-asset-shielded-pool.pdf) referenced above is in turn an extension to the [Zcash Protocol Specification](https://zips.z.cash/protocol/protocol.pdf). Below, the changes from the Zcash Protocol Specification assumed to have been integrated into the Multi-Asset Shielded Pool Specification are listed: * [3.2 Notes](https://zips.z.cash/protocol/protocol.pdf#notes) * Sapling note tuple must include asset type * Note commitment must be parameterized by asset type @@ -391,7 +389,7 @@ Below, the changes from [ZIP 32: Shielded Hierarchical Deterministic Wallets](ht * For extended full viewing keys on the Testnet, the Human-Readable Part is "xfvktest" # Storage Interface Specification -Anoma nodes provide interfaces that allow Anoma clients to query for specific pinned transactions, transactions accepted into the shielded pool, and allowed conversions between various asset types. Below we describe the ABCI paths and the encodings of the responses to each type of query. +Namada nodes provide interfaces that allow Namada clients to query for specific pinned transactions, transactions accepted into the shielded pool, and allowed conversions between various asset types. Below we describe the ABCI paths and the encodings of the responses to each type of query. ## Shielded Transfer Query In order to determine shielded balances belonging to particular keys or spend one's balance, it is necessary to download the transactions that transferred the assets to you. To this end, the nth transaction in the shielded pool can be obtained by getting the value at the storage path `/tx-`. Note that indexing is 0-based. This will return a quadruple of the type below: @@ -414,7 +412,7 @@ When scanning the shielded pool, it is sometimes useful know when to stop scanni ## Pinned Transfer Query A transaction pinned to the key `x` in the shielded pool can be obtained indirectly by getting the value at the storage path `/pin-`. This will return the index of the desired transaction within the shielded pool encoded as a `u64`. At this point, the above shielded transaction query can then be used to obtain the actual transaction bytes. ## Conversion Query -In order for MASP clients to convert older asset types to their latest variants, they need to query nodes for currently valid conversions. This can be done by querying the ABCI path `conv/` where `asset-type` is a hexadecimal encoding of the asset identifier as defined in [Multi-Asset Shielded Pool Specication](https://media.githubusercontent.com/media/anoma/masp/main/docs/multi-asset-shielded-pool.pdf). This will return a quadruple of the type below: +In order for MASP clients to convert older asset types to their latest variants, they need to query nodes for currently valid conversions. This can be done by querying the ABCI path `conv/` where `asset-type` is a hexadecimal encoding of the asset identifier as defined in [Multi-Asset Shielded Pool Specification](https://github.com/anoma/masp/blob/main/docs/multi-asset-shielded-pool.pdf). This will return a quadruple of the type below: ``` ( /// the token address of this asset type @@ -427,4 +425,4 @@ In order for MASP clients to convert older asset types to their latest variants, MerklePath ) ``` -If no conversions are available the amount will be exactly zero, otherwise the amount must contain negative units of the queried asset type. +If no conversions are available, the amount will be exactly zero, otherwise the amount must contain negative units of the queried asset type. diff --git a/documentation/specs/src/masp/trusted-setup.md b/documentation/specs/src/masp/trusted-setup.md index acc14f723d..328a7fe231 100644 --- a/documentation/specs/src/masp/trusted-setup.md +++ b/documentation/specs/src/masp/trusted-setup.md @@ -1,8 +1,7 @@ # Namada Trusted Setup This spec assumes that you have some previous knowledge about Trusted Setup Ceremonies. If not, you might want to check the following two articles: [Setup Ceremonies - ZKProof](https://zkproof.org/2021/06/30/setup-ceremonies/) and [Parameter Generation - Zcash](https://z.cash/technology/paramgen/). -The Namada Trusted Setup (TS) consists of running the phase 2 of the MPC which is a circuit-specific step to construct the multi-asset shielded pool circuit. Our phase 2 takes as input the Powers of Tau (phase 1) ran by Zcash that can be found [here](https://download.z.cash/downloads/powersoftau/). - +The Namada Trusted Setup (TS) consists of running the phase 2 of the MPC which is a circuit-specific step to construct the multi-asset shielded pool circuit. Our phase 2 takes as input the Powers of Tau (phase 1) ran by Zcash that can be found [here](https://download.z.cash/downloads/powersoftau/). You can sign up for the Namada Trusted Setup [here](https://namada.net/trusted-setup.html). ## Contribution flow diff --git a/documentation/specs/src/user-interfaces/external-integrations.md b/documentation/specs/src/user-interfaces/external-integrations.md index f6fd4855b6..c459d52499 100644 --- a/documentation/specs/src/user-interfaces/external-integrations.md +++ b/documentation/specs/src/user-interfaces/external-integrations.md @@ -10,7 +10,7 @@ * Rosetta integration * Datahub integration * WalletConnect integration -* Ledger integration +* [Ledger integration](../masp/ledger-integration.md) * External integrations * Figment * P2P diff --git a/documentation/specs/src/user-interfaces/web-explorer-interface.md b/documentation/specs/src/user-interfaces/web-explorer-interface.md index 9c6225d674..c7dbfa3c80 100644 --- a/documentation/specs/src/user-interfaces/web-explorer-interface.md +++ b/documentation/specs/src/user-interfaces/web-explorer-interface.md @@ -1,9 +1,9 @@ # Web explorer interface * Block explorer - * Display PoS state - * Display governance state - * Display transparent transfers - * Display transfers in and out of the MASP - * Display total values for the MASP + * Displays PoS state + * Displays governance state + * Displays transparent transfers + * Displays transfers in and out of the MASP + * Displays total values for the MASP * Allows tx hashes of shielded transfers to be looked up for confirmation diff --git a/documentation/specs/src/user-interfaces/web-wallet-interface.md b/documentation/specs/src/user-interfaces/web-wallet-interface.md index 615a74178f..d34628ef6a 100644 --- a/documentation/specs/src/user-interfaces/web-wallet-interface.md +++ b/documentation/specs/src/user-interfaces/web-wallet-interface.md @@ -2,7 +2,7 @@ ## Application Features -The application consist of the an UI that allows the user to perform the following actions in an easy to use and consistent web application: +The application consists of a UI that allows the user to perform the following actions in an easy to use and consistent web application: ### Seed Phrase [hifi Designs](https://www.figma.com/file/aiWZpaXjPLW6fDjE7dpFaU/Projects-2021?node-id=4610%3A5890) @@ -16,9 +16,9 @@ The application consist of the an UI that allows the user to perform the followi * When entering the app, the user is being prompted for a password to decrypt the key pair to be able to perform actions [wireframe](https://www.figma.com/file/aiWZpaXjPLW6fDjE7dpFaU/Projects-2021?node-id=6442%3A5801) * Can create accounts derived from the master key pair * Can delete accounts -* User can integrated with Ledger hardware wallet - * Set up flow TBD - * Managing TBD +* User can integrate with Ledger hardware wallet + * Set up + * Management * Can see an overview of the assets in the account and all derived accounts [wireframe](https://www.figma.com/file/aiWZpaXjPLW6fDjE7dpFaU/Projects-2021?node-id=6442%3A5492) * Can see the details of a single asset, containing the following information [wireframe](https://www.figma.com/file/aiWZpaXjPLW6fDjE7dpFaU/Projects-2021?node-id=6442%3A5681) * Balance @@ -26,7 +26,6 @@ The application consist of the an UI that allows the user to perform the followi * Button to initiate a new transfer using this asset ### Transfers -[TBD]() * Can create transparent transfers * Can create shielded transfers * Bi-directional transfer between Namada and ETH @@ -35,7 +34,6 @@ The application consist of the an UI that allows the user to perform the followi * Supports approving transactions with Keplr ### Staking & Governance -[TBD]() * Can bond funds to a list of validators * Can un-bond funds to a list of validators * Can submit proposals diff --git a/documentation/specs/src/user-interfaces/web-wallet.md b/documentation/specs/src/user-interfaces/web-wallet.md index 370a0a5397..2196400f2f 100644 --- a/documentation/specs/src/user-interfaces/web-wallet.md +++ b/documentation/specs/src/user-interfaces/web-wallet.md @@ -142,7 +142,7 @@ User can: [Wireframe 2](https://www.figma.com/file/aiWZpaXjPLW6fDjE7dpFaU/Projects-2021?node-id=6496%3A14405) User can: view 1: -* enter the details (TBD) of the proposal +* enter the details of the proposal * see a summary of the proposal * submit the proposal From f5d64a483fac24f1dcf336f9f383d0a5b16dc6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 21 Oct 2022 17:54:26 +0200 Subject: [PATCH 10/47] split back out the core VPs --- documentation/specs/src/SUMMARY.md | 3 +++ documentation/specs/src/base-ledger/core-concepts.md | 12 ------------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/documentation/specs/src/SUMMARY.md b/documentation/specs/src/SUMMARY.md index 49891b44e9..b7537eb315 100644 --- a/documentation/specs/src/SUMMARY.md +++ b/documentation/specs/src/SUMMARY.md @@ -5,6 +5,9 @@ - [Core Concepts](./base-ledger/core-concepts.md) - [Execution](./base-ledger/execution.md) - [Governance](./base-ledger/governance.md) + - [Default account](./base-ledger/default-account.md) + - [Multisignature account](./base-ledger/multisignature.md) + - [Fungible token](./base-ledger/fungible-token.md) - [Multi-asset shielded pool](./masp.md) - [Ledger integration](./masp/ledger-integration.md) - [Asset type](./masp/asset-type.md) diff --git a/documentation/specs/src/base-ledger/core-concepts.md b/documentation/specs/src/base-ledger/core-concepts.md index 61214acb95..083946f0b1 100644 --- a/documentation/specs/src/base-ledger/core-concepts.md +++ b/documentation/specs/src/base-ledger/core-concepts.md @@ -1,15 +1,3 @@ # Consensus Namada uses [Tendermint Go](https://github.com/tendermint/tendermint) through the [tendermint-rs](https://github.com/heliaxdev/tendermint-rs) bindings in order to provide peer-to-peer transaction gossip, BFT consensus, and state machine replication for Namada's custom state machine. - -## Default account - -The default account validity predicate authorises transactions on the basis of a cryptographic signature. - -## Fungible token - -The fungible token validity predicate authorises token balance changes on the basis of conservation-of-supply and approval-by-sender. - -## k-of-n multisignature - -The k-of-n multisignature validity predicate authorises transactions on the basis of k out of n parties approving them. \ No newline at end of file From 74558d74cb86148753323d3b532fe80403337df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 21 Oct 2022 17:58:06 +0200 Subject: [PATCH 11/47] fix broken link --- documentation/specs/src/base-ledger/governance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index 7372a00539..3079b6e6dc 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -39,7 +39,7 @@ Each proposal will be stored in a sub-key under the internal proposal address. T /\$GovernanceAddress/proposal/epoch/\$id: u64 ``` -An epoch is a range of blocks or time that is defined by the base ledger and made available to the PoS system. This document assumes that epochs are identified by consecutive natural numbers. All the data relevant to PoS are [associated with epochs](../economics/proof-of-stake/bonding-mechanism.md/#epoched-data). +An epoch is a range of blocks or time that is defined by the base ledger and made available to the PoS system. This document assumes that epochs are identified by consecutive natural numbers. All the data relevant to PoS are [associated with epochs](../economics/proof-of-stake/bonding-mechanism.md#epoched-data). - `Author` address field will be used to credit the locked funds if the proposal is approved. - `/\$GovernanceAddress/proposal/\$epoch/\$id` is used for easing the ledger governance execution. `\$epoch` refers to the same value as the on specific in the `grace_epoch` field. From d5205e6cf74e25d07a1263cb5244dc5892469889 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Mon, 24 Oct 2022 10:49:10 +0200 Subject: [PATCH 12/47] ci: invalide cf cache --- .github/workflows/build-and-test-bridge.yml | 2 +- .github/workflows/build-and-test.yml | 2 +- .github/workflows/docs.yml | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test-bridge.yml b/.github/workflows/build-and-test-bridge.yml index cee43b941b..f82286f1d5 100644 --- a/.github/workflows/build-and-test-bridge.yml +++ b/.github/workflows/build-and-test-bridge.yml @@ -228,7 +228,7 @@ jobs: ANOMA_TENDERMINT_WEBSOCKET_TIMEOUT: 20 ANOMA_E2E_USE_PREBUILT_BINARIES: "true" ANOMA_E2E_KEEP_TEMP: "true" - ENV_VAR_TM_STDOUT: "false" + ANOMA_TM_STDOUT: "false" ANOMA_LOG_COLOR: "false" ANOMA_MASP_PARAMS_DIR: "/home/runner/work/masp" ANOMA_LOG: "info" diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1c4cbd3412..3ad569a2fb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -230,7 +230,7 @@ jobs: ANOMA_TENDERMINT_WEBSOCKET_TIMEOUT: 20 ANOMA_E2E_USE_PREBUILT_BINARIES: "true" ANOMA_E2E_KEEP_TEMP: "true" - ENV_VAR_TM_STDOUT: "false" + ANOMA_TM_STDOUT: "false" ANOMA_LOG_COLOR: "false" ANOMA_MASP_PARAMS_DIR: "/home/runner/work/masp" ANOMA_LOG: "info" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0ba9f39b98..5304af902e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -41,18 +41,21 @@ jobs: command: cd documentation/specs && mdbook build cache_subkey: specs cache_version: v1 + distribution_id: E2Y9R2H4P5LYED - name: Build docs folder: documentation/docs bucket: namada-docs-static-website command: cd documentation/docs && mdbook build cache_subkey: docs cache_version: v1 + distribution_id: E2T9UML53913RV - name: Build development docs folder: documentation/dev bucket: namada-dev-static-website command: cargo run --bin namada_encoding_spec && cd documentation/dev && mdbook build cache_subkey: dev cache_version: v1 + distribution_id: E6XPP5KFWXJFQ env: CARGO_INCREMENTAL: 0 @@ -127,6 +130,9 @@ jobs: - name: Publish docs if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} run: aws s3 sync ${{ matrix.make.folder }}/book/html/ s3://${{ matrix.make.bucket }} --region eu-west-1 --delete + - name: Invalidate distribution cache + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + run: aws cloudfront create-invalidation --distribution-id ${{ matrix.make.distribution_id }} --paths "/*" - name: Print sccache stats if: always() run: sccache --show-stats From 9b5a8d917633850b28c8e25520fb27f87c0bcf31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 5 Oct 2022 10:12:59 +0200 Subject: [PATCH 13/47] move ledger's protocol module into shared crate --- apps/src/lib/node/ledger/mod.rs | 1 - shared/src/ledger/mod.rs | 2 ++ {apps/src/lib/node => shared/src}/ledger/protocol/mod.rs | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename {apps/src/lib/node => shared/src}/ledger/protocol/mod.rs (100%) diff --git a/apps/src/lib/node/ledger/mod.rs b/apps/src/lib/node/ledger/mod.rs index dc0dfc184c..965275cc03 100644 --- a/apps/src/lib/node/ledger/mod.rs +++ b/apps/src/lib/node/ledger/mod.rs @@ -1,7 +1,6 @@ mod abortable; mod broadcaster; pub mod events; -pub mod protocol; pub mod rpc; mod shell; mod shims; diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index ef92b1e2d9..22d1b17525 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -7,6 +7,8 @@ pub mod ibc; pub mod native_vp; pub mod parameters; pub mod pos; +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] +pub mod protocol; pub mod slash_fund; pub mod storage; pub mod storage_api; diff --git a/apps/src/lib/node/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs similarity index 100% rename from apps/src/lib/node/ledger/protocol/mod.rs rename to shared/src/ledger/protocol/mod.rs From 84777a2cfe9fde4c9c364b3b375643b51802cdb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 6 Oct 2022 12:14:34 +0200 Subject: [PATCH 14/47] protocol: update imports and add missing rustdoc --- apps/Cargo.toml | 4 +- .../lib/node/ledger/shell/finalize_block.rs | 1 + apps/src/lib/node/ledger/shell/mod.rs | 4 +- apps/src/lib/node/ledger/shell/queries.rs | 8 ++-- shared/Cargo.toml | 2 + shared/src/ledger/protocol/mod.rs | 47 ++++++++++--------- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 45cdb9aa5e..50c1f5b065 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -65,7 +65,7 @@ abciplus = [ ] [dependencies] -namada = {path = "../shared", features = ["wasm-runtime", "ferveo-tpke", "rand", "secp256k1-sign-verify"]} +namada = {path = "../shared", features = ["wasm-runtime", "ferveo-tpke", "rand", "tendermint-rpc", "secp256k1-sign-verify"]} ark-serialize = "0.3.0" ark-std = "0.3.0" # branch = "bat/arse-merkle-tree" @@ -104,7 +104,7 @@ prost = "0.9.0" prost-types = "0.9.0" rand = {version = "0.8", default-features = false} rand_core = {version = "0.6", default-features = false} -rayon = "=1.5.1" +rayon = "=1.5.3" regex = "1.4.5" reqwest = "0.11.4" rlimit = "0.5.4" diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 2080b8d23d..92e06c96b9 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1,5 +1,6 @@ //! Implementation of the `FinalizeBlock` ABCI++ method for the Shell +use namada::ledger::protocol; use namada::types::storage::{BlockHash, Header}; use super::governance::execute_governance_proposals; diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 28167d4eba..ab3b131c37 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -30,7 +30,7 @@ use namada::ledger::storage::write_log::WriteLog; use namada::ledger::storage::{ DBIter, Sha256Hasher, Storage, StorageHasher, DB, }; -use namada::ledger::{ibc, pos}; +use namada::ledger::{ibc, pos, protocol}; use namada::proto::{self, Tx}; use namada::types::chain::ChainId; use namada::types::key::*; @@ -60,7 +60,7 @@ use crate::facade::tower_abci::{request, response}; use crate::node::ledger::events::Event; use crate::node::ledger::shims::abcipp_shim_types::shim; use crate::node::ledger::shims::abcipp_shim_types::shim::response::TxResult; -use crate::node::ledger::{protocol, storage, tendermint_node}; +use crate::node::ledger::{storage, tendermint_node}; #[allow(unused_imports)] use crate::wallet::ValidatorData; use crate::{config, wallet}; diff --git a/apps/src/lib/node/ledger/shell/queries.rs b/apps/src/lib/node/ledger/shell/queries.rs index 53587d5ebf..a9435d565a 100644 --- a/apps/src/lib/node/ledger/shell/queries.rs +++ b/apps/src/lib/node/ledger/shell/queries.rs @@ -2,11 +2,11 @@ use borsh::{BorshDeserialize, BorshSerialize}; use ferveo_common::TendermintValidator; +use namada::ledger::queries::{RequestCtx, ResponseQuery}; +use namada::ledger::storage_api; use namada::types::address::Address; -use namada::types::key; use namada::types::key::dkg_session_keys::DkgPublicKey; -use namada::types::storage::{Key, PrefixValue}; -use namada::types::token::{self, Amount}; +use namada::types::{key, token}; use super::*; use crate::facade::tendermint_proto::crypto::{ProofOp, ProofOps}; @@ -70,7 +70,7 @@ where &self, token: &Address, owner: &Address, - ) -> std::result::Result { + ) -> std::result::Result { let height = self.storage.get_block_height().0; let query_resp = self.read_storage_value( &token::balance_key(token, owner), diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 6469694ea9..f763f1f783 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -37,6 +37,7 @@ wasm-runtime = [ "loupe", "parity-wasm", "pwasm-utils", + "rayon", "wasmer-cache", "wasmer-compiler-singlepass", "wasmer-engine-dylib", @@ -101,6 +102,7 @@ pwasm-utils = {version = "0.18.0", optional = true} rand = {version = "0.8", optional = true} # TODO proptest rexports the RngCore trait but the re-implementations only work for version `0.8`. *sigh* rand_core = {version = "0.6", optional = true} +rayon = {version = "=1.5.3", optional = true} rust_decimal = "1.14.3" serde = {version = "1.0.125", features = ["derive"]} serde_json = "1.0.62" diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index ed776fe21a..86b2a30290 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -2,29 +2,31 @@ use std::collections::BTreeSet; use std::panic; -use namada::ledger::eth_bridge::vp::EthBridge; -use namada::ledger::gas::{self, BlockGasMeter, VpGasMeter}; -use namada::ledger::governance::GovernanceVp; -use namada::ledger::ibc::vp::{Ibc, IbcToken}; -use namada::ledger::native_vp::{self, NativeVp}; -use namada::ledger::parameters::{self, ParametersVp}; -use namada::ledger::pos::{self, PosVP}; -use namada::ledger::slash_fund::SlashFundVp; -use namada::ledger::storage::write_log::WriteLog; -use namada::ledger::storage::{DBIter, Storage, StorageHasher, DB}; -use namada::proto::{self, Tx}; -use namada::types::address::{Address, InternalAddress}; -use namada::types::storage; -use namada::types::transaction::{DecryptedTx, TxResult, TxType, VpsResult}; -use namada::vm::wasm::{TxCache, VpCache}; -use namada::vm::{self, wasm, WasmCacheAccess}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use thiserror::Error; +use crate::ledger::eth_bridge::vp::EthBridge; +use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; +use crate::ledger::governance::GovernanceVp; +use crate::ledger::ibc::vp::{Ibc, IbcToken}; +use crate::ledger::native_vp::{self, NativeVp}; +use crate::ledger::parameters::{self, ParametersVp}; +use crate::ledger::pos::{self, PosVP}; +use crate::ledger::slash_fund::SlashFundVp; +use crate::ledger::storage::write_log::WriteLog; +use crate::ledger::storage::{DBIter, Storage, StorageHasher, DB}; +use crate::proto::{self, Tx}; +use crate::types::address::{Address, InternalAddress}; +use crate::types::storage; +use crate::types::transaction::{DecryptedTx, TxResult, TxType, VpsResult}; +use crate::vm::wasm::{TxCache, VpCache}; +use crate::vm::{self, wasm, WasmCacheAccess}; + +#[allow(missing_docs)] #[derive(Error, Debug)] pub enum Error { #[error("Storage error: {0}")] - StorageError(namada::ledger::storage::Error), + StorageError(crate::ledger::storage::Error), #[error("Error decoding a transaction from bytes: {0}")] TxDecodingError(proto::Error), #[error("Transaction runner error: {0}")] @@ -38,7 +40,7 @@ pub enum Error { #[error("The address {0} doesn't exist")] MissingAddress(Address), #[error("IBC native VP: {0}")] - IbcNativeVpError(namada::ledger::ibc::vp::Error), + IbcNativeVpError(crate::ledger::ibc::vp::Error), #[error("PoS native VP: {0}")] PosNativeVpError(pos::vp::Error), #[error("PoS native VP panicked")] @@ -46,17 +48,18 @@ pub enum Error { #[error("Parameters native VP: {0}")] ParametersNativeVpError(parameters::Error), #[error("IBC Token native VP: {0}")] - IbcTokenNativeVpError(namada::ledger::ibc::vp::IbcTokenError), + IbcTokenNativeVpError(crate::ledger::ibc::vp::IbcTokenError), #[error("Governance native VP error: {0}")] - GovernanceNativeVpError(namada::ledger::governance::vp::Error), + GovernanceNativeVpError(crate::ledger::governance::vp::Error), #[error("SlashFund native VP error: {0}")] - SlashFundNativeVpError(namada::ledger::slash_fund::Error), + SlashFundNativeVpError(crate::ledger::slash_fund::Error), #[error("Ethereum bridge native VP error: {0}")] - EthBridgeNativeVpError(namada::ledger::eth_bridge::vp::Error), + EthBridgeNativeVpError(crate::ledger::eth_bridge::vp::Error), #[error("Access to an internal address {0} is forbidden")] AccessForbidden(InternalAddress), } +/// Result of applying a transaction pub type Result = std::result::Result; /// Apply a given transaction From 2b3653176771fb9dd5aaff6788781b2607a3913d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 6 Oct 2022 12:18:19 +0200 Subject: [PATCH 15/47] add deps for router macro and update `Cargo.lock`s This adds a new feature "async-client" and "tendermint-rpc" to shared crate that if enabled generate async client code for all router's handler methods, and implements it for tendermint-rpc client, respectively. --- Cargo.lock | 10 ++++++++-- shared/Cargo.toml | 16 ++++++++++++++++ wasm/Cargo.lock | 3 +++ wasm_for_tests/wasm_source/Cargo.lock | 3 +++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34ab8ab550..445e824d8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2917,6 +2917,7 @@ dependencies = [ "ark-ec", "ark-serialize", "assert_matches", + "async-trait", "bech32", "borsh", "byte-unit", @@ -2938,6 +2939,7 @@ dependencies = [ "loupe", "namada_proof_of_stake", "parity-wasm", + "paste", "pretty_assertions", "proptest", "prost", @@ -2945,6 +2947,7 @@ dependencies = [ "pwasm-utils", "rand 0.8.5", "rand_core 0.6.4", + "rayon", "rust_decimal", "serde 1.0.145", "serde_json", @@ -2955,8 +2958,11 @@ dependencies = [ "tendermint 0.23.6", "tendermint-proto 0.23.5", "tendermint-proto 0.23.6", + "tendermint-rpc 0.23.5", + "tendermint-rpc 0.23.6", "test-log", "thiserror", + "tokio", "tonic-build", "tracing 0.1.37", "tracing-subscriber 0.3.16", @@ -4083,9 +4089,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" dependencies = [ "autocfg 1.1.0", "crossbeam-deque", diff --git a/shared/Cargo.toml b/shared/Cargo.toml index f763f1f783..d05e002230 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -27,6 +27,7 @@ ibc-mocks-abcipp = [ ] # for integration tests and test utilies testing = [ + "async-client", "proptest", "rand", "rand_core", @@ -50,6 +51,15 @@ wasm-runtime = [ secp256k1-sign-verify = [ "libsecp256k1/hmac", ] +# Enable queries support for an async client +async-client = [ + "async-trait", +] +# tendermint-rpc support +tendermint-rpc = [ + "async-client", + "dep:tendermint-rpc", +] abcipp = [ "ibc-proto-abcipp", @@ -73,6 +83,7 @@ ark-serialize = "0.3" # We switch off "blake2b" because it cannot be compiled to wasm # branch = "bat/arse-merkle-tree" arse-merkle-tree = {package = "sparse-merkle-tree", git = "https://github.com/heliaxdev/sparse-merkle-tree", rev = "04ad1eeb28901b57a7599bbe433b3822965dabe8", default-features = false, features = ["std", "borsh"]} +async-trait = {version = "0.1.51", optional = true} bech32 = "0.8.0" borsh = "0.9.0" chrono = {version = "0.4.22", default-features = false, features = ["clock", "std"]} @@ -94,6 +105,7 @@ itertools = "0.10.0" loupe = {version = "0.1.3", optional = true} libsecp256k1 = {git = "https://github.com/heliaxdev/libsecp256k1", rev = "bbb3bd44a49db361f21d9db80f9a087c194c0ae9", default-features = false, features = ["std", "static-context"]} parity-wasm = {version = "0.42.2", optional = true} +paste = "1.0.9" # A fork with state machine testing proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm", optional = true} prost = "0.9.0" @@ -111,8 +123,10 @@ sha2 = "0.9.3" tempfile = {version = "3.2.0", optional = true} # temporarily using fork work-around for https://github.com/informalsystems/tendermint-rs/issues/971 tendermint-abcipp = {package = "tendermint", git = "https://github.com/heliaxdev/tendermint-rs", rev = "95c52476bc37927218374f94ac8e2a19bd35bec9", optional = true} +tendermint-rpc-abcipp = {package = "tendermint-rpc", git = "https://github.com/heliaxdev/tendermint-rs", rev = "95c52476bc37927218374f94ac8e2a19bd35bec9", features = ["http-client"], optional = true} tendermint-proto-abcipp = {package = "tendermint-proto", git = "https://github.com/heliaxdev/tendermint-rs", rev = "95c52476bc37927218374f94ac8e2a19bd35bec9", optional = true} tendermint = {version = "0.23.6", optional = true} +tendermint-rpc = {version = "0.23.6", features = ["http-client"], optional = true} tendermint-proto = {version = "0.23.6", optional = true} thiserror = "1.0.30" tracing = "0.1.30" @@ -127,12 +141,14 @@ zeroize = "1.5.5" [dev-dependencies] assert_matches = "1.5.0" +async-trait = {version = "0.1.51"} byte-unit = "4.0.13" libsecp256k1 = {git = "https://github.com/heliaxdev/libsecp256k1", rev = "bbb3bd44a49db361f21d9db80f9a087c194c0ae9"} pretty_assertions = "0.7.2" # A fork with state machine testing proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm"} test-log = {version = "0.2.7", default-features = false, features = ["trace"]} +tokio = {version = "1.8.2", default-features = false, features = ["rt", "macros"]} tracing-subscriber = {version = "0.3.7", default-features = false, features = ["env-filter", "fmt"]} [build-dependencies] diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index f9030a471a..b83a53b664 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1357,6 +1357,7 @@ version = "0.8.1" dependencies = [ "ark-bls12-381", "ark-serialize", + "async-trait", "bech32", "borsh", "chrono", @@ -1373,12 +1374,14 @@ dependencies = [ "loupe", "namada_proof_of_stake", "parity-wasm", + "paste", "proptest", "prost", "prost-types", "pwasm-utils", "rand", "rand_core 0.6.4", + "rayon", "rust_decimal", "serde", "serde_json", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index b82f3b3d59..1259bd5cc4 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1357,6 +1357,7 @@ version = "0.8.1" dependencies = [ "ark-bls12-381", "ark-serialize", + "async-trait", "bech32", "borsh", "chrono", @@ -1373,12 +1374,14 @@ dependencies = [ "loupe", "namada_proof_of_stake", "parity-wasm", + "paste", "proptest", "prost", "prost-types", "pwasm-utils", "rand", "rand_core 0.6.4", + "rayon", "rust_decimal", "serde", "serde_json", From 0c9b0bce9307555782742db0a106cbc38447730f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 6 Oct 2022 12:20:45 +0200 Subject: [PATCH 16/47] shared: add new queries router macro to replicate handwritten RPC paths --- shared/src/ledger/mod.rs | 1 + shared/src/ledger/queries/mod.rs | 519 ++++++++++++++++ shared/src/ledger/queries/router.rs | 881 ++++++++++++++++++++++++++++ shared/src/ledger/queries/types.rs | 171 ++++++ 4 files changed, 1572 insertions(+) create mode 100644 shared/src/ledger/queries/mod.rs create mode 100644 shared/src/ledger/queries/router.rs create mode 100644 shared/src/ledger/queries/types.rs diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index 22d1b17525..cbe2528b76 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -9,6 +9,7 @@ pub mod parameters; pub mod pos; #[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] pub mod protocol; +pub mod queries; pub mod slash_fund; pub mod storage; pub mod storage_api; diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs new file mode 100644 index 0000000000..0c66faca1e --- /dev/null +++ b/shared/src/ledger/queries/mod.rs @@ -0,0 +1,519 @@ +//! Ledger read-only queries can be handled and dispatched via the [`RPC`] +//! defined via `router!` macro. + +use tendermint_proto::crypto::{ProofOp, ProofOps}; +#[cfg(any(test, feature = "async-client"))] +pub use types::Client; +pub use types::{ + EncodedResponseQuery, RequestCtx, RequestQuery, ResponseQuery, Router, +}; + +use super::storage::{DBIter, StorageHasher, DB}; +use super::storage_api::{self, ResultExt, StorageRead}; +use crate::types::storage::{self, Epoch, PrefixValue}; +use crate::types::transaction::TxResult; +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] +use crate::types::transaction::{DecryptedTx, TxType}; + +#[macro_use] +mod router; +mod types; + +// Most commonly expected patterns should be declared first +router! {RPC, + // Epoch of the last committed block + ( "epoch" ) -> Epoch = epoch, + + // Raw storage access - read value + ( "value" / [storage_key: storage::Key] ) + -> Option> = storage_value, + + // Dry run a transaction + ( "dry_run_tx" ) -> TxResult = dry_run_tx, + + // Raw storage access - prefix iterator + ( "prefix" / [storage_key: storage::Key] ) + -> Vec = storage_prefix, + + // Raw storage access - is given storage key present? + ( "has_key" / [storage_key: storage::Key] ) + -> bool = storage_has_key, +} + +/// Handle RPC query request in the ledger. On success, returns response with +/// borsh-encoded data. +pub fn handle_path( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, +) -> storage_api::Result +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + RPC.handle(ctx, request) +} + +// Handler helpers: + +/// For queries that only support latest height, check that the given height is +/// not different from latest height, otherwise return an error. +pub fn require_latest_height( + ctx: &RequestCtx<'_, D, H>, + request: &RequestQuery, +) -> storage_api::Result<()> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + if request.height != ctx.storage.last_height { + return Err(storage_api::Error::new_const( + "This query doesn't support arbitrary block heights, only the \ + latest committed block height ('0' can be used as a special \ + value that means the latest block height)", + )); + } + Ok(()) +} + +/// For queries that only support latest height, check that the given height is +/// not different from latest height, otherwise return an error. +pub fn require_no_proof(request: &RequestQuery) -> storage_api::Result<()> { + if request.prove { + return Err(storage_api::Error::new_const( + "This query doesn't support proofs", + )); + } + Ok(()) +} + +// Handlers: + +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] +fn dry_run_tx( + mut ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + use super::gas::BlockGasMeter; + use super::storage::write_log::WriteLog; + use crate::proto::Tx; + + let mut gas_meter = BlockGasMeter::default(); + let mut write_log = WriteLog::default(); + let tx = Tx::try_from(&request.data[..]).into_storage_result()?; + let tx = TxType::Decrypted(DecryptedTx::Decrypted(tx)); + let data = super::protocol::apply_tx( + tx, + request.data.len(), + &mut gas_meter, + &mut write_log, + ctx.storage, + &mut ctx.vp_wasm_cache, + &mut ctx.tx_wasm_cache, + ) + .into_storage_result()?; + Ok(ResponseQuery { + data, + ..ResponseQuery::default() + }) +} + +#[cfg(not(all(feature = "wasm-runtime", feature = "ferveo-tpke")))] +fn dry_run_tx( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + unimplemented!( + "dry_run_tx request handler requires \"wasm-runtime\" and \ + \"ferveo-tpke\" features enabled." + ) +} + +fn epoch( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + require_latest_height(&ctx, request)?; + require_no_proof(request)?; + + let data = ctx.storage.last_epoch; + Ok(ResponseQuery { + data, + ..Default::default() + }) +} + +fn storage_value( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> storage_api::Result>>> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + match ctx + .storage + .read_with_height(&storage_key, request.height) + .into_storage_result()? + { + (Some(value), _gas) => { + let proof = if request.prove { + let proof = ctx + .storage + .get_existence_proof( + &storage_key, + value.clone().into(), + request.height, + ) + .into_storage_result()?; + Some(proof.into()) + } else { + None + }; + Ok(ResponseQuery { + data: Some(value), + proof_ops: proof, + ..Default::default() + }) + } + (None, _gas) => { + let proof = if request.prove { + let proof = ctx + .storage + .get_non_existence_proof(&storage_key, request.height) + .into_storage_result()?; + Some(proof.into()) + } else { + None + }; + Ok(ResponseQuery { + data: None, + proof_ops: proof, + info: format!("No value found for key: {}", storage_key), + }) + } + } +} + +fn storage_prefix( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> storage_api::Result>> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + require_latest_height(&ctx, request)?; + + let (iter, _gas) = ctx.storage.iter_prefix(&storage_key); + let data: storage_api::Result> = iter + .map(|(key, value, _gas)| { + let key = storage::Key::parse(key).into_storage_result()?; + Ok(PrefixValue { key, value }) + }) + .collect(); + let data = data?; + let proof_ops = if request.prove { + let mut ops = vec![]; + for PrefixValue { key, value } in &data { + let proof = ctx + .storage + .get_existence_proof(key, value.clone().into(), request.height) + .into_storage_result()?; + let mut cur_ops: Vec = + proof.ops.into_iter().map(|op| op.into()).collect(); + ops.append(&mut cur_ops); + } + // ops is not empty in this case + Some(ProofOps { ops }) + } else { + None + }; + Ok(ResponseQuery { + data, + proof_ops, + ..Default::default() + }) +} + +fn storage_has_key( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + require_latest_height(&ctx, request)?; + require_no_proof(request)?; + + let data = StorageRead::has_key(ctx.storage, &storage_key)?; + Ok(ResponseQuery { + data, + ..Default::default() + }) +} + +#[cfg(any(test, feature = "tendermint-rpc"))] +/// Provides [`Client`] implementation for Tendermint RPC client +pub mod tm { + use thiserror::Error; + + use super::*; + use crate::types::storage::BlockHeight; + + #[allow(missing_docs)] + #[derive(Error, Debug)] + pub enum Error { + #[error("{0}")] + Tendermint(#[from] tendermint_rpc::Error), + #[error("Decoding error: {0}")] + Decoding(#[from] std::io::Error), + #[error("Info log: {0}, error code: {1}")] + Query(String, u32), + #[error("Invalid block height: {0} (overflown i64)")] + InvalidHeight(BlockHeight), + } + + #[async_trait::async_trait] + impl Client for tendermint_rpc::HttpClient { + type Error = Error; + + async fn request( + &self, + path: String, + data: Option>, + height: Option, + prove: bool, + ) -> Result { + let data = data.unwrap_or_default(); + let height = height + .map(|height| { + tendermint::block::Height::try_from(height.0) + .map_err(|_err| Error::InvalidHeight(height)) + }) + .transpose()?; + let response = tendermint_rpc::Client::abci_query( + self, + // TODO open the private Path constructor in tendermint-rpc + Some(std::str::FromStr::from_str(&path).unwrap()), + data, + height, + prove, + ) + .await?; + match response.code { + tendermint::abci::Code::Ok => Ok(EncodedResponseQuery { + data: response.value, + info: response.info, + proof_ops: response.proof.map(Into::into), + }), + tendermint::abci::Code::Err(code) => { + Err(Error::Query(response.info, code)) + } + } + } + } +} + +/// Queries testing helpers +#[cfg(any(test, feature = "testing"))] +mod testing { + use tempfile::TempDir; + + use super::*; + use crate::ledger::storage::testing::TestStorage; + use crate::types::storage::BlockHeight; + use crate::vm::wasm::{self, TxCache, VpCache}; + use crate::vm::WasmCacheRoAccess; + + /// A test client that has direct access to the storage + pub struct TestClient + where + RPC: Router, + { + /// RPC router + pub rpc: RPC, + /// storage + pub storage: TestStorage, + /// VP wasm compilation cache + pub vp_wasm_cache: VpCache, + /// tx wasm compilation cache + pub tx_wasm_cache: TxCache, + /// VP wasm compilation cache directory + pub vp_cache_dir: TempDir, + /// tx wasm compilation cache directory + pub tx_cache_dir: TempDir, + } + + impl TestClient + where + RPC: Router, + { + #[allow(dead_code)] + /// Initialize a test client for the given root RPC router + pub fn new(rpc: RPC) -> Self { + // Initialize the `TestClient` + let storage = TestStorage::default(); + let (vp_wasm_cache, vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + let (tx_wasm_cache, tx_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + Self { + rpc, + storage, + vp_wasm_cache: vp_wasm_cache.read_only(), + tx_wasm_cache: tx_wasm_cache.read_only(), + vp_cache_dir, + tx_cache_dir, + } + } + } + + #[async_trait::async_trait] + impl Client for TestClient + where + RPC: Router + Sync, + { + type Error = std::io::Error; + + async fn request( + &self, + path: String, + data: Option>, + height: Option, + prove: bool, + ) -> Result { + let data = data.unwrap_or_default(); + let height = height.unwrap_or_default(); + // Handle a path by invoking the `RPC.handle` directly with the + // borrowed storage + let request = RequestQuery { + data, + path, + height, + prove, + }; + let ctx = RequestCtx { + storage: &self.storage, + vp_wasm_cache: self.vp_wasm_cache.clone(), + tx_wasm_cache: self.tx_wasm_cache.clone(), + }; + let response = self.rpc.handle(ctx, &request).unwrap(); + Ok(response) + } + } +} + +#[cfg(test)] +mod test { + use borsh::BorshDeserialize; + + use super::testing::TestClient; + use super::*; + use crate::ledger::storage_api::StorageWrite; + use crate::proto::Tx; + use crate::types::{address, token}; + + const TX_NO_OP_WASM: &str = "../wasm_for_tests/tx_no_op.wasm"; + + #[test] + fn test_queries_router_paths() { + let path = RPC.epoch_path(); + assert_eq!("/epoch", path); + + let token_addr = address::testing::established_address_1(); + let owner = address::testing::established_address_2(); + let key = token::balance_key(&token_addr, &owner); + let path = RPC.storage_value_path(&key); + assert_eq!(format!("/value/{}", key), path); + + let path = RPC.dry_run_tx_path(); + assert_eq!("/dry_run_tx", path); + + let path = RPC.storage_prefix_path(&key); + assert_eq!(format!("/prefix/{}", key), path); + + let path = RPC.storage_has_key_path(&key); + assert_eq!(format!("/has_key/{}", key), path); + } + + #[tokio::test] + async fn test_queries_router_with_client() -> storage_api::Result<()> { + // Initialize the `TestClient` + let mut client = TestClient::new(RPC); + + // Request last committed epoch + let read_epoch = RPC.epoch(&client).await.unwrap(); + let current_epoch = client.storage.last_epoch; + assert_eq!(current_epoch, read_epoch); + + // Request dry run tx + let tx_no_op = std::fs::read(TX_NO_OP_WASM).expect("cannot load wasm"); + let tx = Tx::new(tx_no_op, None); + let tx_bytes = tx.to_bytes(); + let result = RPC + .dry_run_tx_with_options(&client, Some(tx_bytes), None, false) + .await + .unwrap(); + assert!(result.data.is_accepted()); + + // Request storage value for a balance key ... + let token_addr = address::testing::established_address_1(); + let owner = address::testing::established_address_2(); + let balance_key = token::balance_key(&token_addr, &owner); + // ... there should be no value yet. + let read_balance = + RPC.storage_value(&client, &balance_key).await.unwrap(); + assert!(read_balance.is_none()); + + // Request storage prefix iterator + let balance_prefix = token::balance_prefix(&token_addr); + let read_balances = + RPC.storage_prefix(&client, &balance_prefix).await.unwrap(); + assert!(read_balances.is_empty()); + + // Request storage has key + let has_balance_key = + RPC.storage_has_key(&client, &balance_key).await.unwrap(); + assert!(!has_balance_key); + + // Then write some balance ... + let balance = token::Amount::from(1000); + StorageWrite::write(&mut client.storage, &balance_key, balance)?; + // ... there should be the same value now + let read_balance = + RPC.storage_value(&client, &balance_key).await.unwrap(); + assert_eq!( + balance, + token::Amount::try_from_slice(&read_balance.unwrap()).unwrap() + ); + + // Request storage prefix iterator + let balance_prefix = token::balance_prefix(&token_addr); + let read_balances = + RPC.storage_prefix(&client, &balance_prefix).await.unwrap(); + assert_eq!(read_balances.len(), 1); + + // Request storage has key + let has_balance_key = + RPC.storage_has_key(&client, &balance_key).await.unwrap(); + assert!(has_balance_key); + + Ok(()) + } +} diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs new file mode 100644 index 0000000000..69888cb935 --- /dev/null +++ b/shared/src/ledger/queries/router.rs @@ -0,0 +1,881 @@ +//! The main export of this module is the `router!` macro, which can be used to +//! define compile time tree patterns for a router in which the terminal leaves +//! are connected to the given handler functions. +//! +//! Note that for debugging pattern matching issue, you can uncomment +//! all the `println!`s in this module. + +use thiserror::Error; + +/// Router error. +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("Found no matching pattern for the given path {0}")] + WrongPath(String), +} + +/// Find the index of a next slash after the given `start` index in the path. +/// When there are no more slashes, returns the index after the end of the path. +/// +/// # Panics +/// The given `start` must be < `path.len()`. +pub fn find_next_slash_index(path: &str, start: usize) -> usize { + path[start..] + .find('/') + // Offset by the starting position + .map(|i| start + i) + // If not found, go to the end of path + .unwrap_or(path.len()) +} + +/// Invoke the sub-handler or call the handler function with the matched +/// arguments generated by `try_match_segments`. +macro_rules! handle_match { + // Nested router + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, + (sub $router:tt), ( $( $matched_args:ident, )* ), + ) => { + // not used anymore - silence the warning + let _ = $end; + // Undo last '/' advance, the next pattern has to start with `/`. + // This cannot underflow because path cannot be empty and must start + // with `/` + $start -= 1; + // Invoke `handle` on the sub router + return $router.internal_handle($ctx, $request, $start) + }; + + // Handler function + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, + $handle:tt, ( $( $matched_args:ident, )* ), + ) => { + // check that we're at the end of the path - trailing slash is optional + if !($end == $request.path.len() || + // ignore trailing slashes + $end == $request.path.len() - 1 && &$request.path[$end..] == "/") { + // we're not at the end, no match + println!("Not fully matched"); + break + } + // If you get a compile error from here with `expected function, found + // queries::Storage`, you're probably missing the marker `(sub _)` + let result = $handle($ctx, $request, $( $matched_args ),* )?; + let data = borsh::BorshSerialize::try_to_vec(&result.data).into_storage_result()?; + return Ok($crate::ledger::queries::EncodedResponseQuery { + data, + info: result.info, + proof_ops: result.proof_ops, + }); + }; +} + +/// Using TT muncher pattern on the `$tail` pattern, this macro recursively +/// generates path matching logic that `break`s if some parts are unmatched. +macro_rules! try_match_segments { + // sub-pattern handle - this should only be invoked if the current + // $pattern is already matched + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, + { $( $sub_pattern:tt $( -> $_sub_return_ty:path )? = $handle:tt, )* }, + $matched_args:tt, + () + ) => { + // Try to match each sub-patten + $( + // This loop never repeats, it's only used for a breaking + // mechanism when a $pattern is not matched to skip to the + // next one, if any + loop { + #[allow(unused_mut)] + let mut $start = $start; + let mut $end = $end; + // Try to match, parse args and invoke $handle, will + // break the `loop` not matched + try_match_segments!($ctx, $request, $start, $end, + $handle, $matched_args, $sub_pattern + ); + } + )* + }; + + // Terminal tail call, invoked after when all the args in the current + // pattern are matched and the $handle is not sub-pattern + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, $handle:tt, + ( $( $matched_args:ident, )* ), + () + ) => { + handle_match!($ctx, $request, $start, $end, $handle, ( $( $matched_args, )* ), ); + }; + + // Try to match an untyped argument, declares the expected $arg as &str + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, $handle:ident, + ( $( $matched_args:ident, )* ), + ( + [$arg:ident] + $( / $( $tail:tt)/ * )? + ) + ) => { + let $arg = &$request.path[$start..$end]; + // Advanced index past the matched arg + $start = $end; + // advance past next '/', if any + if $start + 1 < $request.path.len() { + $start += 1; + } + $end = find_next_slash_index(&$request.path, $start); + try_match_segments!($ctx, $request, $start, $end, $handle, + ( $( $matched_args, )* $arg, ), ( $( $( $tail )/ * )? ) ); + }; + + // Try to match and parse a typed argument like the case below, but with + // the argument optional. + // Declares the expected $arg into type $t, if it can be parsed. + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, $handle:tt, + ( $( $matched_args:ident, )* ), + ( + [$arg:ident : opt $arg_ty:ty] + $( / $( $tail:tt)/ * )? + ) + ) => { + let $arg: Option<$arg_ty> = match $request.path[$start..$end].parse::<$arg_ty>() { + Ok(parsed) => { + // Only advance if optional argument is present, otherwise stay + // in the same position for the next match, if any. + + $start = $end; + // advance past next '/', if any + if $start + 1 < $request.path.len() { + $start += 1; + } + $end = find_next_slash_index(&$request.path, $start); + + Some(parsed) + }, + Err(_) => + { + // If arg cannot be parsed, ignore it because it's optional + None + } + }; + try_match_segments!($ctx, $request, $start, $end, $handle, + ( $( $matched_args, )* $arg, ), ( $( $( $tail )/ * )? ) ); + }; + + // Special case of the pattern below. When there are no more args in the + // tail and the handle isn't a sub-router (its fragment is ident), we try + // to match the rest of the path till the end. This is specifically needed + // for storage methods, which have `storage::Key` param that includes + // path-like slashes. + // + // Try to match and parse a typed argument, declares the expected $arg into + // type $t, if it can be parsed + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, $handle:ident, + ( $( $matched_args:ident, )* ), + ( + [$arg:ident : $arg_ty:ty] + ) + ) => { + let $arg: $arg_ty; + $end = $request.path.len(); + match $request.path[$start..$end].parse::<$arg_ty>() { + Ok(parsed) => { + println!("Parsed {}", parsed); + $arg = parsed + }, + Err(_) => + { + println!("Cannot parse {} from {}", stringify!($arg_ty), &$request.path[$start..$end]); + // If arg cannot be parsed, try to skip to next pattern + break + } + } + // Invoke the terminal pattern + try_match_segments!($ctx, $request, $start, $end, $handle, + ( $( $matched_args, )* $arg, ), () ); + }; + + // Try to match and parse a typed argument, declares the expected $arg into + // type $t, if it can be parsed + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, $handle:tt, + ( $( $matched_args:ident, )* ), + ( + [$arg:ident : $arg_ty:ty] + $( / $( $tail:tt)/ * )? + ) + ) => { + let $arg: $arg_ty; + match $request.path[$start..$end].parse::<$arg_ty>() { + Ok(parsed) => { + $arg = parsed + }, + Err(_) => + { + println!("Cannot parse {} from {}", stringify!($arg_ty), &$request.path[$start..$end]); + // If arg cannot be parsed, try to skip to next pattern + break + } + } + $start = $end; + // advance past next '/', if any + if $start + 1 < $request.path.len() { + $start += 1; + } + $end = find_next_slash_index(&$request.path, $start); + try_match_segments!($ctx, $request, $start, $end, $handle, + ( $( $matched_args, )* $arg, ), ( $( $( $tail )/ * )? ) ); + }; + + // Try to match an expected string literal + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, $handle:tt, + ( $( $matched_args:ident, )* ), + ( + $expected:literal + $( / $( $tail:tt)/ * )? + ) + ) => { + if &$request.path[$start..$end] == $expected { + // Advanced index past the matched arg + println!("Matched literal {}", $expected); + $start = $end; + } else { + println!("{} doesn't match literal {}", &$request.path[$start..$end], $expected); + // Try to skip to next pattern + break; + } + // advance past next '/', if any + if $start + 1 < $request.path.len() { + $start += 1; + } + $end = find_next_slash_index(&$request.path, $start); + try_match_segments!($ctx, $request, $start, $end, $handle, + ( $( $matched_args, )* ), ( $( $( $tail )/ * )? ) ); + }; +} + +/// Generate a function that tries to match the given pattern and `break`s if +/// any of its parts are unmatched. This layer will check that the path starts +/// with `/` and then invoke `try_match_segments` TT muncher that goes through +/// the patterns. +macro_rules! try_match { + ($ctx:ident, $request:ident, $start:ident, $handle:tt, $segments:tt) => { + // check that the initial char is '/' + if $request.path.is_empty() || &$request.path[..1] != "/" { + println!("Missing initial slash"); + break; + } + // advance past initial '/' + $start += 1; + // Path is too short to match + if $start >= $request.path.len() { + println!("Path is too short"); + break; + } + let mut end = find_next_slash_index(&$request.path, $start); + try_match_segments!( + $ctx, + $request, + $start, + end, + $handle, + (), + $segments + ); + }; +} + +/// Convert literal pattern into a `&[&'static str]` +// TODO sub router pattern is not yet used +#[allow(unused_macros)] +macro_rules! pattern_to_prefix { + ( ( $( $pattern:literal )/ * ) ) => { + &[$( $pattern ),*] + }; + ( $pattern:tt ) => { + compile_error!("sub-router cannot have non-literal prefix patterns") + }; +} + +/// Turn patterns and their handlers into methods for the router, where each +/// dynamic pattern is turned into a parameter for the method. +macro_rules! pattern_and_handler_to_method { + // terminal rule + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + // $( $return_type:path )?, + $return_type:path, + $handle:tt, + () + ) => { + // paste! used to construct the `fn $handle_path`'s name. + paste::paste! { + #[allow(dead_code)] + #[doc = "Get a path to query `" $handle "`."] + pub fn [<$handle _path>](&self, $( $param: &$param_ty ),* ) -> String { + itertools::join( + [ Some(std::borrow::Cow::from(&self.prefix)), $( $prefix ),* ] + .into_iter() + .filter_map(|x| x), "/") + } + + #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] + #[cfg(any(test, feature = "async-client"))] + #[doc = "Request a simple borsh-encoded value from `" $handle "`, \ + without any additional request data, specified block height or \ + proof."] + pub async fn $handle(&self, client: &CLIENT, + $( $param: &$param_ty ),* + ) + -> std::result::Result< + $return_type, + ::Error + > + where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { + let path = self.[<$handle _path>]( $( $param ),* ); + + let data = client.simple_request(path).await?; + + let decoded: $return_type = + borsh::BorshDeserialize::try_from_slice(&data[..])?; + Ok(decoded) + } + + #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] + #[cfg(any(test, feature = "async-client"))] + #[doc = "Request value with optional data (used for e.g. \ + `dry_run_tx`), optionally specified height (supported for \ + `storage_value`) and optional proof (supported for \ + `storage_value` and `storage_prefix`) from `" $handle "`."] + pub async fn [<$handle _with_options>](&self, client: &CLIENT, + data: Option>, + height: Option<$crate::types::storage::BlockHeight>, + prove: bool, + $( $param: &$param_ty ),* + ) + -> std::result::Result< + $crate::ledger::queries::ResponseQuery<$return_type>, + ::Error + > + where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { + let path = self.[<$handle _path>]( $( $param ),* ); + + let $crate::ledger::queries::ResponseQuery { + data, info, proof_ops + } = client.request(path, data, height, prove).await?; + + let decoded: $return_type = + borsh::BorshDeserialize::try_from_slice(&data[..])?; + + Ok($crate::ledger::queries::ResponseQuery { + data: decoded, + info, + proof_ops, + }) + } + } + }; + + // sub-pattern + ( + $param:tt + $prefix:tt + $( $_return_type:path )?, + { $( $sub_pattern:tt $( -> $sub_return_ty:path )? = $handle:tt, )* }, + $pattern:tt + ) => { + $( + // join pattern with each sub-pattern + pattern_and_handler_to_method!( + $param + $prefix + $( $sub_return_ty )?, $handle, $pattern, $sub_pattern + ); + )* + }; + + // literal string arg + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + $( $return_type:path )?, + $handle:tt, + ( $pattern:literal $( / $tail:tt )* ) + ) => { + pattern_and_handler_to_method!( + ( $( $param: $param_ty ),* ) + [ $( { $prefix }, )* { std::option::Option::Some(std::borrow::Cow::from($pattern)) } ] + $( $return_type )?, $handle, ( $( $tail )/ * ) + ); + }; + + // untyped arg + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + $( $return_type:path )?, + $handle:tt, + ( [$name:tt] $( / $tail:tt )* ) + ) => { + pattern_and_handler_to_method!( + ( $( $param: $param_ty, )* $name: str ) + [ $( { $prefix }, )* { std::option::Option::Some(std::borrow::Cow::from($name)) } ] + $( $return_type )?, $handle, ( $( $tail )/ * ) + ); + }; + + // typed arg + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + $( $return_type:path )?, + $handle:tt, + ( [$name:tt: $type:ty] $( / $tail:tt )* ) + ) => { + pattern_and_handler_to_method!( + ( $( $param: $param_ty, )* $name: $type ) + [ $( { $prefix }, )* { std::option::Option::Some(std::borrow::Cow::from($name.to_string())) } ] + $( $return_type )?, $handle, ( $( $tail )/ * ) + ); + }; + + // opt typed arg + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + $( $return_type:path )?, + $handle:tt, + ( [$name:tt: opt $type:ty] $( / $tail:tt )* ) + ) => { + pattern_and_handler_to_method!( + ( $( $param: $param_ty, )* $name: std::option::Option<$type> ) + [ $( { $prefix }, )* { $name.map(|arg| std::borrow::Cow::from(arg.to_string())) } ] + $( $return_type )?, $handle, ( $( $tail )/ * ) + ); + }; + + // join pattern with sub-pattern + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + $( $return_type:path )?, + $handle:tt, + ( $( $pattern:tt )/ * ), ( $( $sub_pattern:tt )/ * ) + ) => { + pattern_and_handler_to_method!( + ( $( $param: $param_ty ),* ) + [ $( { $prefix }, )* ] + $( $return_type )?, + $handle, ( $( $pattern / )* $( $sub_pattern )/ * ) + ); + }; +} + +/// TT muncher macro that generates a `struct $name` with methods for all its +/// handlers. +macro_rules! router_type { + // terminal rule + ($name:ident { $( $methods:item )* }, ) => { + paste::paste! { + #[doc = "`" $name "`path router type"] + pub struct $name { + prefix: String, + } + + impl $name { + #[doc = "Construct this router as a root router"] + const fn new() -> Self { + Self { + prefix: String::new(), + } + } + + #[allow(dead_code)] + #[doc = "Construct this router as a sub-router at the given prefix path"] + const fn sub(prefix: String) -> Self { + Self { + prefix, + } + } + + // paste the generated methods + $( $methods )* + } + } + }; + + // a sub router - recursion + ( + $name:ident { $( $methods:item )* }, + $pattern:tt = (sub $router:ident) + $( ,$tail_pattern:tt $( -> $tail_return_type:path )? = $tail:tt )* + ) => { + paste::paste! { + router_type!{ + $name { + #[doc = "`" $name "` sub-router"] + pub fn [<$router:camel:snake>](&self) -> [<$router:camel>] { + // prefix for a sub can only contain literals + let current_prefix: &[&'static str] = pattern_to_prefix!($pattern); + let path = [&[self.prefix.as_str()][..], current_prefix].concat().join("/"); + [<$router:camel>]::sub(path) + } + $( $methods )* + }, + $( $tail_pattern $( -> $tail_return_type )? = $tail ),* + } + } + }; + + // a sub-pattern - add a method for each handle inside it + ( + $name:ident + { $( $methods:item )* }, + $pattern:tt = { $( $sub_pattern:tt $( -> $sub_return_ty:path )? = $handle:tt, )* } + $( ,$tail_pattern:tt $( -> $tail_return_type:path )? = $tail:tt )* + ) => { + router_type!{ + $name { + $( + // join pattern with each sub-pattern + pattern_and_handler_to_method!( () [] $( $sub_return_ty )?, $handle, + $pattern, $sub_pattern + ); + )* + $( $methods )* + }, + $( $tail_pattern $( -> $tail_return_type )? = $tail ),* + } + }; + + // pattern with a handle - add a method for the handle + ( + $name:ident + { $( $methods:item )* }, + $pattern:tt -> $return_type:path = $handle:tt + $( ,$tail_pattern:tt $( -> $tail_return_type:path )? = $tail:tt )* + ) => { + router_type!{ + $name { + pattern_and_handler_to_method!( () [] $return_type, $handle, $pattern ); + $( $methods )* + }, + $( $tail_pattern $( -> $tail_return_type )? = $tail ),* + } + }; +} + +/// Compile time tree patterns router with type-safe dynamic parameter parsing, +/// automatic routing, type-safe path constructors and optional client query +/// methods (enabled with `feature = "async-client"`). +/// +/// The `router!` macro implements greedy matching algorithm. +#[macro_export] +macro_rules! router { + { $name:ident, $( $pattern:tt $( -> $return_type:path )? = $handle:tt , )* } => ( + + // `paste!` is used to convert the $name cases for a derived type and function name + paste::paste! { + + router_type!{[<$name:camel>] {}, $( $pattern $( -> $return_type )? = $handle ),* } + + impl $crate::ledger::queries::Router for [<$name:camel>] { + // TODO: for some patterns, there's unused assignment of `$end` + #[allow(unused_assignments)] + fn internal_handle( + &self, + ctx: $crate::ledger::queries::RequestCtx<'_, D, H>, + request: &$crate::ledger::queries::RequestQuery, + start: usize + ) -> $crate::ledger::storage_api::Result<$crate::ledger::queries::EncodedResponseQuery> + where + D: 'static + $crate::ledger::storage::DB + for<'iter> $crate::ledger::storage::DBIter<'iter> + Sync, + H: 'static + $crate::ledger::storage::StorageHasher + Sync, + { + + // Import for `.into_storage_result()` + use $crate::ledger::storage_api::ResultExt; + + // Import helper from this crate used inside the macros + use $crate::ledger::queries::router::find_next_slash_index; + + $( + // This loop never repeats, it's only used for a breaking + // mechanism when a $pattern is not matched to skip to the + // next one, if any + loop { + let mut start = start; + // Try to match, parse args and invoke $handle, will + // break the `loop` not matched + try_match!(ctx, request, start, $handle, $pattern); + } + )* + + return Err( + $crate::ledger::queries::router::Error::WrongPath(request.path.clone())) + .into_storage_result(); + } + } + + #[doc = "`" $name "` path router"] + pub const $name: [<$name:camel>] = [<$name:camel>]::new(); + } + + ); +} + +/// You can expand the `handlers!` macro invocation with e.g.: +/// ```shell +/// cargo expand ledger::queries::router::test_rpc_handlers --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib +/// ``` +#[cfg(test)] +mod test_rpc_handlers { + use crate::ledger::queries::{RequestCtx, RequestQuery, ResponseQuery}; + use crate::ledger::storage::{DBIter, StorageHasher, DB}; + use crate::ledger::storage_api; + use crate::types::storage::Epoch; + use crate::types::token; + + /// A little macro to generate boilerplate fo RPC handler functions. + /// These are implemented to return their name as a String, joined by + /// slashes with their argument values turned `to_string()`, if any. + macro_rules! handlers { + ( + // name and params, if any + $( $name:ident $( ( $( $param:ident: $param_ty:ty ),* ) )? ),* + // optional trailing comma + $(,)? ) => { + $( + pub fn $name( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, + $( $( $param: $param_ty ),* )? + ) -> storage_api::Result> + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = stringify!($name).to_owned(); + $( $( + let data = format!("{data}/{}", $param); + )* )? + Ok(ResponseQuery { + data, + ..ResponseQuery::default() + }) + } + )* + }; + } + + // Generate handler functions for the router below + handlers!( + a, + b0i, + b0ii, + b1, + b2i(balance: token::Amount), + b3(a1: token::Amount, a2: token::Amount, a3: token::Amount), + b3i(a1: token::Amount, a2: token::Amount, a3: token::Amount), + b3ii(a1: token::Amount, a2: token::Amount, a3: token::Amount), + x, + y(untyped_arg: &str), + z(untyped_arg: &str), + ); + + /// This handler is hand-written, because the test helper macro doesn't + /// support optional args. + pub fn b3iii( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, + a1: token::Amount, + a2: token::Amount, + a3: Option, + ) -> storage_api::Result> + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = "b3iii".to_owned(); + let data = format!("{data}/{}", a1); + let data = format!("{data}/{}", a2); + let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); + Ok(ResponseQuery { + data, + ..ResponseQuery::default() + }) + } + + /// This handler is hand-written, because the test helper macro doesn't + /// support optional args. + pub fn b3iiii( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, + a1: token::Amount, + a2: token::Amount, + a3: Option, + a4: Option, + ) -> storage_api::Result> + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = "b3iiii".to_owned(); + let data = format!("{data}/{}", a1); + let data = format!("{data}/{}", a2); + let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); + let data = a4.map(|a4| format!("{data}/{}", a4)).unwrap_or(data); + Ok(ResponseQuery { + data, + ..ResponseQuery::default() + }) + } +} + +/// You can expand the `router!` macro invocation with e.g.: +/// ```shell +/// cargo expand ledger::queries::router::test_rpc --features "ferveo-tpke, ibc-mocks, testing, wasm-runtime, tendermint-rpc" --tests --lib +/// ``` +#[cfg(test)] +mod test_rpc { + use super::test_rpc_handlers::*; + use crate::types::storage::Epoch; + use crate::types::token; + + // Setup an RPC router for testing + router! {TEST_RPC, + ( "sub" ) = (sub TEST_SUB_RPC), + ( "a" ) -> String = a, + ( "b" ) = { + ( "0" ) = { + ( "i" ) -> String = b0i, + ( "ii" ) -> String = b0ii, + }, + ( "1" ) -> String = b1, + ( "2" ) = { + ( "i" / [balance: token::Amount] ) -> String = b2i, + }, + ( "3" / [a1: token::Amount] / [a2: token::Amount] ) = { + ( "i" / [a3: token:: Amount] ) -> String = b3i, + ( [a3: token:: Amount] ) -> String = b3, + ( [a3: token:: Amount] / "ii" ) -> String = b3ii, + ( [a3: opt token::Amount] / "iii" ) -> String = b3iii, + ( "iiii" / [a3: opt token::Amount] / "xyz" / [a4: opt Epoch] ) -> String = b3iiii, + }, + }, + } + + router! {TEST_SUB_RPC, + ( "x" ) -> String = x, + ( "y" / [untyped_arg] ) -> String = y, + ( "z" / [untyped_arg] ) -> String = z, + } +} + +#[cfg(test)] +mod test { + use super::test_rpc::TEST_RPC; + use crate::ledger::queries::testing::TestClient; + use crate::ledger::queries::{RequestCtx, RequestQuery, Router}; + use crate::ledger::storage_api; + use crate::types::storage::Epoch; + use crate::types::token; + + /// Test all the possible paths in `TEST_RPC` router. + #[tokio::test] + async fn test_router_macro() -> storage_api::Result<()> { + let client = TestClient::new(TEST_RPC); + + // Test request with an invalid path + let request = RequestQuery { + path: "/invalid".to_owned(), + ..RequestQuery::default() + }; + let ctx = RequestCtx { + storage: &client.storage, + vp_wasm_cache: client.vp_wasm_cache.clone(), + tx_wasm_cache: client.tx_wasm_cache.clone(), + }; + let result = TEST_RPC.handle(ctx, &request); + assert!(result.is_err()); + + // Test requests to valid paths using the router's methods + + let result = TEST_RPC.a(&client).await.unwrap(); + assert_eq!(result, "a"); + + let result = TEST_RPC.b0i(&client).await.unwrap(); + assert_eq!(result, "b0i"); + + let result = TEST_RPC.b0ii(&client).await.unwrap(); + assert_eq!(result, "b0ii"); + + let result = TEST_RPC.b1(&client).await.unwrap(); + assert_eq!(result, "b1"); + + let balance = token::Amount::from(123_000_000); + let result = TEST_RPC.b2i(&client, &balance).await.unwrap(); + assert_eq!(result, format!("b2i/{balance}")); + + let a1 = token::Amount::from(345); + let a2 = token::Amount::from(123_000); + let a3 = token::Amount::from(1_000_999); + let result = TEST_RPC.b3(&client, &a1, &a2, &a3).await.unwrap(); + assert_eq!(result, format!("b3/{a1}/{a2}/{a3}")); + + let result = TEST_RPC.b3i(&client, &a1, &a2, &a3).await.unwrap(); + assert_eq!(result, format!("b3i/{a1}/{a2}/{a3}")); + + let result = TEST_RPC.b3ii(&client, &a1, &a2, &a3).await.unwrap(); + assert_eq!(result, format!("b3ii/{a1}/{a2}/{a3}")); + + let result = + TEST_RPC.b3iii(&client, &a1, &a2, &Some(a3)).await.unwrap(); + assert_eq!(result, format!("b3iii/{a1}/{a2}/{a3}")); + + let result = TEST_RPC.b3iii(&client, &a1, &a2, &None).await.unwrap(); + assert_eq!(result, format!("b3iii/{a1}/{a2}")); + + let result = TEST_RPC + .b3iiii(&client, &a1, &a2, &Some(a3), &None) + .await + .unwrap(); + assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}")); + + let a4 = Epoch::from(10); + let result = TEST_RPC + .b3iiii(&client, &a1, &a2, &Some(a3), &Some(a4)) + .await + .unwrap(); + assert_eq!(result, format!("b3iiii/{a1}/{a2}/{a3}/{a4}")); + + let result = TEST_RPC + .b3iiii(&client, &a1, &a2, &None, &None) + .await + .unwrap(); + assert_eq!(result, format!("b3iiii/{a1}/{a2}")); + + let result = TEST_RPC.test_sub_rpc().x(&client).await.unwrap(); + assert_eq!(result, format!("x")); + + let arg = "test123"; + let result = TEST_RPC.test_sub_rpc().y(&client, arg).await.unwrap(); + assert_eq!(result, format!("y/{arg}")); + + let arg = "test321"; + let result = TEST_RPC.test_sub_rpc().z(&client, arg).await.unwrap(); + assert_eq!(result, format!("z/{arg}")); + + Ok(()) + } +} diff --git a/shared/src/ledger/queries/types.rs b/shared/src/ledger/queries/types.rs new file mode 100644 index 0000000000..00cff84ed9 --- /dev/null +++ b/shared/src/ledger/queries/types.rs @@ -0,0 +1,171 @@ +use tendermint_proto::crypto::ProofOps; + +use crate::ledger::storage::{DBIter, Storage, StorageHasher, DB}; +use crate::ledger::storage_api; +use crate::types::storage::BlockHeight; +#[cfg(feature = "wasm-runtime")] +use crate::vm::wasm::{TxCache, VpCache}; +#[cfg(feature = "wasm-runtime")] +use crate::vm::WasmCacheRoAccess; + +/// A request context provides read-only access to storage and WASM compilation +/// caches to request handlers. +#[derive(Debug, Clone)] +pub struct RequestCtx<'a, D, H> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + /// Storage access + pub storage: &'a Storage, + /// VP WASM compilation cache + #[cfg(feature = "wasm-runtime")] + pub vp_wasm_cache: VpCache, + /// tx WASM compilation cache + #[cfg(feature = "wasm-runtime")] + pub tx_wasm_cache: TxCache, +} + +/// A `Router` handles parsing read-only query requests and dispatching them to +/// their handler functions. A valid query returns a borsh-encoded result. +pub trait Router { + /// Handle a given request using the provided context. This must be invoked + /// on the root `Router` to be able to match the `request.path` fully. + fn handle( + &self, + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + ) -> storage_api::Result + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + self.internal_handle(ctx, request, 0) + } + + /// Internal method which shouldn't be invoked directly. Instead, you may + /// want to call `self.handle()`. + /// + /// Handle a given request using the provided context, starting to + /// try to match `request.path` against the `Router`'s patterns at the + /// given `start` offset. + fn internal_handle( + &self, + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + start: usize, + ) -> storage_api::Result + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync; +} + +/// A client with async request dispatcher method, which can be used to invoke +/// type-safe methods from a root [`Router`], generated via `router!` macro. +#[cfg(any(test, feature = "async-client"))] +#[async_trait::async_trait] +pub trait Client { + /// `std::io::Error` can happen in decoding with + /// `BorshDeserialize::try_from_slice` + type Error: From; + + /// Send a simple query request at the given path. For more options, use the + /// `request` method. + async fn simple_request( + &self, + path: String, + ) -> Result, Self::Error> { + self.request(path, None, None, false) + .await + .map(|response| response.data) + } + + /// Send a query request at the given path. + async fn request( + &self, + path: String, + data: Option>, + height: Option, + prove: bool, + ) -> Result; +} + +/// Temporary domain-type for `tendermint_proto::abci::RequestQuery`, copied +/// from +/// until we are on a branch that has it included. +#[derive(Clone, PartialEq, Eq, Debug, Default)] +pub struct RequestQuery { + /// Raw query bytes. + /// + /// Can be used with or in lieu of `path`. + pub data: Vec, + /// Path of the request, like an HTTP `GET` path. + /// + /// Can be used with or in lieu of `data`. + /// + /// Applications MUST interpret `/store` as a query by key on the + /// underlying store. The key SHOULD be specified in the Data field. + /// Applications SHOULD allow queries over specific types like + /// `/accounts/...` or `/votes/...`. + pub path: String, + /// The block height for which the query should be executed. + /// + /// The default `0` returns data for the latest committed block. Note that + /// this is the height of the block containing the application's Merkle + /// root hash, which represents the state as it was after committing + /// the block at `height - 1`. + pub height: BlockHeight, + /// Whether to return a Merkle proof with the response, if possible. + pub prove: bool, +} + +/// Generic response from a query +#[derive(Clone, Debug, Default)] +pub struct ResponseQuery { + /// Response data to be borsh encoded + pub data: T, + /// Non-deterministic log of the request execution + pub info: String, + /// Optional proof - used for storage value reads which request `prove` + pub proof_ops: Option, +} + +/// [`ResponseQuery`] with borsh-encoded `data` field +pub type EncodedResponseQuery = ResponseQuery>; + +impl RequestQuery { + /// Try to convert tendermint RequestQuery into our [`RequestQuery`] + /// domain type. This tries to convert the block height into our + /// [`BlockHeight`] type, where `0` is treated as a special value to signal + /// to use the latest committed block height as per tendermint ABCI Query + /// spec. A negative block height will cause an error. + pub fn try_from_tm( + storage: &Storage, + tendermint_proto::abci::RequestQuery { + data, + path, + height, + prove, + }: tendermint_proto::abci::RequestQuery, + ) -> Result + where + D: DB + for<'iter> DBIter<'iter>, + H: StorageHasher, + { + let height = match height { + 0 => { + // `0` means last committed height + storage.last_height + } + _ => BlockHeight(height.try_into().map_err(|_| { + format!("Query height cannot be negative, got: {}", height) + })?), + }; + Ok(Self { + data, + path, + height, + prove, + }) + } +} From c5939176b9837fe842b1fef53a58238082aa616e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 6 Oct 2022 12:44:53 +0200 Subject: [PATCH 17/47] apps: replace RPC module and its handlers with new queries mod --- apps/src/lib/client/rpc.rs | 245 ++++++----------- apps/src/lib/node/ledger/mod.rs | 1 - apps/src/lib/node/ledger/rpc.rs | 104 ------- apps/src/lib/node/ledger/shell/governance.rs | 1 + apps/src/lib/node/ledger/shell/mod.rs | 39 --- .../lib/node/ledger/shell/process_proposal.rs | 5 +- apps/src/lib/node/ledger/shell/queries.rs | 260 +++--------------- 7 files changed, 117 insertions(+), 538 deletions(-) delete mode 100644 apps/src/lib/node/ledger/rpc.rs diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 6c1e3fb5f3..488f20f8f9 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -23,6 +23,7 @@ use namada::ledger::pos::types::{ use namada::ledger::pos::{ self, is_validator_slashes_key, BondId, Bonds, PosParams, Slash, Unbonds, }; +use namada::ledger::queries::{self, RPC}; use namada::types::address::Address; use namada::types::governance::{ OfflineProposal, OfflineVote, ProposalResult, ProposalVote, TallyResult, @@ -35,63 +36,30 @@ use namada::types::{address, storage, token}; use crate::cli::{self, args, Context}; use crate::client::tendermint_rpc_types::TxResponse; -use crate::facade::tendermint::abci::Code; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::error::Error as TError; use crate::facade::tendermint_rpc::query::Query; use crate::facade::tendermint_rpc::{ Client, HttpClient, Order, SubscriptionClient, WebSocketClient, }; -use crate::node::ledger::rpc::Path; /// Query the epoch of the last committed block pub async fn query_epoch(args: args::Query) -> Epoch { let client = HttpClient::new(args.ledger_address).unwrap(); - let path = Path::Epoch; - let data = vec![]; - let response = client - .abci_query(Some(path.into()), data, None, false) - .await - .unwrap(); - match response.code { - Code::Ok => match Epoch::try_from_slice(&response.value[..]) { - Ok(epoch) => { - println!("Last committed epoch: {}", epoch); - return epoch; - } - - Err(err) => { - eprintln!("Error decoding the epoch value: {}", err) - } - }, - Code::Err(err) => eprintln!( - "Error in the query {} (error code {})", - response.info, err - ), - } - cli::safe_exit(1) + let epoch = unwrap_client_response(RPC.epoch(&client).await); + println!("Last committed epoch: {}", epoch); + epoch } /// Query the raw bytes of given storage key pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { let client = HttpClient::new(args.query.ledger_address).unwrap(); - let path = Path::Value(args.storage_key); - let data = vec![]; - let response = client - .abci_query(Some(path.into()), data, None, false) - .await - .unwrap(); - match response.code { - Code::Ok => { - println!("{}", HEXLOWER.encode(&response.value)); - } - Code::Err(err) => { - eprintln!( - "Error in the query {} (error code {})", - response.info, err - ); - cli::safe_exit(1) - } + let bytes = unwrap_client_response( + RPC.storage_value(&client, &args.storage_key).await, + ); + match bytes { + Some(bytes) => println!("Found data: 0x{}", HEXLOWER.encode(&bytes)), + None => println!("No data found for key {}", args.storage_key), } } @@ -135,11 +103,9 @@ pub async fn query_balance(ctx: Context, args: args::QueryBalance) { let owner = ctx.get(&owner); for (token, _) in tokens { let prefix = token.to_db_key().into(); - let balances = query_storage_prefix::( - client.clone(), - prefix, - ) - .await; + let balances = + query_storage_prefix::(&client, &prefix) + .await; if let Some(balances) = balances { print_balances(&ctx, balances, &token, Some(&owner)); } @@ -149,7 +115,7 @@ pub async fn query_balance(ctx: Context, args: args::QueryBalance) { let token = ctx.get(&token); let prefix = token.to_db_key().into(); let balances = - query_storage_prefix::(client, prefix).await; + query_storage_prefix::(&client, &prefix).await; if let Some(balances) = balances { print_balances(&ctx, balances, &token, None); } @@ -158,8 +124,7 @@ pub async fn query_balance(ctx: Context, args: args::QueryBalance) { for (token, _) in tokens { let key = token::balance_prefix(&token); let balances = - query_storage_prefix::(client.clone(), key) - .await; + query_storage_prefix::(&client, &key).await; if let Some(balances) = balances { print_balances(&ctx, balances, &token, None); } @@ -660,18 +625,14 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { let owner = ctx.get(&owner); // Find owner's bonds to any validator let bonds_prefix = pos::bonds_for_source_prefix(&owner); - let bonds = query_storage_prefix::( - client.clone(), - bonds_prefix, - ) - .await; + let bonds = + query_storage_prefix::(&client, &bonds_prefix) + .await; // Find owner's unbonds to any validator let unbonds_prefix = pos::unbonds_for_source_prefix(&owner); - let unbonds = query_storage_prefix::( - client.clone(), - unbonds_prefix, - ) - .await; + let unbonds = + query_storage_prefix::(&client, &unbonds_prefix) + .await; let mut total: token::Amount = 0.into(); let mut total_active: token::Amount = 0.into(); @@ -780,18 +741,14 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { (None, None) => { // Find all the bonds let bonds_prefix = pos::bonds_prefix(); - let bonds = query_storage_prefix::( - client.clone(), - bonds_prefix, - ) - .await; + let bonds = + query_storage_prefix::(&client, &bonds_prefix) + .await; // Find all the unbonds let unbonds_prefix = pos::unbonds_prefix(); - let unbonds = query_storage_prefix::( - client.clone(), - unbonds_prefix, - ) - .await; + let unbonds = + query_storage_prefix::(&client, &unbonds_prefix) + .await; let mut total: token::Amount = 0.into(); let mut total_active: token::Amount = 0.into(); @@ -1032,11 +989,9 @@ pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { None => { // Iterate slashes for all validators let slashes_prefix = pos::slashes_prefix(); - let slashes = query_storage_prefix::( - client.clone(), - slashes_prefix, - ) - .await; + let slashes = + query_storage_prefix::(&client, &slashes_prefix) + .await; match slashes { Some(slashes) => { @@ -1075,12 +1030,13 @@ pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { /// Dry run a transaction pub async fn dry_run_tx(ledger_address: &TendermintAddress, tx_bytes: Vec) { let client = HttpClient::new(ledger_address.clone()).unwrap(); - let path = Path::DryRunTx; - let response = client - .abci_query(Some(path.into()), tx_bytes, None, false) - .await - .unwrap(); - println!("{:#?}", response); + let (data, height, prove) = (Some(tx_bytes), None, false); + let result = unwrap_client_response( + RPC.dry_run_tx_with_options(&client, data, height, prove) + .await, + ) + .data; + println!("Dry-run result: {}", result); } /// Get account's public key stored in its storage sub-space @@ -1113,7 +1069,7 @@ pub async fn is_delegator( let client = HttpClient::new(ledger_address).unwrap(); let bonds_prefix = pos::bonds_for_source_prefix(address); let bonds = - query_storage_prefix::(client.clone(), bonds_prefix).await; + query_storage_prefix::(&client, &bonds_prefix).await; bonds.is_some() && bonds.unwrap().count() > 0 } @@ -1123,8 +1079,7 @@ pub async fn is_delegator_at( epoch: Epoch, ) -> bool { let key = pos::bonds_for_source_prefix(address); - let bonds_iter = - query_storage_prefix::(client.clone(), key).await; + let bonds_iter = query_storage_prefix::(client, &key).await; if let Some(mut bonds) = bonds_iter { bonds.any(|(_, bond)| bond.get(epoch).is_some()) } else { @@ -1144,7 +1099,7 @@ pub async fn known_address( Address::Established(_) => { // Established account exists if it has a VP let key = storage::Key::validity_predicate(address); - query_has_storage_key(client, key).await + query_has_storage_key(&client, &key).await } Address::Implicit(_) | Address::Internal(_) => true, } @@ -1293,100 +1248,52 @@ pub async fn query_storage_value( where T: BorshDeserialize, { - let path = Path::Value(key.to_owned()); - let data = vec![]; - let response = client - .abci_query(Some(path.into()), data, None, false) - .await - .unwrap(); - match response.code { - Code::Ok => match T::try_from_slice(&response.value[..]) { - Ok(value) => return Some(value), - Err(err) => eprintln!("Error decoding the value: {}", err), - }, - Code::Err(err) => { - if err == 1 { - return None; - } else { - eprintln!( - "Error in the query {} (error code {})", - response.info, err - ) - } - } - } - cli::safe_exit(1) + let bytes = unwrap_client_response(RPC.storage_value(client, key).await); + bytes.map(|bytes| { + T::try_from_slice(&bytes[..]).unwrap_or_else(|err| { + eprintln!("Error decoding the value: {}", err); + cli::safe_exit(1) + }) + }) } /// Query a range of storage values with a matching prefix and decode them with /// [`BorshDeserialize`]. Returns an iterator of the storage keys paired with /// their associated values. pub async fn query_storage_prefix( - client: HttpClient, - key: storage::Key, + client: &HttpClient, + key: &storage::Key, ) -> Option> where T: BorshDeserialize, { - let path = Path::Prefix(key); - let data = vec![]; - let response = client - .abci_query(Some(path.into()), data, None, false) - .await - .unwrap(); - match response.code { - Code::Ok => { - match Vec::::try_from_slice(&response.value[..]) { - Ok(values) => { - let decode = |PrefixValue { key, value }: PrefixValue| { - match T::try_from_slice(&value[..]) { - Err(_) => None, - Ok(value) => Some((key, value)), - } - }; - return Some(values.into_iter().filter_map(decode)); - } - Err(err) => eprintln!("Error decoding the values: {}", err), - } - } - Code::Err(err) => { - if err == 1 { - return None; - } else { + let values = unwrap_client_response(RPC.storage_prefix(client, key).await); + let decode = + |PrefixValue { key, value }: PrefixValue| match T::try_from_slice( + &value[..], + ) { + Err(err) => { eprintln!( - "Error in the query {} (error code {})", - response.info, err - ) + "Skipping a value for key {}. Error in decoding: {}", + key, err + ); + None } - } + Ok(value) => Some((key, value)), + }; + if values.is_empty() { + None + } else { + Some(values.into_iter().filter_map(decode)) } - cli::safe_exit(1) } /// Query to check if the given storage key exists. pub async fn query_has_storage_key( - client: HttpClient, - key: storage::Key, + client: &HttpClient, + key: &storage::Key, ) -> bool { - let path = Path::HasKey(key); - let data = vec![]; - let response = client - .abci_query(Some(path.into()), data, None, false) - .await - .unwrap(); - match response.code { - Code::Ok => match bool::try_from_slice(&response.value[..]) { - Ok(value) => return value, - Err(err) => eprintln!("Error decoding the value: {}", err), - }, - Code::Err(err) => { - eprintln!( - "Error in the query {} (error code {})", - response.info, err - ) - } - } - cli::safe_exit(1) + unwrap_client_response(RPC.storage_has_key(client, key).await) } /// Represents a query for an event pertaining to the specified transaction @@ -1556,8 +1463,7 @@ pub async fn get_proposal_votes( let vote_prefix_key = gov_storage::get_proposal_vote_prefix_key(proposal_id); let vote_iter = - query_storage_prefix::(client.clone(), vote_prefix_key) - .await; + query_storage_prefix::(client, &vote_prefix_key).await; let mut yay_validators: HashMap = HashMap::new(); let mut yay_delegators: HashMap> = @@ -1662,7 +1568,7 @@ pub async fn get_proposal_offline_votes( { let key = pos::bonds_for_source_prefix(&proposal_vote.address); let bonds_iter = - query_storage_prefix::(client.clone(), key).await; + query_storage_prefix::(client, &key).await; if let Some(bonds) = bonds_iter { for (key, epoched_bonds) in bonds { // Look-up slashes for the validator in this key and @@ -1907,8 +1813,7 @@ pub async fn get_delegators_delegation( _epoch: Epoch, ) -> Vec
{ let key = pos::bonds_for_source_prefix(address); - let bonds_iter = - query_storage_prefix::(client.clone(), key).await; + let bonds_iter = query_storage_prefix::(client, &key).await; let mut delegation_addresses: Vec
= Vec::new(); if let Some(bonds) = bonds_iter { @@ -1970,3 +1875,11 @@ fn lookup_alias(ctx: &Context, addr: &Address) -> String { None => format!("{}", addr), } } + +/// A helper to unwrap client's response. Will shut down process on error. +fn unwrap_client_response(response: Result) -> T { + response.unwrap_or_else(|err| { + eprintln!("Error in the query {}", err); + cli::safe_exit(1) + }) +} diff --git a/apps/src/lib/node/ledger/mod.rs b/apps/src/lib/node/ledger/mod.rs index 965275cc03..9796c18fce 100644 --- a/apps/src/lib/node/ledger/mod.rs +++ b/apps/src/lib/node/ledger/mod.rs @@ -1,7 +1,6 @@ mod abortable; mod broadcaster; pub mod events; -pub mod rpc; mod shell; mod shims; pub mod storage; diff --git a/apps/src/lib/node/ledger/rpc.rs b/apps/src/lib/node/ledger/rpc.rs deleted file mode 100644 index ad3d2f5fcb..0000000000 --- a/apps/src/lib/node/ledger/rpc.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! RPC endpoint is used for ledger state queries - -use std::fmt::Display; -use std::str::FromStr; - -use namada::types::address::Address; -use namada::types::storage; -use thiserror::Error; - -use crate::facade::tendermint::abci::Path as AbciPath; - -/// RPC query path -#[derive(Debug, Clone)] -pub enum Path { - /// Dry run a transaction - DryRunTx, - /// Epoch of the last committed block - Epoch, - /// Read a storage value with exact storage key - Value(storage::Key), - /// Read a range of storage values with a matching key prefix - Prefix(storage::Key), - /// Check if the given storage key exists - HasKey(storage::Key), -} - -#[derive(Debug, Clone)] -pub struct BalanceQuery { - #[allow(dead_code)] - owner: Option
, - #[allow(dead_code)] - token: Option
, -} - -const DRY_RUN_TX_PATH: &str = "dry_run_tx"; -const EPOCH_PATH: &str = "epoch"; -const VALUE_PREFIX: &str = "value"; -const PREFIX_PREFIX: &str = "prefix"; -const HAS_KEY_PREFIX: &str = "has_key"; - -impl Display for Path { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Path::DryRunTx => write!(f, "{}", DRY_RUN_TX_PATH), - Path::Epoch => write!(f, "{}", EPOCH_PATH), - Path::Value(storage_key) => { - write!(f, "{}/{}", VALUE_PREFIX, storage_key) - } - Path::Prefix(storage_key) => { - write!(f, "{}/{}", PREFIX_PREFIX, storage_key) - } - Path::HasKey(storage_key) => { - write!(f, "{}/{}", HAS_KEY_PREFIX, storage_key) - } - } - } -} - -impl FromStr for Path { - type Err = PathParseError; - - fn from_str(s: &str) -> Result { - match s { - DRY_RUN_TX_PATH => Ok(Self::DryRunTx), - EPOCH_PATH => Ok(Self::Epoch), - _ => match s.split_once('/') { - Some((VALUE_PREFIX, storage_key)) => { - let key = storage::Key::parse(storage_key) - .map_err(PathParseError::InvalidStorageKey)?; - Ok(Self::Value(key)) - } - Some((PREFIX_PREFIX, storage_key)) => { - let key = storage::Key::parse(storage_key) - .map_err(PathParseError::InvalidStorageKey)?; - Ok(Self::Prefix(key)) - } - Some((HAS_KEY_PREFIX, storage_key)) => { - let key = storage::Key::parse(storage_key) - .map_err(PathParseError::InvalidStorageKey)?; - Ok(Self::HasKey(key)) - } - _ => Err(PathParseError::InvalidPath(s.to_string())), - }, - } - } -} - -impl From for AbciPath { - fn from(path: Path) -> Self { - let path = path.to_string(); - // TODO: update in tendermint-rs to allow to construct this from owned - // string. It's what `from_str` does anyway - AbciPath::from_str(&path).unwrap() - } -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum PathParseError { - #[error("Unrecognized query path: {0}")] - InvalidPath(String), - #[error("Invalid storage key: {0}")] - InvalidStorageKey(storage::Error), -} diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 979c3c0df1..5ddcaf1673 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -3,6 +3,7 @@ use namada::ledger::governance::utils::{ compute_tally, get_proposal_votes, ProposalEvent, }; use namada::ledger::governance::vp::ADDRESS as gov_address; +use namada::ledger::protocol; use namada::ledger::slash_fund::ADDRESS as slash_fund_address; use namada::ledger::storage::types::encode; use namada::ledger::storage::{DBIter, StorageHasher, DB}; diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index ab3b131c37..52026f40d1 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -18,7 +18,6 @@ use std::mem; use std::path::{Path, PathBuf}; #[allow(unused_imports)] use std::rc::Rc; -use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use namada::ledger::gas::BlockGasMeter; @@ -48,7 +47,6 @@ use num_traits::{FromPrimitive, ToPrimitive}; use thiserror::Error; use tokio::sync::mpsc::UnboundedSender; -use super::rpc; use crate::config::{genesis, TendermintMode}; #[cfg(feature = "abcipp")] use crate::facade::tendermint_proto::abci::response_verify_vote_extension::VerifyStatus; @@ -559,43 +557,6 @@ where response } - /// Simulate validation and application of a transaction. - fn dry_run_tx(&self, tx_bytes: &[u8]) -> response::Query { - let mut response = response::Query::default(); - let mut gas_meter = BlockGasMeter::default(); - let mut write_log = WriteLog::default(); - let mut vp_wasm_cache = self.vp_wasm_cache.read_only(); - let mut tx_wasm_cache = self.tx_wasm_cache.read_only(); - match Tx::try_from(tx_bytes) { - Ok(tx) => { - let tx = TxType::Decrypted(DecryptedTx::Decrypted(tx)); - match protocol::apply_tx( - tx, - tx_bytes.len(), - &mut gas_meter, - &mut write_log, - &self.storage, - &mut vp_wasm_cache, - &mut tx_wasm_cache, - ) - .map_err(Error::TxApply) - { - Ok(result) => response.info = result.to_string(), - Err(error) => { - response.code = 1; - response.log = format!("{}", error); - } - } - response - } - Err(err) => { - response.code = 1; - response.log = format!("{}", Error::TxDecoding(err)); - response - } - } - } - /// Lookup a validator's keypair for their established account from their /// wallet. If the node is not validator, this function returns None #[allow(dead_code)] diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 5572e00495..5fa3e92763 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -150,9 +150,8 @@ where } } else { // check that the fee payer has sufficient balance - let balance = self - .get_balance(&tx.fee.token, &tx.fee_payer()) - .unwrap_or_default(); + let balance = + self.get_balance(&tx.fee.token, &tx.fee_payer()); if tx.fee.amount <= balance { TxResult { diff --git a/apps/src/lib/node/ledger/shell/queries.rs b/apps/src/lib/node/ledger/shell/queries.rs index a9435d565a..e53ea91417 100644 --- a/apps/src/lib/node/ledger/shell/queries.rs +++ b/apps/src/lib/node/ledger/shell/queries.rs @@ -9,7 +9,6 @@ use namada::types::key::dkg_session_keys::DkgPublicKey; use namada::types::{key, token}; use super::*; -use crate::facade::tendermint_proto::crypto::{ProofOp, ProofOps}; use crate::node::ledger::response; impl Shell @@ -22,39 +21,39 @@ where /// the default if `path` is not a supported string. /// INVARIANT: This method must be stateless. pub fn query(&self, query: request::Query) -> response::Query { - use rpc::Path; - let height = match query.height { - 0 => self.storage.get_block_height().0, - 1.. => BlockHeight(query.height as u64), - _ => { + let ctx = RequestCtx { + storage: &self.storage, + vp_wasm_cache: self.vp_wasm_cache.read_only(), + tx_wasm_cache: self.tx_wasm_cache.read_only(), + }; + + // Convert request to domain-type + let request = match namada::ledger::queries::RequestQuery::try_from_tm( + &self.storage, + query, + ) { + Ok(request) => request, + Err(err) => { return response::Query { code: 1, - info: format!( - "The query height is invalid: {}", - query.height - ), + info: format!("Unexpected query: {}", err), ..Default::default() }; } }; - match Path::from_str(&query.path) { - Ok(path) => match path { - Path::DryRunTx => self.dry_run_tx(&query.data), - Path::Epoch => { - let (epoch, _gas) = self.storage.get_last_epoch(); - let value = namada::ledger::storage::types::encode(&epoch); - response::Query { - value, - ..Default::default() - } - } - Path::Value(storage_key) => { - self.read_storage_value(&storage_key, height, query.prove) - } - Path::Prefix(storage_key) => { - self.read_storage_prefix(&storage_key, height, query.prove) - } - Path::HasKey(storage_key) => self.has_storage_key(&storage_key), + + // Invoke the root RPC handler - returns borsh-encoded data on success + let result = namada::ledger::queries::handle_path(ctx, &request); + match result { + Ok(ResponseQuery { + data, + info, + proof_ops, + }) => response::Query { + value: data, + info, + proof_ops, + ..Default::default() }, Err(err) => response::Query { code: 1, @@ -70,205 +69,16 @@ where &self, token: &Address, owner: &Address, - ) -> std::result::Result { - let height = self.storage.get_block_height().0; - let query_resp = self.read_storage_value( + ) -> token::Amount { + let balance = storage_api::StorageRead::read( + &self.storage, &token::balance_key(token, owner), - height, - false, ); - if query_resp.code != 0 { - Err(format!( - "Unable to read token {} balance of the given address {}", - token, owner - )) - } else { - BorshDeserialize::try_from_slice(&query_resp.value[..]).map_err( - |_| { - "Unable to deserialize the balance of the given address" - .into() - }, - ) - } - } - - /// Query to read a value from storage - pub fn read_storage_value( - &self, - key: &Key, - height: BlockHeight, - is_proven: bool, - ) -> response::Query { - match self.storage.read_with_height(key, height) { - Ok((Some(value), _gas)) => { - let proof_ops = if is_proven { - match self.storage.get_existence_proof( - key, - value.clone().into(), - height, - ) { - Ok(proof) => Some(proof.into()), - Err(err) => { - return response::Query { - code: 2, - info: format!("Storage error: {}", err), - ..Default::default() - }; - } - } - } else { - None - }; - response::Query { - value, - proof_ops, - ..Default::default() - } - } - Ok((None, _gas)) => { - let proof_ops = if is_proven { - match self.storage.get_non_existence_proof(key, height) { - Ok(proof) => Some(proof.into()), - Err(err) => { - return response::Query { - code: 2, - info: format!("Storage error: {}", err), - ..Default::default() - }; - } - } - } else { - None - }; - response::Query { - code: 1, - info: format!("No value found for key: {}", key), - proof_ops, - ..Default::default() - } - } - Err(err) => response::Query { - code: 2, - info: format!("Storage error: {}", err), - ..Default::default() - }, - } - } - - /// Query to read a range of values from storage with a matching prefix. The - /// value in successful response is a [`Vec`] encoded with - /// [`BorshSerialize`]. - pub fn read_storage_prefix( - &self, - key: &Key, - height: BlockHeight, - is_proven: bool, - ) -> response::Query { - if height != self.storage.get_block_height().0 { - return response::Query { - code: 2, - info: format!( - "Prefix read works with only the latest height: height {}", - height - ), - ..Default::default() - }; - } - let (iter, _gas) = self.storage.iter_prefix(key); - let mut iter = iter.peekable(); - if iter.peek().is_none() { - response::Query { - code: 1, - info: format!("No value found for key: {}", key), - ..Default::default() - } - } else { - let values: std::result::Result< - Vec, - namada::types::storage::Error, - > = iter - .map(|(key, value, _gas)| { - let key = Key::parse(key)?; - Ok(PrefixValue { key, value }) - }) - .collect(); - match values { - Ok(values) => { - let proof_ops = if is_proven { - let mut ops = vec![]; - for PrefixValue { key, value } in &values { - match self.storage.get_existence_proof( - key, - value.clone().into(), - height, - ) { - Ok(p) => { - let mut cur_ops: Vec = p - .ops - .into_iter() - .map(|op| { - #[cfg(feature = "abcipp")] - { - ProofOp { - r#type: op.field_type, - key: op.key, - data: op.data, - } - } - #[cfg(not(feature = "abcipp"))] - { - op.into() - } - }) - .collect(); - ops.append(&mut cur_ops); - } - Err(err) => { - return response::Query { - code: 2, - info: format!("Storage error: {}", err), - ..Default::default() - }; - } - } - } - // ops is not empty in this case - Some(ProofOps { ops }) - } else { - None - }; - let value = values.try_to_vec().unwrap(); - response::Query { - value, - proof_ops, - ..Default::default() - } - } - Err(err) => response::Query { - code: 1, - info: format!( - "Error parsing a storage key {}: {}", - key, err - ), - ..Default::default() - }, - } - } - } - - /// Query to check if a storage key exists. - fn has_storage_key(&self, key: &Key) -> response::Query { - match self.storage.has_key(key) { - Ok((has_key, _gas)) => response::Query { - value: has_key.try_to_vec().unwrap(), - ..Default::default() - }, - Err(err) => response::Query { - code: 2, - info: format!("Storage error: {}", err), - ..Default::default() - }, - } + // Storage read must not fail, but there might be no value, in which + // case default (0) is returned + balance + .expect("Storage read in the protocol must not fail") + .unwrap_or_default() } /// Lookup data about a validator from their protocol signing key From 66c632c2ff9e050f06c1ddcb1eeacc591439e7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 7 Oct 2022 15:04:14 +0200 Subject: [PATCH 18/47] changelog: add #553 --- .changelog/unreleased/improvements/553-rpc-queries-router.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changelog/unreleased/improvements/553-rpc-queries-router.md diff --git a/.changelog/unreleased/improvements/553-rpc-queries-router.md b/.changelog/unreleased/improvements/553-rpc-queries-router.md new file mode 100644 index 0000000000..877ac77c20 --- /dev/null +++ b/.changelog/unreleased/improvements/553-rpc-queries-router.md @@ -0,0 +1,4 @@ +- Replace the handcrafted RPC paths with a new `router!` macro RPC queries + definition that handles dynamic path matching, type-safe handler function + dispatch and also generates type-safe client methods for the queries. + ([#553](https://github.com/anoma/namada/pull/553)) \ No newline at end of file From 68135260ad7488fe752a6c62d06625f2da7211da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 24 Oct 2022 12:34:12 +0200 Subject: [PATCH 19/47] wasm: update checksums --- wasm/checksums.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 01140354ba..f3e11c7ea5 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,18 +1,18 @@ { - "tx_bond.wasm": "tx_bond.38c037a51f9215c2be9c1b01f647251ffdc96a02a0c958c5d3db4ee36ccde43b.wasm", - "tx_ibc.wasm": "tx_ibc.5f86477029d987073ebfec66019dc991b0bb8b80717d4885b860f910916cbcdd.wasm", - "tx_init_account.wasm": "tx_init_account.8d901bce15d1ab63a591def00421183a651d4d5e09ace4291bf0a9044692741d.wasm", - "tx_init_nft.wasm": "tx_init_nft.1991808f44c1c24d4376a3d46b602bed27575f6c0359095c53f37b9225050ffc.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.716cd08d59b26bd75815511f03e141e6ac27bc0b7d7be10a71b04559244722c2.wasm", - "tx_init_validator.wasm": "tx_init_validator.611edff2746f71cdaa7547a84a96676b555821f00af8375a28f8dab7ae9fc9fa.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.3f20f1a86da43cc475ccc127428944bd177d40fbe2d2d1588c6fadd069cbe4b2.wasm", - "tx_transfer.wasm": "tx_transfer.5653340103a32e6685f9668ec24855f65ae17bcc43035c2559a13f5c47bb67af.wasm", - "tx_unbond.wasm": "tx_unbond.71e66ac6f792123a2aaafd60b3892d74a7d0e7a03c3ea34f15fea9089010b810.wasm", - "tx_update_vp.wasm": "tx_update_vp.6d291dadb43545a809ba33fe26582b7984c67c65f05e363a93dbc62e06a33484.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.ff3def7b4bb0c46635bd6d544ac1745362757ce063feb8142d2ed9ab207f2a12.wasm", - "tx_withdraw.wasm": "tx_withdraw.ba1a743cf8914a353d7706777e0b1a37e20cd271b16e022fd3b50ad28971291f.wasm", - "vp_nft.wasm": "vp_nft.4471284b5c5f3e28c973f0a2ad2dde52ebe4a1dcd5dc15e93b380706fd0e35ea.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.7d7eb09cddc7ae348417da623e21ec4a4f8c78f15ae12de5abe7087eeab1e0db.wasm", - "vp_token.wasm": "vp_token.4a5436f7519de15c80103557add57e8d06e766e1ec1f7a642ffca252be01c5d0.wasm", - "vp_user.wasm": "vp_user.729b18aab60e8ae09b75b5f067658f30459a5ccfcd34f909b88da96523681019.wasm" + "tx_bond.wasm": "tx_bond.99da6abae7acd0a67341b8581bc2b9667cadd8d8c947086a6ed62cb8e5ab9f01.wasm", + "tx_ibc.wasm": "tx_ibc.449da8289b55d2e8ee4b80c4a271f0f82c14bc52f5f1cc141fc2fc25d7c379dc.wasm", + "tx_init_account.wasm": "tx_init_account.c7cf064e8d03315763d0a1c75b9fc0d44d98eab6a2943a82053a8d555861a14e.wasm", + "tx_init_nft.wasm": "tx_init_nft.7e7a5a2678da391ee89b02f08c75fab8b9f48a40e9d4d871d159694080ca41c0.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.be6c8b24bc0419a7242df6ffada532a17730fe13180fce16a91b310892d6ebad.wasm", + "tx_init_validator.wasm": "tx_init_validator.419c2cd5ddfdcc728ea41903e92ed05e60679086babf0feb8146a1a5c1c7ad79.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.d5dcf0139e3fc332474db7ad9fd74f03af3c50433833a222a8ecf9880faedc1e.wasm", + "tx_transfer.wasm": "tx_transfer.15a74bbc4093bb0fd3e7943f597f88a444d6e7ea6e3a47401430e01945fe9ceb.wasm", + "tx_unbond.wasm": "tx_unbond.64ac67930786fc9d18631ed2d3a225a261114129a9ff57986347c904367efac5.wasm", + "tx_update_vp.wasm": "tx_update_vp.7148b2fef2f9a438ec8e3bf42d1e120ce690f0f69bb2b1c481711ee8b22cef54.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.5488e66b41ea1c45efdb6152fe8897c37e731ae97958db024bf1905651e0f54c.wasm", + "tx_withdraw.wasm": "tx_withdraw.976687bb02cde635a97de500ea72631f37b50516d34f72d5da3ca82b9617fe57.wasm", + "vp_nft.wasm": "vp_nft.92e1c20e54e67a8baa00bbeb61b3712cca32f34bd7e63e4f7f5da23bc303529a.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.ddbda2d7f226d40a337eb5176f96050501996d6db8f71fa99c21382f6a631b41.wasm", + "vp_token.wasm": "vp_token.60879bfd767808fe6400096cb1527fe44c81e1893a4ff9ce593a3d36e89a45f6.wasm", + "vp_user.wasm": "vp_user.a51b5650c3303789857d4af18d1d4b342bfa5974fcb2b8d6eca906be998168c5.wasm" } \ No newline at end of file From 56b385c5f5f59326554b18722c039da1aeea786e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 24 Oct 2022 14:04:28 +0200 Subject: [PATCH 20/47] switch to tendermint-rs with consensus timeout --- Cargo.lock | 12 ++++++------ Cargo.toml | 14 +++++++------- wasm/Cargo.lock | 8 ++++---- wasm/Cargo.toml | 8 ++++---- wasm_for_tests/wasm_source/Cargo.lock | 8 ++++---- wasm_for_tests/wasm_source/Cargo.toml | 8 ++++---- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34ab8ab550..3b6140c842 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4986,7 +4986,7 @@ dependencies = [ [[package]] name = "tendermint" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "async-trait", "bytes 1.2.1", @@ -5027,7 +5027,7 @@ dependencies = [ [[package]] name = "tendermint-config" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "flex-error", "serde 1.0.145", @@ -5053,7 +5053,7 @@ dependencies = [ [[package]] name = "tendermint-light-client-verifier" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "derive_more", "flex-error", @@ -5082,7 +5082,7 @@ dependencies = [ [[package]] name = "tendermint-proto" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "bytes 1.2.1", "flex-error", @@ -5132,7 +5132,7 @@ dependencies = [ [[package]] name = "tendermint-rpc" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "async-trait", "async-tungstenite", @@ -5180,7 +5180,7 @@ dependencies = [ [[package]] name = "tendermint-testgen" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "ed25519-dalek", "gumdrop", diff --git a/Cargo.toml b/Cargo.toml index 4d63512550..5cfa43de02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,13 +34,13 @@ async-process = {git = "https://github.com/heliaxdev/async-process.git", rev = " # borsh-derive-internal = {path = "../borsh-rs/borsh-derive-internal"} # borsh-schema-derive-internal = {path = "../borsh-rs/borsh-schema-derive-internal"} -# patched to a commit on the `eth-bridge-integration` branch of our fork -tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-config = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-rpc = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} +# patched to a commit on the `eth-bridge-integration+consensus-timeout` branch of our fork +tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-config = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-rpc = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} # patched to a commit on the `eth-bridge-integration` branch of our fork ibc = {git = "https://github.com/heliaxdev/ibc-rs.git", rev = "f4703dfe2c1f25cc431279ab74f10f3e0f6827e2"} diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index f9030a471a..cd717113ad 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "tendermint" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "async-trait", "bytes", @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "tendermint-light-client-verifier" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "derive_more", "flex-error", @@ -2360,7 +2360,7 @@ dependencies = [ [[package]] name = "tendermint-proto" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "bytes", "flex-error", @@ -2377,7 +2377,7 @@ dependencies = [ [[package]] name = "tendermint-testgen" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "ed25519-dalek", "gumdrop", diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index d6f164a445..086fa05cae 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -14,10 +14,10 @@ borsh-derive = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223 borsh-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} borsh-schema-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} # patched to a commit on the `eth-bridge-integration` branch of our fork -tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} +tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} # patched to a commit on the `eth-bridge-integration` branch of our fork ibc = {git = "https://github.com/heliaxdev/ibc-rs.git", rev = "f4703dfe2c1f25cc431279ab74f10f3e0f6827e2"} diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index b82f3b3d59..06c908061c 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -2314,7 +2314,7 @@ dependencies = [ [[package]] name = "tendermint" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "async-trait", "bytes", @@ -2342,7 +2342,7 @@ dependencies = [ [[package]] name = "tendermint-light-client-verifier" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "derive_more", "flex-error", @@ -2354,7 +2354,7 @@ dependencies = [ [[package]] name = "tendermint-proto" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "bytes", "flex-error", @@ -2371,7 +2371,7 @@ dependencies = [ [[package]] name = "tendermint-testgen" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=87be41b8c9cc2850830f4d8028c1fe1bd9f96284#87be41b8c9cc2850830f4d8028c1fe1bd9f96284" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" dependencies = [ "ed25519-dalek", "gumdrop", diff --git a/wasm_for_tests/wasm_source/Cargo.toml b/wasm_for_tests/wasm_source/Cargo.toml index 8d9f4f4dc9..4d0b4c1b77 100644 --- a/wasm_for_tests/wasm_source/Cargo.toml +++ b/wasm_for_tests/wasm_source/Cargo.toml @@ -38,10 +38,10 @@ borsh-derive = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223 borsh-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} borsh-schema-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} # patched to a commit on the `eth-bridge-integration` branch of our fork -tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} -tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "87be41b8c9cc2850830f4d8028c1fe1bd9f96284"} +tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} # patched to a commit on the `eth-bridge-integration` branch of our fork ibc = {git = "https://github.com/heliaxdev/ibc-rs.git", rev = "f4703dfe2c1f25cc431279ab74f10f3e0f6827e2"} From f8e60cf680db0825a26b996e4ed88f4b4a54c88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 24 Oct 2022 14:06:03 +0200 Subject: [PATCH 21/47] add back consensus timeout commit config for abciplus --- apps/src/lib/node/ledger/tendermint_node.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/src/lib/node/ledger/tendermint_node.rs b/apps/src/lib/node/ledger/tendermint_node.rs index a19a5d7f13..cbd5cd4879 100644 --- a/apps/src/lib/node/ledger/tendermint_node.rs +++ b/apps/src/lib/node/ledger/tendermint_node.rs @@ -336,6 +336,12 @@ async fn update_tendermint_config( config.instrumentation.namespace = tendermint_config.instrumentation_namespace; + #[cfg(feature = "abciplus")] + { + config.consensus.timeout_commit = + tendermint_config.consensus_timeout_commit; + } + let mut file = OpenOptions::new() .write(true) .truncate(true) From 7918b7cd0c7b4a30b5a9c24487a4e29a11fceb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 24 Oct 2022 14:24:53 +0200 Subject: [PATCH 22/47] changelog: add #671 --- .../unreleased/bug-fixes/671-add-consensus-commit-timeout.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/671-add-consensus-commit-timeout.md diff --git a/.changelog/unreleased/bug-fixes/671-add-consensus-commit-timeout.md b/.changelog/unreleased/bug-fixes/671-add-consensus-commit-timeout.md new file mode 100644 index 0000000000..6c058a9904 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/671-add-consensus-commit-timeout.md @@ -0,0 +1,2 @@ +- Add back consensus commit timeout configuration set in tendermint + ([#671](https://github.com/anoma/namada/pull/671)) \ No newline at end of file From 31084fbabe4163b7e7e839673dc726b5b58e598f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Oct 2022 13:44:00 +0000 Subject: [PATCH 23/47] [ci] wasm checksums update --- wasm/checksums.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 01140354ba..387324b85b 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,18 +1,18 @@ { - "tx_bond.wasm": "tx_bond.38c037a51f9215c2be9c1b01f647251ffdc96a02a0c958c5d3db4ee36ccde43b.wasm", - "tx_ibc.wasm": "tx_ibc.5f86477029d987073ebfec66019dc991b0bb8b80717d4885b860f910916cbcdd.wasm", - "tx_init_account.wasm": "tx_init_account.8d901bce15d1ab63a591def00421183a651d4d5e09ace4291bf0a9044692741d.wasm", - "tx_init_nft.wasm": "tx_init_nft.1991808f44c1c24d4376a3d46b602bed27575f6c0359095c53f37b9225050ffc.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.716cd08d59b26bd75815511f03e141e6ac27bc0b7d7be10a71b04559244722c2.wasm", - "tx_init_validator.wasm": "tx_init_validator.611edff2746f71cdaa7547a84a96676b555821f00af8375a28f8dab7ae9fc9fa.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.3f20f1a86da43cc475ccc127428944bd177d40fbe2d2d1588c6fadd069cbe4b2.wasm", - "tx_transfer.wasm": "tx_transfer.5653340103a32e6685f9668ec24855f65ae17bcc43035c2559a13f5c47bb67af.wasm", - "tx_unbond.wasm": "tx_unbond.71e66ac6f792123a2aaafd60b3892d74a7d0e7a03c3ea34f15fea9089010b810.wasm", + "tx_bond.wasm": "tx_bond.547d3e2a9b11a956b4a9ea5c1ffb9287c425f42b15ef7a91726f4d5940bc9df3.wasm", + "tx_ibc.wasm": "tx_ibc.0858949e1bab245ed2b39f2678c01cb820cf9387d1bd1496702d800d7098fed1.wasm", + "tx_init_account.wasm": "tx_init_account.9609d1babc7dec840308f0f99d1c9d8fb6bd362ea3be45b2a7c6d465ea216af4.wasm", + "tx_init_nft.wasm": "tx_init_nft.3c81d2873688d14447cff850e5c29344dea355ab1967e3a93c9e64a21d7dcfd7.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.87a196e34860a557e945a51a459923c0a63426d866de89251d2d2c98cd882359.wasm", + "tx_init_validator.wasm": "tx_init_validator.1f607e48b79ef12a67d7d3e0264d72ea4c10d7179e779635dc85c9c7f8b0e19d.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.7306628cae2b4d63c440b08177104ab5e0e58390b5dc10d61cec10b223740a7d.wasm", + "tx_transfer.wasm": "tx_transfer.80c5235292204d078c9545935d1ac751982ea6d7b4a772fd0791fe3182658a20.wasm", + "tx_unbond.wasm": "tx_unbond.ce96a4317e545c5d683405ec515504b64f2749ad8411bde7d5503e36b477e11e.wasm", "tx_update_vp.wasm": "tx_update_vp.6d291dadb43545a809ba33fe26582b7984c67c65f05e363a93dbc62e06a33484.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.ff3def7b4bb0c46635bd6d544ac1745362757ce063feb8142d2ed9ab207f2a12.wasm", - "tx_withdraw.wasm": "tx_withdraw.ba1a743cf8914a353d7706777e0b1a37e20cd271b16e022fd3b50ad28971291f.wasm", - "vp_nft.wasm": "vp_nft.4471284b5c5f3e28c973f0a2ad2dde52ebe4a1dcd5dc15e93b380706fd0e35ea.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.7d7eb09cddc7ae348417da623e21ec4a4f8c78f15ae12de5abe7087eeab1e0db.wasm", - "vp_token.wasm": "vp_token.4a5436f7519de15c80103557add57e8d06e766e1ec1f7a642ffca252be01c5d0.wasm", - "vp_user.wasm": "vp_user.729b18aab60e8ae09b75b5f067658f30459a5ccfcd34f909b88da96523681019.wasm" + "tx_vote_proposal.wasm": "tx_vote_proposal.821cb26c29e2a152045dd0a4d7edd21174799497103373a080d792de05c21a3f.wasm", + "tx_withdraw.wasm": "tx_withdraw.fe7072fc4e19089c990040d7444e8b8cf985c48cd2d2993570727f8a16ace7e4.wasm", + "vp_nft.wasm": "vp_nft.413108d61c1a5c185e314ee0ca33bb41da626e5377a51d258d3054722ee4183d.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.e2caea6984d6771dc5f98667f4a55147416aa7f0ba1da0be3926f93b106594f9.wasm", + "vp_token.wasm": "vp_token.8f8feb38762ed40a62279686bb8aeff29c3ad7507ea42c981e24b64db2ecb18e.wasm", + "vp_user.wasm": "vp_user.78f90ab9a197bc39db82a378b9c202b32dcdac0c73d82bf48ee37d5220333516.wasm" } \ No newline at end of file From b91d89ee269b6ad6e8c620724817069107141b8a Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 18 Oct 2022 11:26:49 +0200 Subject: [PATCH 24/47] feat: change native token to nam --- apps/src/lib/cli.rs | 2 +- apps/src/lib/client/tx.rs | 6 +- .../lib/node/ledger/shell/finalize_block.rs | 12 +-- apps/src/lib/node/ledger/shell/governance.rs | 4 +- apps/src/lib/node/ledger/shell/init_chain.rs | 2 +- apps/src/lib/node/ledger/shell/mod.rs | 4 +- .../lib/node/ledger/shell/prepare_proposal.rs | 6 +- .../lib/node/ledger/shell/process_proposal.rs | 18 ++-- .../dev/src/explore/design/actors.md | 4 +- .../explore/design/ledger/pos-integration.md | 4 +- .../dev/src/explore/design/ledger/vp.md | 4 +- .../src/specs/ledger/default-transactions.md | 4 +- genesis/dev.toml | 6 +- genesis/e2e-tests-single-node.toml | 8 +- proof_of_stake/src/lib.rs | 4 +- shared/src/ledger/governance/mod.rs | 4 +- shared/src/ledger/governance/vp.rs | 6 +- shared/src/ledger/ibc/vp/mod.rs | 4 +- shared/src/ledger/pos/mod.rs | 4 +- shared/src/ledger/slash_fund/mod.rs | 2 +- shared/src/types/address.rs | 4 +- shared/src/types/transaction/mod.rs | 6 +- shared/src/types/transaction/wrapper.rs | 8 +- tests/src/e2e/ledger_tests.rs | 98 +++++++++---------- tests/src/e2e/setup.rs | 2 +- tx_prelude/src/governance.rs | 4 +- wasm/wasm_source/src/vp_testnet_faucet.rs | 6 +- wasm/wasm_source/src/vp_user.rs | 8 +- 28 files changed, 122 insertions(+), 122 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 8e4c7f78c9..d64a5fbf8a 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1296,7 +1296,7 @@ pub mod args { const FEE_AMOUNT: ArgDefault = arg_default("fee-amount", DefaultFn(|| token::Amount::from(0))); const FEE_TOKEN: ArgDefaultFromCtx = - arg_default_from_ctx("fee-token", DefaultFn(|| "XAN".into())); + arg_default_from_ctx("fee-token", DefaultFn(|| "NAM".into())); const FORCE: ArgFlag = flag("force"); const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); const GAS_LIMIT: ArgDefault = diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index e749a681c6..ab302fef1b 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -11,7 +11,7 @@ use itertools::Either::*; use namada::ledger::governance::storage as gov_storage; use namada::ledger::pos::{BondId, Bonds, Unbonds}; use namada::proto::Tx; -use namada::types::address::{xan as m1t, Address}; +use namada::types::address::{nam, Address}; use namada::types::governance::{ OfflineProposal, OfflineVote, Proposal, ProposalVote, }; @@ -639,7 +639,7 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { safe_exit(1) }; - let balance = rpc::get_token_balance(&client, &m1t(), &proposal.author) + let balance = rpc::get_token_balance(&client, &nam(), &proposal.author) .await .unwrap_or_default(); if balance @@ -911,7 +911,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { // Check bond's source (source for delegation or validator for self-bonds) // balance let bond_source = source.as_ref().unwrap_or(&validator); - let balance_key = token::balance_key(&address::xan(), bond_source); + let balance_key = token::balance_key(&address::nam(), bond_source); let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); match rpc::query_storage_value::(&client, &balance_key).await { diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 2080b8d23d..098719dc96 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -321,7 +321,7 @@ where /// are covered by the e2e tests. #[cfg(test)] mod test_finalize_block { - use namada::types::address::xan; + use namada::types::address::nam; use namada::types::storage::Epoch; use namada::types::transaction::{EncryptionKey, Fee}; @@ -349,7 +349,7 @@ mod test_finalize_block { let wrapper = WrapperTx::new( Fee { amount: i.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -420,7 +420,7 @@ mod test_finalize_block { let wrapper = WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -472,7 +472,7 @@ mod test_finalize_block { let wrapper = WrapperTx { fee: Fee { amount: 0.into(), - token: xan(), + token: nam(), }, pk: keypair.ref_to(), epoch: Epoch(0), @@ -538,7 +538,7 @@ mod test_finalize_block { let wrapper_tx = WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -569,7 +569,7 @@ mod test_finalize_block { let wrapper_tx = WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 979c3c0df1..828ff05524 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -6,7 +6,7 @@ use namada::ledger::governance::vp::ADDRESS as gov_address; use namada::ledger::slash_fund::ADDRESS as slash_fund_address; use namada::ledger::storage::types::encode; use namada::ledger::storage::{DBIter, StorageHasher, DB}; -use namada::types::address::{xan as m1t, Address}; +use namada::types::address::{nam, Address}; use namada::types::governance::TallyResult; use namada::types::storage::Epoch; use namada::types::token; @@ -179,7 +179,7 @@ where // transfer proposal locked funds shell .storage - .transfer(&m1t(), funds, &gov_address, &transfer_address); + .transfer(&nam(), funds, &gov_address, &transfer_address); } Ok(proposals_result) diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 8cb6842c50..d556fc1afd 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -222,7 +222,7 @@ where // Account balance (tokens no staked in PoS) self.storage .write( - &token::balance_key(&address::xan(), addr), + &token::balance_key(&address::nam(), addr), validator .non_staked_balance .try_to_vec() diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 28167d4eba..f095d5edb7 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -641,7 +641,7 @@ mod test_utils { use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::{BlockStateWrite, MerkleTree, Sha256Hasher}; - use namada::types::address::{xan, EstablishedAddressGen}; + use namada::types::address::{nam, EstablishedAddressGen}; use namada::types::chain::ChainId; use namada::types::hash::Hash; use namada::types::key::*; @@ -861,7 +861,7 @@ mod test_utils { let wrapper = WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index c41f2c2083..6251740181 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -140,7 +140,7 @@ pub(super) mod record { #[cfg(test)] mod test_prepare_proposal { use borsh::BorshSerialize; - use namada::types::address::xan; + use namada::types::address::nam; use namada::types::storage::Epoch; use namada::types::transaction::{Fee, WrapperTx}; @@ -189,7 +189,7 @@ mod test_prepare_proposal { WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -244,7 +244,7 @@ mod test_prepare_proposal { let wrapper_tx = WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 5572e00495..c3938b327c 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -189,7 +189,7 @@ where mod test_process_proposal { use borsh::BorshDeserialize; use namada::proto::SignedTxData; - use namada::types::address::xan; + use namada::types::address::nam; use namada::types::hash::Hash; use namada::types::key::*; use namada::types::storage::Epoch; @@ -217,7 +217,7 @@ mod test_process_proposal { let wrapper = WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -264,7 +264,7 @@ mod test_process_proposal { let mut wrapper = WrapperTx::new( Fee { amount: 100.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -346,7 +346,7 @@ mod test_process_proposal { let wrapper = WrapperTx::new( Fee { amount: 1.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -399,7 +399,7 @@ mod test_process_proposal { let wrapper = WrapperTx::new( Fee { amount: Amount::whole(1_000_100), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -447,7 +447,7 @@ mod test_process_proposal { let wrapper = WrapperTx::new( Fee { amount: i.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -511,7 +511,7 @@ mod test_process_proposal { let wrapper = WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -570,7 +570,7 @@ mod test_process_proposal { let mut wrapper = WrapperTx::new( Fee { amount: 0.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -623,7 +623,7 @@ mod test_process_proposal { let wrapper = WrapperTx { fee: Fee { amount: 0.into(), - token: xan(), + token: nam(), }, pk: keypair.ref_to(), epoch: Epoch(0), diff --git a/documentation/dev/src/explore/design/actors.md b/documentation/dev/src/explore/design/actors.md index 88e26f3d64..6172505adb 100644 --- a/documentation/dev/src/explore/design/actors.md +++ b/documentation/dev/src/explore/design/actors.md @@ -1,6 +1,6 @@ # Actors and Incentives -Namada consists of various actors fulfilling various roles in the network. They are all incentivized to act for the good of the network. The native Namada token `XAN` is used to settle transaction fees and pay for the incentives in Namada. +Namada consists of various actors fulfilling various roles in the network. They are all incentivized to act for the good of the network. The native Namada token `NAM` is used to settle transaction fees and pay for the incentives in Namada. ## Fees associated with a transaction @@ -9,7 +9,7 @@ Users of Namada can - transfer private assets they hold to other users and - barter assets with other users. -Each transaction may be associated with the following fees, paid in `XAN`: +Each transaction may be associated with the following fees, paid in `NAM`: - **Execution fees** to compensate for computing, storage and memory costs, charges at 2 stages: - **initial fee (init_f)**: charged before the transaction is settled diff --git a/documentation/dev/src/explore/design/ledger/pos-integration.md b/documentation/dev/src/explore/design/ledger/pos-integration.md index d441e5ddca..dda5abc748 100644 --- a/documentation/dev/src/explore/design/ledger/pos-integration.md +++ b/documentation/dev/src/explore/design/ledger/pos-integration.md @@ -27,7 +27,7 @@ All [the data relevant to the PoS system](https://specs.namada.net/economics/pro - `validator/{validator_address}/address_raw_hash` (required): raw hash of validator's address associated with the address is used for look-up of validator address from a raw hash - TBA (e.g. alias, website, description, delegation commission rate, etc.) -Only XAN tokens can be staked in bonds. The tokens being staked (bonds and unbonds amounts) are kept in the PoS account under `{xan_address}/balance/{pos_address}` until they are withdrawn. +Only NAM tokens can be staked in bonds. The tokens being staked (bonds and unbonds amounts) are kept in the PoS account under `{nam_address}/balance/{pos_address}` until they are withdrawn. ## Initialization @@ -39,7 +39,7 @@ Staking rewards for validators are rewarded in Tendermint's method `BeginBlock` To a validator who proposed a block (`block.header.proposer_address`), the system rewards tokens based on the `block_proposer_reward` PoS parameter and each validator that voted on a block (`block.last_commit_info.validator` who `signed_last_block`) receives `block_vote_reward`. -All the fees that are charged in a transaction execution (DKG transaction wrapper fee and transactions applied in a block) are transferred into a fee pool, which is another special account controlled by the PoS module. Note that the fee pool account may contain tokens other than the staking token XAN. +All the fees that are charged in a transaction execution (DKG transaction wrapper fee and transactions applied in a block) are transferred into a fee pool, which is another special account controlled by the PoS module. Note that the fee pool account may contain tokens other than the staking token NAM. - TODO describe the fee pool, related to , and diff --git a/documentation/dev/src/explore/design/ledger/vp.md b/documentation/dev/src/explore/design/ledger/vp.md index 7cf939f331..46eb28e571 100644 --- a/documentation/dev/src/explore/design/ledger/vp.md +++ b/documentation/dev/src/explore/design/ledger/vp.md @@ -39,9 +39,9 @@ The Proof-of-Stake slash pool is a simple account with a native VP which can rec The [fungible token VP](https://github.com/anoma/anoma/tree/master/wasm/wasm_source) allows to associate accounts balances of a specific token under its account. -For illustration, users `Albert` and `Bertha` might hold some amount of token with the address `XAN`. Their balances would be stored in the `XAN`'s storage sub-space under the storage keys `@XAN/balance/@Albert` and `@XAN/balance/@Bertha`, respectively. When `Albert` or `Bertha` attempt to transact with their `XAN` tokens, its validity predicate would be triggered to check: +For illustration, users `Albert` and `Bertha` might hold some amount of token with the address `NAM`. Their balances would be stored in the `NAM`'s storage sub-space under the storage keys `@NAM/balance/@Albert` and `@NAM/balance/@Bertha`, respectively. When `Albert` or `Bertha` attempt to transact with their `NAM` tokens, its validity predicate would be triggered to check: -- the total supply of `XAN` token is preserved (i.e. inputs = outputs) +- the total supply of `NAM` token is preserved (i.e. inputs = outputs) - the senders (users whose balance has been deducted) are checked that their validity predicate has also been triggered Note that the fungible token VP doesn't need to know whether any of involved users accepted or rejected the transaction, because if any of the involved users rejects it, the whole transaction will be rejected. diff --git a/documentation/dev/src/specs/ledger/default-transactions.md b/documentation/dev/src/specs/ledger/default-transactions.md index 78ed5b2098..fb254f0cd3 100644 --- a/documentation/dev/src/specs/ledger/default-transactions.md +++ b/documentation/dev/src/specs/ledger/default-transactions.md @@ -36,13 +36,13 @@ Attach [UpdateVp](../encoding.md#updatevp) to the `data`. ### tx_bond -Self-bond `amount` of XAN token from `validator` (without `source`) or delegate to `validator` from `source`. +Self-bond `amount` of NAM token from `validator` (without `source`) or delegate to `validator` from `source`. Attach [Bond](../encoding.md#bond) to the `data`. ### tx_unbond -Unbond self-bonded `amount` of XAN token from the `validator` (without `source`) or unbond delegation from the `source` to the `validator`. +Unbond self-bonded `amount` of NAM token from the `validator` (without `source`) or unbond delegation from the `source` to the `validator`. Attach [Bond](../encoding.md#bond) to the `data`. diff --git a/genesis/dev.toml b/genesis/dev.toml index 29a4b4d791..fc95244e14 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -26,10 +26,10 @@ net_address = "127.0.0.1:26656" # Some tokens present at genesis. -[token.xan] +[token.nam] address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" vp = "vp_token" -[token.xan.balances] +[token.nam.balances] # In token balances, we can use: # 1. An address any account a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz = 1000000 @@ -168,7 +168,7 @@ light_client_attack_slash_rate = 500 # Governance parameters. [gov_params] -# minimum amount of xan token to lock +# minimum amount of nam token to lock min_proposal_fund = 500 # proposal code size in bytes max_proposal_code_size = 300000 diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 0e3a6d3fc8..95e51173c6 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -5,7 +5,7 @@ genesis_time = "2021-09-30T10:00:00Z" [validator.validator-0] -# Validator's staked XAN at genesis. +# Validator's staked NAM at genesis. tokens = 200000 # Amount of the validator's genesis token balance which is not staked. non_staked_balance = 1000000000000 @@ -20,10 +20,10 @@ net_address = "127.0.0.1:27656" # Some tokens present at genesis. -[token.XAN] +[token.NAM] address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" vp = "vp_token" -[token.XAN.balances] +[token.NAM.balances] Albert = 1000000 "Albert.public_key" = 100 Bertha = 1000000 @@ -168,7 +168,7 @@ light_client_attack_slash_rate = 500 # Governance parameters. [gov_params] -# minimum amount of xan token to lock +# minimum amount of nam token to lock min_proposal_fund = 500 # proposal code size in bytes max_proposal_code_size = 300000 diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 7676ee7d2e..6e5f4e2196 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -101,7 +101,7 @@ pub trait PosReadOnly { const POS_ADDRESS: Self::Address; /// Address of the staking token - /// TODO: this should be `const`, but in the ledger `address::xan` is not a + /// TODO: this should be `const`, but in the ledger `address::nam` is not a /// `const fn` fn staking_token_address() -> Self::Address; @@ -577,7 +577,7 @@ pub trait PosBase { /// Address of the PoS account const POS_ADDRESS: Self::Address; /// Address of the staking token - /// TODO: this should be `const`, but in the ledger `address::xan` is not a + /// TODO: this should be `const`, but in the ledger `address::nam` is not a /// `const fn` fn staking_token_address() -> Self::Address; /// Address of the slash pool, into which slashed tokens are transferred. diff --git a/shared/src/ledger/governance/mod.rs b/shared/src/ledger/governance/mod.rs index 72aa7fb3ce..a4e0c80211 100644 --- a/shared/src/ledger/governance/mod.rs +++ b/shared/src/ledger/governance/mod.rs @@ -17,7 +17,7 @@ pub use vp::Result; use self::storage as gov_storage; use crate::ledger::native_vp::{Ctx, NativeVp}; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; -use crate::types::address::{xan as m1t, Address, InternalAddress}; +use crate::types::address::{nam, Address, InternalAddress}; use crate::types::storage::Key; use crate::types::token as token_storage; use crate::vm::WasmCacheAccess; @@ -235,7 +235,7 @@ where KeyType::COUNTER(vp::validate_counter_key) } else if gov_storage::is_parameter_key(value) { KeyType::PARAMETER(vp::validate_parameter_key) - } else if token_storage::is_balance_key(&m1t(), value).is_some() { + } else if token_storage::is_balance_key(&nam(), value).is_some() { KeyType::BALANCE(vp::validate_balance_key) } else if gov_storage::is_governance_key(value) { KeyType::UNKNOWN_GOVERNANCE(vp::validate_unknown_governance_key) diff --git a/shared/src/ledger/governance/vp.rs b/shared/src/ledger/governance/vp.rs index 7537cd5f01..c01e9d56bd 100644 --- a/shared/src/ledger/governance/vp.rs +++ b/shared/src/ledger/governance/vp.rs @@ -8,7 +8,7 @@ use crate::ledger::native_vp::{self, Ctx}; use crate::ledger::pos::{self as pos_storage, BondId, Bonds}; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; use crate::ledger::vp_env::VpEnv; -use crate::types::address::{xan as m1t, Address, InternalAddress}; +use crate::types::address::{nam, Address, InternalAddress}; use crate::types::storage::{Epoch, Key}; use crate::types::token; use crate::vm::WasmCacheAccess; @@ -53,7 +53,7 @@ where H: 'static + StorageHasher, CA: 'static + WasmCacheAccess, { - let balance_key = token::balance_key(&m1t(), &ADDRESS); + let balance_key = token::balance_key(&nam(), &ADDRESS); let min_funds_parameter_key = gov_storage::get_min_proposal_fund_key(); let min_funds_parameter: Option = read(ctx, &min_funds_parameter_key, ReadType::PRE).ok(); @@ -164,7 +164,7 @@ where CA: 'static + WasmCacheAccess, { let funds_key = gov_storage::get_funds_key(proposal_id); - let balance_key = token::balance_key(&m1t(), &ADDRESS); + let balance_key = token::balance_key(&nam(), &ADDRESS); let min_funds_parameter_key = gov_storage::get_min_proposal_fund_key(); let min_funds_parameter: Option = read(ctx, &min_funds_parameter_key, ReadType::PRE).ok(); diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index b5dec0293c..6ff04180bb 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -1463,7 +1463,7 @@ mod tests { source_port: get_port_id(), source_channel: get_channel_id(), token: Some(Coin { - denom: "XAN".to_string(), + denom: "NAM".to_string(), amount: 100u64.to_string(), }), sender: Signer::new("sender"), @@ -1722,7 +1722,7 @@ mod tests { source_port: get_port_id(), source_channel: get_channel_id(), token: Some(Coin { - denom: "XAN".to_string(), + denom: "NAM".to_string(), amount: 100u64.to_string(), }), sender: Signer::new("sender"), diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index 3b498727df..0b1617c7c3 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -26,9 +26,9 @@ pub const ADDRESS: Address = Address::Internal(InternalAddress::PoS); pub const SLASH_POOL_ADDRESS: Address = Address::Internal(InternalAddress::PosSlashPool); -/// Address of the staking token (XAN) +/// Address of the staking token (NAM) pub fn staking_token_address() -> Address { - address::xan() + address::nam() } /// Initialize storage in the genesis block. diff --git a/shared/src/ledger/slash_fund/mod.rs b/shared/src/ledger/slash_fund/mod.rs index 0d452af2e5..b944dc0bf2 100644 --- a/shared/src/ledger/slash_fund/mod.rs +++ b/shared/src/ledger/slash_fund/mod.rs @@ -12,7 +12,7 @@ use self::storage as slash_fund_storage; use super::governance::vp::is_proposal_accepted; use crate::ledger::native_vp::{self, Ctx, NativeVp}; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; -use crate::types::address::{xan as nam, Address, InternalAddress}; +use crate::types::address::{nam, Address, InternalAddress}; use crate::types::storage::Key; use crate::types::token; use crate::vm::WasmCacheAccess; diff --git a/shared/src/types/address.rs b/shared/src/types/address.rs index 6f047730fe..57a9f299cf 100644 --- a/shared/src/types/address.rs +++ b/shared/src/types/address.rs @@ -488,7 +488,7 @@ impl Display for InternalAddress { } /// Temporary helper for testing -pub fn xan() -> Address { +pub fn nam() -> Address { Address::decode("atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5").expect("The token address decoding shouldn't fail") } @@ -526,7 +526,7 @@ pub fn kartoffel() -> Address { /// informal currency codes. pub fn tokens() -> HashMap { vec![ - (xan(), "XAN"), + (nam(), "NAM"), (btc(), "BTC"), (eth(), "ETH"), (dot(), "DOT"), diff --git a/shared/src/types/transaction/mod.rs b/shared/src/types/transaction/mod.rs index a7d5ee864b..508e28db92 100644 --- a/shared/src/types/transaction/mod.rs +++ b/shared/src/types/transaction/mod.rs @@ -343,7 +343,7 @@ pub mod tx_types { #[cfg(test)] mod test_process_tx { use super::*; - use crate::types::address::xan; + use crate::types::address::nam; use crate::types::storage::Epoch; fn gen_keypair() -> common::SecretKey { @@ -427,7 +427,7 @@ pub mod tx_types { let wrapper = WrapperTx::new( Fee { amount: 10.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -462,7 +462,7 @@ pub mod tx_types { let wrapper = WrapperTx::new( Fee { amount: 10.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), diff --git a/shared/src/types/transaction/wrapper.rs b/shared/src/types/transaction/wrapper.rs index ad27b807aa..e0e6dab6be 100644 --- a/shared/src/types/transaction/wrapper.rs +++ b/shared/src/types/transaction/wrapper.rs @@ -337,7 +337,7 @@ pub mod wrapper_tx { mod test_wrapper_tx { use super::*; use crate::proto::SignedTxData; - use crate::types::address::xan; + use crate::types::address::nam; fn gen_keypair() -> common::SecretKey { use rand::prelude::ThreadRng; @@ -360,7 +360,7 @@ pub mod wrapper_tx { let wrapper = WrapperTx::new( Fee { amount: 10.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), @@ -386,7 +386,7 @@ pub mod wrapper_tx { let mut wrapper = WrapperTx::new( Fee { amount: 10.into(), - token: xan(), + token: nam(), }, &gen_keypair(), Epoch(0), @@ -418,7 +418,7 @@ pub mod wrapper_tx { let mut tx = WrapperTx::new( Fee { amount: 10.into(), - token: xan(), + token: nam(), }, &keypair, Epoch(0), diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index a998844764..d7c8a30026 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -95,7 +95,7 @@ fn test_node_connectivity() -> Result<()> { "--target", ALBERT, "--token", - XAN, + NAM, "--amount", "10.1", "--fee-amount", @@ -103,7 +103,7 @@ fn test_node_connectivity() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -129,7 +129,7 @@ fn test_node_connectivity() -> Result<()> { "--owner", ALBERT, "--token", - XAN, + NAM, "--ledger-address", ledger_rpc, ] @@ -141,7 +141,7 @@ fn test_node_connectivity() -> Result<()> { for ledger_rpc in &[validator_0_rpc, validator_1_rpc, non_validator_rpc] { let mut client = run!(test, Bin::Client, query_balance_args(ledger_rpc), Some(40))?; - client.exp_string("XAN: 1000010.1")?; + client.exp_string("NAM: 1000010.1")?; client.assert_success(); } @@ -283,7 +283,7 @@ fn ledger_txs_and_queries() -> Result<()> { "--target", ALBERT, "--token", - XAN, + NAM, "--amount", "10.1", "--fee-amount", @@ -291,7 +291,7 @@ fn ledger_txs_and_queries() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ], @@ -308,7 +308,7 @@ fn ledger_txs_and_queries() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ], @@ -326,7 +326,7 @@ fn ledger_txs_and_queries() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc ], @@ -347,7 +347,7 @@ fn ledger_txs_and_queries() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ], @@ -379,12 +379,12 @@ fn ledger_txs_and_queries() -> Result<()> { "--owner", BERTHA, "--token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ], // expect a decimal - r"XAN: \d+(\.\d+)?", + r"NAM: \d+(\.\d+)?", ), ]; for (query_args, expected) in &query_args_and_expected_response { @@ -396,8 +396,8 @@ fn ledger_txs_and_queries() -> Result<()> { let christel = find_address(&test, CHRISTEL)?; // as setup in `genesis/e2e-tests-single-node.toml` let christel_balance = token::Amount::whole(1000000); - let xan = find_address(&test, XAN)?; - let storage_key = token::balance_key(&xan, &christel).to_string(); + let nam = find_address(&test, NAM)?; + let storage_key = token::balance_key(&nam, &christel).to_string(); let query_args_and_expected_response = vec![ // 7. Query storage key and get hex-encoded raw bytes ( @@ -447,7 +447,7 @@ fn invalid_transactions() -> Result<()> { let transfer = token::Transfer { source: find_address(&test, DAEWON)?, target: find_address(&test, ALBERT)?, - token: find_address(&test, XAN)?, + token: find_address(&test, NAM)?, sub_prefix: None, amount: token::Amount::whole(1), }; @@ -474,7 +474,7 @@ fn invalid_transactions() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -528,7 +528,7 @@ fn invalid_transactions() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, // Force to ignore client check that fails on the balance check of the // source address "--force", @@ -604,7 +604,7 @@ fn pos_bonds() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -627,7 +627,7 @@ fn pos_bonds() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -647,7 +647,7 @@ fn pos_bonds() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -670,7 +670,7 @@ fn pos_bonds() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -710,7 +710,7 @@ fn pos_bonds() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -731,7 +731,7 @@ fn pos_bonds() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -747,7 +747,7 @@ fn pos_bonds() -> Result<()> { /// 1. Run the ledger node with shorter epochs for faster progression /// 2. Initialize a new validator account /// 3. Submit a delegation to the new validator -/// 4. Transfer some XAN to the new validator +/// 4. Transfer some NAM to the new validator /// 5. Submit a self-bond for the new validator /// 6. Wait for the pipeline epoch /// 7. Check the new validator's voting power @@ -800,7 +800,7 @@ fn pos_init_validator() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -817,7 +817,7 @@ fn pos_init_validator() -> Result<()> { "--target", &new_validator_key, "--token", - XAN, + NAM, "--amount", "0.5", "--fee-amount", @@ -825,7 +825,7 @@ fn pos_init_validator() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -846,7 +846,7 @@ fn pos_init_validator() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -854,7 +854,7 @@ fn pos_init_validator() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // 4. Transfer some XAN to the new validator + // 4. Transfer some NAM to the new validator let tx_args = vec![ "transfer", "--source", @@ -862,7 +862,7 @@ fn pos_init_validator() -> Result<()> { "--target", new_validator, "--token", - XAN, + NAM, "--amount", "10999.5", "--fee-amount", @@ -870,7 +870,7 @@ fn pos_init_validator() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -890,7 +890,7 @@ fn pos_init_validator() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -958,7 +958,7 @@ fn ledger_many_txs_in_a_block() -> Result<()> { "--target", ALBERT, "--token", - XAN, + NAM, "--amount", "10.1", "--fee-amount", @@ -966,7 +966,7 @@ fn ledger_many_txs_in_a_block() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", ]); @@ -1075,7 +1075,7 @@ fn proposal_submission() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -1146,13 +1146,13 @@ fn proposal_submission() -> Result<()> { "--owner", ALBERT, "--token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("XAN: 999500")?; + client.exp_string("NAM: 999500")?; client.assert_success(); // 5. Query token balance governance @@ -1161,13 +1161,13 @@ fn proposal_submission() -> Result<()> { "--owner", GOVERNANCE_ADDRESS, "--token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("XAN: 500")?; + client.exp_string("NAM: 500")?; client.assert_success(); // 6. Submit an invalid proposal @@ -1248,13 +1248,13 @@ fn proposal_submission() -> Result<()> { "--owner", ALBERT, "--token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("XAN: 999500")?; + client.exp_string("NAM: 999500")?; client.assert_success(); // 9. Send a yay vote from a validator @@ -1353,13 +1353,13 @@ fn proposal_submission() -> Result<()> { "--owner", ALBERT, "--token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("XAN: 1000000")?; + client.exp_string("NAM: 1000000")?; client.assert_success(); // 13. Check if governance funds are 0 @@ -1368,13 +1368,13 @@ fn proposal_submission() -> Result<()> { "--owner", GOVERNANCE_ADDRESS, "--token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("XAN: 0")?; + client.exp_string("NAM: 0")?; client.assert_success(); // // 14. Query parameters @@ -1424,7 +1424,7 @@ fn proposal_offline() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -1859,7 +1859,7 @@ fn test_genesis_validators() -> Result<()> { "--target", validator_1_alias, "--token", - XAN, + NAM, "--amount", "10.1", "--fee-amount", @@ -1867,7 +1867,7 @@ fn test_genesis_validators() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; @@ -2003,7 +2003,7 @@ fn double_signing_gets_slashed() -> Result<()> { "--target", ALBERT, "--token", - XAN, + NAM, "--amount", "10.1", "--fee-amount", @@ -2011,7 +2011,7 @@ fn double_signing_gets_slashed() -> Result<()> { "--gas-limit", "0", "--fee-token", - XAN, + NAM, "--ledger-address", &validator_one_rpc, ]; diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index b20f14d54f..3d6ff5e951 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -766,7 +766,7 @@ pub mod constants { pub const GOVERNANCE_ADDRESS: &str = "governance"; // Fungible token addresses - pub const XAN: &str = "XAN"; + pub const NAM: &str = "NAM"; pub const BTC: &str = "BTC"; pub const ETH: &str = "ETH"; pub const DOT: &str = "DOT"; diff --git a/tx_prelude/src/governance.rs b/tx_prelude/src/governance.rs index 2dbab74a9e..bb0e6cb6f9 100644 --- a/tx_prelude/src/governance.rs +++ b/tx_prelude/src/governance.rs @@ -2,7 +2,7 @@ use namada::ledger::governance::storage; use namada::ledger::governance::vp::ADDRESS as governance_address; -use namada::types::address::xan as m1t; +use namada::types::address::nam; use namada::types::token::Amount; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, @@ -59,7 +59,7 @@ pub fn init_proposal(ctx: &mut Ctx, data: InitProposalData) -> TxResult { ctx, &data.author, &governance_address, - &m1t(), + &nam(), None, min_proposal_funds, ) diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 9582791565..35e2dd8fbc 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -131,7 +131,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); - let token = address::xan(); + let token = address::nam(); let amount = token::Amount::from(10_098_123); // Spawn the accounts to be able to modify their storage @@ -267,7 +267,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); - let token = address::xan(); + let token = address::nam(); let amount = token::Amount::from(amount); // Spawn the accounts to be able to modify their storage @@ -300,7 +300,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); - let token = address::xan(); + let token = address::nam(); let amount = token::Amount::from(amount); // Spawn the accounts to be able to modify their storage diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 256dc6bb17..078e528013 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -231,7 +231,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); - let token = address::xan(); + let token = address::nam(); let amount = token::Amount::from(10_098_123); // Spawn the accounts to be able to modify their storage @@ -275,7 +275,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); - let token = address::xan(); + let token = address::nam(); let amount = token::Amount::from(10_098_123); // Spawn the accounts to be able to modify their storage @@ -321,7 +321,7 @@ mod tests { let keypair = key::testing::keypair_1(); let public_key = keypair.ref_to(); let target = address::testing::established_address_2(); - let token = address::xan(); + let token = address::nam(); let amount = token::Amount::from(10_098_123); // Spawn the accounts to be able to modify their storage @@ -371,7 +371,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); - let token = address::xan(); + let token = address::nam(); let amount = token::Amount::from(10_098_123); // Spawn the accounts to be able to modify their storage From 77bfe4706a005dcacde2483fdfe183605c0d546f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 25 Oct 2022 11:21:41 +0200 Subject: [PATCH 25/47] changelog: add #632 --- .changelog/unreleased/miscellaneous/632-xan-to-nam.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/miscellaneous/632-xan-to-nam.md diff --git a/.changelog/unreleased/miscellaneous/632-xan-to-nam.md b/.changelog/unreleased/miscellaneous/632-xan-to-nam.md new file mode 100644 index 0000000000..8f9f2c155c --- /dev/null +++ b/.changelog/unreleased/miscellaneous/632-xan-to-nam.md @@ -0,0 +1,2 @@ +- Renamed native token from XAN to NAM + ([#632](https://github.com/anoma/namada/pull/632)) \ No newline at end of file From a699404009f1d43d9239ce633cd25e200eb99898 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Tue, 25 Oct 2022 13:05:08 +0200 Subject: [PATCH 26/47] ci: use mold linker --- .github/workflows/build-and-test-bridge.yml | 4 ++++ .github/workflows/build-and-test.yml | 4 ++++ .github/workflows/release.yml | 2 ++ 3 files changed, 10 insertions(+) diff --git a/.github/workflows/build-and-test-bridge.yml b/.github/workflows/build-and-test-bridge.yml index cee43b941b..5222feea5a 100644 --- a/.github/workflows/build-and-test-bridge.yml +++ b/.github/workflows/build-and-test-bridge.yml @@ -177,6 +177,8 @@ jobs: restore-keys: ${{ runner.os }}-${{ matrix.make.cache_key }}-${{ matrix.make.cache_version }}-cargo- - name: Start sccache server run: sccache --start-server + - name: Install mold linker + uses: rui314/setup-mold@v1 - name: Build run: make build${{ matrix.make.suffix }} - name: Build test @@ -316,6 +318,8 @@ jobs: restore-keys: ${{ runner.os }}-${{ matrix.make.cache_key }}-${{ matrix.make.cache_version }}-cargo- - name: Start sccache server run: sccache --start-server + - name: Install mold linker + uses: rui314/setup-mold@v1 - name: Build run: make build-release${{ matrix.make.suffix }} - name: Upload target binaries diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1c4cbd3412..f4f0e398d6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -179,6 +179,8 @@ jobs: restore-keys: ${{ runner.os }}-${{ matrix.make.cache_key }}-${{ matrix.make.cache_version }}-cargo- - name: Start sccache server run: sccache --start-server + - name: Install mold linker + uses: rui314/setup-mold@v1 - name: Build run: make build${{ matrix.make.suffix }} - name: Build test @@ -316,6 +318,8 @@ jobs: ~/.cargo/git key: ${{ runner.os }}-${{ matrix.make.cache_key }}-${{ matrix.make.cache_version }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-${{ matrix.make.cache_key }}-${{ matrix.make.cache_version }}-cargo- + - name: Install mold linker + uses: rui314/setup-mold@v1 - name: Start sccache server run: sccache --start-server - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cbc1e9207..e65b1fe459 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,6 +74,8 @@ jobs: restore-keys: ${{ runner.os }}-anoma-release-${{ matrix.anoma_cache_version }} - name: Start sccache server run: sccache --start-server + - name: Install mold linker + uses: rui314/setup-mold@v1 - name: ${{ matrix.make.name }} run: make ${{ matrix.make.command }} - name: Upload binaries package From 95f62941ef3b0089ea58e2ce4ba701fba240259c Mon Sep 17 00:00:00 2001 From: James Hiew Date: Tue, 25 Oct 2022 19:00:26 +0100 Subject: [PATCH 27/47] fix: namadan should log at info by default --- apps/src/bin/anoma-node/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/src/bin/anoma-node/main.rs b/apps/src/bin/anoma-node/main.rs index 49cbd886c8..375471ab7f 100644 --- a/apps/src/bin/anoma-node/main.rs +++ b/apps/src/bin/anoma-node/main.rs @@ -11,7 +11,7 @@ fn main() -> Result<()> { color_eyre::install()?; // init logging - let default_directive = Directive::from_str("anoma=info")?; + let default_directive = Directive::from_str("namada=info")?; logging::init_from_env_or(default_directive)?; // run the CLI From 3b0117e6e957f18d229e673b63355940a2baad89 Mon Sep 17 00:00:00 2001 From: brentstone Date: Tue, 25 Oct 2022 14:08:33 -0400 Subject: [PATCH 28/47] some doc comment edits --- shared/src/ledger/queries/router.rs | 4 ++-- shared/src/ledger/queries/types.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index 69888cb935..da4cbbb900 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -15,7 +15,7 @@ pub enum Error { WrongPath(String), } -/// Find the index of a next slash after the given `start` index in the path. +/// Find the index of a next forward slash after the given `start` index in the path. /// When there are no more slashes, returns the index after the end of the path. /// /// # Panics @@ -646,7 +646,7 @@ mod test_rpc_handlers { use crate::types::storage::Epoch; use crate::types::token; - /// A little macro to generate boilerplate fo RPC handler functions. + /// A little macro to generate boilerplate for RPC handler functions. /// These are implemented to return their name as a String, joined by /// slashes with their argument values turned `to_string()`, if any. macro_rules! handlers { diff --git a/shared/src/ledger/queries/types.rs b/shared/src/ledger/queries/types.rs index 00cff84ed9..c7b349ddc0 100644 --- a/shared/src/ledger/queries/types.rs +++ b/shared/src/ledger/queries/types.rs @@ -27,7 +27,7 @@ where } /// A `Router` handles parsing read-only query requests and dispatching them to -/// their handler functions. A valid query returns a borsh-encoded result. +/// their handler functions. A valid query returns a borsh-encoded result. pub trait Router { /// Handle a given request using the provided context. This must be invoked /// on the root `Router` to be able to match the `request.path` fully. From 18895717e48ba108bfb9e8fdecb5152dba9dca67 Mon Sep 17 00:00:00 2001 From: Awa Sun Yin <11296013+awasunyin@users.noreply.github.com> Date: Wed, 26 Oct 2022 11:46:22 +0200 Subject: [PATCH 29/47] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6888f89e87..b96cb34229 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ ## Overview -[Namada](http://namada.net) is a sovereign proof-of-stake blockchain, using Tendermint BFT -consensus, that enables multi-asset shielded transfers for any native +[Namada](http://namada.net) is a Proof-of-Stake L1 for interchain asset-agnostic privacy. Namada uses Tendermint BFT +consensus and enables multi-asset shielded transfers for any native or non-native asset. Namada features full IBC protocol support, a natively integrated Ethereum bridge, a modern proof-of-stake system with automatic reward compounding and cubic slashing, and a From 33d31167bf6407b7c5f1bcf161b4947d3b9d2e9b Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Wed, 26 Oct 2022 14:03:59 +0200 Subject: [PATCH 30/47] ci: fix mold usage --- .github/workflows/build-and-test-bridge.yml | 21 ++++++++++++++++++--- .github/workflows/build-and-test.yml | 19 +++++++++++++++++-- .github/workflows/release.yml | 2 -- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test-bridge.yml b/.github/workflows/build-and-test-bridge.yml index 5a6d574184..15bcfdea1d 100644 --- a/.github/workflows/build-and-test-bridge.yml +++ b/.github/workflows/build-and-test-bridge.yml @@ -109,6 +109,7 @@ jobs: matrix: os: [ubuntu-latest] nightly_version: [nightly-2022-05-20] + mold_version: [1.6.0] make: - name: ABCI suffix: '' @@ -178,11 +179,17 @@ jobs: - name: Start sccache server run: sccache --start-server - name: Install mold linker - uses: rui314/setup-mold@v1 + run: | + wget -q -O- https://github.com/rui314/mold/releases/download/v${{ matrix.mold_version }}/mold-${{ matrix.mold_version }}-x86_64-linux.tar.gz | tar -xz + mv mold-${{ matrix.mold_version }}-x86_64-linux/bin/mold /usr/local/bin - name: Build run: make build${{ matrix.make.suffix }} + env: + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Build test run: make build-test${{ matrix.make.suffix }} + env: + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Download wasm artifacts uses: actions/download-artifact@v3 with: @@ -190,6 +197,8 @@ jobs: path: ./wasm - name: Run unit test run: make test-unit${{ matrix.make.suffix }} + env: + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Wait for release binaries uses: lewagon/wait-on-check-action@master with: @@ -234,6 +243,7 @@ jobs: ANOMA_LOG_COLOR: "false" ANOMA_MASP_PARAMS_DIR: "/home/runner/work/masp" ANOMA_LOG: "info" + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Upload e2e logs if: success() || failure() uses: actions/upload-artifact@v3 @@ -242,7 +252,7 @@ jobs: path: | /tmp/.*/logs/ /tmp/.*/e2e-test.*/setup/validator-*/.anoma/logs/*.log - retention-days: 5 + retention-days: 3 - name: Print sccache stats if: always() run: sccache --show-stats @@ -257,6 +267,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] + mold_version: [1.6.0] make: - name: ABCI Release build suffix: '' @@ -319,9 +330,13 @@ jobs: - name: Start sccache server run: sccache --start-server - name: Install mold linker - uses: rui314/setup-mold@v1 + run: | + wget -q -O- https://github.com/rui314/mold/releases/download/v${{ matrix.mold_version }}/mold-${{ matrix.mold_version }}-x86_64-linux.tar.gz | tar -xz + mv mold-${{ matrix.mold_version }}-x86_64-linux/bin/mold /usr/local/bin - name: Build run: make build-release${{ matrix.make.suffix }} + env: + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Upload target binaries uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a6dfebde45..5396e17915 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -111,6 +111,7 @@ jobs: matrix: os: [ubuntu-latest] nightly_version: [nightly-2022-05-20] + mold_version: [1.6.0] make: - name: ABCI suffix: '' @@ -180,11 +181,17 @@ jobs: - name: Start sccache server run: sccache --start-server - name: Install mold linker - uses: rui314/setup-mold@v1 + run: | + wget -q -O- https://github.com/rui314/mold/releases/download/v${{ matrix.mold_version }}/mold-${{ matrix.mold_version }}-x86_64-linux.tar.gz | tar -xz + mv mold-${{ matrix.mold_version }}-x86_64-linux/bin/mold /usr/local/bin - name: Build run: make build${{ matrix.make.suffix }} + env: + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Build test run: make build-test${{ matrix.make.suffix }} + env: + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Download wasm artifacts uses: actions/download-artifact@v3 with: @@ -192,6 +199,8 @@ jobs: path: ./wasm - name: Run unit test run: make test-unit${{ matrix.make.suffix }} + env: + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Wait for release binaries uses: lewagon/wait-on-check-action@master with: @@ -236,6 +245,7 @@ jobs: ANOMA_LOG_COLOR: "false" ANOMA_MASP_PARAMS_DIR: "/home/runner/work/masp" ANOMA_LOG: "info" + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Upload e2e logs if: success() || failure() uses: actions/upload-artifact@v3 @@ -259,6 +269,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] + mold_version: [1.6.0] make: - name: ABCI Release build suffix: '' @@ -319,11 +330,15 @@ jobs: key: ${{ runner.os }}-${{ matrix.make.cache_key }}-${{ matrix.make.cache_version }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-${{ matrix.make.cache_key }}-${{ matrix.make.cache_version }}-cargo- - name: Install mold linker - uses: rui314/setup-mold@v1 + run: | + wget -q -O- https://github.com/rui314/mold/releases/download/v${{ matrix.mold_version }}/mold-${{ matrix.mold_version }}-x86_64-linux.tar.gz | tar -xz + mv mold-${{ matrix.mold_version }}-x86_64-linux/bin/mold /usr/local/bin - name: Start sccache server run: sccache --start-server - name: Build run: make build-release${{ matrix.make.suffix }} + env: + RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Upload target binaries uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e65b1fe459..5cbc1e9207 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,8 +74,6 @@ jobs: restore-keys: ${{ runner.os }}-anoma-release-${{ matrix.anoma_cache_version }} - name: Start sccache server run: sccache --start-server - - name: Install mold linker - uses: rui314/setup-mold@v1 - name: ${{ matrix.make.name }} run: make ${{ matrix.make.command }} - name: Upload binaries package From 10a1a6d071edddeed96da91ba3d99808d0851966 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli <> Date: Wed, 26 Oct 2022 15:15:00 +0200 Subject: [PATCH 31/47] ci: fix workflow name --- .github/workflows/build-and-test-bridge.yml | 4 ++-- .github/workflows/build-and-test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test-bridge.yml b/.github/workflows/build-and-test-bridge.yml index 15bcfdea1d..0ed0437ce4 100644 --- a/.github/workflows/build-and-test-bridge.yml +++ b/.github/workflows/build-and-test-bridge.yml @@ -115,7 +115,7 @@ jobs: suffix: '' cache_key: anoma cache_version: v1 - wait_for: anoma-release-eth (ubuntu-latest, ABCI Release build, anoma-e2e-release, v1) + wait_for: anoma-release-eth (ubuntu-latest, 1.6.0, ABCI Release build, anoma-e2e-release, v1) tendermint_artifact: tendermint-unreleased-ad825dcadbd4b98c3f91ce5a711e4fb36a69c377 env: @@ -200,7 +200,7 @@ jobs: env: RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Wait for release binaries - uses: lewagon/wait-on-check-action@master + uses: lewagon/wait-on-check-action@@v1.2.0 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} check-name: ${{ matrix.make.wait_for }} diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5396e17915..6ee13b06f3 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -117,7 +117,7 @@ jobs: suffix: '' cache_key: anoma cache_version: v1 - wait_for: anoma-release (ubuntu-latest, ABCI Release build, anoma-e2e-release, v1) + wait_for: anoma-release (ubuntu-latest, 1.6.0, ABCI Release build, anoma-e2e-release, v1) tendermint_artifact: tendermint-unreleased-ad825dcadbd4b98c3f91ce5a711e4fb36a69c377 env: @@ -202,7 +202,7 @@ jobs: env: RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold" - name: Wait for release binaries - uses: lewagon/wait-on-check-action@master + uses: lewagon/wait-on-check-action@v1.2.0 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} check-name: ${{ matrix.make.wait_for }} From c1d8f04549bee772d43549abfcf0ffa162294e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 27 Oct 2022 11:37:30 +0200 Subject: [PATCH 32/47] ledger/queries: fix require_no_proof doc-string --- shared/src/ledger/queries/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index 0c66faca1e..bf3d490b5c 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -75,8 +75,8 @@ where Ok(()) } -/// For queries that only support latest height, check that the given height is -/// not different from latest height, otherwise return an error. +/// For queries that do not support proofs, check that proof is not requested, +/// otherwise return an error. pub fn require_no_proof(request: &RequestQuery) -> storage_api::Result<()> { if request.prove { return Err(storage_api::Error::new_const( From ddc62be79c685552075883d387887012d775577e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 27 Oct 2022 11:37:52 +0200 Subject: [PATCH 33/47] ledger/queries: comment out `println`s for router path matching dbg --- shared/src/ledger/queries/router.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index da4cbbb900..257cf61ad2 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -57,7 +57,7 @@ macro_rules! handle_match { // ignore trailing slashes $end == $request.path.len() - 1 && &$request.path[$end..] == "/") { // we're not at the end, no match - println!("Not fully matched"); + // println!("Not fully matched"); break } // If you get a compile error from here with `expected function, found @@ -186,12 +186,12 @@ macro_rules! try_match_segments { $end = $request.path.len(); match $request.path[$start..$end].parse::<$arg_ty>() { Ok(parsed) => { - println!("Parsed {}", parsed); + // println!("Parsed {}", parsed); $arg = parsed }, Err(_) => { - println!("Cannot parse {} from {}", stringify!($arg_ty), &$request.path[$start..$end]); + // println!("Cannot parse {} from {}", stringify!($arg_ty), &$request.path[$start..$end]); // If arg cannot be parsed, try to skip to next pattern break } @@ -218,7 +218,7 @@ macro_rules! try_match_segments { }, Err(_) => { - println!("Cannot parse {} from {}", stringify!($arg_ty), &$request.path[$start..$end]); + // println!("Cannot parse {} from {}", stringify!($arg_ty), &$request.path[$start..$end]); // If arg cannot be parsed, try to skip to next pattern break } @@ -244,10 +244,10 @@ macro_rules! try_match_segments { ) => { if &$request.path[$start..$end] == $expected { // Advanced index past the matched arg - println!("Matched literal {}", $expected); + // println!("Matched literal {}", $expected); $start = $end; } else { - println!("{} doesn't match literal {}", &$request.path[$start..$end], $expected); + // println!("{} doesn't match literal {}", &$request.path[$start..$end], $expected); // Try to skip to next pattern break; } @@ -269,14 +269,14 @@ macro_rules! try_match { ($ctx:ident, $request:ident, $start:ident, $handle:tt, $segments:tt) => { // check that the initial char is '/' if $request.path.is_empty() || &$request.path[..1] != "/" { - println!("Missing initial slash"); + // println!("Missing initial slash"); break; } // advance past initial '/' $start += 1; // Path is too short to match if $start >= $request.path.len() { - println!("Path is too short"); + // println!("Path is too short"); break; } let mut end = find_next_slash_index(&$request.path, $start); From aed037bcad5b731e6197a1ad108b5b61cfee7a85 Mon Sep 17 00:00:00 2001 From: James Hiew Date: Thu, 27 Oct 2022 15:31:58 +0100 Subject: [PATCH 34/47] Add changelog --- .changelog/unreleased/bug-fixes/702-fix-default-node-logging.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/702-fix-default-node-logging.md diff --git a/.changelog/unreleased/bug-fixes/702-fix-default-node-logging.md b/.changelog/unreleased/bug-fixes/702-fix-default-node-logging.md new file mode 100644 index 0000000000..146815a45d --- /dev/null +++ b/.changelog/unreleased/bug-fixes/702-fix-default-node-logging.md @@ -0,0 +1,2 @@ +- Fix info logs to show by default for namadan + ([#702](https://github.com/anoma/namada/pull/702)) \ No newline at end of file From d7071667da959f9d2c3957ecb8819aeb1200e37b Mon Sep 17 00:00:00 2001 From: James Hiew Date: Fri, 28 Oct 2022 16:27:17 +0100 Subject: [PATCH 35/47] Log at INFO by default for namadan --- apps/src/bin/anoma-node/main.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/src/bin/anoma-node/main.rs b/apps/src/bin/anoma-node/main.rs index 375471ab7f..b37feb3cdb 100644 --- a/apps/src/bin/anoma-node/main.rs +++ b/apps/src/bin/anoma-node/main.rs @@ -1,18 +1,15 @@ mod cli; -use std::str::FromStr; - use color_eyre::eyre::Result; use namada_apps::logging; -use tracing_subscriber::filter::Directive; +use tracing_subscriber::filter::LevelFilter; fn main() -> Result<()> { // init error reporting color_eyre::install()?; // init logging - let default_directive = Directive::from_str("namada=info")?; - logging::init_from_env_or(default_directive)?; + logging::init_from_env_or(LevelFilter::INFO)?; // run the CLI cli::main() From a16180f9b9744f4d778a57fc8444f54682062a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 28 Oct 2022 18:55:48 +0200 Subject: [PATCH 36/47] make fmt --- shared/src/ledger/queries/router.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index 257cf61ad2..317c532cb7 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -15,8 +15,9 @@ pub enum Error { WrongPath(String), } -/// Find the index of a next forward slash after the given `start` index in the path. -/// When there are no more slashes, returns the index after the end of the path. +/// Find the index of a next forward slash after the given `start` index in the +/// path. When there are no more slashes, returns the index after the end of the +/// path. /// /// # Panics /// The given `start` must be < `path.len()`. From e71c7752878d88fedb6567404bf1eb43c7d88ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 7 Oct 2022 16:04:16 +0200 Subject: [PATCH 37/47] move the current RPC patterns under "shell" sub-router --- apps/src/lib/client/rpc.rs | 15 +- shared/src/ledger/queries/mod.rs | 309 +------------------------- shared/src/ledger/queries/router.rs | 4 +- shared/src/ledger/queries/shell.rs | 332 ++++++++++++++++++++++++++++ 4 files changed, 348 insertions(+), 312 deletions(-) create mode 100644 shared/src/ledger/queries/shell.rs diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 488f20f8f9..26f60313f0 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -46,7 +46,7 @@ use crate::facade::tendermint_rpc::{ /// Query the epoch of the last committed block pub async fn query_epoch(args: args::Query) -> Epoch { let client = HttpClient::new(args.ledger_address).unwrap(); - let epoch = unwrap_client_response(RPC.epoch(&client).await); + let epoch = unwrap_client_response(RPC.shell().epoch(&client).await); println!("Last committed epoch: {}", epoch); epoch } @@ -55,7 +55,7 @@ pub async fn query_epoch(args: args::Query) -> Epoch { pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { let client = HttpClient::new(args.query.ledger_address).unwrap(); let bytes = unwrap_client_response( - RPC.storage_value(&client, &args.storage_key).await, + RPC.shell().storage_value(&client, &args.storage_key).await, ); match bytes { Some(bytes) => println!("Found data: 0x{}", HEXLOWER.encode(&bytes)), @@ -1032,7 +1032,8 @@ pub async fn dry_run_tx(ledger_address: &TendermintAddress, tx_bytes: Vec) { let client = HttpClient::new(ledger_address.clone()).unwrap(); let (data, height, prove) = (Some(tx_bytes), None, false); let result = unwrap_client_response( - RPC.dry_run_tx_with_options(&client, data, height, prove) + RPC.shell() + .dry_run_tx_with_options(&client, data, height, prove) .await, ) .data; @@ -1248,7 +1249,8 @@ pub async fn query_storage_value( where T: BorshDeserialize, { - let bytes = unwrap_client_response(RPC.storage_value(client, key).await); + let bytes = + unwrap_client_response(RPC.shell().storage_value(client, key).await); bytes.map(|bytes| { T::try_from_slice(&bytes[..]).unwrap_or_else(|err| { eprintln!("Error decoding the value: {}", err); @@ -1267,7 +1269,8 @@ pub async fn query_storage_prefix( where T: BorshDeserialize, { - let values = unwrap_client_response(RPC.storage_prefix(client, key).await); + let values = + unwrap_client_response(RPC.shell().storage_prefix(client, key).await); let decode = |PrefixValue { key, value }: PrefixValue| match T::try_from_slice( &value[..], @@ -1293,7 +1296,7 @@ pub async fn query_has_storage_key( client: &HttpClient, key: &storage::Key, ) -> bool { - unwrap_client_response(RPC.storage_has_key(client, key).await) + unwrap_client_response(RPC.shell().storage_has_key(client, key).await) } /// Represents a query for an event pertaining to the specified transaction diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index bf3d490b5c..4d160dd9e0 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -1,7 +1,7 @@ //! Ledger read-only queries can be handled and dispatched via the [`RPC`] //! defined via `router!` macro. -use tendermint_proto::crypto::{ProofOp, ProofOps}; +use shell::{Shell, SHELL}; #[cfg(any(test, feature = "async-client"))] pub use types::Client; pub use types::{ @@ -9,35 +9,17 @@ pub use types::{ }; use super::storage::{DBIter, StorageHasher, DB}; -use super::storage_api::{self, ResultExt, StorageRead}; -use crate::types::storage::{self, Epoch, PrefixValue}; -use crate::types::transaction::TxResult; -#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] -use crate::types::transaction::{DecryptedTx, TxType}; +use super::storage_api; #[macro_use] mod router; +mod shell; mod types; // Most commonly expected patterns should be declared first router! {RPC, - // Epoch of the last committed block - ( "epoch" ) -> Epoch = epoch, - - // Raw storage access - read value - ( "value" / [storage_key: storage::Key] ) - -> Option> = storage_value, - - // Dry run a transaction - ( "dry_run_tx" ) -> TxResult = dry_run_tx, - - // Raw storage access - prefix iterator - ( "prefix" / [storage_key: storage::Key] ) - -> Vec = storage_prefix, - - // Raw storage access - is given storage key present? - ( "has_key" / [storage_key: storage::Key] ) - -> bool = storage_has_key, + // Shell provides storage read access, block metadata and can dry-run a tx + ( "shell" ) = (sub SHELL), } /// Handle RPC query request in the ledger. On success, returns response with @@ -86,188 +68,6 @@ pub fn require_no_proof(request: &RequestQuery) -> storage_api::Result<()> { Ok(()) } -// Handlers: - -#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] -fn dry_run_tx( - mut ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, -) -> storage_api::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - use super::gas::BlockGasMeter; - use super::storage::write_log::WriteLog; - use crate::proto::Tx; - - let mut gas_meter = BlockGasMeter::default(); - let mut write_log = WriteLog::default(); - let tx = Tx::try_from(&request.data[..]).into_storage_result()?; - let tx = TxType::Decrypted(DecryptedTx::Decrypted(tx)); - let data = super::protocol::apply_tx( - tx, - request.data.len(), - &mut gas_meter, - &mut write_log, - ctx.storage, - &mut ctx.vp_wasm_cache, - &mut ctx.tx_wasm_cache, - ) - .into_storage_result()?; - Ok(ResponseQuery { - data, - ..ResponseQuery::default() - }) -} - -#[cfg(not(all(feature = "wasm-runtime", feature = "ferveo-tpke")))] -fn dry_run_tx( - _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, -) -> storage_api::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - unimplemented!( - "dry_run_tx request handler requires \"wasm-runtime\" and \ - \"ferveo-tpke\" features enabled." - ) -} - -fn epoch( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, -) -> storage_api::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - require_latest_height(&ctx, request)?; - require_no_proof(request)?; - - let data = ctx.storage.last_epoch; - Ok(ResponseQuery { - data, - ..Default::default() - }) -} - -fn storage_value( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, - storage_key: storage::Key, -) -> storage_api::Result>>> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - match ctx - .storage - .read_with_height(&storage_key, request.height) - .into_storage_result()? - { - (Some(value), _gas) => { - let proof = if request.prove { - let proof = ctx - .storage - .get_existence_proof( - &storage_key, - value.clone().into(), - request.height, - ) - .into_storage_result()?; - Some(proof.into()) - } else { - None - }; - Ok(ResponseQuery { - data: Some(value), - proof_ops: proof, - ..Default::default() - }) - } - (None, _gas) => { - let proof = if request.prove { - let proof = ctx - .storage - .get_non_existence_proof(&storage_key, request.height) - .into_storage_result()?; - Some(proof.into()) - } else { - None - }; - Ok(ResponseQuery { - data: None, - proof_ops: proof, - info: format!("No value found for key: {}", storage_key), - }) - } - } -} - -fn storage_prefix( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, - storage_key: storage::Key, -) -> storage_api::Result>> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - require_latest_height(&ctx, request)?; - - let (iter, _gas) = ctx.storage.iter_prefix(&storage_key); - let data: storage_api::Result> = iter - .map(|(key, value, _gas)| { - let key = storage::Key::parse(key).into_storage_result()?; - Ok(PrefixValue { key, value }) - }) - .collect(); - let data = data?; - let proof_ops = if request.prove { - let mut ops = vec![]; - for PrefixValue { key, value } in &data { - let proof = ctx - .storage - .get_existence_proof(key, value.clone().into(), request.height) - .into_storage_result()?; - let mut cur_ops: Vec = - proof.ops.into_iter().map(|op| op.into()).collect(); - ops.append(&mut cur_ops); - } - // ops is not empty in this case - Some(ProofOps { ops }) - } else { - None - }; - Ok(ResponseQuery { - data, - proof_ops, - ..Default::default() - }) -} - -fn storage_has_key( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, - storage_key: storage::Key, -) -> storage_api::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - require_latest_height(&ctx, request)?; - require_no_proof(request)?; - - let data = StorageRead::has_key(ctx.storage, &storage_key)?; - Ok(ResponseQuery { - data, - ..Default::default() - }) -} - #[cfg(any(test, feature = "tendermint-rpc"))] /// Provides [`Client`] implementation for Tendermint RPC client pub mod tm { @@ -418,102 +218,3 @@ mod testing { } } } - -#[cfg(test)] -mod test { - use borsh::BorshDeserialize; - - use super::testing::TestClient; - use super::*; - use crate::ledger::storage_api::StorageWrite; - use crate::proto::Tx; - use crate::types::{address, token}; - - const TX_NO_OP_WASM: &str = "../wasm_for_tests/tx_no_op.wasm"; - - #[test] - fn test_queries_router_paths() { - let path = RPC.epoch_path(); - assert_eq!("/epoch", path); - - let token_addr = address::testing::established_address_1(); - let owner = address::testing::established_address_2(); - let key = token::balance_key(&token_addr, &owner); - let path = RPC.storage_value_path(&key); - assert_eq!(format!("/value/{}", key), path); - - let path = RPC.dry_run_tx_path(); - assert_eq!("/dry_run_tx", path); - - let path = RPC.storage_prefix_path(&key); - assert_eq!(format!("/prefix/{}", key), path); - - let path = RPC.storage_has_key_path(&key); - assert_eq!(format!("/has_key/{}", key), path); - } - - #[tokio::test] - async fn test_queries_router_with_client() -> storage_api::Result<()> { - // Initialize the `TestClient` - let mut client = TestClient::new(RPC); - - // Request last committed epoch - let read_epoch = RPC.epoch(&client).await.unwrap(); - let current_epoch = client.storage.last_epoch; - assert_eq!(current_epoch, read_epoch); - - // Request dry run tx - let tx_no_op = std::fs::read(TX_NO_OP_WASM).expect("cannot load wasm"); - let tx = Tx::new(tx_no_op, None); - let tx_bytes = tx.to_bytes(); - let result = RPC - .dry_run_tx_with_options(&client, Some(tx_bytes), None, false) - .await - .unwrap(); - assert!(result.data.is_accepted()); - - // Request storage value for a balance key ... - let token_addr = address::testing::established_address_1(); - let owner = address::testing::established_address_2(); - let balance_key = token::balance_key(&token_addr, &owner); - // ... there should be no value yet. - let read_balance = - RPC.storage_value(&client, &balance_key).await.unwrap(); - assert!(read_balance.is_none()); - - // Request storage prefix iterator - let balance_prefix = token::balance_prefix(&token_addr); - let read_balances = - RPC.storage_prefix(&client, &balance_prefix).await.unwrap(); - assert!(read_balances.is_empty()); - - // Request storage has key - let has_balance_key = - RPC.storage_has_key(&client, &balance_key).await.unwrap(); - assert!(!has_balance_key); - - // Then write some balance ... - let balance = token::Amount::from(1000); - StorageWrite::write(&mut client.storage, &balance_key, balance)?; - // ... there should be the same value now - let read_balance = - RPC.storage_value(&client, &balance_key).await.unwrap(); - assert_eq!( - balance, - token::Amount::try_from_slice(&read_balance.unwrap()).unwrap() - ); - - // Request storage prefix iterator - let balance_prefix = token::balance_prefix(&token_addr); - let read_balances = - RPC.storage_prefix(&client, &balance_prefix).await.unwrap(); - assert_eq!(read_balances.len(), 1); - - // Request storage has key - let has_balance_key = - RPC.storage_has_key(&client, &balance_key).await.unwrap(); - assert!(has_balance_key); - - Ok(()) - } -} diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index 317c532cb7..332cad3f93 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -495,7 +495,7 @@ macro_rules! router_type { impl $name { #[doc = "Construct this router as a root router"] - const fn new() -> Self { + pub const fn new() -> Self { Self { prefix: String::new(), } @@ -503,7 +503,7 @@ macro_rules! router_type { #[allow(dead_code)] #[doc = "Construct this router as a sub-router at the given prefix path"] - const fn sub(prefix: String) -> Self { + pub const fn sub(prefix: String) -> Self { Self { prefix, } diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs new file mode 100644 index 0000000000..2304a0421e --- /dev/null +++ b/shared/src/ledger/queries/shell.rs @@ -0,0 +1,332 @@ +use tendermint_proto::crypto::{ProofOp, ProofOps}; + +use crate::ledger::queries::types::{RequestCtx, RequestQuery, ResponseQuery}; +use crate::ledger::queries::{require_latest_height, require_no_proof}; +use crate::ledger::storage::{DBIter, StorageHasher, DB}; +use crate::ledger::storage_api::{self, ResultExt, StorageRead}; +use crate::types::storage::{self, Epoch, PrefixValue}; +use crate::types::transaction::TxResult; +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] +use crate::types::transaction::{DecryptedTx, TxType}; + +router! {SHELL, + // Epoch of the last committed block + ( "epoch" ) -> Epoch = epoch, + + // Raw storage access - read value + ( "value" / [storage_key: storage::Key] ) + -> Option> = storage_value, + + // Dry run a transaction + ( "dry_run_tx" ) -> TxResult = dry_run_tx, + + // Raw storage access - prefix iterator + ( "prefix" / [storage_key: storage::Key] ) + -> Vec = storage_prefix, + + // Raw storage access - is given storage key present? + ( "has_key" / [storage_key: storage::Key] ) + -> bool = storage_has_key, +} + +// Handlers: + +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] +fn dry_run_tx( + mut ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + use crate::ledger::gas::BlockGasMeter; + use crate::ledger::protocol; + use crate::ledger::storage::write_log::WriteLog; + use crate::proto::Tx; + + let mut gas_meter = BlockGasMeter::default(); + let mut write_log = WriteLog::default(); + let tx = Tx::try_from(&request.data[..]).into_storage_result()?; + let tx = TxType::Decrypted(DecryptedTx::Decrypted(tx)); + let data = protocol::apply_tx( + tx, + request.data.len(), + &mut gas_meter, + &mut write_log, + ctx.storage, + &mut ctx.vp_wasm_cache, + &mut ctx.tx_wasm_cache, + ) + .into_storage_result()?; + Ok(ResponseQuery { + data, + ..ResponseQuery::default() + }) +} + +#[cfg(not(all(feature = "wasm-runtime", feature = "ferveo-tpke")))] +fn dry_run_tx( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + unimplemented!( + "dry_run_tx request handler requires \"wasm-runtime\" and \ + \"ferveo-tpke\" features enabled." + ) +} + +fn epoch( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + require_latest_height(&ctx, request)?; + require_no_proof(request)?; + + let data = ctx.storage.last_epoch; + Ok(ResponseQuery { + data, + ..Default::default() + }) +} + +fn storage_value( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> storage_api::Result>>> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + match ctx + .storage + .read_with_height(&storage_key, request.height) + .into_storage_result()? + { + (Some(data), _gas) => { + let proof = if request.prove { + let proof = ctx + .storage + .get_existence_proof( + &storage_key, + data.clone(), + request.height, + ) + .into_storage_result()?; + Some(proof.into()) + } else { + None + }; + Ok(ResponseQuery { + data: Some(data), + proof_ops: proof, + ..Default::default() + }) + } + (None, _gas) => { + let proof = if request.prove { + let proof = ctx + .storage + .get_non_existence_proof(&storage_key, request.height) + .into_storage_result()?; + Some(proof.into()) + } else { + None + }; + Ok(ResponseQuery { + data: None, + proof_ops: proof, + info: format!("No value found for key: {}", storage_key), + }) + } + } +} + +fn storage_prefix( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> storage_api::Result>> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + require_latest_height(&ctx, request)?; + + let (iter, _gas) = ctx.storage.iter_prefix(&storage_key); + let data: storage_api::Result> = iter + .map(|(key, value, _gas)| { + let key = storage::Key::parse(key).into_storage_result()?; + Ok(PrefixValue { key, value }) + }) + .collect(); + let data = data?; + let proof_ops = if request.prove { + let mut ops = vec![]; + for PrefixValue { key, value } in &data { + let proof = ctx + .storage + .get_existence_proof(key, value.clone(), request.height) + .into_storage_result()?; + let mut cur_ops: Vec = + proof.ops.into_iter().map(|op| op.into()).collect(); + ops.append(&mut cur_ops); + } + // ops is not empty in this case + Some(ProofOps { ops }) + } else { + None + }; + Ok(ResponseQuery { + data, + proof_ops, + ..Default::default() + }) +} + +fn storage_has_key( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + require_latest_height(&ctx, request)?; + require_no_proof(request)?; + + let data = StorageRead::has_key(ctx.storage, &storage_key)?; + Ok(ResponseQuery { + data, + ..Default::default() + }) +} + +#[cfg(test)] +mod test { + use borsh::BorshDeserialize; + + use crate::ledger::queries::testing::TestClient; + use crate::ledger::queries::RPC; + use crate::ledger::storage_api::{self, StorageWrite}; + use crate::proto::Tx; + use crate::types::{address, token}; + + const TX_NO_OP_WASM: &str = "../wasm_for_tests/tx_no_op.wasm"; + + #[test] + fn test_shell_queries_router_paths() { + let path = RPC.shell().epoch_path(); + assert_eq!("/shell/epoch", path); + + let token_addr = address::testing::established_address_1(); + let owner = address::testing::established_address_2(); + let key = token::balance_key(&token_addr, &owner); + let path = RPC.shell().storage_value_path(&key); + assert_eq!(format!("/shell/value/{}", key), path); + + let path = RPC.shell().dry_run_tx_path(); + assert_eq!("/shell/dry_run_tx", path); + + let path = RPC.shell().storage_prefix_path(&key); + assert_eq!(format!("/shell/prefix/{}", key), path); + + let path = RPC.shell().storage_has_key_path(&key); + assert_eq!(format!("/shell/has_key/{}", key), path); + } + + #[tokio::test] + async fn test_shell_queries_router_with_client() -> storage_api::Result<()> + { + // Initialize the `TestClient` + let mut client = TestClient::new(RPC); + + // Request last committed epoch + let read_epoch = RPC.shell().epoch(&client).await.unwrap(); + let current_epoch = client.storage.last_epoch; + assert_eq!(current_epoch, read_epoch); + + // Request dry run tx + let tx_no_op = std::fs::read(TX_NO_OP_WASM).expect("cannot load wasm"); + let tx = Tx::new(tx_no_op, None); + let tx_bytes = tx.to_bytes(); + let result = RPC + .shell() + .dry_run_tx_with_options(&client, Some(tx_bytes), None, false) + .await + .unwrap(); + assert!(result.data.is_accepted()); + + // Request storage value for a balance key ... + let token_addr = address::testing::established_address_1(); + let owner = address::testing::established_address_2(); + let balance_key = token::balance_key(&token_addr, &owner); + // ... there should be no value yet. + let read_balance = RPC + .shell() + .storage_value(&client, &balance_key) + .await + .unwrap(); + assert!(read_balance.is_none()); + + // Request storage prefix iterator + let balance_prefix = token::balance_prefix(&token_addr); + let read_balances = RPC + .shell() + .storage_prefix(&client, &balance_prefix) + .await + .unwrap(); + assert!(read_balances.is_empty()); + + // Request storage has key + let has_balance_key = RPC + .shell() + .storage_has_key(&client, &balance_key) + .await + .unwrap(); + assert!(!has_balance_key); + + // Then write some balance ... + let balance = token::Amount::from(1000); + StorageWrite::write(&mut client.storage, &balance_key, balance)?; + // ... there should be the same value now + let read_balance = RPC + .shell() + .storage_value(&client, &balance_key) + .await + .unwrap(); + assert_eq!( + balance, + token::Amount::try_from_slice(&read_balance.unwrap()).unwrap() + ); + + // Request storage prefix iterator + let balance_prefix = token::balance_prefix(&token_addr); + let read_balances = RPC + .shell() + .storage_prefix(&client, &balance_prefix) + .await + .unwrap(); + assert_eq!(read_balances.len(), 1); + + // Request storage has key + let has_balance_key = RPC + .shell() + .storage_has_key(&client, &balance_key) + .await + .unwrap(); + assert!(has_balance_key); + + Ok(()) + } +} From e0f3187c3c7ab79bb40be021167019596438abf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 14 Oct 2022 16:38:56 +0200 Subject: [PATCH 38/47] router: add `with_options` for handlers that use request/response --- apps/src/lib/client/rpc.rs | 32 ++-- shared/src/ledger/queries/mod.rs | 11 ++ shared/src/ledger/queries/router.rs | 249 ++++++++++++++++++++++------ shared/src/ledger/queries/shell.rs | 58 +++---- 4 files changed, 248 insertions(+), 102 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 26f60313f0..f6af362aeb 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -54,10 +54,12 @@ pub async fn query_epoch(args: args::Query) -> Epoch { /// Query the raw bytes of given storage key pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { let client = HttpClient::new(args.query.ledger_address).unwrap(); - let bytes = unwrap_client_response( - RPC.shell().storage_value(&client, &args.storage_key).await, + let response = unwrap_client_response( + RPC.shell() + .storage_value(&client, None, None, false, &args.storage_key) + .await, ); - match bytes { + match response.data { Some(bytes) => println!("Found data: 0x{}", HEXLOWER.encode(&bytes)), None => println!("No data found for key {}", args.storage_key), } @@ -1032,9 +1034,7 @@ pub async fn dry_run_tx(ledger_address: &TendermintAddress, tx_bytes: Vec) { let client = HttpClient::new(ledger_address.clone()).unwrap(); let (data, height, prove) = (Some(tx_bytes), None, false); let result = unwrap_client_response( - RPC.shell() - .dry_run_tx_with_options(&client, data, height, prove) - .await, + RPC.shell().dry_run_tx(&client, data, height, prove).await, ) .data; println!("Dry-run result: {}", result); @@ -1249,9 +1249,12 @@ pub async fn query_storage_value( where T: BorshDeserialize, { - let bytes = - unwrap_client_response(RPC.shell().storage_value(client, key).await); - bytes.map(|bytes| { + let response = unwrap_client_response( + RPC.shell() + .storage_value(client, None, None, false, key) + .await, + ); + response.data.map(|bytes| { T::try_from_slice(&bytes[..]).unwrap_or_else(|err| { eprintln!("Error decoding the value: {}", err); cli::safe_exit(1) @@ -1269,8 +1272,11 @@ pub async fn query_storage_prefix( where T: BorshDeserialize, { - let values = - unwrap_client_response(RPC.shell().storage_prefix(client, key).await); + let values = unwrap_client_response( + RPC.shell() + .storage_prefix(client, None, None, false, key) + .await, + ); let decode = |PrefixValue { key, value }: PrefixValue| match T::try_from_slice( &value[..], @@ -1284,10 +1290,10 @@ where } Ok(value) => Some((key, value)), }; - if values.is_empty() { + if values.data.is_empty() { None } else { - Some(values.into_iter().filter_map(decode)) + Some(values.data.into_iter().filter_map(decode)) } } diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index 4d160dd9e0..8b31376be4 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -68,6 +68,17 @@ pub fn require_no_proof(request: &RequestQuery) -> storage_api::Result<()> { Ok(()) } +/// For queries that don't use request data, require that there are no data +/// attached. +pub fn require_no_data(request: &RequestQuery) -> storage_api::Result<()> { + if !request.data.is_empty() { + return Err(storage_api::Error::new_const( + "This query doesn't accept request data", + )); + } + Ok(()) +} + #[cfg(any(test, feature = "tendermint-rpc"))] /// Provides [`Client`] implementation for Tendermint RPC client pub mod tm { diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index 332cad3f93..33773d6eb4 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -48,21 +48,19 @@ macro_rules! handle_match { return $router.internal_handle($ctx, $request, $start) }; - // Handler function + // Handler function that uses a request (`with_options`) ( $ctx:ident, $request:ident, $start:ident, $end:ident, - $handle:tt, ( $( $matched_args:ident, )* ), + (with_options $handle:tt), ( $( $matched_args:ident, )* ), ) => { // check that we're at the end of the path - trailing slash is optional if !($end == $request.path.len() || // ignore trailing slashes $end == $request.path.len() - 1 && &$request.path[$end..] == "/") { // we're not at the end, no match - // println!("Not fully matched"); + println!("Not fully matched"); break } - // If you get a compile error from here with `expected function, found - // queries::Storage`, you're probably missing the marker `(sub _)` let result = $handle($ctx, $request, $( $matched_args ),* )?; let data = borsh::BorshSerialize::try_to_vec(&result.data).into_storage_result()?; return Ok($crate::ledger::queries::EncodedResponseQuery { @@ -71,6 +69,35 @@ macro_rules! handle_match { proof_ops: result.proof_ops, }); }; + + // Handler function that doesn't use the request, just the path args, if any + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, + $handle:tt, ( $( $matched_args:ident, )* ), + ) => { + // check that we're at the end of the path - trailing slash is optional + if !($end == $request.path.len() || + // ignore trailing slashes + $end == $request.path.len() - 1 && &$request.path[$end..] == "/") { + // we're not at the end, no match + // println!("Not fully matched"); + break + } + // Check that the request is not sent with unsupported non-default + $crate::ledger::queries::require_latest_height(&$ctx, $request)?; + $crate::ledger::queries::require_no_proof($request)?; + $crate::ledger::queries::require_no_data($request)?; + + // If you get a compile error from here with `expected function, found + // queries::Storage`, you're probably missing the marker `(sub _)` + let data = $handle($ctx, $( $matched_args ),* )?; + let data = borsh::BorshSerialize::try_to_vec(&data).into_storage_result()?; + return Ok($crate::ledger::queries::EncodedResponseQuery { + data, + info: Default::default(), + proof_ops: None, + }); + }; } /// Using TT muncher pattern on the `$tail` pattern, this macro recursively @@ -168,16 +195,18 @@ macro_rules! try_match_segments { ( $( $matched_args, )* $arg, ), ( $( $( $tail )/ * )? ) ); }; - // Special case of the pattern below. When there are no more args in the - // tail and the handle isn't a sub-router (its fragment is ident), we try - // to match the rest of the path till the end. This is specifically needed - // for storage methods, which have `storage::Key` param that includes - // path-like slashes. + // Special case of the typed argument pattern below. When there are no more + // args in the tail and the handle isn't a sub-router (its handler is + // ident), we try to match the rest of the path till the end. + // + // This is specifically needed for storage methods, which have + // `storage::Key` param that includes path-like slashes. // // Try to match and parse a typed argument, declares the expected $arg into // type $t, if it can be parsed ( - $ctx:ident, $request:ident, $start:ident, $end:ident, $handle:ident, + $ctx:ident, $request:ident, $start:ident, $end:ident, + $handle:ident, ( $( $matched_args:ident, )* ), ( [$arg:ident : $arg_ty:ty] @@ -202,6 +231,41 @@ macro_rules! try_match_segments { ( $( $matched_args, )* $arg, ), () ); }; + // One more special case of the typed argument pattern below for a handler + // `with_options`, where we try to match the rest of the path till the end. + // + // This is specifically needed for storage methods, which have + // `storage::Key` param that includes path-like slashes. + // + // Try to match and parse a typed argument, declares the expected $arg into + // type $t, if it can be parsed + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, + (with_options $handle:ident), + ( $( $matched_args:ident, )* ), + ( + [$arg:ident : $arg_ty:ty] + ) + ) => { + let $arg: $arg_ty; + $end = $request.path.len(); + match $request.path[$start..$end].parse::<$arg_ty>() { + Ok(parsed) => { + println!("Parsed {}", parsed); + $arg = parsed + }, + Err(_) => + { + println!("Cannot parse {} from {}", stringify!($arg_ty), &$request.path[$start..$end]); + // If arg cannot be parsed, try to skip to next pattern + break + } + } + // Invoke the terminal pattern + try_match_segments!($ctx, $request, $start, $end, (with_options $handle), + ( $( $matched_args, )* $arg, ), () ); + }; + // Try to match and parse a typed argument, declares the expected $arg into // type $t, if it can be parsed ( @@ -308,13 +372,12 @@ macro_rules! pattern_to_prefix { /// Turn patterns and their handlers into methods for the router, where each /// dynamic pattern is turned into a parameter for the method. macro_rules! pattern_and_handler_to_method { - // terminal rule + // terminal rule for $handle that uses request (`with_options`) ( ( $( $param:tt: $param_ty:ty ),* ) [ $( { $prefix:expr } ),* ] - // $( $return_type:path )?, $return_type:path, - $handle:tt, + (with_options $handle:tt), () ) => { // paste! used to construct the `fn $handle_path`'s name. @@ -328,29 +391,6 @@ macro_rules! pattern_and_handler_to_method { .filter_map(|x| x), "/") } - #[allow(dead_code)] - #[allow(clippy::too_many_arguments)] - #[cfg(any(test, feature = "async-client"))] - #[doc = "Request a simple borsh-encoded value from `" $handle "`, \ - without any additional request data, specified block height or \ - proof."] - pub async fn $handle(&self, client: &CLIENT, - $( $param: &$param_ty ),* - ) - -> std::result::Result< - $return_type, - ::Error - > - where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { - let path = self.[<$handle _path>]( $( $param ),* ); - - let data = client.simple_request(path).await?; - - let decoded: $return_type = - borsh::BorshDeserialize::try_from_slice(&data[..])?; - Ok(decoded) - } - #[allow(dead_code)] #[allow(clippy::too_many_arguments)] #[cfg(any(test, feature = "async-client"))] @@ -358,7 +398,7 @@ macro_rules! pattern_and_handler_to_method { `dry_run_tx`), optionally specified height (supported for \ `storage_value`) and optional proof (supported for \ `storage_value` and `storage_prefix`) from `" $handle "`."] - pub async fn [<$handle _with_options>](&self, client: &CLIENT, + pub async fn $handle(&self, client: &CLIENT, data: Option>, height: Option<$crate::types::storage::BlockHeight>, prove: bool, @@ -387,6 +427,50 @@ macro_rules! pattern_and_handler_to_method { } }; + // terminal rule that $handle that doesn't use request + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + $return_type:path, + $handle:tt, + () + ) => { + // paste! used to construct the `fn $handle_path`'s name. + paste::paste! { + #[allow(dead_code)] + #[doc = "Get a path to query `" $handle "`."] + pub fn [<$handle _path>](&self, $( $param: &$param_ty ),* ) -> String { + itertools::join( + [ Some(std::borrow::Cow::from(&self.prefix)), $( $prefix ),* ] + .into_iter() + .filter_map(|x| x), "/") + } + + #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] + #[cfg(any(test, feature = "async-client"))] + #[doc = "Request a simple borsh-encoded value from `" $handle "`, \ + without any additional request data, specified block height or \ + proof."] + pub async fn $handle(&self, client: &CLIENT, + $( $param: &$param_ty ),* + ) + -> std::result::Result< + $return_type, + ::Error + > + where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { + let path = self.[<$handle _path>]( $( $param ),* ); + + let data = client.simple_request(path).await?; + + let decoded: $return_type = + borsh::BorshDeserialize::try_from_slice(&data[..])?; + Ok(decoded) + } + } + }; + // sub-pattern ( $param:tt @@ -581,6 +665,61 @@ macro_rules! router_type { /// methods (enabled with `feature = "async-client"`). /// /// The `router!` macro implements greedy matching algorithm. +/// +/// ## Examples +/// +/// ```rust,ignore +/// router! {ROOT, +/// // This pattern matches `/pattern_a/something`, where `something` can be +/// // parsed with `FromStr` into `ArgType`. +/// ( "pattern_a" / [typed_dynamic_arg: ArgType] ) -> ReturnType = handler, +/// +/// ( "pattern_b" / [optional_dynamic_arg: opt ArgType] ) -> ReturnType = +/// handler, +/// +/// // Untyped dynamic arg is a string slice `&str` +/// ( "pattern_c" / [untyped_dynamic_arg] ) -> ReturnType = handler, +/// +/// // The handler additionally receives the `RequestQuery`, which can have +/// // some data attached, specified block height and ask for a proof. It +/// // returns `ResponseQuery`, which can have some `info` string and a proof. +/// ( "pattern_d" ) -> ReturnType = (with_options handler), +/// +/// ( "another" / "pattern" / "that" / "goes" / "deep" ) -> ReturnType = handler, +/// +/// // Inlined sub-tree +/// ( "subtree" / [this_is_fine: ArgType] ) = { +/// ( "a" ) -> u64 = a_handler, +/// ( "b" / [another_arg] ) -> u64 = b_handler, +/// } +/// +/// // Imported sub-router - The prefix can only have literal segments +/// ( "sub" / "no_dynamic_args" ) = (sub SUB_ROUTER), +/// } +/// +/// router! {SUB_ROUTER, +/// ( "pattern" ) -> ReturnType = handler, +/// } +/// ``` +/// +/// Handler functions used in the patterns should have the expected signature: +/// ```rust,ignore +/// fn handler(ctx: RequestCtx<'_, D, H>, args ...) +/// -> storage_api::Result +/// where +/// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, +/// H: 'static + StorageHasher + Sync; +/// ``` +/// +/// If the handler wants to support request options, it can be defined as +/// `(with_options $handler)` and then the expected signature is: +/// ```rust,ignore +/// fn handler(ctx: RequestCtx<'_, D, H>, request: &RequestQuery, args +/// ...) -> storage_api::Result> +/// where +/// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, +/// H: 'static + StorageHasher + Sync; +/// ``` #[macro_export] macro_rules! router { { $name:ident, $( $pattern:tt $( -> $return_type:path )? = $handle:tt , )* } => ( @@ -659,9 +798,8 @@ mod test_rpc_handlers { $( pub fn $name( _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, $( $( $param: $param_ty ),* )? - ) -> storage_api::Result> + ) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -670,10 +808,7 @@ mod test_rpc_handlers { $( $( let data = format!("{data}/{}", $param); )* )? - Ok(ResponseQuery { - data, - ..ResponseQuery::default() - }) + Ok(data) } )* }; @@ -698,11 +833,10 @@ mod test_rpc_handlers { /// support optional args. pub fn b3iii( _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, a1: token::Amount, a2: token::Amount, a3: Option, - ) -> storage_api::Result> + ) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -711,22 +845,18 @@ mod test_rpc_handlers { let data = format!("{data}/{}", a1); let data = format!("{data}/{}", a2); let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); - Ok(ResponseQuery { - data, - ..ResponseQuery::default() - }) + Ok(data) } /// This handler is hand-written, because the test helper macro doesn't /// support optional args. pub fn b3iiii( _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, a1: token::Amount, a2: token::Amount, a3: Option, a4: Option, - ) -> storage_api::Result> + ) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -736,6 +866,20 @@ mod test_rpc_handlers { let data = format!("{data}/{}", a2); let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); let data = a4.map(|a4| format!("{data}/{}", a4)).unwrap_or(data); + Ok(data) + } + + /// This handler is hand-written, because the test helper macro doesn't + /// support handlers with `with_options`. + pub fn c( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, + ) -> storage_api::Result> + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = "c".to_owned(); Ok(ResponseQuery { data, ..ResponseQuery::default() @@ -774,6 +918,7 @@ mod test_rpc { ( "iiii" / [a3: opt token::Amount] / "xyz" / [a4: opt Epoch] ) -> String = b3iiii, }, }, + ( "c" ) -> String = (with_options c), } router! {TEST_SUB_RPC, diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 2304a0421e..8ba800023c 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -1,7 +1,7 @@ use tendermint_proto::crypto::{ProofOp, ProofOps}; +use crate::ledger::queries::require_latest_height; use crate::ledger::queries::types::{RequestCtx, RequestQuery, ResponseQuery}; -use crate::ledger::queries::{require_latest_height, require_no_proof}; use crate::ledger::storage::{DBIter, StorageHasher, DB}; use crate::ledger::storage_api::{self, ResultExt, StorageRead}; use crate::types::storage::{self, Epoch, PrefixValue}; @@ -15,14 +15,14 @@ router! {SHELL, // Raw storage access - read value ( "value" / [storage_key: storage::Key] ) - -> Option> = storage_value, + -> Option> = (with_options storage_value), // Dry run a transaction - ( "dry_run_tx" ) -> TxResult = dry_run_tx, + ( "dry_run_tx" ) -> TxResult = (with_options dry_run_tx), // Raw storage access - prefix iterator ( "prefix" / [storage_key: storage::Key] ) - -> Vec = storage_prefix, + -> Vec = (with_options storage_prefix), // Raw storage access - is given storage key present? ( "has_key" / [storage_key: storage::Key] ) @@ -80,22 +80,13 @@ where ) } -fn epoch( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, -) -> storage_api::Result> +fn epoch(ctx: RequestCtx<'_, D, H>) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - require_latest_height(&ctx, request)?; - require_no_proof(request)?; - let data = ctx.storage.last_epoch; - Ok(ResponseQuery { - data, - ..Default::default() - }) + Ok(data) } fn storage_value( @@ -112,13 +103,13 @@ where .read_with_height(&storage_key, request.height) .into_storage_result()? { - (Some(data), _gas) => { + (Some(value), _gas) => { let proof = if request.prove { let proof = ctx .storage .get_existence_proof( &storage_key, - data.clone(), + value.clone().into(), request.height, ) .into_storage_result()?; @@ -127,7 +118,7 @@ where None }; Ok(ResponseQuery { - data: Some(data), + data: Some(value), proof_ops: proof, ..Default::default() }) @@ -175,7 +166,7 @@ where for PrefixValue { key, value } in &data { let proof = ctx .storage - .get_existence_proof(key, value.clone(), request.height) + .get_existence_proof(key, value.clone().into(), request.height) .into_storage_result()?; let mut cur_ops: Vec = proof.ops.into_iter().map(|op| op.into()).collect(); @@ -195,21 +186,14 @@ where fn storage_has_key( ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, storage_key: storage::Key, -) -> storage_api::Result> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - require_latest_height(&ctx, request)?; - require_no_proof(request)?; - let data = StorageRead::has_key(ctx.storage, &storage_key)?; - Ok(ResponseQuery { - data, - ..Default::default() - }) + Ok(data) } #[cfg(test)] @@ -262,7 +246,7 @@ mod test { let tx_bytes = tx.to_bytes(); let result = RPC .shell() - .dry_run_tx_with_options(&client, Some(tx_bytes), None, false) + .dry_run_tx(&client, Some(tx_bytes), None, false) .await .unwrap(); assert!(result.data.is_accepted()); @@ -274,19 +258,19 @@ mod test { // ... there should be no value yet. let read_balance = RPC .shell() - .storage_value(&client, &balance_key) + .storage_value(&client, None, None, false, &balance_key) .await .unwrap(); - assert!(read_balance.is_none()); + assert!(read_balance.data.is_none()); // Request storage prefix iterator let balance_prefix = token::balance_prefix(&token_addr); let read_balances = RPC .shell() - .storage_prefix(&client, &balance_prefix) + .storage_prefix(&client, None, None, false, &balance_prefix) .await .unwrap(); - assert!(read_balances.is_empty()); + assert!(read_balances.data.is_empty()); // Request storage has key let has_balance_key = RPC @@ -302,22 +286,22 @@ mod test { // ... there should be the same value now let read_balance = RPC .shell() - .storage_value(&client, &balance_key) + .storage_value(&client, None, None, false, &balance_key) .await .unwrap(); assert_eq!( balance, - token::Amount::try_from_slice(&read_balance.unwrap()).unwrap() + token::Amount::try_from_slice(&read_balance.data.unwrap()).unwrap() ); // Request storage prefix iterator let balance_prefix = token::balance_prefix(&token_addr); let read_balances = RPC .shell() - .storage_prefix(&client, &balance_prefix) + .storage_prefix(&client, None, None, false, &balance_prefix) .await .unwrap(); - assert_eq!(read_balances.len(), 1); + assert_eq!(read_balances.data.len(), 1); // Request storage has key let has_balance_key = RPC From c87d5bf68794e835e4e79c6f31e258820e85d3cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Oct 2022 13:50:06 +0000 Subject: [PATCH 39/47] [ci] wasm checksums update --- wasm/checksums.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index f3e11c7ea5..98d4134fd3 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,18 +1,18 @@ { - "tx_bond.wasm": "tx_bond.99da6abae7acd0a67341b8581bc2b9667cadd8d8c947086a6ed62cb8e5ab9f01.wasm", - "tx_ibc.wasm": "tx_ibc.449da8289b55d2e8ee4b80c4a271f0f82c14bc52f5f1cc141fc2fc25d7c379dc.wasm", - "tx_init_account.wasm": "tx_init_account.c7cf064e8d03315763d0a1c75b9fc0d44d98eab6a2943a82053a8d555861a14e.wasm", - "tx_init_nft.wasm": "tx_init_nft.7e7a5a2678da391ee89b02f08c75fab8b9f48a40e9d4d871d159694080ca41c0.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.be6c8b24bc0419a7242df6ffada532a17730fe13180fce16a91b310892d6ebad.wasm", - "tx_init_validator.wasm": "tx_init_validator.419c2cd5ddfdcc728ea41903e92ed05e60679086babf0feb8146a1a5c1c7ad79.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.d5dcf0139e3fc332474db7ad9fd74f03af3c50433833a222a8ecf9880faedc1e.wasm", - "tx_transfer.wasm": "tx_transfer.15a74bbc4093bb0fd3e7943f597f88a444d6e7ea6e3a47401430e01945fe9ceb.wasm", - "tx_unbond.wasm": "tx_unbond.64ac67930786fc9d18631ed2d3a225a261114129a9ff57986347c904367efac5.wasm", - "tx_update_vp.wasm": "tx_update_vp.7148b2fef2f9a438ec8e3bf42d1e120ce690f0f69bb2b1c481711ee8b22cef54.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.5488e66b41ea1c45efdb6152fe8897c37e731ae97958db024bf1905651e0f54c.wasm", - "tx_withdraw.wasm": "tx_withdraw.976687bb02cde635a97de500ea72631f37b50516d34f72d5da3ca82b9617fe57.wasm", - "vp_nft.wasm": "vp_nft.92e1c20e54e67a8baa00bbeb61b3712cca32f34bd7e63e4f7f5da23bc303529a.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.ddbda2d7f226d40a337eb5176f96050501996d6db8f71fa99c21382f6a631b41.wasm", - "vp_token.wasm": "vp_token.60879bfd767808fe6400096cb1527fe44c81e1893a4ff9ce593a3d36e89a45f6.wasm", - "vp_user.wasm": "vp_user.a51b5650c3303789857d4af18d1d4b342bfa5974fcb2b8d6eca906be998168c5.wasm" + "tx_bond.wasm": "tx_bond.72f7ca706910728e7cd2699d225147634981f2bd82fa5c5e1800f33dd7a9268f.wasm", + "tx_ibc.wasm": "tx_ibc.cf61f60f726b00c4e4e26a2bfbb54d5a9fb0503aeb7ae46d9cfcd269417c6de4.wasm", + "tx_init_account.wasm": "tx_init_account.be35e9136ce7c62236ef40a0ec3a4fbfdd1c1c5999b0943c0495895c574ac01b.wasm", + "tx_init_nft.wasm": "tx_init_nft.b8dd99751cf701dcc04ccdd795a37c84ad6e37c833cf2d83ca674b1a5b8b7246.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.da041bb1412b6d4bb303232aaf6bec9369138d4a94b70e5b2e2b87dadb0a47b9.wasm", + "tx_init_validator.wasm": "tx_init_validator.628232b3c034a63d11bb6b75be4f4ed831c41cacf1b710ee7cb6fd94d889d12e.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.9bccf7930e21c59a03ff0aa7c85210bec8a320a87ed3d9c4bf000f98ade0cea2.wasm", + "tx_transfer.wasm": "tx_transfer.22f49259ce8c1534473959d699bbbfecb5b42499e9752785aa597c54f059e54b.wasm", + "tx_unbond.wasm": "tx_unbond.197405a2903fc1bf4a1b8f4bb2d901b9b0c455443d567907bd317d756afb16a5.wasm", + "tx_update_vp.wasm": "tx_update_vp.bb01d77ae24013ba7652c723bb4e446607b34dff10e4f01de4a6640aa80d282a.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.55f84360fc7f4cec4542e53272017ecae22e004bac0faf62550c8711895bbae5.wasm", + "tx_withdraw.wasm": "tx_withdraw.69dfa7f299a28ce25190402b231d2dd184431c5c3b9a691aae7b77a366c6d78b.wasm", + "vp_nft.wasm": "vp_nft.8234618f0a3de3d7a6dd75d1463d42a50a357b9783a83525c0093297a0b69738.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.92e4bb1ac583963ebe69a818d670c72e0db2370fe7a5ab2216060603f8e18440.wasm", + "vp_token.wasm": "vp_token.34405f1e1568f6478606de9cd8bb3ff1ffb78f1aa14cfc32861b1c2cf4b6eddd.wasm", + "vp_user.wasm": "vp_user.b70ceb1616f51aae27672c1d4c1705392716dca185e0503d61b3457c4e773f78.wasm" } \ No newline at end of file From a966969a107c674cd0a1525a27b06e757853c9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 28 Oct 2022 18:21:03 +0200 Subject: [PATCH 40/47] RPC: fix storage_value to return data as read w/o re-encoding in Option --- apps/src/lib/client/rpc.rs | 30 ++++++++-- shared/src/ledger/queries/router.rs | 85 +++++++++++++++++++++++++---- shared/src/ledger/queries/shell.rs | 42 ++++++++------ 3 files changed, 123 insertions(+), 34 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index f6af362aeb..d3f1303f41 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -59,9 +59,10 @@ pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { .storage_value(&client, None, None, false, &args.storage_key) .await, ); - match response.data { - Some(bytes) => println!("Found data: 0x{}", HEXLOWER.encode(&bytes)), - None => println!("No data found for key {}", args.storage_key), + if !response.data.is_empty() { + println!("Found data: 0x{}", HEXLOWER.encode(&response.data)); + } else { + println!("No data found for key {}", args.storage_key); } } @@ -1249,17 +1250,34 @@ pub async fn query_storage_value( where T: BorshDeserialize, { + // In case `T` is a unit (only thing that encodes to 0 bytes), we have to + // use `storage_has_key` instead of `storage_value`, because `storage_value` + // returns 0 bytes when the key is not found. + let maybe_unit = T::try_from_slice(&[]); + if let Ok(unit) = maybe_unit { + return if unwrap_client_response( + RPC.shell().storage_has_key(client, key).await, + ) { + Some(unit) + } else { + None + }; + } + let response = unwrap_client_response( RPC.shell() .storage_value(client, None, None, false, key) .await, ); - response.data.map(|bytes| { - T::try_from_slice(&bytes[..]).unwrap_or_else(|err| { + if response.data.is_empty() { + return None; + } + T::try_from_slice(&response.data[..]) + .map(Some) + .unwrap_or_else(|err| { eprintln!("Error decoding the value: {}", err); cli::safe_exit(1) }) - }) } /// Query a range of storage values with a matching prefix and decode them with diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index 33773d6eb4..e4823e5ad7 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -62,12 +62,10 @@ macro_rules! handle_match { break } let result = $handle($ctx, $request, $( $matched_args ),* )?; - let data = borsh::BorshSerialize::try_to_vec(&result.data).into_storage_result()?; - return Ok($crate::ledger::queries::EncodedResponseQuery { - data, - info: result.info, - proof_ops: result.proof_ops, - }); + // The handle must take care of encoding if needed and return `Vec`. + // This is because for `storage_value` the bytes are returned verbatim + // as read from storage. + return Ok(result); }; // Handler function that doesn't use the request, just the path args, if any @@ -91,6 +89,7 @@ macro_rules! handle_match { // If you get a compile error from here with `expected function, found // queries::Storage`, you're probably missing the marker `(sub _)` let data = $handle($ctx, $( $matched_args ),* )?; + // Encode the returned data with borsh let data = borsh::BorshSerialize::try_to_vec(&data).into_storage_result()?; return Ok($crate::ledger::queries::EncodedResponseQuery { data, @@ -372,6 +371,61 @@ macro_rules! pattern_to_prefix { /// Turn patterns and their handlers into methods for the router, where each /// dynamic pattern is turned into a parameter for the method. macro_rules! pattern_and_handler_to_method { + // Special terminal rule for `storage_value` handle from + // `shared/src/ledger/queries/shell.rs` that returns `Vec` which should + // not be decoded from response.data, but instead return as is + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + $return_type:path, + (with_options storage_value), + () + ) => { + // paste! used to construct the `fn $handle_path`'s name. + paste::paste! { + #[allow(dead_code)] + #[doc = "Get a path to query `storage_value`."] + pub fn storage_value_path(&self, $( $param: &$param_ty ),* ) -> String { + itertools::join( + [ Some(std::borrow::Cow::from(&self.prefix)), $( $prefix ),* ] + .into_iter() + .filter_map(|x| x), "/") + } + + #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] + #[cfg(any(test, feature = "async-client"))] + #[doc = "Request value with optional data (used for e.g. \ + `dry_run_tx`), optionally specified height (supported for \ + `storage_value`) and optional proof (supported for \ + `storage_value` and `storage_prefix`) from `storage_value`."] + pub async fn storage_value(&self, client: &CLIENT, + data: Option>, + height: Option<$crate::types::storage::BlockHeight>, + prove: bool, + $( $param: &$param_ty ),* + ) + -> std::result::Result< + $crate::ledger::queries::ResponseQuery>, + ::Error + > + where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { + println!("IMMA VEC!!!!!!"); + let path = self.storage_value_path( $( $param ),* ); + + let $crate::ledger::queries::ResponseQuery { + data, info, proof_ops + } = client.request(path, data, height, prove).await?; + + Ok($crate::ledger::queries::ResponseQuery { + data, + info, + proof_ops, + }) + } + } + }; + // terminal rule for $handle that uses request (`with_options`) ( ( $( $param:tt: $param_ty:ty ),* ) @@ -409,6 +463,7 @@ macro_rules! pattern_and_handler_to_method { ::Error > where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { + println!("IMMA not a VEC!!!!!!"); let path = self.[<$handle _path>]( $( $param ),* ); let $crate::ledger::queries::ResponseQuery { @@ -682,7 +737,8 @@ macro_rules! router_type { /// /// // The handler additionally receives the `RequestQuery`, which can have /// // some data attached, specified block height and ask for a proof. It -/// // returns `ResponseQuery`, which can have some `info` string and a proof. +/// // returns `EncodedResponseQuery` (the `data` must be encoded, if +/// // necessary), which can have some `info` string and a proof. /// ( "pattern_d" ) -> ReturnType = (with_options handler), /// /// ( "another" / "pattern" / "that" / "goes" / "deep" ) -> ReturnType = handler, @@ -780,9 +836,13 @@ macro_rules! router { /// ``` #[cfg(test)] mod test_rpc_handlers { - use crate::ledger::queries::{RequestCtx, RequestQuery, ResponseQuery}; + use borsh::BorshSerialize; + + use crate::ledger::queries::{ + EncodedResponseQuery, RequestCtx, RequestQuery, ResponseQuery, + }; use crate::ledger::storage::{DBIter, StorageHasher, DB}; - use crate::ledger::storage_api; + use crate::ledger::storage_api::{self, ResultExt}; use crate::types::storage::Epoch; use crate::types::token; @@ -874,12 +934,12 @@ mod test_rpc_handlers { pub fn c( _ctx: RequestCtx<'_, D, H>, _request: &RequestQuery, - ) -> storage_api::Result> + ) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - let data = "c".to_owned(); + let data = "c".to_owned().try_to_vec().into_storage_result()?; Ok(ResponseQuery { data, ..ResponseQuery::default() @@ -1011,6 +1071,9 @@ mod test { .unwrap(); assert_eq!(result, format!("b3iiii/{a1}/{a2}")); + let result = TEST_RPC.c(&client, None, None, false).await.unwrap(); + assert_eq!(result.data, format!("c")); + let result = TEST_RPC.test_sub_rpc().x(&client).await.unwrap(); assert_eq!(result, format!("x")); diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 8ba800023c..62f32c0f87 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -1,7 +1,8 @@ +use borsh::BorshSerialize; use tendermint_proto::crypto::{ProofOp, ProofOps}; -use crate::ledger::queries::require_latest_height; -use crate::ledger::queries::types::{RequestCtx, RequestQuery, ResponseQuery}; +use crate::ledger::queries::types::{RequestCtx, RequestQuery}; +use crate::ledger::queries::{require_latest_height, EncodedResponseQuery}; use crate::ledger::storage::{DBIter, StorageHasher, DB}; use crate::ledger::storage_api::{self, ResultExt, StorageRead}; use crate::types::storage::{self, Epoch, PrefixValue}; @@ -15,7 +16,7 @@ router! {SHELL, // Raw storage access - read value ( "value" / [storage_key: storage::Key] ) - -> Option> = (with_options storage_value), + -> Vec = (with_options storage_value), // Dry run a transaction ( "dry_run_tx" ) -> TxResult = (with_options dry_run_tx), @@ -35,7 +36,7 @@ router! {SHELL, fn dry_run_tx( mut ctx: RequestCtx<'_, D, H>, request: &RequestQuery, -) -> storage_api::Result> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -59,9 +60,11 @@ where &mut ctx.tx_wasm_cache, ) .into_storage_result()?; - Ok(ResponseQuery { + let data = data.try_to_vec().into_storage_result()?; + Ok(EncodedResponseQuery { data, - ..ResponseQuery::default() + proof_ops: None, + info: Default::default(), }) } @@ -69,7 +72,7 @@ where fn dry_run_tx( _ctx: RequestCtx<'_, D, H>, _request: &RequestQuery, -) -> storage_api::Result> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -89,11 +92,15 @@ where Ok(data) } +/// Returns data with `vec![]` when the storage key is not found. For all +/// borsh-encoded types, it is safe to check `data.is_empty()` to see if the +/// value was found, except for unit - see `fn query_storage_value` in +/// `apps/src/lib/client/rpc.rs` for unit type handling via `storage_has_key`. fn storage_value( ctx: RequestCtx<'_, D, H>, request: &RequestQuery, storage_key: storage::Key, -) -> storage_api::Result>>> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -117,10 +124,10 @@ where } else { None }; - Ok(ResponseQuery { - data: Some(value), + Ok(EncodedResponseQuery { + data: value, proof_ops: proof, - ..Default::default() + info: Default::default(), }) } (None, _gas) => { @@ -133,8 +140,8 @@ where } else { None }; - Ok(ResponseQuery { - data: None, + Ok(EncodedResponseQuery { + data: vec![], proof_ops: proof, info: format!("No value found for key: {}", storage_key), }) @@ -146,7 +153,7 @@ fn storage_prefix( ctx: RequestCtx<'_, D, H>, request: &RequestQuery, storage_key: storage::Key, -) -> storage_api::Result>> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -177,7 +184,8 @@ where } else { None }; - Ok(ResponseQuery { + let data = data.try_to_vec().into_storage_result()?; + Ok(EncodedResponseQuery { data, proof_ops, ..Default::default() @@ -261,7 +269,7 @@ mod test { .storage_value(&client, None, None, false, &balance_key) .await .unwrap(); - assert!(read_balance.data.is_none()); + assert!(read_balance.data.is_empty()); // Request storage prefix iterator let balance_prefix = token::balance_prefix(&token_addr); @@ -291,7 +299,7 @@ mod test { .unwrap(); assert_eq!( balance, - token::Amount::try_from_slice(&read_balance.data.unwrap()).unwrap() + token::Amount::try_from_slice(&read_balance.data).unwrap() ); // Request storage prefix iterator From 7830c175a8c01b8ef5bc22285c1a88bce73cd730 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 20 Oct 2022 12:25:30 +0200 Subject: [PATCH 41/47] Custom proposal and vote memos in governance spec --- .../src/explore/design/ledger/governance.md | 44 +++--- .../specs/src/base-ledger/governance.md | 135 +++++++++++------- 2 files changed, 110 insertions(+), 69 deletions(-) diff --git a/documentation/dev/src/explore/design/ledger/governance.md b/documentation/dev/src/explore/design/ledger/governance.md index da26c8e989..54eeb0578e 100644 --- a/documentation/dev/src/explore/design/ledger/governance.md +++ b/documentation/dev/src/explore/design/ledger/governance.md @@ -1,10 +1,10 @@ # Governance -Namada introduce a governance mechanism to propose and apply protocol changes with and without the need for an hard fork. Anyone holding some M1T will be able to prosose some changes to which delegators and validator will cast their yay or nay votes. Governance on Namada supports both signaling and voting mechanism. The difference between the the two, is that the former is needed when the changes require an hard fork. In cases where the chain is not able to produce blocks anymore, Namada relies an off chain signaling mechanism to agree on a common strategy. +Namada introduces a governance mechanism to propose and apply protocol changes with and without the need for a hard fork. Anyone holding some NAM will be able to propose some changes to which delegators and validators will cast their yay or nay votes. Governance on Namada supports both signaling and voting mechanism. The difference between the two, is that the former is needed when the changes require a hard fork. In cases where the chain is not able to produce blocks anymore, Namada relies on an off-chain signaling mechanism to agree on a common strategy. ## Governance & SlashFund addresses -Governance introduce two internal address with their corresponding native vps: +Governance introduces two internal addresses with their corresponding native vps: - Governance address, which is in charge of validating on-chain proposals and votes - SlashFund address, which is in charge of holding slashed funds @@ -20,7 +20,7 @@ Also, it introduces some protocol parameters: ## On-chain proposals -On-chain proposals are created under the `governance_address` storage space and, by default, this storage space is initialized with following storage keys: +On-chain proposals are created under the `governance_address` storage space and, by default, this storage space is initialized with the following storage keys: ``` /$GovernanceAddress/counter: u64 @@ -32,11 +32,12 @@ On-chain proposals are created under the `governance_address` storage space and, /$GovernanceAddress/min_proposal_grace_epochs: u64 ``` -In order to create a valid proposal, a transaction need to modify these storage keys: +In order to create a valid proposal, a transaction needs to modify these storage keys: ``` -/$GovernanceAddress/proposal/$id/content : Vec -/$GovernanceAddress/proposal/$id/author : Address +/$GovernanceAddress/proposal/$id/content: Vec +/$GovernanceAddress/proposal/$id/author: Address +/$GovernanceAddress/proposal/$id/proposal_type: ProposalType /$GovernanceAddress/proposal/$id/startEpoch: Epoch /$GovernanceAddress/proposal/$id/endEpoch: Epoch /$GovernanceAddress/proposal/$id/graceEpoch: Epoch @@ -48,7 +49,7 @@ and follow these rules: - `$id` must be equal to `counter + 1`. - `startEpoch` must: - - be grater than `currentEpoch`, where current epoch is the epoch in which the transaction is executed and included in a block + - be greater than `currentEpoch`, where current epoch is the epoch in which the transaction is executed and included in a block - be a multiple of `min_proposal_period`. - `endEpoch` must: - be at least `min_proposal_period` epochs greater than `startEpoch` @@ -60,13 +61,18 @@ and follow these rules: - `funds` must be equal to `min_proposal_fund` and should be moved to the `governance_address`. - `content` should follow the `Namada Improvement Proposal schema` and must be less than `max_proposal_content_size` kibibytes. - `author` must be a valid address on-chain +- `proposal_type` defines: + - the optional payload (memo) attached to the vote + - which actors should be allowed to vote (delegators and validators or validators only) + - the threshold to be used in the tally process + - the optional wasm code attached to the proposal -A proposal gets accepted if, at least 2/3 of the total voting power (computed at the epoch definied in the `startEpoch` field) vote `yay`. If the proposal is accepted, the locked funds are returned to the address definied in the `proposal_author` field, otherwise are moved to the slash fund address. +A proposal gets accepted if enough `yay` votes (net of the voting power) to match the threshold specified by `proposal_type` (computed at the epoch defined in the `endEpoch` field) are reached. If the proposal is accepted, the locked funds are returned to the address defined in the `proposal_author` field, otherwise are moved to the slash fund address. The `proposal_code` field can execute arbitrary code in the form of a wasm transaction. If the proposal gets accepted, the code is executed in the first block of the epoch following the `graceEpoch`. -Proposal can be submitted by any address as long as the above rules are respected. Votes can be casted only by active validators and delegator (at epoch `startEpoch` or less). -Moreover, validator can vote only during the first 2/3 of the voting period (from `startEpoch` and 2/3 of `endEpoch` - `startEpoch`). +Proposals can be submitted by any address as long as the above rules are respected. Votes can be cast only by active validators and delegators (at epoch `endEpoch` or less):: `proposal_type` could impose more constraints on this. +Moreover, if delegators are allowed to vote, validators can vote only during the first 2/3 of the voting period (from `startEpoch` and 2/3 of `endEpoch` - `startEpoch`). The preferred content template (`Namada Improvement Proposal schema`) is the following: @@ -84,24 +90,24 @@ The preferred content template (`Namada Improvement Proposal schema`) is the fol } ``` -In order to vote a proposal, a transaction should modify the following storage key: +In order to vote on a proposal, a transaction should modify the following storage key: ``` /$GovernanceAddress/proposal/$id/vote/$validator_address/$voter_address: ProposalVote ``` -where ProposalVote is a borsh encoded string containing either `yay` or `nay`, `$validator_address` is the delegation validator address and the `$voter_address` is the address of who is voting. A voter can be cast for each delegation. +where `ProposalVote` is an enum representing a `Yay` or `Nay` vote: the yay variant also contains the specific memo (if any) required for that proposal. `$validator_address` is the delegation validator address and the `$voter_address` is the address of who is voting. A voter can be cast for each delegation. -Vote is valid if it follow this rules: +Vote is valid if it follows these rules: -- vote can be sent only by validator or delegators -- validator can vote only during the first 2/3 of the total voting period, delegator can vote for the whole voting period +- vote can be sent only by validator or delegators (also depending on `proposal_type`) +- if delegators can vote, validators can vote only during the first 2/3 of the total voting period, delegators can vote for the whole voting period -The outcome of a proposal is compute at the epoch specific in the `endEpoch` field and executed at `graceEpoch` field (if it contains a non-empty `proposalCode` field). -A proposal is accepted only if more than 2/3 of the voting power vote `yay`. +The outcome of a proposal is computed at the epoch specific in the `endEpoch` field and executed at `graceEpoch` field (if it contains a non-empty `proposalCode` field). +A proposal is accepted only if enough `yay` votes (net of the voting power) to match the threshold set in `proposal_type` is reached. If a proposal gets accepted, the locked funds will be reimbursed to the author. In case it gets rejected, the locked funds will be moved to slash fund. ## Off-chain proposal -In case where its not possibile to run a proposal online (for example, when the chain is halted), an offline mechanism can be used. -The ledger offers the possibility to create and sign proposal which are verified against a specific chain epoch. +In cases where it's not possible to run a proposal online (for example, when the chain is halted), an offline mechanism can be used. +The ledger offers the possibility to create and sign proposals that are verified against a specific chain epoch. diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index 40c7281af9..5159e5af7b 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -1,11 +1,13 @@ # Namada Governance -Namada introduces a governance mechanism to propose and apply protocol changes with or without the need for a hard fork. Anyone holding some `NAM` will be able to propose some changes to which delegators and validators will cast their `yay` or `nay` votes. Governance on Namada supports both `signaling` and `voting` mechanisms. The difference between the the two is that the former is needed when the changes require a hard fork. In cases where the chain is not able to produce blocks anymore, Namada relies on [off chain](#off-chain-protocol) signaling to agree on a common move. +Namada introduces a governance mechanism to propose and apply protocol changes with or without the need for a hard fork. Anyone holding some `NAM` will be able to propose some changes to which delegators and validators will cast their `yay` or `nay` votes: in addition it will also be possible to attach some payloads to votes, in specific cases, to embed additional information. Governance on Namada supports both `signaling` and `voting` mechanisms. The difference between the two is that the former is needed when the changes require a hard fork. In cases where the chain is not able to produce blocks anymore, Namada relies on [off chain](#off-chain-protocol) signaling to agree on a common move. ## On-chain protocol ### Governance Address + Governance adds 2 internal addresses: + - `GovernanceAddress` - `SlashFundAddress` @@ -13,11 +15,13 @@ The first internal address contains all the proposals under its address space. The second internal address holds the funds of rejected proposals. ### Governance storage + Each proposal will be stored in a sub-key under the internal proposal address. The storage keys involved are: ``` /\$GovernanceAddress/proposal/\$id/content: Vec /\$GovernanceAddress/proposal/\$id/author: Address +/\$GovernanceAddress/proposal/\$id/proposal_type: ProposalType /\$GovernanceAddress/proposal/\$id/start_epoch: Epoch /\$GovernanceAddress/proposal/\$id/end_epoch: Epoch /\$GovernanceAddress/proposal/\$id/grace_epoch: Epoch @@ -27,7 +31,7 @@ Each proposal will be stored in a sub-key under the internal proposal address. T ``` - `Author` address field will be used to credit the locked funds if the proposal is approved. -- `/\$GovernanceAddress/proposal/\$epoch/\$id` is used for easing the ledger governance execution. `\$epoch` refers to the same value as the on specific in the `grace_epoch` field. +- `/\$GovernanceAddress/proposal/\$epoch/\$id` is used for easing the ledger governance execution. `\$epoch` refers to the same value as the one specified in the `grace_epoch` field. - The `content` value should follow a standard format. We leverage a similar format to what is described in the [BIP2](https://github.com/bitcoin/bips/blob/master/bip-0002.mediawiki#bip-format-and-structure) document: ```json @@ -44,6 +48,15 @@ Each proposal will be stored in a sub-key under the internal proposal address. T } ``` +The `ProposalType` imply different combinations of: + +- the optional payload (memo) attached to the vote +- which actors should be allowed to vote (delegators and validators or validators only) +- the threshold to be used in the tally process +- the optional wasm code attached to the proposal + +The correct logic to handle these different types will be hardcoded in protocol. We'll also rely on type checking to strictly enforce the correctness of a proposal given its type. These two approaches combined will prevent a user from deviating from the intended logic for a certain proposal type (e.g. providing a wasm code when it's not needed or allowing only validators to vote when also delegators should, etc...) + `GovernanceAddress` parameters and global storage keys are: ``` @@ -74,32 +87,33 @@ This is to leverage the `NAM` VP to check that the funds were correctly locked. The governance subkey, `/\$GovernanceAddress/proposal/\$id/funds` will be used after the tally step to know the exact amount of tokens to refund or move to Treasury. ### GovernanceAddress VP -Just like Pos, also governance has his own storage space. The `GovernanceAddress` validity predicate task is to check the integrity and correctness of new proposals. A proposal, to be correct, must satisfy the following: + +Just like Pos, also governance has its own storage space. The `GovernanceAddress` validity predicate task is to check the integrity and correctness of new proposals. A proposal, to be correct, must satisfy the following: - Mandatory storage writes are: - - counter - - author - - funds - - voting_start epoch - - voting_end epoch - - grace_epoch + - counter + - author + - proposal_type + - funds + - voting_start epoch + - voting_end epoch + - grace_epoch - Lock some funds >= `min_proposal_fund` - Contains a unique ID - Contains a start, end and grace Epoch - The difference between StartEpoch and EndEpoch should be >= `min_proposal_period`. - Should contain a text describing the proposal with length < `max_proposal_content_size` characters. -- Vote can be done only by a delegator or validator -- Validator can vote only in the initial 2/3 of the whole proposal duration (`end_epoch` - `start_epoch`) -- Due to the previous requirement, the following must be true,`(EndEpoch - StartEpoch) % 3 == 0` -- If defined, `proposalCode` should be the wasm bytecode representation of - the changes. This code is triggered in case the proposal has a position outcome. +- Vote can be done only by a delegator or validator (further constraints can be applied depending on the proposal type) +- If delegators are allowed to vote, than validators can vote only in the initial 2/3 of the whole proposal duration (`end_epoch` - `start_epoch`) +- Due to the previous requirement, the following must be true, `(EndEpoch - StartEpoch) % 3 == 0` +- If defined, `proposalCode` should be the wasm bytecode representation of the changes. This code is triggered in case the proposal has a position outcome. - The difference between `grace_epoch` and `end_epoch` should be of at least `min_proposal_grace_epochs` Once a proposal has been created, nobody can modify any of its fields. -If `proposal_code` is `Empty` or `None` , the proposal upgrade will need to be done via hard fork. +If `proposal_code` is `Empty` or `None`, the proposal upgrade will need to be done via hard fork, unless this is a specific type of proposal: in this case the protocol can directly apply the required changes. -It is possible to check the actual implementation [here](https://github.com/anoma/namada/blob/master/shared/src/ledger/governance/mod.rs#L69). +It is possible to check the actual implementation [here](https://github.com/anoma/namada/blob/main/shared/src/ledger/governance/mod.rs#L69). -Example of `proposalCode` could be: +Examples of `proposalCode` could be: - storage writes to change some protocol parameter - storage writes to restore a slash - storage writes to change a non-native vp @@ -108,44 +122,47 @@ This means that corresponding VPs need to handle these cases. ### Proposal Transactions -The proposal transaction will have the following structure, where `author` address will be the refund address. +The on-chain proposal transaction will have the following structure, where `author` address will be the refund address. ```rust -struct OnChainProposal { - id: u64 - content: Vec - author: Address - votingStartEpoch: Epoch - votingEndEpoch: Epoch - graceEpoch: Epoch - proposalCode: Option> +struct Proposal { + id: u64, + content: Vec, + author: Address, + proposal_type: ProposalType, + votingStartEpoch: Epoch, + votingEndEpoch: Epoch, + graceEpoch: Epoch, } ``` +The optional proposal wasm code will be embedded inside the `ProposalType` enum variants to better perform validation through type checking. + ### Vote transaction Vote transactions have the following structure: ```rust struct OnChainVote { - id: u64 - voter: Address - yay: bool + id: u64, + voter: Address, + yay: ProposalVote, } ``` Vote transaction creates or modifies the following storage key: ``` -/\$GovernanceAddress/proposal/\$id/vote/\$delegation_address/\$voter_address: Enum(yay|nay) +/\$GovernanceAddress/proposal/\$id/vote/\$delegation_address/\$voter_address: ProposalVote ``` -The storage key will only be created if the transaction is signed either by -a validator or a delegator. -Validators will be able to vote only for 2/3 of the total voting period, while delegators can vote until the end of the voting period. +where `ProposalVote` is an enum representing a `Yay` or `Nay` vote: the yay variant also contains the specific memo (if any) required for that proposal. + +The storage key will only be created if the transaction is signed either by a validator or a delegator. In case a vote misses a required memo or carries a memo with an invalid format, the vote will be discarded at validation time (VP) and it won't be written to storage. -If a delegator votes opposite to its validator, this will *override* the -corresponding vote of this validator (e.g. if a delegator has a voting power of 200 and votes opposite to the delegator holding these tokens, than 200 will be subtracted from the voting power of the involved validator). +If delegators are allowed to vote, validators will be able to vote only for 2/3 of the total voting period, while delegators can vote until the end of the voting period. + +If a delegator votes differently than its validator, this will *override* the corresponding vote of this validator (e.g. if a delegator has a voting power of 200 and votes opposite to the delegator holding these tokens, than 200 will be subtracted from the voting power of the involved validator). As a small form of space/gas optimization, if a delegator votes accordingly to its validator, the vote will not actually be submitted to the chain. This logic is applied only if the following conditions are satisfied: @@ -153,36 +170,49 @@ As a small form of space/gas optimization, if a delegator votes accordingly to i - The vote is submitted in the last third of the voting period (the one exclusive to delegators). This second condition is necessary to prevent a validator from changing its vote after a delegator vote has been submitted, effectively stealing the delegator's vote. ### Tally -At the beginning of each new epoch (and only then), in the `FinalizeBlock` event, tallying will occur for all the proposals ending at this epoch (specified via the `grace_epoch` field of the proposal). -The proposal has a positive outcome if 2/3 of the staked `NAM` total is voting `yay`. Tallying is computed with the following rules: -- Sum all the voting power of validators that voted `yay` -- For any validator that voted `yay`, subtract the voting power of any delegation that voted `nay` -- Add voting power for any delegation that voted `yay` (whose corresponding validator didn't vote `yay`) -- If the aformentioned sum divided by the total voting power is >= `2/3`, the proposal outcome is positive otherwise negative. +At the beginning of each new epoch (and only then), in the `finalize_block` function, tallying will occur for all the proposals ending at this epoch (specified via the `grace_epoch` field of the proposal). +The proposal has a positive outcome if the threshold specified by the `ProposalType` is reached. This means that enough `yay` votes must have been collected: the threshold is relative to the staked `NAM` total. + +Tallying, when no `memo` is required, is computed with the following rules: + +1. Sum all the voting power of validators that voted `yay` +2. For any validator that voted `yay`, subtract the voting power of any delegation that voted `nay` +3. Add voting power for any delegation that voted `yay` (whose corresponding validator didn't vote `yay`) +4. If the aforementioned sum divided by the total voting power is greater or equal to the threshold set by `ProposalType`, the proposal outcome is positive otherwise negative. -All the computation above must be made at the epoch specified in the `start_epoch` field of the proposal. +If votes carry a `memo`, instead, the `yay` votes must be evaluated net of it. The protocol will implement the correct logic to make sense of these memos and compute the tally correctly: -It is possible to check the actual implementation [here](https://github.com/anoma/namada/blob/master/shared/src/ledger/governance/utils.rs#L68). +1. Sum all the voting power of validators that voted `yay` with a specific memo, effectively splitting the `yay` votes into different subgroups +2. For any validator that voted `yay`, subtract the voting power of any delegation that voted `nay` or voted `yay` with a different memo +3. Add voting power for any delegation that voted `yay` (whose corresponding validator voted `nay` or `yay` with a different memo) +4. From the `yay` subgroups select the one that got the greatest amount of voting power +5. If the aforementioned voting power divided by the total voting power is greater or equal to the threshold set by `ProposalType`, the proposal outcome is positive otherwise negative. + +All the computation will be done on data collected at the epoch specified in the `end_epoch` field of the proposal. + +It is possible to check the actual implementation [here](https://github.com/anoma/namada/blob/main/shared/src/ledger/governance/utils.rs#L68). ### Refund and Proposal Execution mechanism -Together with tallying, in the first block at the beginning of each epoch, in the `FinalizeBlock` event, the protocol will manage the execution of accepted proposals and refunding. For each ended proposal with a positive outcome, it will refund the locked funds from `GovernanceAddress` to the proposal author address (specified in the proposal `author` field). For each proposal that has been rejected, instead, the locked funds will be moved to the `SlashFundAddress`. Moreover, if the proposal had a positive outcome and `proposal_code` is defined, these changes will be executed right away. -To summarize the execution of governance in the `FinalizeBlock` event: -If the proposal outcome is positive and current epoch is equal to the proposal `grace_epoch`, in the `FinalizeBlock` event: +Together with tallying, in the first block at the beginning of each epoch, in the `finalize_block` function, the protocol will manage the execution of accepted proposals and refunding. For each ended proposal with a positive outcome, it will refund the locked funds from `GovernanceAddress` to the proposal author address (specified in the proposal `author` field). For each proposal that has been rejected, instead, the locked funds will be moved to the `SlashFundAddress`. Moreover, if the proposal had a positive outcome and `proposal_code` is defined, these changes will be executed right away. +To summarize the execution of governance in the `finalize_block` function: + +If the proposal outcome is positive and current epoch is equal to the proposal `grace_epoch`, in the `finalize_block` function: - transfer the locked funds to the proposal `author` - execute any changes specified by `proposal_code` -In case the proposal was rejected or if any error, in the `FinalizeBlock` event: +In case the proposal was rejected or if any error, in the `finalize_block` function: - transfer the locked funds to `SlashFundAddress` The result is then signaled by creating and inserting a [`Tendermint Event`](https://github.com/tendermint/tendermint/blob/ab0835463f1f89dcadf83f9492e98d85583b0e71/docs/spec/abci/abci.md#events. - ## SlashFundAddress + Funds locked in `SlashFundAddress` address should be spendable only by proposals. ### SlashFundAddress storage + ``` /\$SlashFundAddress/?: Vec ``` @@ -193,6 +223,7 @@ The funds will be stored under: ``` ### SlashFundAddress VP + The slash_fund validity predicate will approve a transfer only if the transfer has been made by the protocol (by checking the existence of `/\$GovernanceAddress/pending/\$proposal_id` storage key) It is possible to check the actual implementation [here](https://github.com/anoma/namada/blob/main/shared/src/ledger/slash_fund/mod.rs#L70). @@ -200,8 +231,10 @@ It is possible to check the actual implementation [here](https://github.com/anom ## Off-chain protocol ### Create proposal + A CLI command to create a signed JSON representation of the proposal. The JSON will have the following structure: + ``` { content: Base64>, @@ -212,12 +245,13 @@ JSON will have the following structure: } ``` -The signature is produced over the hash of the concatenation of: `content`, `author`, `votingStart` and `votingEnd`. +The signature is produced over the hash of the concatenation of: `content`, `author`, `votingStart` and `votingEnd`. Proposal types are not supported off-chain. ### Create vote A CLI command to create a signed JSON representation of a vote. The JSON will have the following structure: + ``` { proposalHash: Base64>, @@ -227,9 +261,10 @@ will have the following structure: } ``` -The proposalHash is produced over the concatenation of: `content`, `author`, `votingStart`, `votingEnd`, `voter` and `vote`. +The proposalHash is produced over the concatenation of: `content`, `author`, `votingStart`, `votingEnd`, `voter` and `vote`. Vote memos are not supported off-chain. ### Tally + Same mechanism as [on chain](#tally) tally but instead of reading the data from storage it will require a list of serialized json votes. ## Interfaces From 106dbba32173a5d2ef88b1c5295ce63d8013455c Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 21 Oct 2022 12:36:59 +0200 Subject: [PATCH 42/47] Rwnames `type`, adds examples --- .../src/explore/design/ledger/governance.md | 12 ++--- .../specs/src/base-ledger/governance.md | 46 ++++++++++++++++--- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/documentation/dev/src/explore/design/ledger/governance.md b/documentation/dev/src/explore/design/ledger/governance.md index 54eeb0578e..d9e578eed5 100644 --- a/documentation/dev/src/explore/design/ledger/governance.md +++ b/documentation/dev/src/explore/design/ledger/governance.md @@ -37,7 +37,7 @@ In order to create a valid proposal, a transaction needs to modify these storage ``` /$GovernanceAddress/proposal/$id/content: Vec /$GovernanceAddress/proposal/$id/author: Address -/$GovernanceAddress/proposal/$id/proposal_type: ProposalType +/$GovernanceAddress/proposal/$id/type: ProposalType /$GovernanceAddress/proposal/$id/startEpoch: Epoch /$GovernanceAddress/proposal/$id/endEpoch: Epoch /$GovernanceAddress/proposal/$id/graceEpoch: Epoch @@ -61,17 +61,17 @@ and follow these rules: - `funds` must be equal to `min_proposal_fund` and should be moved to the `governance_address`. - `content` should follow the `Namada Improvement Proposal schema` and must be less than `max_proposal_content_size` kibibytes. - `author` must be a valid address on-chain -- `proposal_type` defines: +- `type` defines: - the optional payload (memo) attached to the vote - which actors should be allowed to vote (delegators and validators or validators only) - the threshold to be used in the tally process - the optional wasm code attached to the proposal -A proposal gets accepted if enough `yay` votes (net of the voting power) to match the threshold specified by `proposal_type` (computed at the epoch defined in the `endEpoch` field) are reached. If the proposal is accepted, the locked funds are returned to the address defined in the `proposal_author` field, otherwise are moved to the slash fund address. +A proposal gets accepted if enough `yay` votes (net of the voting power) to match the threshold specified by `ProposalType` (computed at the epoch defined in the `endEpoch` field) are reached. If the proposal is accepted, the locked funds are returned to the address defined in the `proposal_author` field, otherwise are moved to the slash fund address. The `proposal_code` field can execute arbitrary code in the form of a wasm transaction. If the proposal gets accepted, the code is executed in the first block of the epoch following the `graceEpoch`. -Proposals can be submitted by any address as long as the above rules are respected. Votes can be cast only by active validators and delegators (at epoch `endEpoch` or less):: `proposal_type` could impose more constraints on this. +Proposals can be submitted by any address as long as the above rules are respected. Votes can be cast only by active validators and delegators (at epoch `endEpoch` or less): the proposal type could impose more constraints on this. Moreover, if delegators are allowed to vote, validators can vote only during the first 2/3 of the voting period (from `startEpoch` and 2/3 of `endEpoch` - `startEpoch`). The preferred content template (`Namada Improvement Proposal schema`) is the following: @@ -100,11 +100,11 @@ where `ProposalVote` is an enum representing a `Yay` or `Nay` vote: the yay vari Vote is valid if it follows these rules: -- vote can be sent only by validator or delegators (also depending on `proposal_type`) +- vote can be sent only by validator or delegators (also depending on the proposal type) - if delegators can vote, validators can vote only during the first 2/3 of the total voting period, delegators can vote for the whole voting period The outcome of a proposal is computed at the epoch specific in the `endEpoch` field and executed at `graceEpoch` field (if it contains a non-empty `proposalCode` field). -A proposal is accepted only if enough `yay` votes (net of the voting power) to match the threshold set in `proposal_type` is reached. +A proposal is accepted only if enough `yay` votes (net of the voting power) to match the threshold set in `ProposalType` is reached. If a proposal gets accepted, the locked funds will be reimbursed to the author. In case it gets rejected, the locked funds will be moved to slash fund. ## Off-chain proposal diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index 5159e5af7b..8ce75441d1 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -21,7 +21,7 @@ Each proposal will be stored in a sub-key under the internal proposal address. T ``` /\$GovernanceAddress/proposal/\$id/content: Vec /\$GovernanceAddress/proposal/\$id/author: Address -/\$GovernanceAddress/proposal/\$id/proposal_type: ProposalType +/\$GovernanceAddress/proposal/\$id/type: ProposalType /\$GovernanceAddress/proposal/\$id/start_epoch: Epoch /\$GovernanceAddress/proposal/\$id/end_epoch: Epoch /\$GovernanceAddress/proposal/\$id/grace_epoch: Epoch @@ -50,12 +50,12 @@ Each proposal will be stored in a sub-key under the internal proposal address. T The `ProposalType` imply different combinations of: -- the optional payload (memo) attached to the vote +- the optional wasm code attached to the proposal - which actors should be allowed to vote (delegators and validators or validators only) - the threshold to be used in the tally process -- the optional wasm code attached to the proposal +- the optional payload (memo) attached to the vote -The correct logic to handle these different types will be hardcoded in protocol. We'll also rely on type checking to strictly enforce the correctness of a proposal given its type. These two approaches combined will prevent a user from deviating from the intended logic for a certain proposal type (e.g. providing a wasm code when it's not needed or allowing only validators to vote when also delegators should, etc...) +The correct logic to handle these different types will be hardcoded in protocol. We'll also rely on type checking to strictly enforce the correctness of a proposal given its type. These two approaches combined will prevent a user from deviating from the intended logic for a certain proposal type (e.g. providing a wasm code when it's not needed or allowing only validators to vote when also delegators should, etc...). More details on the specific types supported can be found in the [relative](#supported-proposal-types) section of this document. `GovernanceAddress` parameters and global storage keys are: @@ -86,13 +86,47 @@ The governance machinery also relies on a subkey stored under the `NAM` token ad This is to leverage the `NAM` VP to check that the funds were correctly locked. The governance subkey, `/\$GovernanceAddress/proposal/\$id/funds` will be used after the tally step to know the exact amount of tokens to refund or move to Treasury. +### Supported proposal types + +At the moment, Namada supports 3 types of governance proposals: + +```rust +pub enum ProposalType { + /// Carries the optional proposal code path + Custom(Option), + PGFCouncil, + ETHBridge, +} +``` + +`Custom` represents a generic proposal with the following properties: + +- Can carry a wasm code to be executed in case the proposal passes +- Allows both validators and delegators to vote +- Requires 2/3 of the total voting power to succeed +- Doesn't expect any memo attached to the votes + +`PGFCouncil` is a specific proposal to elect the council for _Public Goods Funding_: + +- Doesn't carry any wasm code +- Allows both validators and delegators to vote +- Requires 1/3 of the total voting power to vote for the same council +- Expect every vote to carry a memo in the form of a tuple `(Vec
, BudgetCap)` + +`ETHBridge` is aimed at regulating actions on the bridge like the update of the Ethereum smart contracts or the withdrawing of all the funds from the `Vault` : + +- Doesn't carry any wasm code +- Allows only validators to vote +- Requires 2/3 of the validators' total voting power to succeed +- Expect every vote to carry a memo in the form of a tuple `(Action, Signature)` + ### GovernanceAddress VP Just like Pos, also governance has its own storage space. The `GovernanceAddress` validity predicate task is to check the integrity and correctness of new proposals. A proposal, to be correct, must satisfy the following: - Mandatory storage writes are: - counter - author - - proposal_type + - type - funds - voting_start epoch - voting_end epoch @@ -129,7 +163,7 @@ struct Proposal { id: u64, content: Vec, author: Address, - proposal_type: ProposalType, + r#type: ProposalType, votingStartEpoch: Epoch, votingEndEpoch: Epoch, graceEpoch: Epoch, From 6f058e05d440cc758c149118bf9daf53a495665d Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 28 Oct 2022 15:21:32 +0200 Subject: [PATCH 43/47] Fixes pgf council vote format --- documentation/specs/src/base-ledger/governance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index 8ce75441d1..1b46201e82 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -111,7 +111,7 @@ pub enum ProposalType { - Doesn't carry any wasm code - Allows both validators and delegators to vote - Requires 1/3 of the total voting power to vote for the same council -- Expect every vote to carry a memo in the form of a tuple `(Vec
, BudgetCap)` +- Expect every vote to carry a memo in the form of a tuple `Set<(Set
, BudgetCap)>` `ETHBridge` is aimed at regulating actions on the bridge like the update of the Ethereum smart contracts or the withdrawing of all the funds from the `Vault` : From 5454327eea01ba8b069a3fc99cf82b90a1ac40d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 31 Oct 2022 15:54:52 +0100 Subject: [PATCH 44/47] queries: fix unused import in wasm build --- shared/src/ledger/queries/shell.rs | 35 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 62f32c0f87..7491af4945 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -6,10 +6,12 @@ use crate::ledger::queries::{require_latest_height, EncodedResponseQuery}; use crate::ledger::storage::{DBIter, StorageHasher, DB}; use crate::ledger::storage_api::{self, ResultExt, StorageRead}; use crate::types::storage::{self, Epoch, PrefixValue}; +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] use crate::types::transaction::TxResult; #[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] use crate::types::transaction::{DecryptedTx, TxType}; +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] router! {SHELL, // Epoch of the last committed block ( "epoch" ) -> Epoch = epoch, @@ -30,6 +32,24 @@ router! {SHELL, -> bool = storage_has_key, } +#[cfg(not(all(feature = "wasm-runtime", feature = "ferveo-tpke")))] +router! {SHELL, + // Epoch of the last committed block + ( "epoch" ) -> Epoch = epoch, + + // Raw storage access - read value + ( "value" / [storage_key: storage::Key] ) + -> Vec = (with_options storage_value), + + // Raw storage access - prefix iterator + ( "prefix" / [storage_key: storage::Key] ) + -> Vec = (with_options storage_prefix), + + // Raw storage access - is given storage key present? + ( "has_key" / [storage_key: storage::Key] ) + -> bool = storage_has_key, +} + // Handlers: #[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] @@ -68,21 +88,6 @@ where }) } -#[cfg(not(all(feature = "wasm-runtime", feature = "ferveo-tpke")))] -fn dry_run_tx( - _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, -) -> storage_api::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - unimplemented!( - "dry_run_tx request handler requires \"wasm-runtime\" and \ - \"ferveo-tpke\" features enabled." - ) -} - fn epoch(ctx: RequestCtx<'_, D, H>) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, From 27f324cfa46898415328ca41242bec3a739834a5 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 31 Oct 2022 12:24:05 -0400 Subject: [PATCH 45/47] changelog: add #569 --- .changelog/unreleased/improvements/569-rpc-sub-shell.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/569-rpc-sub-shell.md diff --git a/.changelog/unreleased/improvements/569-rpc-sub-shell.md b/.changelog/unreleased/improvements/569-rpc-sub-shell.md new file mode 100644 index 0000000000..96f0a8bd3b --- /dev/null +++ b/.changelog/unreleased/improvements/569-rpc-sub-shell.md @@ -0,0 +1,2 @@ +- Move all shell RPC endpoints under the /shell path. This is a breaking change + to RPC consumers. ([#569](https://github.com/anoma/namada/pull/569)) \ No newline at end of file From 0ad15490b06b36c06928a33d6cb98bb5bffc21b2 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 31 Oct 2022 13:06:30 -0400 Subject: [PATCH 46/47] release.sh: list prerequisites in a comment --- scripts/release.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.sh b/scripts/release.sh index b32ff70764..71f99dd22a 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,4 +1,5 @@ #!/bin/sh +# depends on cargo-release 0.21.4, git 2.24.0 or later, unclog 0.5.0 set -e if [ -z "$1" ]; then From 6cbce4a4dcdd3811679f5eb4aa7c82f51dff74e6 Mon Sep 17 00:00:00 2001 From: "Raymond E. Pasco" Date: Mon, 31 Oct 2022 13:08:17 -0400 Subject: [PATCH 47/47] Namada 0.9.0 --- .../671-add-consensus-commit-timeout.md | 0 .../bug-fixes/702-fix-default-node-logging.md | 0 .../features/658-add-client-block-query.md | 0 .../improvements/553-rpc-queries-router.md | 0 .../improvements/569-rpc-sub-shell.md | 0 .../miscellaneous/632-xan-to-nam.md | 0 .changelog/v0.9.0/summary.md | 1 + CHANGELOG.md | 30 ++++++++++++++++++ Cargo.lock | 18 +++++------ apps/Cargo.toml | 2 +- encoding_spec/Cargo.toml | 2 +- macros/Cargo.toml | 2 +- proof_of_stake/Cargo.toml | 2 +- shared/Cargo.toml | 2 +- tests/Cargo.toml | 2 +- tx_prelude/Cargo.toml | 2 +- vm_env/Cargo.toml | 2 +- vp_prelude/Cargo.toml | 2 +- wasm/Cargo.lock | 20 ++++++------ wasm/checksums.json | 24 +++++++------- wasm/tx_template/Cargo.toml | 2 +- wasm/vp_template/Cargo.toml | 2 +- wasm/wasm_source/Cargo.toml | 2 +- wasm_for_tests/tx_memory_limit.wasm | Bin 135502 -> 135502 bytes wasm_for_tests/tx_mint_tokens.wasm | Bin 237764 -> 238349 bytes wasm_for_tests/tx_proposal_code.wasm | Bin 212270 -> 212408 bytes wasm_for_tests/tx_read_storage_key.wasm | Bin 154943 -> 154884 bytes wasm_for_tests/tx_write_storage_key.wasm | Bin 228606 -> 229035 bytes wasm_for_tests/vp_always_false.wasm | Bin 160341 -> 160689 bytes wasm_for_tests/vp_always_true.wasm | Bin 160683 -> 160689 bytes wasm_for_tests/vp_eval.wasm | Bin 161259 -> 161263 bytes wasm_for_tests/vp_memory_limit.wasm | Bin 162871 -> 163346 bytes wasm_for_tests/vp_read_storage_key.wasm | Bin 176352 -> 176354 bytes wasm_for_tests/wasm_source/Cargo.lock | 16 +++++----- wasm_for_tests/wasm_source/Cargo.toml | 2 +- 35 files changed, 83 insertions(+), 52 deletions(-) rename .changelog/{unreleased => v0.9.0}/bug-fixes/671-add-consensus-commit-timeout.md (100%) rename .changelog/{unreleased => v0.9.0}/bug-fixes/702-fix-default-node-logging.md (100%) rename .changelog/{unreleased => v0.9.0}/features/658-add-client-block-query.md (100%) rename .changelog/{unreleased => v0.9.0}/improvements/553-rpc-queries-router.md (100%) rename .changelog/{unreleased => v0.9.0}/improvements/569-rpc-sub-shell.md (100%) rename .changelog/{unreleased => v0.9.0}/miscellaneous/632-xan-to-nam.md (100%) create mode 100644 .changelog/v0.9.0/summary.md diff --git a/.changelog/unreleased/bug-fixes/671-add-consensus-commit-timeout.md b/.changelog/v0.9.0/bug-fixes/671-add-consensus-commit-timeout.md similarity index 100% rename from .changelog/unreleased/bug-fixes/671-add-consensus-commit-timeout.md rename to .changelog/v0.9.0/bug-fixes/671-add-consensus-commit-timeout.md diff --git a/.changelog/unreleased/bug-fixes/702-fix-default-node-logging.md b/.changelog/v0.9.0/bug-fixes/702-fix-default-node-logging.md similarity index 100% rename from .changelog/unreleased/bug-fixes/702-fix-default-node-logging.md rename to .changelog/v0.9.0/bug-fixes/702-fix-default-node-logging.md diff --git a/.changelog/unreleased/features/658-add-client-block-query.md b/.changelog/v0.9.0/features/658-add-client-block-query.md similarity index 100% rename from .changelog/unreleased/features/658-add-client-block-query.md rename to .changelog/v0.9.0/features/658-add-client-block-query.md diff --git a/.changelog/unreleased/improvements/553-rpc-queries-router.md b/.changelog/v0.9.0/improvements/553-rpc-queries-router.md similarity index 100% rename from .changelog/unreleased/improvements/553-rpc-queries-router.md rename to .changelog/v0.9.0/improvements/553-rpc-queries-router.md diff --git a/.changelog/unreleased/improvements/569-rpc-sub-shell.md b/.changelog/v0.9.0/improvements/569-rpc-sub-shell.md similarity index 100% rename from .changelog/unreleased/improvements/569-rpc-sub-shell.md rename to .changelog/v0.9.0/improvements/569-rpc-sub-shell.md diff --git a/.changelog/unreleased/miscellaneous/632-xan-to-nam.md b/.changelog/v0.9.0/miscellaneous/632-xan-to-nam.md similarity index 100% rename from .changelog/unreleased/miscellaneous/632-xan-to-nam.md rename to .changelog/v0.9.0/miscellaneous/632-xan-to-nam.md diff --git a/.changelog/v0.9.0/summary.md b/.changelog/v0.9.0/summary.md new file mode 100644 index 0000000000..9e2ad72e5c --- /dev/null +++ b/.changelog/v0.9.0/summary.md @@ -0,0 +1 @@ +Namada 0.9.0 is a scheduled minor release. diff --git a/CHANGELOG.md b/CHANGELOG.md index b7095801d5..e4cd01a127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # CHANGELOG +## v0.9.0 + +Namada 0.9.0 is a scheduled minor release. + +### BUG FIXES + +- Add back consensus commit timeout configuration set in tendermint + ([#671](https://github.com/anoma/namada/pull/671)) +- Fix info logs to show by default for namadan + ([#702](https://github.com/anoma/namada/pull/702)) + +### FEATURES + +- Client: Add a command to query the last committed block's hash, height and + timestamp. ([#658](https://github.com/anoma/namada/issues/658)) + +### IMPROVEMENTS + +- Replace the handcrafted RPC paths with a new `router!` macro RPC queries + definition that handles dynamic path matching, type-safe handler function + dispatch and also generates type-safe client methods for the queries. + ([#553](https://github.com/anoma/namada/pull/553)) +- Move all shell RPC endpoints under the /shell path. This is a breaking change + to RPC consumers. ([#569](https://github.com/anoma/namada/pull/569)) + +### MISCELLANEOUS + +- Renamed native token from XAN to NAM + ([#632](https://github.com/anoma/namada/pull/632)) + ## v0.8.1 Namada 0.8.1 is a point release focused on standardizing Tendermint diff --git a/Cargo.lock b/Cargo.lock index d8c0db72d6..3d7588b5b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2911,7 +2911,7 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "namada" -version = "0.8.1" +version = "0.9.0" dependencies = [ "ark-bls12-381", "ark-ec", @@ -2978,7 +2978,7 @@ dependencies = [ [[package]] name = "namada_apps" -version = "0.8.1" +version = "0.9.0" dependencies = [ "ark-serialize", "ark-std", @@ -3061,7 +3061,7 @@ dependencies = [ [[package]] name = "namada_encoding_spec" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "itertools", @@ -3072,7 +3072,7 @@ dependencies = [ [[package]] name = "namada_macros" -version = "0.8.1" +version = "0.9.0" dependencies = [ "quote", "syn", @@ -3080,7 +3080,7 @@ dependencies = [ [[package]] name = "namada_proof_of_stake" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "derivative", @@ -3090,7 +3090,7 @@ dependencies = [ [[package]] name = "namada_tests" -version = "0.8.1" +version = "0.9.0" dependencies = [ "assert_cmd", "borsh", @@ -3124,7 +3124,7 @@ dependencies = [ [[package]] name = "namada_tx_prelude" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", @@ -3136,7 +3136,7 @@ dependencies = [ [[package]] name = "namada_vm_env" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", @@ -3144,7 +3144,7 @@ dependencies = [ [[package]] name = "namada_vp_prelude" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 50c1f5b065..87ad660beb 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -6,7 +6,7 @@ license = "GPL-3.0" name = "namada_apps" readme = "../README.md" resolver = "2" -version = "0.8.1" +version = "0.9.0" default-run = "namada" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/encoding_spec/Cargo.toml b/encoding_spec/Cargo.toml index e6ca933d05..a62dbd2e40 100644 --- a/encoding_spec/Cargo.toml +++ b/encoding_spec/Cargo.toml @@ -6,7 +6,7 @@ license = "GPL-3.0" name = "namada_encoding_spec" readme = "../README.md" resolver = "2" -version = "0.8.1" +version = "0.9.0" [features] default = [] diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 0061419311..0021720010 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "namada_macros" resolver = "2" -version = "0.8.1" +version = "0.9.0" [lib] proc-macro = true diff --git a/proof_of_stake/Cargo.toml b/proof_of_stake/Cargo.toml index 82522bc3d6..d6ee686121 100644 --- a/proof_of_stake/Cargo.toml +++ b/proof_of_stake/Cargo.toml @@ -6,7 +6,7 @@ license = "GPL-3.0" name = "namada_proof_of_stake" readme = "../README.md" resolver = "2" -version = "0.8.1" +version = "0.9.0" [features] default = [] diff --git a/shared/Cargo.toml b/shared/Cargo.toml index d05e002230..87bae5ef94 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "namada" resolver = "2" -version = "0.8.1" +version = "0.9.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/tests/Cargo.toml b/tests/Cargo.toml index dc3bf7b8aa..74056ba00b 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" license = "GPL-3.0" name = "namada_tests" resolver = "2" -version = "0.8.1" +version = "0.9.0" [features] default = ["wasm-runtime"] diff --git a/tx_prelude/Cargo.toml b/tx_prelude/Cargo.toml index 992a2146e1..11449b9460 100644 --- a/tx_prelude/Cargo.toml +++ b/tx_prelude/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "namada_tx_prelude" resolver = "2" -version = "0.8.1" +version = "0.9.0" [features] default = [] diff --git a/vm_env/Cargo.toml b/vm_env/Cargo.toml index f2c8854f85..d3da9c79e0 100644 --- a/vm_env/Cargo.toml +++ b/vm_env/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "namada_vm_env" resolver = "2" -version = "0.8.1" +version = "0.9.0" [features] default = ["abciplus"] diff --git a/vp_prelude/Cargo.toml b/vp_prelude/Cargo.toml index f270a17d9f..15a0741a81 100644 --- a/vp_prelude/Cargo.toml +++ b/vp_prelude/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "namada_vp_prelude" resolver = "2" -version = "0.8.1" +version = "0.9.0" [features] default = [] diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 42b7925c41..9d63d52b84 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1353,7 +1353,7 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "namada" -version = "0.8.1" +version = "0.9.0" dependencies = [ "ark-bls12-381", "ark-serialize", @@ -1405,7 +1405,7 @@ dependencies = [ [[package]] name = "namada_macros" -version = "0.8.1" +version = "0.9.0" dependencies = [ "quote", "syn", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "namada_proof_of_stake" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "derivative", @@ -1423,7 +1423,7 @@ dependencies = [ [[package]] name = "namada_tests" -version = "0.8.1" +version = "0.9.0" dependencies = [ "chrono", "concat-idents", @@ -1442,7 +1442,7 @@ dependencies = [ [[package]] name = "namada_tx_prelude" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", @@ -1454,7 +1454,7 @@ dependencies = [ [[package]] name = "namada_vm_env" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", @@ -1462,7 +1462,7 @@ dependencies = [ [[package]] name = "namada_vp_prelude" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", @@ -1474,7 +1474,7 @@ dependencies = [ [[package]] name = "namada_wasm" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "getrandom", @@ -2529,7 +2529,7 @@ dependencies = [ [[package]] name = "tx_template" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "getrandom", @@ -2582,7 +2582,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vp_template" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "getrandom", diff --git a/wasm/checksums.json b/wasm/checksums.json index 0c800987ae..496d1c7a0f 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,15 +1,15 @@ { - "tx_bond.wasm": "tx_bond.e7f429f1c6a74e495b63536d6fb7edaf9d5a974a526a96c9b872454adaa77873.wasm", - "tx_ibc.wasm": "tx_ibc.76c60067e0e9d3bd4917155276a37224ace3459aa329b4914de7a82f5367c7d6.wasm", - "tx_init_account.wasm": "tx_init_account.488131fce36a8b551a932906ded4be4097558f3242270476bd7ccdea964c3112.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.7062643ffde883370f74f91cd1c86d04e60b8d2c1a8d37da781c6f36423c5da7.wasm", - "tx_init_validator.wasm": "tx_init_validator.2809120146e218c1af8f55bb9d1ece9191e1e6d10df6d5c495f3a97ec64b17b5.wasm", - "tx_transfer.wasm": "tx_transfer.0f4f9756a7819f7cb9ef8395958135144b9fdbe05ec86cb2b4f4f607cea268b8.wasm", - "tx_unbond.wasm": "tx_unbond.f8f920433b5baa6fd79f12fd102677a46ffa80bc7915d3a322305a3c3035d385.wasm", + "tx_bond.wasm": "tx_bond.04d6847800dad11990b42e8f2981a4a79d06d6d0c981c3d70c929e5b6a4f348b.wasm", + "tx_ibc.wasm": "tx_ibc.6ab530398ed8e276a8af7f231edbfae984b7e84eeb854714ba9339c5bed9d330.wasm", + "tx_init_account.wasm": "tx_init_account.578d987351e6ae42baa7849ae167e3ba33f3a62dba51cd47b0fa6d3ea6e4f128.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.71e27610210622fa53c3de58351761cca839681a4f450d4eff6b46bde3ae85a5.wasm", + "tx_init_validator.wasm": "tx_init_validator.269f065ff683782db2fdcac6e2485e80cbebb98929671a42eeb01703e0bbd8f5.wasm", + "tx_transfer.wasm": "tx_transfer.784325cf7763faf8d75797960cda6fbabbd343f3c6f7e6785f60f5e0911a6bb5.wasm", + "tx_unbond.wasm": "tx_unbond.ed13fa636d138ac4e35f2b4f31a6b4d3bed67e6b998dc6325f90711a2aca3704.wasm", "tx_update_vp.wasm": "tx_update_vp.c4050e597116203eba5afde946a014afb067bdeaaae417377214a80c38a3786b.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.f0c331e253cdcaa74fcfd9842f310eaf672b75fa92bd317c61a5881c9860706c.wasm", - "tx_withdraw.wasm": "tx_withdraw.77a3ceca1a3bff148b97191a54e64791f9a1f6c2f96d6f0ea9a5a6c2e5cff207.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.65780c1dc01240273feecb28e3e8669302429ea6571e44b0b344b1971a6ef234.wasm", - "vp_token.wasm": "vp_token.45c712bff1f49ee0fffc3fd7b5acdf2b838e2baf875a821b304faeb54ca2ed74.wasm", - "vp_user.wasm": "vp_user.fae7699c34cf9fac656454ec6fc4009065ea19027d4b3dd45d3534e5b1be266f.wasm" + "tx_vote_proposal.wasm": "tx_vote_proposal.ece325881aad1c8a29f715c2f435c3335e08e51eed837c00ce0f7bbaddbefe50.wasm", + "tx_withdraw.wasm": "tx_withdraw.408fc10b3744c398258124e5e48e3449f6baf82a263df26911586a3382fbceb9.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.ae9a681dc2c1bd244b0575474fa4a364af56fa75833950693ca52ab25018c97d.wasm", + "vp_token.wasm": "vp_token.468de153dc5ce3af208bd762de3e85be48bc631012ec5f0947af95168da6cb93.wasm", + "vp_user.wasm": "vp_user.c101016a85a72f40da7f33e5d9061cfd2e3274eaac75a71c59c9ab4ed9896ffd.wasm" } \ No newline at end of file diff --git a/wasm/tx_template/Cargo.toml b/wasm/tx_template/Cargo.toml index a86dcbb07a..038afdc488 100644 --- a/wasm/tx_template/Cargo.toml +++ b/wasm/tx_template/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "tx_template" resolver = "2" -version = "0.8.1" +version = "0.9.0" [lib] crate-type = ["cdylib"] diff --git a/wasm/vp_template/Cargo.toml b/wasm/vp_template/Cargo.toml index ca5ff03922..037fa2590a 100644 --- a/wasm/vp_template/Cargo.toml +++ b/wasm/vp_template/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "vp_template" resolver = "2" -version = "0.8.1" +version = "0.9.0" [lib] crate-type = ["cdylib"] diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index c2a572d46c..f306196c39 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "namada_wasm" resolver = "2" -version = "0.8.1" +version = "0.9.0" [lib] crate-type = ["cdylib"] diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index dd2372605d64f675a874dd0604fa352f0803151c..73ccad15877107760d2d5d861cd9f64ffd87099f 100755 GIT binary patch delta 582 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 diff --git a/wasm_for_tests/tx_proposal_code.wasm b/wasm_for_tests/tx_proposal_code.wasm index 47f531479d56c1d3bb155054de3a96a4222e7dac..842332c6867562563294f41a6da6232459dbff63 100755 GIT binary patch delta 30589 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 delta 30358 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 delta 6193 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 diff --git a/wasm_for_tests/tx_write_storage_key.wasm b/wasm_for_tests/tx_write_storage_key.wasm index 44a87a74972b6bb06592b0a648b5cc5c73ae41bb..1e640ff9c9978dfa07872091dcf3c982cd1226a2 100755 GIT binary patch delta 14807 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 UGLx8pM`|6zrEX5G(>^czKdU2qrT_o{ delta 14266 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 delta 16718 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 diff --git a/wasm_for_tests/vp_always_true.wasm b/wasm_for_tests/vp_always_true.wasm index 8b3edc3de407b0a3aba8e31a4b0bf13ff573b80d..250c422f9beb6a719bf998964ff75aebef9f79f5 100755 GIT binary patch delta 2092 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 L<*$dA0l)ei63;rC delta 1449 zcma)4TTEP46g~UgJ9K95oiaeh!tl7XJO*24XnD*7I>Vg@!!RfWrAnN(js_@`R2xBx zq>(D5<)a4c{%8|x0uL=#Q4F-CHbpBO&QQgkDsNr=K8Le0_E>9MbM(=c_Q#*-*zYutXyb6bHrK=y zrm|YjB$)-i|LwxJ6U+B6HA^B>@qVNbeeN>Yi{0)asKvYPIjF|BJeMdE^qzuRZON-} z7{hz@$H0IG{SmUW;e3lh0d<>~8R@t(i9@~}AmfQZHY}pm=LQj@zGAY^`y5=7aLQL> z;gXuGg4M7C!j=>v#{)tO)TO9^B2crkBq@Soh=~pVNVNS25x?`>WP>7X)DtnSp<05g zif}ccoE&ut7-|^K7`w2*6t3Q4Nvknz0ojaSHq`l3qZX5-%Bo0B3t@{u-SBcjadAe< z#w1bgNjul~x}HRn3HW*>Z=0;=n-p&6#?>a$WRrwPJF@Yb+K31IJte8jOPyH(y}^Jm zS|rLD>-#(#@X&6T1I75sJ_!eX>6jjrz>2nDUAmh(;w?p@UaHAr#I9gtgFzyd^+}6F z6^ab9D6P$@fb{`J*uv>&08OAUMRajd-XbylDY!ptf_9_2)>!JN)8tHJd#FSk3LOG| zWm5YlYz2N|3V&{l>W4dWF=vzFPO9gobOzC^CMIbGO|v=)KaY0v8`J1+R_S*QHD9Bj zRvw$7`xmhRdeCAmH3Gib+E4#*$(yI`YzrB{r5)~Y2ztqLPvx$v+~}-?MeT>qw$(;l zPiF#rjm`0PvTw&Pf(t_jKBIq|b+8`jXghQoz>jyH=^`6Uyb6BpqeP7!`s3Nt>>F@|St-ZAtQad>?OERuwy#PL(9CVUh9&87nR{ZiiW^!(i9499- z>XpEQuZ{b_qkTC3im$05jL8I%@Gv2paNcTV+v&_9CGpwt;)O=kx)!(SqLb?CH*2K+Imam8rBeiZ5V_Wg) zO&hx2zwG=s{U0;e^mh`Yn6N&}*}E}X(C|$D_QXJMPolre@nRng!ANbTKNo`D(}bye*@gB99O@_O}bi=3_a#DH43Cum6Q3Y?Y&{>8Q}|&AYhA jL02cClAud&Jny0VUP9G+oDK$r&HR!0!h7)rz<2)w$w)$H diff --git a/wasm_for_tests/vp_memory_limit.wasm b/wasm_for_tests/vp_memory_limit.wasm index 81f3ff61cfc51352378107c4921e0dba424c4898..dd7c2dc59d5f865d78e77fe00fd6e7c75b55a3a2 100755 GIT binary patch delta 18984 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 delta 18564 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

%$JE$H4SaU*1HL_#`ZmAZd5^f*Z3Ps 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 diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 8a556a4f23..1cbb2ff069 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1353,7 +1353,7 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "namada" -version = "0.8.1" +version = "0.9.0" dependencies = [ "ark-bls12-381", "ark-serialize", @@ -1405,7 +1405,7 @@ dependencies = [ [[package]] name = "namada_macros" -version = "0.8.1" +version = "0.9.0" dependencies = [ "quote", "syn", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "namada_proof_of_stake" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "derivative", @@ -1423,7 +1423,7 @@ dependencies = [ [[package]] name = "namada_tests" -version = "0.8.1" +version = "0.9.0" dependencies = [ "chrono", "concat-idents", @@ -1442,7 +1442,7 @@ dependencies = [ [[package]] name = "namada_tx_prelude" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", @@ -1454,7 +1454,7 @@ dependencies = [ [[package]] name = "namada_vm_env" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", @@ -1462,7 +1462,7 @@ dependencies = [ [[package]] name = "namada_vp_prelude" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "namada", @@ -1474,7 +1474,7 @@ dependencies = [ [[package]] name = "namada_wasm_for_tests" -version = "0.8.1" +version = "0.9.0" dependencies = [ "borsh", "getrandom", diff --git a/wasm_for_tests/wasm_source/Cargo.toml b/wasm_for_tests/wasm_source/Cargo.toml index 4d0b4c1b77..07d4f3a491 100644 --- a/wasm_for_tests/wasm_source/Cargo.toml +++ b/wasm_for_tests/wasm_source/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "namada_wasm_for_tests" resolver = "2" -version = "0.8.1" +version = "0.9.0" [lib] crate-type = ["cdylib"]