diff --git a/.changelog/unreleased/improvements/4901-comet-tx-indexer.md b/.changelog/unreleased/improvements/4901-comet-tx-indexer.md new file mode 100644 index 00000000000..8ae1922d77f --- /dev/null +++ b/.changelog/unreleased/improvements/4901-comet-tx-indexer.md @@ -0,0 +1,3 @@ +- Added tx event with CometBFT matching tx hash, which can be used to query + CometBFT tx indexer (if enabled) ([\#4901](https://github.com/namada- + net/namada/pull/4901)) \ No newline at end of file diff --git a/.changelog/unreleased/miscellaneous/4618-comet-0.38.md b/.changelog/unreleased/miscellaneous/4618-comet-0.38.md new file mode 100644 index 00000000000..436de288221 --- /dev/null +++ b/.changelog/unreleased/miscellaneous/4618-comet-0.38.md @@ -0,0 +1 @@ +- Updated CometBFT to 0.38 ([\#4618](https://github.com/anoma/namada/pull/4618)) \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 201ccd6775f..0a0afc59110 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: rust-docs: container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 runs-on: [self-hosted, 4vcpu-8ram-ubuntu22-namada-x86] timeout-minutes: 20 @@ -91,7 +91,7 @@ jobs: lints: container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 runs-on: [self-hosted, 8vcpu-16ram-ubuntu22-namada-x86] timeout-minutes: 15 @@ -129,7 +129,7 @@ jobs: timeout-minutes: 10 runs-on: [self-hosted, 4vcpu-8ram-ubuntu22-namada-x86] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 strategy: fail-fast: true matrix: @@ -176,7 +176,7 @@ jobs: test-wasm: timeout-minutes: 30 container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 runs-on: [self-hosted, 4vcpu-8ram-ubuntu22-namada-x86] needs: [build-wasm] @@ -223,7 +223,7 @@ jobs: test-unit: runs-on: [self-hosted, 8vcpu-16ram-ubuntu22-namada-x86] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 timeout-minutes: 30 needs: [build-wasm] @@ -276,7 +276,7 @@ jobs: check-packages: runs-on: [self-hosted, 8vcpu-16ram-ubuntu22-namada-x86] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 timeout-minutes: 15 steps: @@ -312,7 +312,7 @@ jobs: test-integration: runs-on: [self-hosted, 16vcpu-32ram-ubuntu22-namada-x86] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 timeout-minutes: 120 needs: [build-wasm] @@ -365,7 +365,7 @@ jobs: check-benchmarks: runs-on: [self-hosted, 16vcpu-32ram-ubuntu22-namada-x86] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 if: github.event.pull_request.draft == false || contains(github.head_ref, 'mergify/merge-queue') || contains(github.ref_name, 'mergify/merge-queue') timeout-minutes: 35 needs: [build-wasm] @@ -413,7 +413,7 @@ jobs: build-binaries: runs-on: [self-hosted, 16vcpu-32ram-ubuntu22-namada-x86] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 timeout-minutes: 25 steps: @@ -464,7 +464,7 @@ jobs: test-e2e: runs-on: [self-hosted, 4vcpu-8ram-ubuntu22-namada-x86] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 if: github.event.pull_request.draft == false || contains(github.head_ref, 'mergify/merge-queue') || contains(github.ref_name, 'mergify/merge-queue') needs: [build-wasm, build-binaries] timeout-minutes: 50 @@ -635,7 +635,7 @@ jobs: test-e2e-with-device-automation: runs-on: [self-hosted, 4vcpu-8ram-ubuntu22-namada-x86] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 if: github.event.pull_request.draft == false || contains(github.head_ref, 'mergify/merge-queue') || contains(github.ref_name, 'mergify/merge-queue') needs: [build-wasm, build-binaries] timeout-minutes: 50 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ac43611e24..dba2ffc5ab6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: wasm: runs-on: [ubuntu-latest] container: - image: ghcr.io/heliaxdev/namada-ci:namada-v2.3.0 + image: ghcr.io/heliaxdev/namada-ci:namada-v3.0.0-alpha.1 steps: - name: Checkout repo uses: actions/checkout@v4 @@ -114,4 +114,4 @@ jobs: draft: true files: ./**/*.tar.gz tag_name: ${{ steps.get_version.outputs.version }} - name: Namada ${{ steps.get_version.outputs.version-without-v }} \ No newline at end of file + name: Namada ${{ steps.get_version.outputs.version-without-v }} diff --git a/.github/workflows/scripts/hermes.txt b/.github/workflows/scripts/hermes.txt index feaae22bac7..7d846401c53 100644 --- a/.github/workflows/scripts/hermes.txt +++ b/.github/workflows/scripts/hermes.txt @@ -1 +1 @@ -1.13.0 +0.0.1-namada-comet-0.38 diff --git a/Cargo.lock b/Cargo.lock index ef51643cb77..f63cd8b80db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5850,47 +5850,6 @@ dependencies = [ "namada_tx", ] -[[package]] -name = "namada_ethereum_bridge" -version = "0.251.0" -dependencies = [ - "assert_matches", - "borsh", - "data-encoding", - "ethabi", - "ethers", - "eyre", - "itertools 0.14.0", - "konst", - "linkme", - "namada_account", - "namada_core", - "namada_events", - "namada_gas", - "namada_governance", - "namada_macros", - "namada_migrations", - "namada_parameters", - "namada_proof_of_stake", - "namada_state", - "namada_storage", - "namada_systems", - "namada_token", - "namada_trans_token", - "namada_tx", - "namada_vm", - "namada_vote_ext", - "namada_vp", - "namada_vp_env", - "proptest", - "rand", - "serde", - "smooth-operator", - "thiserror 2.0.12", - "toml", - "tracing", -] - [[package]] name = "namada_events" version = "0.251.0" @@ -6054,19 +6013,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "namada_light_sdk" -version = "0.251.0" -dependencies = [ - "borsh", - "namada_sdk", - "prost 0.13.5", - "serde_json", - "tendermint-config", - "tendermint-rpc", - "tokio", -] - [[package]] name = "namada_macros" version = "0.251.0" @@ -6141,7 +6087,6 @@ dependencies = [ "namada_sdk", "namada_test_utils", "namada_vm", - "namada_vote_ext", "namada_vp", "num-rational", "num-traits", @@ -6261,7 +6206,6 @@ dependencies = [ "nam-jubjub", "namada_account", "namada_core", - "namada_ethereum_bridge", "namada_events", "namada_gas", "namada_governance", @@ -6276,7 +6220,6 @@ dependencies = [ "namada_token", "namada_tx", "namada_vm", - "namada_vote_ext", "namada_vp", "namada_wallet", "num-traits", @@ -6646,20 +6589,6 @@ dependencies = [ "namada_core", ] -[[package]] -name = "namada_vote_ext" -version = "0.251.0" -dependencies = [ - "borsh", - "data-encoding", - "linkme", - "namada_core", - "namada_macros", - "namada_migrations", - "namada_tx", - "serde", -] - [[package]] name = "namada_vp" version = "0.251.0" diff --git a/Cargo.toml b/Cargo.toml index e3196c576ad..dbcd2b5b1ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,11 @@ members = [ "crates/controller", "crates/core", "crates/encoding_spec", - "crates/ethereum_bridge", "crates/events", "crates/gas", "crates/governance", "crates/ibc", "crates/io", - "crates/light_sdk", "crates/macros", "crates/migrations", "crates/merkle_tree", @@ -37,7 +35,6 @@ members = [ "crates/tx_prelude", "crates/vm", "crates/vm_env", - "crates/vote_ext", "crates/vp", "crates/vp_env", "crates/vp_prelude", @@ -71,13 +68,11 @@ namada_apps_lib = { version = "0.251.0", path = "crates/apps_lib" } namada_controller = { version = "0.251.0", path = "crates/controller" } namada_core = { version = "0.251.0", path = "crates/core" } namada_encoding_spec = { version = "0.251.0", path = "crates/encoding_spec" } -namada_ethereum_bridge = { version = "0.251.0", path = "crates/ethereum_bridge" } namada_events = { version = "0.251.0", path = "crates/events" } namada_gas = { version = "0.251.0", path = "crates/gas" } namada_governance = { version = "0.251.0", path = "crates/governance" } namada_ibc = { version = "0.251.0", path = "crates/ibc" } namada_io = { version = "0.251.0", path = "crates/io" } -namada_light_sdk = { version = "0.251.0", path = "crates/light_sdk" } namada_macros = { version = "0.251.0", path = "crates/macros" } namada_migrations = { version = "0.251.0", path = "crates/migrations" } namada_merkle_tree = { version = "0.251.0", path = "crates/merkle_tree" } @@ -99,7 +94,6 @@ namada_tx_env = { version = "0.251.0", path = "crates/tx_env" } namada_tx_prelude = { version = "0.251.0", path = "crates/tx_prelude" } namada_vm = { version = "0.251.0", path = "crates/vm", default-features = false } namada_vm_env = { version = "0.251.0", path = "crates/vm_env" } -namada_vote_ext = { version = "0.251.0", path = "crates/vote_ext" } namada_vp = { version = "0.251.0", path = "crates/vp" } namada_vp_env = { version = "0.251.0", path = "crates/vp_env" } namada_vp_prelude = { version = "0.251.0", path = "crates/vp_prelude" } diff --git a/Makefile b/Makefile index 49f2e04431b..b2495fd9069 100644 --- a/Makefile +++ b/Makefile @@ -36,12 +36,10 @@ crates += namada_apps_lib crates += namada_benchmarks crates += namada_core crates += namada_encoding_spec -crates += namada_ethereum_bridge crates += namada_events crates += namada_gas crates += namada_governance crates += namada_ibc -crates += namada_light_sdk crates += namada_macros crates += namada_merkle_tree crates += namada_parameters @@ -61,7 +59,6 @@ crates += namada_tx_env crates += namada_tx_prelude crates += namada_vm crates += namada_vm_env -crates += namada_vote_ext crates += namada_vp crates += namada_vp_env crates += namada_vp_prelude diff --git a/crates/apps/Cargo.toml b/crates/apps/Cargo.toml index 29b534de9c5..a7b5ad187d5 100644 --- a/crates/apps/Cargo.toml +++ b/crates/apps/Cargo.toml @@ -53,7 +53,6 @@ default = ["migrations"] mainnet = ["namada_apps_lib/mainnet"] jemalloc = ["namada_node/jemalloc"] migrations = ["namada_apps_lib/migrations"] -namada-eth-bridge = ["namada_apps_lib/namada-eth-bridge"] [dependencies] namada_apps_lib.workspace = true diff --git a/crates/apps/src/bin/namada/cli.rs b/crates/apps/src/bin/namada/cli.rs index a6dd33a1501..9a929a5fba1 100644 --- a/crates/apps/src/bin/namada/cli.rs +++ b/crates/apps/src/bin/namada/cli.rs @@ -23,7 +23,6 @@ fn handle_command(cmd: cli::cmds::Namada, raw_sub_cmd: String) -> Result<()> { cmd, cli::cmds::Namada::Node(_) | cli::cmds::Namada::Client(_) - | cli::cmds::Namada::Relayer(_) | cli::cmds::Namada::Wallet(_) ); @@ -58,9 +57,6 @@ fn handle_command(cmd: cli::cmds::Namada, raw_sub_cmd: String) -> Result<()> { handle_subcommand("namadac", sub_args) } cli::cmds::Namada::Wallet(_) => handle_subcommand("namadaw", sub_args), - cli::cmds::Namada::Relayer(_) | cli::cmds::Namada::EthBridgePool(_) => { - handle_subcommand("namadar", sub_args) - } cli::cmds::Namada::Complete(cli::cmds::Complete( cli::args::Complete { shell }, )) => { @@ -75,7 +71,6 @@ fn handle_command(cmd: cli::cmds::Namada, raw_sub_cmd: String) -> Result<()> { (cli::namada_node_app(version), "namadan"), (cli::namada_client_app(version), "namadac"), (cli::namada_wallet_app(version), "namadaw"), - (cli::namada_relayer_app(version), "namadar"), ] { match shell { cli::args::Shell::Bash => { diff --git a/crates/apps_lib/Cargo.toml b/crates/apps_lib/Cargo.toml index f34e6711693..a143cd7bc31 100644 --- a/crates/apps_lib/Cargo.toml +++ b/crates/apps_lib/Cargo.toml @@ -20,7 +20,6 @@ mainnet = ["namada_sdk/mainnet"] testing = ["lazy_static", "namada_sdk/testing"] benches = ["lazy_static", "namada_sdk/benches"] migrations = ["namada_migrations", "namada_sdk/migrations", "linkme"] -namada-eth-bridge = ["namada_sdk/namada-eth-bridge"] [dependencies] namada_core.workspace = true diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index 32ec3e07b05..10721c1f5b4 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -9,13 +9,11 @@ pub mod api; pub mod client; pub mod context; -pub mod relayer; mod utils; pub mod wallet; use clap::{ArgGroup, ArgMatches, ColorChoice}; use color_eyre::eyre::Result; -use namada_sdk::io::StdIo; use utils::*; pub use utils::{Cmd, safe_exit}; @@ -28,14 +26,11 @@ const APP_NAME: &str = "Namada"; const NODE_CMD: &str = "node"; const CLIENT_CMD: &str = "client"; const WALLET_CMD: &str = "wallet"; -const RELAYER_CMD: &str = "relayer"; pub mod cmds { use super::args::CliTypes; use super::utils::*; - use super::{ - ArgMatches, CLIENT_CMD, NODE_CMD, RELAYER_CMD, WALLET_CMD, args, - }; + use super::{ArgMatches, CLIENT_CMD, NODE_CMD, WALLET_CMD, args}; use crate::wrap; /// Commands for `namada` binary. @@ -44,16 +39,12 @@ pub mod cmds { pub enum Namada { // Sub-binary-commands Node(NamadaNode), - Relayer(NamadaRelayer), Client(NamadaClient), Wallet(NamadaWallet), // Inlined commands from the node. Ledger(Ledger), - // Inlined commands from the relayer. - EthBridgePool(EthBridgePool), - // Inlined commands from the client. TxCustom(TxCustom), TxTransparentTransfer(TxTransparentTransfer), @@ -178,47 +169,6 @@ pub mod cmds { } } - /// Used as top-level commands (`Cmd` instance) in `namadar` binary. - /// Used as sub-commands (`SubCmd` instance) in `namada` binary. - #[derive(Clone, Debug)] - #[allow(clippy::large_enum_variant)] - pub enum NamadaRelayer { - EthBridgePool(EthBridgePool), - ValidatorSet(ValidatorSet), - } - - impl Cmd for NamadaRelayer { - fn add_sub(app: App) -> App { - app.subcommand(EthBridgePool::def()) - .subcommand(ValidatorSet::def()) - } - - fn parse(matches: &ArgMatches) -> Option { - let eth_bridge_pool = - SubCmd::parse(matches).map(Self::EthBridgePool); - let validator_set = SubCmd::parse(matches).map(Self::ValidatorSet); - eth_bridge_pool.or(validator_set) - } - } - - impl SubCmd for NamadaRelayer { - const CMD: &'static str = RELAYER_CMD; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .and_then(::parse) - } - - fn def() -> App { - ::add_sub( - App::new(Self::CMD) - .about(wrap!("Relayer sub-commands.")) - .subcommand_required(true), - ) - } - } - /// Used as top-level commands (`Cmd` instance) in `namadac` binary. /// Used as sub-commands (`SubCmd` instance) in `namada` binary. #[derive(Clone, Debug)] @@ -263,8 +213,6 @@ pub mod cmds { .subcommand(TxCommissionRateChange::def().display_order(2)) .subcommand(TxChangeConsensusKey::def().display_order(2)) .subcommand(TxMetadataChange::def().display_order(2)) - // Ethereum bridge transactions - .subcommand(AddToEthBridgePool::def().display_order(3)) // PGF transactions .subcommand(TxUpdateStewardCommission::def().display_order(4)) .subcommand(TxResignSteward::def().display_order(4)) @@ -402,8 +350,6 @@ pub mod cmds { let query_metadata = Self::parse_with_ctx(matches, QueryMetaData); let query_ibc_rate_limit = Self::parse_with_ctx(matches, QueryIbcRateLimit); - let add_to_eth_bridge_pool = - Self::parse_with_ctx(matches, AddToEthBridgePool); let shielded_sync = Self::parse_with_ctx(matches, ShieldedSync); let gen_ibc_shielding = Self::parse_with_ctx(matches, GenIbcShieldingTransfer); @@ -433,7 +379,6 @@ pub mod cmds { .or(withdraw) .or(redelegate) .or(claim_rewards) - .or(add_to_eth_bridge_pool) .or(tx_update_steward_commission) .or(tx_resign_steward) .or(query_epoch) @@ -530,7 +475,6 @@ pub mod cmds { Withdraw(Withdraw), ClaimRewards(ClaimRewards), Redelegate(Redelegate), - AddToEthBridgePool(AddToEthBridgePool), TxUpdateStewardCommission(TxUpdateStewardCommission), TxResignSteward(TxResignSteward), QueryEpoch(QueryEpoch), @@ -2922,415 +2866,6 @@ pub mod cmds { } } - /// Used as sub-commands (`SubCmd` instance) in `namadar` binary. - #[derive(Clone, Debug)] - pub enum EthBridgePool { - /// The [`super::Context`] provides access to the wallet and the - /// config. It will generate a new wallet and config, if they - /// don't exist. - WithContext(EthBridgePoolWithCtx), - /// Utils don't have [`super::Context`], only the global arguments. - WithoutContext(EthBridgePoolWithoutCtx), - } - - /// Ethereum Bridge pool commands requiring [`super::Context`]. - #[derive(Clone, Debug)] - pub enum EthBridgePoolWithCtx { - /// Get a recommendation on a batch of transfers - /// to relay. - RecommendBatch(RecommendBatch), - } - - /// Ethereum Bridge pool commands not requiring [`super::Context`]. - #[derive(Clone, Debug)] - pub enum EthBridgePoolWithoutCtx { - /// Construct a proof that a set of transfers is in the pool. - /// This can be used to relay transfers across the - /// bridge to Ethereum. - ConstructProof(ConstructProof), - /// Construct and relay a Bridge pool proof to - /// Ethereum directly. - RelayProof(RelayProof), - /// Query the contents of the pool. - QueryPool(QueryEthBridgePool), - /// Query to provable contents of the pool. - QuerySigned(QuerySignedBridgePool), - /// Check the confirmation status of `TransferToEthereum` - /// events. - QueryRelays(QueryRelayProgress), - } - - impl Cmd for EthBridgePool { - fn add_sub(app: App) -> App { - app.subcommand(RecommendBatch::def().display_order(1)) - .subcommand(ConstructProof::def().display_order(1)) - .subcommand(RelayProof::def().display_order(1)) - .subcommand(QueryEthBridgePool::def().display_order(1)) - .subcommand(QuerySignedBridgePool::def().display_order(1)) - .subcommand(QueryRelayProgress::def().display_order(1)) - } - - fn parse(matches: &ArgMatches) -> Option { - use EthBridgePoolWithCtx::*; - use EthBridgePoolWithoutCtx::*; - - let recommend = Self::parse_with_ctx(matches, RecommendBatch); - let construct_proof = - Self::parse_without_ctx(matches, ConstructProof); - let relay_proof = Self::parse_without_ctx(matches, RelayProof); - let query_pool = Self::parse_without_ctx(matches, QueryPool); - let query_signed = Self::parse_without_ctx(matches, QuerySigned); - let query_relays = Self::parse_without_ctx(matches, QueryRelays); - - construct_proof - .or(recommend) - .or(relay_proof) - .or(query_pool) - .or(query_signed) - .or(query_relays) - } - } - - impl EthBridgePool { - /// A helper method to parse sub cmds with context - fn parse_with_ctx( - matches: &ArgMatches, - sub_to_self: impl Fn(T) -> EthBridgePoolWithCtx, - ) -> Option { - T::parse(matches).map(|sub| Self::WithContext(sub_to_self(sub))) - } - - /// A helper method to parse sub cmds without context - fn parse_without_ctx( - matches: &ArgMatches, - sub_to_self: impl Fn(T) -> EthBridgePoolWithoutCtx, - ) -> Option { - T::parse(matches).map(|sub| Self::WithoutContext(sub_to_self(sub))) - } - } - - impl SubCmd for EthBridgePool { - const CMD: &'static str = "ethereum-bridge-pool"; - - fn parse(matches: &ArgMatches) -> Option { - matches.subcommand_matches(Self::CMD).and_then(Cmd::parse) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Functionality for interacting with the Ethereum bridge \ - pool. This pool holds transfers waiting to be relayed to \ - Ethereum." - )) - .subcommand_required(true) - .subcommand(ConstructProof::def().display_order(1)) - .subcommand(RecommendBatch::def().display_order(1)) - .subcommand(RelayProof::def().display_order(1)) - .subcommand(QueryEthBridgePool::def().display_order(1)) - .subcommand(QuerySignedBridgePool::def().display_order(1)) - .subcommand(QueryRelayProgress::def().display_order(1)) - } - } - - #[derive(Clone, Debug)] - pub struct AddToEthBridgePool(pub args::EthereumBridgePool); - - impl SubCmd for AddToEthBridgePool { - const CMD: &'static str = "add-erc20-transfer"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::EthereumBridgePool::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!("Add a new transfer to the Ethereum Bridge pool.")) - .arg_required_else_help(true) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct ConstructProof(pub args::BridgePoolProof); - - impl SubCmd for ConstructProof { - const CMD: &'static str = "construct-proof"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::BridgePoolProof::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Construct a merkle proof that the given transfers are in \ - the pool." - )) - .arg_required_else_help(true) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct RelayProof(pub args::RelayBridgePoolProof); - - impl SubCmd for RelayProof { - const CMD: &'static str = "relay-proof"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::RelayBridgePoolProof::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Construct a merkle proof that the given transfers are in \ - the pool and relay it to Ethereum." - )) - .arg_required_else_help(true) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct RecommendBatch(pub args::RecommendBatch); - - impl SubCmd for RecommendBatch { - const CMD: &'static str = "recommend-batch"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::RecommendBatch::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Get a recommended batch of transfers from the bridge \ - pool to relay to Ethereum." - )) - .arg_required_else_help(true) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct QueryEthBridgePool(pub args::QueryWithoutCtx); - - impl SubCmd for QueryEthBridgePool { - const CMD: &'static str = "query"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::QueryWithoutCtx::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!("Get the contents of the Ethereum Bridge pool.")) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct QuerySignedBridgePool(pub args::QueryWithoutCtx); - - impl SubCmd for QuerySignedBridgePool { - const CMD: &'static str = "query-signed"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::QueryWithoutCtx::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Get the contents of the Ethereum Bridge pool with a \ - signed Merkle root." - )) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct QueryRelayProgress(pub args::QueryWithoutCtx); - - impl SubCmd for QueryRelayProgress { - const CMD: &'static str = "query-relayed"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::QueryWithoutCtx::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Get the confirmation status of transfers to Ethereum." - )) - .add_args::>() - } - } - - /// Used as sub-commands (`SubCmd` instance) in `namadar` binary. - #[derive(Clone, Debug)] - pub enum ValidatorSet { - /// Query the Bridge validator set in Namada, at the given epoch, - /// or the latest one, if none is provided. - BridgeValidatorSet(BridgeValidatorSet), - /// Query the Governance validator set in Namada, at the given epoch, - /// or the latest one, if none is provided. - GovernanceValidatorSet(GovernanceValidatorSet), - /// Query an Ethereum ABI encoding of a proof of the consensus - /// validator set in Namada, at the given epoch, or the next - /// one, if none is provided. - ValidatorSetProof(ValidatorSetProof), - /// Relay a validator set update to Namada's Ethereum bridge - /// smart contracts. - ValidatorSetUpdateRelay(ValidatorSetUpdateRelay), - } - - impl SubCmd for ValidatorSet { - const CMD: &'static str = "validator-set"; - - fn parse(matches: &ArgMatches) -> Option { - matches.subcommand_matches(Self::CMD).and_then(|matches| { - let bridge_validator_set = BridgeValidatorSet::parse(matches) - .map(Self::BridgeValidatorSet); - let governance_validator_set = - GovernanceValidatorSet::parse(matches) - .map(Self::GovernanceValidatorSet); - let validator_set_proof = ValidatorSetProof::parse(matches) - .map(Self::ValidatorSetProof); - let relay = ValidatorSetUpdateRelay::parse(matches) - .map(Self::ValidatorSetUpdateRelay); - bridge_validator_set - .or(governance_validator_set) - .or(validator_set_proof) - .or(relay) - }) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Validator set queries, that return data in a format to \ - be consumed by the Namada Ethereum bridge smart \ - contracts." - )) - .subcommand_required(true) - .subcommand(BridgeValidatorSet::def().display_order(1)) - .subcommand(GovernanceValidatorSet::def().display_order(1)) - .subcommand(ValidatorSetProof::def().display_order(1)) - .subcommand(ValidatorSetUpdateRelay::def().display_order(1)) - } - } - - #[derive(Clone, Debug)] - pub struct BridgeValidatorSet(pub args::BridgeValidatorSet); - - impl SubCmd for BridgeValidatorSet { - const CMD: &'static str = "bridge"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::BridgeValidatorSet::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Query the Bridge validator set in Namada, at the given \ - epoch, or the latest one, if none is provided." - )) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct GovernanceValidatorSet( - pub args::GovernanceValidatorSet, - ); - - impl SubCmd for GovernanceValidatorSet { - const CMD: &'static str = "governance"; - - fn parse(matches: &ArgMatches) -> Option { - matches.subcommand_matches(Self::CMD).map(|matches| { - Self(args::GovernanceValidatorSet::parse(matches)) - }) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Query the Governance validator set in Namada, at the \ - given epoch, or the latest one, if none is provided." - )) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct ValidatorSetProof(pub args::ValidatorSetProof); - - impl SubCmd for ValidatorSetProof { - const CMD: &'static str = "proof"; - - fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| Self(args::ValidatorSetProof::parse(matches))) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Query an Ethereum ABI encoding of a proof of the \ - consensus validator set in Namada, at the requested \ - epoch, or the next one, if no epoch is provided." - )) - .add_args::>() - } - } - - #[derive(Clone, Debug)] - pub struct ValidatorSetUpdateRelay( - pub args::ValidatorSetUpdateRelay, - ); - - impl SubCmd for ValidatorSetUpdateRelay { - const CMD: &'static str = "relay"; - - fn parse(matches: &ArgMatches) -> Option { - matches.subcommand_matches(Self::CMD).map(|matches| { - Self(args::ValidatorSetUpdateRelay::parse(matches)) - }) - } - - fn def() -> App { - App::new(Self::CMD) - .about(wrap!( - "Relay a validator set update to Namada's Ethereum bridge \ - smart contracts." - )) - .add_args::>() - } - } - #[derive(Clone, Debug)] pub struct PkToTmAddress(pub args::PkToTmAddress); @@ -3408,12 +2943,9 @@ pub mod args { use namada_sdk::address::{Address, EstablishedAddress}; pub use namada_sdk::args::*; use namada_sdk::chain::{ChainId, ChainIdPrefix}; - use namada_sdk::collections::HashMap; use namada_sdk::dec::Dec; - use namada_sdk::ethereum_events::EthAddress; use namada_sdk::hash::Hash; use namada_sdk::ibc::core::host::types::identifiers::{ChannelId, PortId}; - use namada_sdk::keccak::KeccakHash; use namada_sdk::key::*; use namada_sdk::masp::utils::RetryStrategy; use namada_sdk::storage::{self, BlockHeight, Epoch}; @@ -3421,13 +2953,12 @@ pub mod args { use namada_sdk::token::NATIVE_MAX_DECIMAL_PLACES; use namada_sdk::tx::data::GasLimit; pub use namada_sdk::tx::{ - TX_BECOME_VALIDATOR_WASM, TX_BOND_WASM, TX_BRIDGE_POOL_WASM, - TX_CHANGE_COMMISSION_WASM, TX_CHANGE_CONSENSUS_KEY_WASM, - TX_CHANGE_METADATA_WASM, TX_CLAIM_REWARDS_WASM, - TX_DEACTIVATE_VALIDATOR_WASM, TX_IBC_WASM, TX_INIT_ACCOUNT_WASM, - TX_INIT_PROPOSAL, TX_REACTIVATE_VALIDATOR_WASM, TX_REDELEGATE_WASM, - TX_RESIGN_STEWARD, TX_REVEAL_PK, TX_TRANSFER_WASM, TX_UNBOND_WASM, - TX_UNJAIL_VALIDATOR_WASM, TX_UPDATE_ACCOUNT_WASM, + TX_BECOME_VALIDATOR_WASM, TX_BOND_WASM, TX_CHANGE_COMMISSION_WASM, + TX_CHANGE_CONSENSUS_KEY_WASM, TX_CHANGE_METADATA_WASM, + TX_CLAIM_REWARDS_WASM, TX_DEACTIVATE_VALIDATOR_WASM, TX_IBC_WASM, + TX_INIT_ACCOUNT_WASM, TX_INIT_PROPOSAL, TX_REACTIVATE_VALIDATOR_WASM, + TX_REDELEGATE_WASM, TX_RESIGN_STEWARD, TX_REVEAL_PK, TX_TRANSFER_WASM, + TX_UNBOND_WASM, TX_UNJAIL_VALIDATOR_WASM, TX_UPDATE_ACCOUNT_WASM, TX_UPDATE_STEWARD_COMMISSION, TX_VOTE_PROPOSAL, TX_WITHDRAW_WASM, VP_USER_WASM, }; @@ -3485,7 +3016,6 @@ pub mod args { "pool-gas-token", DefaultFn(|| "".parse().unwrap()), ); - pub const BRIDGE_POOL_TARGET: Arg = arg("target"); pub const BROADCAST_ONLY: ArgFlag = flag("broadcast-only"); pub const CHAIN_ID: Arg = arg("chain-id"); pub const CHAIN_ID_OPT: ArgOpt = CHAIN_ID.opt(); @@ -3529,12 +3059,9 @@ pub mod args { pub const DUMP_WRAPPER_TX: ArgFlag = flag("dump-wrapper-tx"); pub const DUMP_CONVERSION_TREE: ArgFlag = flag("dump-conversion-tree"); pub const EPOCH: ArgOpt = arg_opt("epoch"); - pub const ERC20: Arg = arg("erc20"); pub const ETH_CONFIRMATIONS: Arg = arg("confirmations"); pub const ETH_GAS: ArgOpt = arg_opt("eth-gas"); pub const ETH_GAS_PRICE: ArgOpt = arg_opt("eth-gas-price"); - pub const ETH_ADDRESS: Arg = arg("ethereum-address"); - pub const ETH_ADDRESS_OPT: ArgOpt = ETH_ADDRESS.opt(); pub const ETH_RPC_ENDPOINT: ArgDefault = arg_default( "eth-rpc-endpoint", DefaultFn(|| "http://localhost:8545".into()), @@ -4134,488 +3661,6 @@ pub mod args { } } - impl CliToSdk> for EthereumBridgePool { - type Error = std::io::Error; - - fn to_sdk( - self, - ctx: &mut Context, - ) -> Result, Self::Error> { - let tx = self.tx.to_sdk(ctx)?; - let chain_ctx = ctx.borrow_chain_or_exit(); - Ok(EthereumBridgePool:: { - nut: self.nut, - tx, - asset: self.asset, - recipient: self.recipient, - sender: chain_ctx.get(&self.sender), - amount: self.amount, - fee_amount: self.fee_amount, - fee_payer: self - .fee_payer - .map(|fee_payer| chain_ctx.get(&fee_payer)), - fee_token: chain_ctx.get(&self.fee_token).into(), - code_path: self.code_path, - }) - } - } - - impl Args for EthereumBridgePool { - fn parse(matches: &ArgMatches) -> Self { - let tx = Tx::parse(matches); - let asset = ERC20.parse(matches); - let recipient = BRIDGE_POOL_TARGET.parse(matches); - let sender = SOURCE.parse(matches); - let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); - let fee_amount = - InputAmount::Unvalidated(BRIDGE_POOL_GAS_AMOUNT.parse(matches)); - let fee_payer = BRIDGE_POOL_GAS_PAYER.parse(matches); - let fee_token = BRIDGE_POOL_GAS_TOKEN.parse(matches); - let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); - let nut = NUT.parse(matches); - Self { - tx, - asset, - recipient, - sender, - amount, - fee_amount, - fee_payer, - fee_token, - code_path, - nut, - } - } - - fn def(app: App) -> App { - app.add_args::>() - .arg( - ERC20.def().help(wrap!( - "The Ethereum address of the ERC20 token." - )), - ) - .arg( - BRIDGE_POOL_TARGET.def().help(wrap!( - "The Ethereum address receiving the tokens." - )), - ) - .arg( - SOURCE - .def() - .help(wrap!("The Namada address sending the tokens.")), - ) - .arg(AMOUNT.def().help(wrap!( - "The amount of tokens being sent across the bridge." - ))) - .arg(BRIDGE_POOL_GAS_AMOUNT.def().help(wrap!( - "The amount of gas you wish to pay to have this transfer \ - relayed to Ethereum." - ))) - .arg(BRIDGE_POOL_GAS_PAYER.def().help(wrap!( - "The Namada address of the account paying the gas. By \ - default, it is the same as the source." - ))) - .arg(BRIDGE_POOL_GAS_TOKEN.def().help(wrap!( - "The token for paying the Bridge pool gas fees. Defaults \ - to NAM." - ))) - .arg(NUT.def().help(wrap!( - "Add Non Usable Tokens (NUTs) to the Bridge pool. These \ - are usually obtained from invalid transfers to Namada." - ))) - } - } - - impl CliToSdk> for RecommendBatch { - type Error = std::convert::Infallible; - - fn to_sdk( - self, - ctx: &mut Context, - ) -> Result, Self::Error> { - let query = self.query.to_sdk(ctx)?; - let chain_ctx = ctx.borrow_chain_or_exit(); - - Ok(RecommendBatch:: { - query, - max_gas: self.max_gas, - gas: self.gas, - conversion_table: { - let file = std::io::BufReader::new( - std::fs::File::open(self.conversion_table).expect( - "Failed to open the provided file to the \ - conversion table", - ), - ); - let table: HashMap = - serde_json::from_reader(file) - .expect("Failed to parse conversion table"); - table - .into_iter() - .map(|(token, conversion_rate)| { - let token_from_ctx = - FromContext::
::new(token); - let address = chain_ctx.get(&token_from_ctx); - let alias = token_from_ctx.raw; - ( - address, - BpConversionTableEntry { - alias, - conversion_rate, - }, - ) - }) - .collect() - }, - }) - } - } - - impl Args for RecommendBatch { - fn parse(matches: &ArgMatches) -> Self { - let query = Query::parse(matches); - let max_gas = MAX_ETH_GAS.parse(matches); - let gas = ETH_GAS.parse(matches); - let conversion_table = CONVERSION_TABLE.parse(matches); - Self { - query, - max_gas, - gas, - conversion_table, - } - } - - fn def(app: App) -> App { - app.add_args::>() - .arg(MAX_ETH_GAS.def().help(wrap!( - "The maximum amount Ethereum gas that can be spent during \ - the relay call." - ))) - .arg(ETH_GAS.def().help(wrap!( - "Under ideal conditions, relaying transfers will yield a \ - net profit. If that is not possible, setting this \ - optional value will result in a batch transfer that \ - costs as close to the given value as possible without \ - exceeding it." - ))) - .arg(CONVERSION_TABLE.def().help(wrap!( - "Path to a JSON object containing a mapping between token \ - aliases (or addresses) and their conversion rates in gwei" - ))) - } - } - - impl CliToSdkCtxless> for BridgePoolProof { - fn to_sdk_ctxless(self) -> BridgePoolProof { - BridgePoolProof:: { - ledger_address: self.ledger_address, - transfers: self.transfers, - relayer: self.relayer, - } - } - } - - impl Args for BridgePoolProof { - fn parse(matches: &ArgMatches) -> Self { - let ledger_address = LEDGER_ADDRESS.parse(matches); - let hashes = HASH_LIST.parse(matches); - let relayer = RELAYER.parse(matches); - Self { - ledger_address, - transfers: hashes - .split_whitespace() - .map(|hash| { - KeccakHash::try_from(hash).unwrap_or_else(|_| { - tracing::info!( - "Could not parse '{}' as a Keccak hash.", - hash - ); - safe_exit(1) - }) - }) - .collect(), - relayer, - } - } - - fn def(app: App) -> App { - app.arg(LEDGER_ADDRESS.def().help(LEDGER_ADDRESS_ABOUT)) - .arg(HASH_LIST.def().help(wrap!( - "Whitespace separated Keccak hash list of transfers in \ - the Bridge pool." - ))) - .arg(RELAYER.def().help(wrap!( - "The rewards address for relaying this proof." - ))) - } - } - - impl CliToSdkCtxless> - for RelayBridgePoolProof - { - fn to_sdk_ctxless(self) -> RelayBridgePoolProof { - RelayBridgePoolProof:: { - ledger_address: self.ledger_address, - transfers: self.transfers, - relayer: self.relayer, - confirmations: self.confirmations, - eth_rpc_endpoint: (), - gas: self.gas, - gas_price: self.gas_price, - eth_addr: self.eth_addr, - sync: self.sync, - } - } - } - - impl Args for RelayBridgePoolProof { - fn parse(matches: &ArgMatches) -> Self { - let ledger_address = LEDGER_ADDRESS.parse(matches); - let hashes = HASH_LIST.parse(matches); - let relayer = RELAYER.parse(matches); - let gas = ETH_GAS.parse(matches); - let gas_price = ETH_GAS_PRICE.parse(matches); - let eth_rpc_endpoint = ETH_RPC_ENDPOINT.parse(matches); - let eth_addr = ETH_ADDRESS_OPT.parse(matches); - let confirmations = ETH_CONFIRMATIONS.parse(matches); - let sync = ETH_SYNC.parse(matches); - Self { - ledger_address, - sync, - transfers: hashes - .split(' ') - .map(|hash| { - KeccakHash::try_from(hash).unwrap_or_else(|_| { - tracing::info!( - "Could not parse '{}' as a Keccak hash.", - hash - ); - safe_exit(1) - }) - }) - .collect(), - relayer, - gas, - gas_price, - eth_rpc_endpoint, - eth_addr, - confirmations, - } - } - - fn def(app: App) -> App { - app.arg(LEDGER_ADDRESS.def().help(LEDGER_ADDRESS_ABOUT)) - .arg(HASH_LIST.def().help(wrap!( - "Whitespace separated Keccak hash list of transfers in \ - the Bridge pool." - ))) - .arg(RELAYER.def().help(wrap!( - "The rewards address for relaying this proof." - ))) - .arg(ETH_ADDRESS_OPT.def().help(wrap!( - "The address of the Ethereum wallet to pay the gas fees. \ - If unset, the default wallet is used." - ))) - .arg(ETH_GAS.def().help(wrap!( - "The Ethereum gas that can be spent during the relay call." - ))) - .arg(ETH_GAS_PRICE.def().help(wrap!( - "The price of Ethereum gas, during the relay call." - ))) - .arg( - ETH_RPC_ENDPOINT - .def() - .help(wrap!("The Ethereum RPC endpoint.")), - ) - .arg(ETH_CONFIRMATIONS.def().help(wrap!( - "The number of block confirmations on Ethereum." - ))) - .arg(ETH_SYNC.def().help(wrap!( - "Synchronize with the network, or exit immediately, if \ - the Ethereum node has fallen behind." - ))) - } - } - - impl CliToSdkCtxless> - for BridgeValidatorSet - { - fn to_sdk_ctxless(self) -> BridgeValidatorSet { - BridgeValidatorSet:: { - ledger_address: self.ledger_address, - epoch: self.epoch, - } - } - } - - impl Args for BridgeValidatorSet { - fn parse(matches: &ArgMatches) -> Self { - let ledger_address = LEDGER_ADDRESS.parse(matches); - let epoch = EPOCH.parse(matches); - Self { - ledger_address, - epoch, - } - } - - fn def(app: App) -> App { - app.arg(LEDGER_ADDRESS.def().help(LEDGER_ADDRESS_ABOUT)) - .arg(EPOCH.def().help(wrap!( - "The epoch of the Bridge set of validators to query." - ))) - } - } - - impl CliToSdkCtxless> - for GovernanceValidatorSet - { - fn to_sdk_ctxless(self) -> GovernanceValidatorSet { - GovernanceValidatorSet:: { - ledger_address: self.ledger_address, - epoch: self.epoch, - } - } - } - - impl Args for GovernanceValidatorSet { - fn parse(matches: &ArgMatches) -> Self { - let ledger_address = LEDGER_ADDRESS.parse(matches); - let epoch = EPOCH.parse(matches); - Self { - ledger_address, - epoch, - } - } - - fn def(app: App) -> App { - app.arg(LEDGER_ADDRESS.def().help(LEDGER_ADDRESS_ABOUT)) - .arg(EPOCH.def().help(wrap!( - "The epoch of the Governance set of validators to query." - ))) - } - } - - impl CliToSdkCtxless> - for ValidatorSetProof - { - fn to_sdk_ctxless(self) -> ValidatorSetProof { - ValidatorSetProof:: { - ledger_address: self.ledger_address, - epoch: self.epoch, - } - } - } - - impl Args for ValidatorSetProof { - fn parse(matches: &ArgMatches) -> Self { - let ledger_address = LEDGER_ADDRESS.parse(matches); - let epoch = EPOCH.parse(matches); - Self { - ledger_address, - epoch, - } - } - - fn def(app: App) -> App { - app.arg(LEDGER_ADDRESS.def().help(LEDGER_ADDRESS_ABOUT)) - .arg(EPOCH.def().help(wrap!( - "The epoch of the set of validators to be proven." - ))) - } - } - - impl CliToSdkCtxless> - for ValidatorSetUpdateRelay - { - fn to_sdk_ctxless(self) -> ValidatorSetUpdateRelay { - ValidatorSetUpdateRelay:: { - daemon: self.daemon, - ledger_address: self.ledger_address, - confirmations: self.confirmations, - eth_rpc_endpoint: (), - epoch: self.epoch, - gas: self.gas, - gas_price: self.gas_price, - eth_addr: self.eth_addr, - sync: self.sync, - retry_dur: self.retry_dur, - success_dur: self.success_dur, - } - } - } - - impl Args for ValidatorSetUpdateRelay { - fn parse(matches: &ArgMatches) -> Self { - let ledger_address = LEDGER_ADDRESS.parse(matches); - let daemon = DAEMON_MODE.parse(matches); - let epoch = EPOCH.parse(matches); - let gas = ETH_GAS.parse(matches); - let gas_price = ETH_GAS_PRICE.parse(matches); - let eth_rpc_endpoint = ETH_RPC_ENDPOINT.parse(matches); - let eth_addr = ETH_ADDRESS_OPT.parse(matches); - let confirmations = ETH_CONFIRMATIONS.parse(matches); - let sync = ETH_SYNC.parse(matches); - let retry_dur = - DAEMON_MODE_RETRY_DUR.parse(matches).map(|dur| dur.0); - let success_dur = - DAEMON_MODE_SUCCESS_DUR.parse(matches).map(|dur| dur.0); - Self { - ledger_address, - sync, - daemon, - epoch, - gas, - gas_price, - confirmations, - eth_rpc_endpoint, - eth_addr, - retry_dur, - success_dur, - } - } - - fn def(app: App) -> App { - app.arg(LEDGER_ADDRESS.def().help(LEDGER_ADDRESS_ABOUT)) - .arg(DAEMON_MODE.def().help(wrap!( - "Run in daemon mode, which will continuously perform \ - validator set updates." - ))) - .arg(DAEMON_MODE_RETRY_DUR.def().help(wrap!( - "The amount of time to sleep between failed daemon mode \ - relays." - ))) - .arg(DAEMON_MODE_SUCCESS_DUR.def().help(wrap!( - "The amount of time to sleep between successful daemon \ - mode relays." - ))) - .arg(ETH_ADDRESS_OPT.def().help(wrap!( - "The address of the Ethereum wallet to pay the gas fees. \ - If unset, the default wallet is used." - ))) - .arg(EPOCH.def().help(wrap!( - "The epoch of the set of validators to relay." - ))) - .arg(ETH_GAS.def().help(wrap!( - "The Ethereum gas that can be spent during the relay call." - ))) - .arg(ETH_GAS_PRICE.def().help(wrap!( - "The price of Ethereum gas, during the relay call." - ))) - .arg( - ETH_RPC_ENDPOINT - .def() - .help(wrap!("The Ethereum RPC endpoint.")), - ) - .arg(ETH_CONFIRMATIONS.def().help(wrap!( - "The number of block confirmations on Ethereum." - ))) - .arg(ETH_SYNC.def().help(wrap!( - "Synchronize with the network, or exit immediately, if \ - the Ethereum node has fallen behind." - ))) - } - } - impl CliToSdk> for TxCustom { type Error = std::io::Error; @@ -7788,12 +6833,10 @@ pub mod args { type Address = WalletAddress; type BalanceOwner = WalletBalanceOwner; type BlockHeight = BlockHeight; - type BpConversionTable = PathBuf; type ConfigRpcTendermintAddress = ConfigRpcAddress; type Data = PathBuf; type DatedSpendingKey = WalletDatedSpendingKey; type DatedViewingKey = WalletDatedViewingKey; - type EthereumAddress = String; type Keypair = WalletKeypair; type MaspIndexerAddress = String; type PaymentAddress = WalletPaymentAddr; @@ -9324,41 +8367,6 @@ pub fn namada_wallet_cli( cmds::NamadaWallet::parse_or_print_help(app) } -pub enum NamadaRelayer { - EthBridgePoolWithCtx(Box<(cmds::EthBridgePoolWithCtx, Context)>), - EthBridgePoolWithoutCtx(cmds::EthBridgePoolWithoutCtx), - ValidatorSet(cmds::ValidatorSet), -} - -pub fn namada_relayer_cli(version: &'static str) -> Result { - let app = namada_relayer_app(version); - let matches = app.clone().get_matches(); - match Cmd::parse(&matches) { - Some(cmd) => match cmd { - cmds::NamadaRelayer::EthBridgePool( - cmds::EthBridgePool::WithContext(sub_cmd), - ) => { - let global_args = args::Global::parse(&matches); - let context = Context::new::(global_args)?; - Ok(NamadaRelayer::EthBridgePoolWithCtx(Box::new(( - sub_cmd, context, - )))) - } - cmds::NamadaRelayer::EthBridgePool( - cmds::EthBridgePool::WithoutContext(sub_cmd), - ) => Ok(NamadaRelayer::EthBridgePoolWithoutCtx(sub_cmd)), - cmds::NamadaRelayer::ValidatorSet(sub_cmd) => { - Ok(NamadaRelayer::ValidatorSet(sub_cmd)) - } - }, - None => { - let mut app = app; - app.print_help().unwrap(); - safe_exit(2); - } - } -} - pub fn namada_app(version: &'static str) -> App { let app = App::new(APP_NAME) .version(version) @@ -9398,11 +8406,3 @@ pub fn namada_wallet_app(version: &'static str) -> App { .arg_required_else_help(true); cmds::NamadaWallet::add_sub(args::Global::def(app)) } - -pub fn namada_relayer_app(version: &'static str) -> App { - let app = App::new(APP_NAME) - .version(version) - .about("Namada relayer command line interface.") - .subcommand_required(true); - cmds::NamadaRelayer::add_sub(args::Global::def(app)) -} diff --git a/crates/apps_lib/src/cli/api.rs b/crates/apps_lib/src/cli/api.rs index d9737b5174d..e063fcc4846 100644 --- a/crates/apps_lib/src/cli/api.rs +++ b/crates/apps_lib/src/cli/api.rs @@ -19,7 +19,7 @@ pub trait CliClient: Client + Send + Sync + 'static { impl CliClient for HttpClient { fn from_tendermint_address(address: &TendermintUrl) -> Self { HttpClient::builder(address.clone().try_into().unwrap()) - .compat_mode(CompatMode::V0_37) + .compat_mode(CompatMode::V0_38) .timeout(std::time::Duration::from_secs(30)) .build() .unwrap() diff --git a/crates/apps_lib/src/cli/client.rs b/crates/apps_lib/src/cli/client.rs index c55dac91df3..4928e0694f5 100644 --- a/crates/apps_lib/src/cli/client.rs +++ b/crates/apps_lib/src/cli/client.rs @@ -440,27 +440,6 @@ impl CliApi { let namada = ctx.to_sdk(client, io); tx::gen_ibc_shielding_transfer(&namada, args).await?; } - #[cfg(feature = "namada-eth-bridge")] - Sub::AddToEthBridgePool(args) => { - let args = args.0; - let chain_ctx = ctx.borrow_mut_chain_or_exit(); - let ledger_address = - chain_ctx.get(&args.tx.ledger_address); - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - let args = args.to_sdk(&mut ctx)?; - let namada = ctx.to_sdk(client, io); - tx::submit_bridge_pool_tx(&namada, args).await?; - } - #[cfg(not(feature = "namada-eth-bridge"))] - Sub::AddToEthBridgePool(_) => { - display_line!( - &io, - "The Namada Ethereum bridge is disabled" - ); - } Sub::TxUnjailValidator(TxUnjailValidator(args)) => { let chain_ctx = ctx.borrow_mut_chain_or_exit(); let ledger_address = diff --git a/crates/apps_lib/src/cli/context.rs b/crates/apps_lib/src/cli/context.rs index 7c4d570d5c7..0a1baa25533 100644 --- a/crates/apps_lib/src/cli/context.rs +++ b/crates/apps_lib/src/cli/context.rs @@ -15,9 +15,8 @@ use namada_core::masp::{ BalanceOwner, ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, TransferSource, TransferTarget, }; -use namada_sdk::address::{Address, InternalAddress}; +use namada_sdk::address::Address; use namada_sdk::chain::ChainId; -use namada_sdk::ethereum_events::EthAddress; use namada_sdk::ibc::trace::{ibc_token, is_ibc_denom, is_nft_trace}; use namada_sdk::io::Io; use namada_sdk::key::*; @@ -443,21 +442,7 @@ impl ArgFromContext for Address { let raw = raw.as_ref(); // An address can be either raw (bech32m encoding) FromStr::from_str(raw) - // An Ethereum address - .or_else(|_| { - if raw.len() == 42 && raw.starts_with("0x") { - { - raw.parse::() - .map(|addr| { - Address::Internal(InternalAddress::Erc20(addr)) - }) - .map_err(|_| SkipErr) - } - } else { - Err(SkipErr) - } - }) - // An IBC token + // Or an IBC token .or_else(|_| { is_ibc_denom(raw) .map(|(trace_path, base_denom)| { diff --git a/crates/apps_lib/src/cli/relayer.rs b/crates/apps_lib/src/cli/relayer.rs deleted file mode 100644 index e539a6143d6..00000000000 --- a/crates/apps_lib/src/cli/relayer.rs +++ /dev/null @@ -1,165 +0,0 @@ -use color_eyre::eyre::Result; -use namada_sdk::io::Io; - -use crate::cli; -use crate::cli::api::{CliApi, CliClient}; - -impl CliApi { - #[cfg(not(feature = "namada-eth-bridge"))] - pub async fn handle_relayer_command( - _client: Option, - _cmd: cli::NamadaRelayer, - io: impl Io, - ) -> Result<()> - where - C: CliClient, - { - use namada_sdk::io::display_line; - - display_line!(&io, "The Namada Ethereum bridge is disabled"); - Ok(()) - } - - #[cfg(feature = "namada-eth-bridge")] - pub async fn handle_relayer_command( - client: Option, - cmd: cli::NamadaRelayer, - io: impl Io, - ) -> Result<()> - where - C: CliClient, - { - use namada_sdk::eth_bridge::{bridge_pool, validator_set}; - - use crate::cli::args::{CliToSdk, CliToSdkCtxless}; - use crate::cli::cmds::*; - use crate::cli::utils::get_eth_rpc_client; - - match cmd { - cli::NamadaRelayer::EthBridgePoolWithCtx(boxed) => { - let (sub, mut ctx) = *boxed; - match sub { - EthBridgePoolWithCtx::RecommendBatch(RecommendBatch( - args, - )) => { - let chain_ctx = ctx.borrow_mut_chain_or_exit(); - let ledger_address = - chain_ctx.get(&args.query.ledger_address); - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - let args = args.to_sdk(&mut ctx)?; - let namada = ctx.to_sdk(client, io); - bridge_pool::recommend_batch(&namada, args).await?; - } - } - } - cli::NamadaRelayer::EthBridgePoolWithoutCtx(sub) => match sub { - EthBridgePoolWithoutCtx::ConstructProof(ConstructProof( - args, - )) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&args.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - let args = args.to_sdk_ctxless(); - bridge_pool::construct_proof(&client, &io, args).await?; - } - EthBridgePoolWithoutCtx::RelayProof(RelayProof(args)) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&args.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - let eth_client = - get_eth_rpc_client(&args.eth_rpc_endpoint).await; - let args = args.to_sdk_ctxless(); - bridge_pool::relay_bridge_pool_proof( - eth_client, &client, &io, args, - ) - .await?; - } - EthBridgePoolWithoutCtx::QueryPool(QueryEthBridgePool( - query, - )) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&query.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - bridge_pool::query_bridge_pool(&client, &io).await?; - } - EthBridgePoolWithoutCtx::QuerySigned( - QuerySignedBridgePool(query), - ) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&query.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - bridge_pool::query_signed_bridge_pool(&client, &io).await?; - } - EthBridgePoolWithoutCtx::QueryRelays(QueryRelayProgress( - query, - )) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&query.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - bridge_pool::query_relay_progress(&client, &io).await?; - } - }, - cli::NamadaRelayer::ValidatorSet(sub) => match sub { - ValidatorSet::BridgeValidatorSet(BridgeValidatorSet(args)) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&args.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - let args = args.to_sdk_ctxless(); - validator_set::query_bridge_validator_set( - &client, &io, args, - ) - .await?; - } - ValidatorSet::GovernanceValidatorSet( - GovernanceValidatorSet(args), - ) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&args.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - let args = args.to_sdk_ctxless(); - validator_set::query_governnace_validator_set( - &client, &io, args, - ) - .await?; - } - ValidatorSet::ValidatorSetProof(ValidatorSetProof(args)) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&args.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - let args = args.to_sdk_ctxless(); - validator_set::query_validator_set_update_proof( - &client, &io, args, - ) - .await?; - } - ValidatorSet::ValidatorSetUpdateRelay( - ValidatorSetUpdateRelay(args), - ) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address(&args.ledger_address) - }); - client.wait_until_node_is_synced(&io).await?; - let eth_client = - get_eth_rpc_client(&args.eth_rpc_endpoint).await; - let args = args.to_sdk_ctxless(); - validator_set::relay_validator_set_update( - eth_client, &client, &io, args, - ) - .await?; - } - }, - } - Ok(()) - } -} diff --git a/crates/apps_lib/src/cli/utils.rs b/crates/apps_lib/src/cli/utils.rs index c33e4cf1554..a39c5d8b568 100644 --- a/crates/apps_lib/src/cli/utils.rs +++ b/crates/apps_lib/src/cli/utils.rs @@ -3,28 +3,15 @@ use std::fmt::Debug; use std::io::Write; use std::marker::PhantomData; use std::str::FromStr; -use std::sync::Arc; use clap::{ArgAction, ArgMatches}; use color_eyre::eyre::Result; -use data_encoding::HEXLOWER_PERMISSIVE; -use namada_sdk::eth_bridge::ethers::core::k256::elliptic_curve::SecretKey as Secp256k1Sk; -use namada_sdk::eth_bridge::ethers::middleware::SignerMiddleware; -use namada_sdk::eth_bridge::ethers::providers::{Http, Middleware, Provider}; -use namada_sdk::eth_bridge::ethers::signers::{Signer, Wallet}; use super::args; use super::context::Context; use crate::cli::api::CliIo; use crate::cli::context::FromContext; -/// Environment variable where Ethereum relayer private -/// keys are stored. -// TODO(namada#2029): remove this in favor of getting eth keys from -// namadaw, ledger, or something more secure -#[cfg_attr(not(feature = "namada-eth-bridge"), allow(dead_code))] -const RELAYER_KEY_ENV_VAR: &str = "NAMADA_RELAYER_KEY"; - // We only use static strings pub type App = clap::Command; pub type ClapArg = clap::Arg; @@ -437,32 +424,3 @@ pub fn safe_exit(_: i32) -> ! { panic!("Test failed because the client exited unexpectedly.") } - -/// Load an Ethereum wallet from the environment. -#[cfg_attr(not(feature = "namada-eth-bridge"), allow(dead_code))] -fn get_eth_signer_from_env(chain_id: u64) -> Option { - let relayer_key = std::env::var(RELAYER_KEY_ENV_VAR).ok()?; - let relayer_key = HEXLOWER_PERMISSIVE.decode(relayer_key.as_ref()).ok()?; - let relayer_key = Secp256k1Sk::from_slice(&relayer_key).ok()?; - - let wallet: Wallet<_> = relayer_key.into(); - let wallet = wallet.with_chain_id(chain_id); - - Some(wallet) -} - -/// Return an Ethereum RPC client. -#[cfg_attr(not(feature = "namada-eth-bridge"), allow(dead_code))] -pub async fn get_eth_rpc_client(url: &str) -> Arc { - let client = Provider::::try_from(url) - .expect("Failed to instantiate Ethereum RPC client"); - let chain_id = client - .get_chainid() - .await - .expect("Failed to query chain id") - .as_u64(); - let signer = get_eth_signer_from_env(chain_id).unwrap_or_else(|| { - panic!("Failed to get Ethereum key from {RELAYER_KEY_ENV_VAR} env var") - }); - Arc::new(SignerMiddleware::new(client, signer)) -} diff --git a/crates/apps_lib/src/client/rpc.rs b/crates/apps_lib/src/client/rpc.rs index 4f416e60113..31120839127 100644 --- a/crates/apps_lib/src/client/rpc.rs +++ b/crates/apps_lib/src/client/rpc.rs @@ -314,9 +314,6 @@ async fn lookup_token_alias( owner: Option<&Address>, ) -> String { match token { - Address::Internal(InternalAddress::Erc20(eth_addr)) => { - eth_addr.to_canonical() - } Address::Internal(InternalAddress::IbcToken(_)) => { let ibc_denom = rpc::query_ibc_denom(context, token.to_string(), owner).await; diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 70eb38ebcd7..344feba7b4f 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -285,32 +285,6 @@ where } } -pub async fn submit_bridge_pool_tx( - namada: &N, - args: args::EthereumBridgePool, -) -> Result<(), error::Error> { - let bridge_pool_tx_data = args.clone().build(namada).await?; - - if let Some(dump_tx) = args.tx.dump_tx { - tx::dump_tx( - namada.io(), - dump_tx, - args.tx.output_folder, - bridge_pool_tx_data.0, - )?; - } else { - batch_opt_reveal_pk_and_submit( - namada, - &args.tx, - &[&args.sender], - bridge_pool_tx_data, - ) - .await?; - } - - Ok(()) -} - pub async fn submit_custom( namada: &N, args: args::TxCustom, diff --git a/crates/apps_lib/src/config/ethereum_bridge/ledger.rs b/crates/apps_lib/src/config/ethereum_bridge/ledger.rs deleted file mode 100644 index ccde2830822..00000000000 --- a/crates/apps_lib/src/config/ethereum_bridge/ledger.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Runtime configuration for a validator node. -#[allow(unused_imports)] -use namada_sdk::ethereum_events::EthereumEvent; -use serde::{Deserialize, Serialize}; - -/// Default [Ethereum JSON-RPC](https://ethereum.org/en/developers/docs/apis/json-rpc/) endpoint used by the oracle -pub const DEFAULT_ORACLE_RPC_ENDPOINT: &str = "http://127.0.0.1:8545"; - -/// The default maximum number of Ethereum events the channel between -/// the oracle and the shell can hold. -pub const ORACLE_CHANNEL_BUFFER_SIZE: usize = 1000; - -/// The mode in which to run the Ethereum bridge. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum Mode { - /// The oracle will listen to the Ethereum JSON-RPC endpoint as - /// specified in the `oracle_rpc_endpoint` setting. - RemoteEndpoint, - /// Instead of the oracle listening for events using an Ethereum - /// JSON-RPC endpoint, an endpoint will be exposed by the ledger - /// itself for submission of Borsh-serialized [`EthereumEvent`] - /// instances. Mostly useful for testing purposes. - SelfHostedEndpoint, - /// Do not run any components of the Ethereum bridge. - Off, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Config { - /// The mode in which to run the Ethereum node and oracle setup of this - /// validator. - pub mode: Mode, - /// The Ethereum JSON-RPC endpoint that the Ethereum event oracle will use - /// to listen for events from the Ethereum bridge smart contracts - pub oracle_rpc_endpoint: String, - /// The size of bounded channel between the Ethereum oracle and main - /// ledger subprocesses. This is the number of Ethereum events that - /// can be held in the channel. The default is 1000. - pub channel_buffer_size: usize, -} - -impl Default for Config { - fn default() -> Self { - Self { - mode: Mode::RemoteEndpoint, - oracle_rpc_endpoint: DEFAULT_ORACLE_RPC_ENDPOINT.to_owned(), - channel_buffer_size: ORACLE_CHANNEL_BUFFER_SIZE, - } - } -} diff --git a/crates/apps_lib/src/config/ethereum_bridge/mod.rs b/crates/apps_lib/src/config/ethereum_bridge/mod.rs deleted file mode 100644 index 370e1150a28..00000000000 --- a/crates/apps_lib/src/config/ethereum_bridge/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod ledger; diff --git a/crates/apps_lib/src/config/genesis.rs b/crates/apps_lib/src/config/genesis.rs index a29658eabe5..8e006c51c57 100644 --- a/crates/apps_lib/src/config/genesis.rs +++ b/crates/apps_lib/src/config/genesis.rs @@ -17,7 +17,6 @@ use namada_migrations::*; use namada_sdk::address::{Address, EstablishedAddress}; use namada_sdk::borsh::{BorshDeserialize, BorshSerialize}; use namada_sdk::collections::HashMap; -use namada_sdk::eth_bridge::EthereumBridgeParams; use namada_sdk::governance::parameters::GovernanceParameters; use namada_sdk::governance::pgf::parameters::PgfParameters; use namada_sdk::key::*; @@ -261,8 +260,6 @@ pub struct Genesis { pub pos_params: OwnedPosParams, pub gov_params: GovernanceParameters, pub pgf_params: PgfParameters, - // Ethereum bridge config - pub ethereum_bridge_params: Option, } impl Genesis { @@ -420,8 +417,7 @@ pub struct Parameters { /// Modify the default genesis file (namada/genesis/localnet/) to /// accommodate testing. /// -/// This includes adding the Ethereum bridge parameters and -/// adding a specified number of validators. +/// This includes adding a specified number of validators. #[allow(clippy::arithmetic_side_effects)] #[cfg(any(test, feature = "benches", feature = "testing"))] pub fn make_dev_genesis( @@ -431,10 +427,7 @@ pub fn make_dev_genesis( use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; - use namada_sdk::address::testing::wnam; use namada_sdk::chain::ChainIdPrefix; - use namada_sdk::eth_bridge::{Contracts, UpgradeableContract}; - use namada_sdk::ethereum_events::EthAddress; use namada_sdk::key::*; use namada_sdk::proof_of_stake::types::ValidatorMetaData; use namada_sdk::tx::standalone_signature; @@ -464,20 +457,6 @@ pub fn make_dev_genesis( Duration::from_secs(30).into(), ); - // Add Ethereum bridge params. - genesis.parameters.eth_bridge_params = Some(templates::EthBridgeParams { - eth_start_height: Default::default(), - min_confirmations: Default::default(), - contracts: Contracts { - native_erc20: wnam(), - bridge: UpgradeableContract { - address: EthAddress([0; 20]), - version: Default::default(), - }, - }, - erc20_whitelist: vec![], - }); - // Use the default token address for matching tokens let default_tokens: HashMap = defaults::tokens() .into_iter() diff --git a/crates/apps_lib/src/config/genesis/chain.rs b/crates/apps_lib/src/config/genesis/chain.rs index f3604c3bbe2..c3c36427e78 100644 --- a/crates/apps_lib/src/config/genesis/chain.rs +++ b/crates/apps_lib/src/config/genesis/chain.rs @@ -11,7 +11,6 @@ use namada_sdk::address::{ }; use namada_sdk::borsh::{BorshDeserialize, BorshSerialize, BorshSerializeExt}; use namada_sdk::chain::{ChainId, ChainIdPrefix}; -use namada_sdk::eth_bridge::EthereumBridgeParams; use namada_sdk::governance::pgf::parameters::PgfParameters; use namada_sdk::hash::Hash; use namada_sdk::ibc::parameters::{IbcParameters, IbcTokenRateLimits}; @@ -191,8 +190,6 @@ impl Finalized { InternalAddress::PoS, InternalAddress::Masp, InternalAddress::Ibc, - InternalAddress::EthBridge, - InternalAddress::EthBridgePool, InternalAddress::Governance, InternalAddress::Pgf, ] { @@ -460,25 +457,6 @@ impl Finalized { self.parameters.pgf_params.clone() } - pub fn get_eth_bridge_params(&self) -> Option { - if let Some(templates::EthBridgeParams { - eth_start_height, - min_confirmations, - contracts, - erc20_whitelist, - }) = self.parameters.eth_bridge_params.clone() - { - Some(EthereumBridgeParams { - eth_start_height, - min_confirmations, - erc20_whitelist, - contracts, - }) - } else { - None - } - } - pub fn get_ibc_params(&self) -> IbcParameters { let templates::IbcParams { default_mint_limit, @@ -816,7 +794,6 @@ pub struct FinalizedParameters { pub pos_params: templates::PosParams, pub gov_params: templates::GovernanceParams, pub pgf_params: PgfParameters, - pub eth_bridge_params: Option, pub ibc_params: templates::IbcParams, } @@ -827,7 +804,6 @@ impl FinalizedParameters { pos_params, gov_params, pgf_params, - eth_bridge_params, ibc_params, }: templates::Parameters, ) -> Self { @@ -842,7 +818,6 @@ impl FinalizedParameters { pos_params, gov_params, pgf_params: finalized_pgf_params, - eth_bridge_params, ibc_params, } } diff --git a/crates/apps_lib/src/config/genesis/templates.rs b/crates/apps_lib/src/config/genesis/templates.rs index 4df147be939..6aad0450889 100644 --- a/crates/apps_lib/src/config/genesis/templates.rs +++ b/crates/apps_lib/src/config/genesis/templates.rs @@ -10,14 +10,11 @@ use namada_migrations::*; use namada_sdk::address::Address; use namada_sdk::borsh::{BorshDeserialize, BorshSerialize}; use namada_sdk::dec::Dec; -use namada_sdk::eth_bridge::storage::parameters::{ - Contracts, Erc20WhitelistEntry, MinimumConfirmations, -}; use namada_sdk::parameters::ProposalBytes; +use namada_sdk::token; use namada_sdk::token::{ Amount, DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES, }; -use namada_sdk::{ethereum_structs, token}; use serde::{Deserialize, Serialize}; use super::transactions::{self, Transactions}; @@ -233,7 +230,6 @@ pub struct Parameters { pub pos_params: PosParams, pub gov_params: GovernanceParams, pub pgf_params: PgfParams, - pub eth_bridge_params: Option, pub ibc_params: IbcParams, } @@ -475,30 +471,6 @@ pub struct PgfParams { valid: PhantomData, } -#[derive( - Clone, - Debug, - Deserialize, - Serialize, - BorshDeserialize, - BorshDeserializer, - BorshSerialize, - PartialEq, - Eq, -)] -pub struct EthBridgeParams { - /// Initial Ethereum block height when events will first be extracted from. - pub eth_start_height: ethereum_structs::BlockHeight, - /// Minimum number of confirmations needed to trust an Ethereum branch. - /// This must be at least one. - pub min_confirmations: MinimumConfirmations, - /// List of ERC20 token types whitelisted at genesis time. - pub erc20_whitelist: Vec, - /// The addresses of the Ethereum contracts that need to be directly known - /// by validators. - pub contracts: Contracts, -} - #[derive( Clone, Debug, @@ -888,7 +860,6 @@ pub fn validate_parameters( pos_params, gov_params, pgf_params, - eth_bridge_params, ibc_params, } = parameters; match parameters.denominate(tokens) { @@ -908,7 +879,6 @@ pub fn validate_parameters( .maximum_number_of_stewards, valid: Default::default(), }, - eth_bridge_params, ibc_params, }), } diff --git a/crates/apps_lib/src/config/mod.rs b/crates/apps_lib/src/config/mod.rs index 9433fc59118..715177ee5ae 100644 --- a/crates/apps_lib/src/config/mod.rs +++ b/crates/apps_lib/src/config/mod.rs @@ -1,6 +1,5 @@ //! Node and client configuration -pub mod ethereum_bridge; pub mod genesis; pub mod global; pub mod utils; @@ -95,7 +94,6 @@ pub struct Ledger { pub chain_id: ChainId, pub shell: Shell, pub cometbft: TendermintConfig, - pub ethereum_bridge: ethereum_bridge::ledger::Config, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -161,7 +159,6 @@ impl Ledger { snapshots_to_keep: None, }, cometbft: tendermint_config, - ethereum_bridge: ethereum_bridge::ledger::Config::default(), } } diff --git a/crates/benches/Cargo.toml b/crates/benches/Cargo.toml index 0e23b4d6b82..22f58450c8a 100644 --- a/crates/benches/Cargo.toml +++ b/crates/benches/Cargo.toml @@ -38,7 +38,6 @@ harness = false path = "mempool_validate.rs" [features] -namada-eth-bridge = ["namada_apps_lib/namada-eth-bridge"] [dependencies] diff --git a/crates/benches/native_vps.rs b/crates/benches/native_vps.rs index 3c0c78a7711..5734b49ee3f 100644 --- a/crates/benches/native_vps.rs +++ b/crates/benches/native_vps.rs @@ -15,10 +15,6 @@ use masp_proofs::group::GroupEncoding; use masp_proofs::sapling::BatchValidator; use namada_apps_lib::address::{self, Address, InternalAddress}; use namada_apps_lib::collections::HashMap; -use namada_apps_lib::eth_bridge::read_native_erc20_address; -use namada_apps_lib::eth_bridge::storage::eth_bridge_queries::is_bridge_comptime_enabled; -use namada_apps_lib::eth_bridge::storage::whitelist; -use namada_apps_lib::eth_bridge_pool::{GasFee, PendingTransfer}; use namada_apps_lib::gas::{GasMeterKind, TxGasMeter, VpGasMeter}; use namada_apps_lib::governance::pgf::storage::steward::StewardDetail; use namada_apps_lib::governance::storage::proposal::ProposalType; @@ -48,8 +44,8 @@ use namada_apps_lib::token::masp::{ use namada_apps_lib::token::{Amount, Transfer}; use namada_apps_lib::tx::{BatchedTx, Code, Section, Tx}; use namada_apps_lib::validation::{ - EthBridgeNutVp, EthBridgePoolVp, EthBridgeVp, GovernanceVp, IbcVp, - IbcVpContext, MaspVp, MultitokenVp, ParametersVp, PgfVp, PosVp, + GovernanceVp, IbcVp, IbcVpContext, MaspVp, MultitokenVp, ParametersVp, + PgfVp, PosVp, }; use namada_apps_lib::wallet::defaults; use namada_apps_lib::{ @@ -58,10 +54,9 @@ use namada_apps_lib::{ }; use namada_node::bench_utils::{ ALBERT_PAYMENT_ADDRESS, ALBERT_SPENDING_KEY, BERTHA_PAYMENT_ADDRESS, - BenchShell, BenchShieldedCtx, TX_BRIDGE_POOL_WASM, TX_IBC_WASM, - TX_INIT_PROPOSAL_WASM, TX_RESIGN_STEWARD, TX_TRANSFER_WASM, - TX_UPDATE_STEWARD_COMMISSION, TX_VOTE_PROPOSAL_WASM, - generate_foreign_key_tx, + BenchShell, BenchShieldedCtx, TX_IBC_WASM, TX_INIT_PROPOSAL_WASM, + TX_RESIGN_STEWARD, TX_TRANSFER_WASM, TX_UPDATE_STEWARD_COMMISSION, + TX_VOTE_PROPOSAL_WASM, generate_foreign_key_tx, }; use namada_vm::wasm::VpCache; use namada_vm::wasm::run::VpEvalWasm; @@ -1302,254 +1297,6 @@ fn pgf(c: &mut Criterion) { group.finish(); } -fn eth_bridge_nut(c: &mut Criterion) { - if !is_bridge_comptime_enabled() { - return; - } - - let bench_shell = BenchShell::default(); - let mut shell = bench_shell.write(); - let native_erc20_addres = read_native_erc20_address(&shell.state).unwrap(); - - let signed_tx = { - let data = PendingTransfer { - transfer: namada_apps_lib::eth_bridge_pool::TransferToEthereum { - kind: - namada_apps_lib::eth_bridge_pool::TransferToEthereumKind::Erc20, - asset: native_erc20_addres, - recipient: namada_apps_lib::ethereum_events::EthAddress([1u8; 20]), - sender: defaults::albert_address(), - amount: Amount::from(1), - }, - gas_fee: GasFee { - amount: Amount::from(100), - payer: defaults::albert_address(), - token: shell.state.in_mem().native_token.clone(), - }, - }; - shell.generate_tx( - TX_BRIDGE_POOL_WASM, - data, - None, - None, - vec![&defaults::albert_keypair()], - ) - }; - - // Run the tx to validate - let verifiers_from_tx = shell.execute_tx(&signed_tx.to_ref()); - - let (verifiers, keys_changed) = shell - .state - .write_log() - .verifiers_and_changed_keys(&verifiers_from_tx); - - let vp_address = - Address::Internal(InternalAddress::Nut(native_erc20_addres)); - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = Ctx::new( - &vp_address, - &shell.state, - &signed_tx.tx, - &signed_tx.cmt, - &TxIndex(0), - &gas_meter, - &keys_changed, - &verifiers, - shell.vp_wasm_cache.clone(), - GasMeterKind::MutGlobal, - ); - - c.bench_function("vp_eth_bridge_nut", |b| { - b.iter(|| { - assert!( - EthBridgeNutVp::validate_tx( - &ctx, - &signed_tx.to_ref(), - ctx.keys_changed, - ctx.verifiers, - ) - .is_ok() - ) - }) - }); -} - -fn eth_bridge(c: &mut Criterion) { - if !is_bridge_comptime_enabled() { - return; - } - - let bench_shell = BenchShell::default(); - let mut shell = bench_shell.write(); - let native_erc20_addres = read_native_erc20_address(&shell.state).unwrap(); - - let signed_tx = { - let data = PendingTransfer { - transfer: namada_apps_lib::eth_bridge_pool::TransferToEthereum { - kind: - namada_apps_lib::eth_bridge_pool::TransferToEthereumKind::Erc20, - asset: native_erc20_addres, - recipient: namada_apps_lib::ethereum_events::EthAddress([1u8; 20]), - sender: defaults::albert_address(), - amount: Amount::from(1), - }, - gas_fee: GasFee { - amount: Amount::from(100), - payer: defaults::albert_address(), - token: shell.state.in_mem().native_token.clone(), - }, - }; - shell.generate_tx( - TX_BRIDGE_POOL_WASM, - data, - None, - None, - vec![&defaults::albert_keypair()], - ) - }; - - // Run the tx to validate - let verifiers_from_tx = shell.execute_tx(&signed_tx.to_ref()); - - let (verifiers, keys_changed) = shell - .state - .write_log() - .verifiers_and_changed_keys(&verifiers_from_tx); - - let vp_address = Address::Internal(InternalAddress::EthBridge); - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = Ctx::new( - &vp_address, - &shell.state, - &signed_tx.tx, - &signed_tx.cmt, - &TxIndex(0), - &gas_meter, - &keys_changed, - &verifiers, - shell.vp_wasm_cache.clone(), - GasMeterKind::MutGlobal, - ); - - c.bench_function("vp_eth_bridge", |b| { - b.iter(|| { - assert!( - EthBridgeVp::validate_tx( - &ctx, - &signed_tx.to_ref(), - ctx.keys_changed, - ctx.verifiers, - ) - .is_ok() - ) - }) - }); -} - -fn eth_bridge_pool(c: &mut Criterion) { - if !is_bridge_comptime_enabled() { - return; - } - - // NOTE: this vp is one of the most expensive but its cost comes from the - // numerous accesses to storage that we already account for, so no need to - // benchmark specific sections of it like for the ibc native vp - let bench_shell = BenchShell::default(); - let mut shell = bench_shell.write(); - let native_erc20_addres = read_native_erc20_address(&shell.state).unwrap(); - - // Whitelist NAM token - let cap_key = whitelist::Key { - asset: native_erc20_addres, - suffix: whitelist::KeyType::Cap, - } - .into(); - shell.state.write(&cap_key, Amount::from(1_000)).unwrap(); - - let whitelisted_key = whitelist::Key { - asset: native_erc20_addres, - suffix: whitelist::KeyType::Whitelisted, - } - .into(); - shell.state.write(&whitelisted_key, true).unwrap(); - - let denom_key = whitelist::Key { - asset: native_erc20_addres, - suffix: whitelist::KeyType::Denomination, - } - .into(); - shell.state.write(&denom_key, 0).unwrap(); - - let signed_tx = { - let data = PendingTransfer { - transfer: namada_apps_lib::eth_bridge_pool::TransferToEthereum { - kind: - namada_apps_lib::eth_bridge_pool::TransferToEthereumKind::Erc20, - asset: native_erc20_addres, - recipient: namada_apps_lib::ethereum_events::EthAddress([1u8; 20]), - sender: defaults::albert_address(), - amount: Amount::from(1), - }, - gas_fee: GasFee { - amount: Amount::from(100), - payer: defaults::albert_address(), - token: shell.state.in_mem().native_token.clone(), - }, - }; - shell.generate_tx( - TX_BRIDGE_POOL_WASM, - data, - None, - None, - vec![&defaults::albert_keypair()], - ) - }; - - // Run the tx to validate - let verifiers_from_tx = shell.execute_tx(&signed_tx.to_ref()); - - let (verifiers, keys_changed) = shell - .state - .write_log() - .verifiers_and_changed_keys(&verifiers_from_tx); - - let vp_address = Address::Internal(InternalAddress::EthBridgePool); - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = Ctx::new( - &vp_address, - &shell.state, - &signed_tx.tx, - &signed_tx.cmt, - &TxIndex(0), - &gas_meter, - &keys_changed, - &verifiers, - shell.vp_wasm_cache.clone(), - GasMeterKind::MutGlobal, - ); - - c.bench_function("vp_eth_bridge_pool", |b| { - b.iter(|| { - assert!( - EthBridgePoolVp::validate_tx( - &ctx, - &signed_tx.to_ref(), - ctx.keys_changed, - ctx.verifiers, - ) - .is_ok() - ) - }) - }); -} - fn parameters(c: &mut Criterion) { let mut group = c.benchmark_group("vp_parameters"); @@ -1849,9 +1596,6 @@ criterion_group!( masp_batch_output_proofs_validate, vp_multitoken, pgf, - eth_bridge_nut, - eth_bridge, - eth_bridge_pool, parameters, pos, ibc_vp_validate_action, diff --git a/crates/core/src/address.rs b/crates/core/src/address.rs index d95e22e2696..ef5cd3dd183 100644 --- a/crates/core/src/address.rs +++ b/crates/core/src/address.rs @@ -1,6 +1,8 @@ //! Implements transparent addresses as described in [Accounts //! Addresses](docs/src/explore/design/ledger/accounts.md#addresses). +#![allow(deprecated)] // Triggered by deprecated ETH bridge addresses + mod raw; use std::fmt::{Debug, Display}; @@ -16,7 +18,6 @@ use namada_migrations::*; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use crate::ethereum_events::EthAddress; use crate::ibc::IbcTokenHash; use crate::key::PublicKeyHash; use crate::{impl_display_and_from_str_via_format, key, string_encoding}; @@ -65,8 +66,6 @@ pub const PGF: Address = Address::Internal(InternalAddress::Pgf); pub const MASP: Address = Address::Internal(InternalAddress::Masp); /// Internal Multitoken address pub const MULTITOKEN: Address = Address::Internal(InternalAddress::Multitoken); -/// Internal Eth bridge address -pub const ETH_BRIDGE: Address = Address::Internal(InternalAddress::EthBridge); /// Address with temporary storage is used to pass data from txs to VPs which is /// never committed to DB pub const TEMP_STORAGE: Address = @@ -121,22 +120,10 @@ impl From> for Address { Address::Internal(InternalAddress::Governance) } raw::Discriminant::Ibc => Address::Internal(InternalAddress::Ibc), - raw::Discriminant::EthBridge => { - Address::Internal(InternalAddress::EthBridge) - } - raw::Discriminant::BridgePool => { - Address::Internal(InternalAddress::EthBridgePool) - } raw::Discriminant::Multitoken => { Address::Internal(InternalAddress::Multitoken) } raw::Discriminant::Pgf => Address::Internal(InternalAddress::Pgf), - raw::Discriminant::Erc20 => Address::Internal( - InternalAddress::Erc20(EthAddress(*raw_addr.data())), - ), - raw::Discriminant::Nut => Address::Internal(InternalAddress::Nut( - EthAddress(*raw_addr.data()), - )), raw::Discriminant::IbcToken => Address::Internal( InternalAddress::IbcToken(IbcTokenHash(*raw_addr.data())), ), @@ -147,6 +134,11 @@ impl From> for Address { raw::Discriminant::ReplayProtection => { Address::Internal(InternalAddress::ReplayProtection) } + #[allow(deprecated)] + raw::Discriminant::EthBridge + | raw::Discriminant::BridgePool + | raw::Discriminant::Erc20 + | raw::Discriminant::Nut => unimplemented!(), } } } @@ -214,15 +206,13 @@ impl<'addr> From<&'addr Address> for raw::Address<'addr, raw::Validated> { .validate() .expect("This raw address is valid") } - Address::Internal(InternalAddress::Erc20(EthAddress(eth_addr))) => { + Address::Internal(InternalAddress::Erc20) => { raw::Address::from_discriminant(raw::Discriminant::Erc20) - .with_data_array_ref(eth_addr) .validate() .expect("This raw address is valid") } - Address::Internal(InternalAddress::Nut(EthAddress(eth_addr))) => { + Address::Internal(InternalAddress::Nut) => { raw::Address::from_discriminant(raw::Discriminant::Nut) - .with_data_array_ref(eth_addr) .validate() .expect("This raw address is valid") } @@ -619,13 +609,17 @@ pub enum InternalAddress { /// Governance address Governance, /// Bridge to Ethereum + #[deprecated] EthBridge, /// The pool of transactions to be relayed to Ethereum + #[deprecated] EthBridgePool, /// ERC20 token for Ethereum bridge - Erc20(EthAddress), + #[deprecated] + Erc20, /// Non-usable ERC20 tokens - Nut(EthAddress), + #[deprecated] + Nut, /// Multitoken Multitoken, /// Pgf @@ -653,8 +647,8 @@ impl Display for InternalAddress { Self::IbcToken(hash) => format!("IbcToken: {}", hash), Self::EthBridge => "EthBridge".to_string(), Self::EthBridgePool => "EthBridgePool".to_string(), - Self::Erc20(eth_addr) => format!("Erc20: {}", eth_addr), - Self::Nut(eth_addr) => format!("Non-usable token: {eth_addr}"), + Self::Erc20 => "Erc20".to_string(), + Self::Nut => "Non-usable token".to_string(), Self::Multitoken => "Multitoken".to_string(), Self::Pgf => "PGF".to_string(), Self::Masp => "MASP".to_string(), @@ -671,8 +665,6 @@ impl InternalAddress { match alias { "pos" => Some(InternalAddress::PoS), "ibc" => Some(InternalAddress::Ibc), - "ethbridge" => Some(InternalAddress::EthBridge), - "bridgepool" => Some(InternalAddress::EthBridgePool), "governance" => Some(InternalAddress::Governance), "masp" => Some(InternalAddress::Masp), "replayprotection" => Some(InternalAddress::ReplayProtection), @@ -934,8 +926,8 @@ pub mod testing { InternalAddress::IbcToken(_) => {} InternalAddress::EthBridge => {} InternalAddress::EthBridgePool => {} - InternalAddress::Erc20(_) => {} - InternalAddress::Nut(_) => {} + InternalAddress::Erc20 => {} + InternalAddress::Nut => {} InternalAddress::Pgf => {} InternalAddress::Masp => {} InternalAddress::Multitoken => {} @@ -950,10 +942,6 @@ pub mod testing { Just(InternalAddress::Parameters), arb_ibc_token(), Just(InternalAddress::Governance), - Just(InternalAddress::EthBridge), - Just(InternalAddress::EthBridgePool), - arb_erc20(), - arb_nut(), Just(InternalAddress::Multitoken), Just(InternalAddress::Pgf), Just(InternalAddress::Masp), @@ -983,18 +971,6 @@ pub mod testing { }) } - fn arb_erc20() -> impl Strategy { - proptest::array::uniform20(proptest::num::u8::ANY).prop_map(|addr| { - InternalAddress::Erc20(crate::ethereum_events::EthAddress(addr)) - }) - } - - fn arb_nut() -> impl Strategy { - proptest::array::uniform20(proptest::num::u8::ANY).prop_map(|addr| { - InternalAddress::Nut(crate::ethereum_events::EthAddress(addr)) - }) - } - /// NAM token address for testing pub fn nam() -> Address { Address::decode("tnam1q99c37u38grkdcc2qze0hz4zjjd8zr3yucd3mzgz") @@ -1037,15 +1013,6 @@ pub mod testing { .expect("The token address decoding shouldn't fail") } - /// Imaginary eth address for testing - pub const fn wnam() -> EthAddress { - // "DEADBEEF DEADBEEF DEADBEEF DEADBEEF DEADBEEF" - EthAddress([ - 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, 222, - 173, 190, 239, 222, 173, 190, 239, - ]) - } - /// A hash map of tokens addresses with their informal currency codes and /// number of decimal places. pub fn tokens() -> HashMap<&'static str, Denomination> { diff --git a/crates/core/src/address/raw.rs b/crates/core/src/address/raw.rs index 05a4df1ff5f..aca814818e2 100644 --- a/crates/core/src/address/raw.rs +++ b/crates/core/src/address/raw.rs @@ -50,16 +50,20 @@ pub enum Discriminant { /// IBC raw address. Ibc = 6, /// Ethereum bridge raw address. + #[deprecated] EthBridge = 7, /// Bridge pool raw address. + #[deprecated] BridgePool = 8, /// Multitoken raw address. Multitoken = 9, /// Public goods funding raw address. Pgf = 10, /// ERC20 raw address. + #[deprecated] Erc20 = 11, /// NUT raw address. + #[deprecated] Nut = 12, /// IBC token raw address. IbcToken = 13, @@ -116,8 +120,6 @@ impl Address<'_, S> { self.discriminant, Discriminant::Implicit | Discriminant::Established - | Discriminant::Erc20 - | Discriminant::Nut | Discriminant::IbcToken, ) } diff --git a/crates/core/src/eth_abi.rs b/crates/core/src/eth_abi.rs deleted file mode 100644 index 866c9bfedc0..00000000000 --- a/crates/core/src/eth_abi.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! This module defines encoding methods compatible with Ethereum -//! smart contracts. - -use std::marker::PhantomData; - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -#[doc(inline)] -pub use ethabi::token::Token; - -use crate::keccak::{KeccakHash, keccak_hash}; -use crate::key::{Signable, SignableEthMessage}; - -/// A container for data types that are able to be Ethereum ABI-encoded. -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema)] -#[repr(transparent)] -pub struct EncodeCell { - /// ABI-encoded value of type `T`. - encoded_data: Vec, - /// Indicate we do not own values of type `T`. - /// - /// Passing `PhantomData` here would trigger the drop checker, - /// which is not the desired behavior, since we own an encoded value - /// of `T`, not a value of `T` itself. - _marker: PhantomData<*const T>, -} - -impl AsRef<[u8]> for EncodeCell { - fn as_ref(&self) -> &[u8] { - &self.encoded_data - } -} - -impl ::std::cmp::Eq for EncodeCell {} - -impl ::std::cmp::PartialEq for EncodeCell { - fn eq(&self, other: &Self) -> bool { - self.encoded_data == other.encoded_data - } -} - -impl ::std::cmp::PartialOrd for EncodeCell { - fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> { - Some(self.cmp(other)) - } -} - -impl ::std::cmp::Ord for EncodeCell { - fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { - self.encoded_data.cmp(&other.encoded_data) - } -} - -impl EncodeCell { - /// Return a new ABI encoded value of type `T`. - pub fn new(value: &T) -> Self - where - T: Encode, - { - let encoded_data = { - let tokens = value.tokenize(); - ethabi::encode(tokens.as_slice()) - }; - Self { - encoded_data, - _marker: PhantomData, - } - } - - /// Here the type information is not compiler deduced, - /// proceed with caution! - pub fn new_from(tokens: [Token; N]) -> Self { - Self { - encoded_data: ethabi::encode(&tokens), - _marker: PhantomData, - } - } - - /// Return the underlying ABI encoded value. - pub fn into_inner(self) -> Vec { - self.encoded_data - } -} - -/// Contains a method to encode data to a format compatible with Ethereum. -pub trait Encode: Sized { - /// Encodes a struct into a sequence of ABI - /// [`Token`] instances. - fn tokenize(&self) -> [Token; N]; - - /// Returns the encoded [`Token`] instances, in a type-safe enclosure. - fn encode(&self) -> EncodeCell { - EncodeCell::new(self) - } - - /// Encodes a slice of [`Token`] instances, and returns the - /// keccak hash of the encoded string. - fn keccak256(&self) -> KeccakHash { - keccak_hash(self.encode().into_inner().as_slice()) - } - - /// Encodes a slice of [`Token`] instances, and returns the - /// keccak hash of the encoded string appended to an Ethereum - /// signature header. This can then be signed. - fn signable_keccak256(&self) -> KeccakHash { - let message = self.keccak256(); - SignableEthMessage::as_signable(&message) - } -} - -/// Represents an Ethereum encoding method equivalent -/// to `abi.encode`. -pub type AbiEncode = [Token; N]; - -impl Encode for AbiEncode { - #[inline] - fn tokenize(&self) -> [Token; N] { - self.clone() - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use data_encoding::HEXLOWER; - use ethabi::ethereum_types::U256; - use tiny_keccak::{Hasher, Keccak}; - - use super::*; - use crate::ethereum_events::EthAddress; - - /// Checks if we get the same result as `abi.encode`, for some given - /// input data. - #[test] - fn test_abi_encode() { - let expected = "0x000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000"; - let expected = HEXLOWER - .decode(&expected.as_bytes()[2..]) - .expect("Test failed"); - let got = AbiEncode::encode(&[ - Token::Uint(U256::from(42u64)), - Token::String("test".into()), - ]); - assert_eq!(expected, got.into_inner()); - } - - /// Sanity check our keccak hash implementation. - #[test] - fn test_keccak_hash_impl() { - let expected = - "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"; - assert_eq!( - expected, - &HEXLOWER.encode( - &{ - let mut st = Keccak::v256(); - let mut output = [0; 32]; - st.update(b"hello"); - st.finalize(&mut output); - output - }[..] - ) - ); - } - - /// Test that the methods for converting a keccak hash to/from - /// a string type are inverses. - #[test] - fn test_hex_roundtrip() { - let original = - "1C8AFF950685C2ED4BC3174F3472287B56D9517B9C948127319A09A7A36DEAC8"; - let keccak_hash: KeccakHash = original.try_into().expect("Test failed"); - assert_eq!(keccak_hash.to_string().as_str(), original); - } - - #[test] - fn test_abi_encode_address() { - let address = - EthAddress::from_str("0xF0457e703bf0B9dEb1a6003FFD71C77E44575f95") - .expect("Test failed"); - let expected = "0x000000000000000000000000f0457e703bf0b9deb1a6003ffd71c77e44575f95"; - let expected = HEXLOWER - .decode(&expected.as_bytes()[2..]) - .expect("Test failed"); - let encoded = ethabi::encode(&[Token::Address(address.0.into())]); - assert_eq!(expected, encoded); - } -} diff --git a/crates/core/src/eth_bridge_pool.rs b/crates/core/src/eth_bridge_pool.rs deleted file mode 100644 index 670e3706e84..00000000000 --- a/crates/core/src/eth_bridge_pool.rs +++ /dev/null @@ -1,470 +0,0 @@ -//! The necessary type definitions for the contents of the -//! Ethereum bridge pool - -use std::borrow::Cow; - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use ethabi::token::Token; -use namada_macros::{BorshDeserializer, StorageKeys}; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use serde::{Deserialize, Serialize}; - -use super::address::InternalAddress; -use super::keccak::KeccakHash; -use super::storage::{self, KeySeg}; -use crate as namada_core; // This is needed for `StorageKeys` macro -use crate::address::Address; -use crate::borsh::BorshSerializeExt; -use crate::eth_abi::Encode; -use crate::ethereum_events::{ - EthAddress, TransferToEthereum as TransferToEthereumEvent, -}; -use crate::hash::Hash as HashDigest; -use crate::storage::{DbKeySeg, Key}; -use crate::token::Amount; - -/// The main address of the Ethereum bridge pool -pub const BRIDGE_POOL_ADDRESS: Address = - Address::Internal(InternalAddress::EthBridgePool); - -/// Bridge pool key segments. -#[derive(StorageKeys)] -pub struct Segments { - /// Signed root storage key - pub signed_root: &'static str, - /// Bridge pool nonce storage key - pub bridge_pool_nonce: &'static str, -} - -/// Check if a key is for a pending transfer -pub fn is_pending_transfer_key(key: &storage::Key) -> bool { - let segment = match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(segment)] - if addr == &BRIDGE_POOL_ADDRESS => - { - segment.as_str() - } - _ => return false, - }; - !Segments::ALL.iter().any(|s| s == &segment) -} - -/// Get the storage key for the transfers in the pool -pub fn get_pending_key(transfer: &PendingTransfer) -> Key { - get_key_from_hash(&transfer.keccak256()) -} - -/// Get the storage key for the transfers using the hash -pub fn get_key_from_hash(hash: &KeccakHash) -> Key { - Key { - segments: vec![ - DbKeySeg::AddressSeg(BRIDGE_POOL_ADDRESS), - hash.to_db_key(), - ], - } -} - -/// A version used in our Ethereuem smart contracts -const VERSION: u8 = 1; - -/// A namespace used in our Ethereuem smart contracts -const NAMESPACE: &str = "transfer"; - -/// Transfer to Ethereum kinds. -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, -)] -pub enum TransferToEthereumKind { - /// Transfer ERC20 assets from Namada to Ethereum. - /// - /// These transfers burn wrapped ERC20 assets in Namada, once - /// they have been confirmed. - Erc20, - /// Refund non-usable tokens. - /// - /// These Bridge pool transfers should be crafted for assets - /// that have been transferred to Namada, that had either not - /// been whitelisted or whose token caps had been exceeded in - /// Namada at the time of the transfer. - Nut, -} - -impl std::fmt::Display for TransferToEthereumKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Erc20 => write!(f, "ERC20"), - Self::Nut => write!(f, "NUT"), - } - } -} - -/// Additional data appended to a [`TransferToEthereumEvent`] to -/// construct a [`PendingTransfer`]. -#[derive( - Debug, - Clone, - Hash, - PartialOrd, - PartialEq, - Ord, - Eq, - Serialize, - Deserialize, - BorshSerialize, - BorshDeserialize, - /* BorshSchema, */ -)] -pub struct PendingTransferAppendix<'transfer> { - /// The kind of the pending transfer to Ethereum. - pub kind: Cow<'transfer, TransferToEthereumKind>, - /// The sender of the transfer. - pub sender: Cow<'transfer, Address>, - /// The amount of gas fees paid by the user - /// sending this transfer. - pub gas_fee: Cow<'transfer, GasFee>, -} - -impl From for PendingTransferAppendix<'static> { - #[inline] - fn from(pending: PendingTransfer) -> Self { - Self { - kind: Cow::Owned(pending.transfer.kind), - sender: Cow::Owned(pending.transfer.sender), - gas_fee: Cow::Owned(pending.gas_fee), - } - } -} - -impl<'t> From<&'t PendingTransfer> for PendingTransferAppendix<'t> { - #[inline] - fn from(pending: &'t PendingTransfer) -> Self { - Self { - kind: Cow::Borrowed(&pending.transfer.kind), - sender: Cow::Borrowed(&pending.transfer.sender), - gas_fee: Cow::Borrowed(&pending.gas_fee), - } - } -} - -impl PendingTransferAppendix<'_> { - /// Calculate the checksum of this [`PendingTransferAppendix`]. - pub fn checksum(&self) -> HashDigest { - let serialized = self.serialize_to_vec(); - HashDigest::sha256(serialized) - } -} - -/// A transfer message to be submitted to Ethereum -/// to move assets from Namada across the bridge. -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Debug, - Clone, - Hash, - PartialOrd, - PartialEq, - Ord, - Eq, - Serialize, - Deserialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct TransferToEthereum { - /// The kind of transfer to Ethereum. - pub kind: TransferToEthereumKind, - /// The type of token - pub asset: EthAddress, - /// The recipient address - pub recipient: EthAddress, - /// The sender of the transfer - pub sender: Address, - /// The amount to be transferred - pub amount: Amount, -} - -/// A transfer message to Ethereum sitting in the -/// bridge pool, waiting to be relayed -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Debug, - Clone, - Hash, - PartialOrd, - PartialEq, - Ord, - Eq, - Serialize, - Deserialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct PendingTransfer { - /// Transfer to Ethereum data. - pub transfer: TransferToEthereum, - /// Amount of gas fees paid by the user - /// sending the transfer. - pub gas_fee: GasFee, -} - -/// Construct a token address from an ERC20 address. -pub fn erc20_token_address(address: &EthAddress) -> Address { - Address::Internal(InternalAddress::Erc20(*address)) -} - -/// Construct a NUT token address from an ERC20 address. -pub fn erc20_nut_address(address: &EthAddress) -> Address { - Address::Internal(InternalAddress::Nut(*address)) -} - -impl PendingTransfer { - /// Get a token [`Address`] from this [`PendingTransfer`]. - #[inline] - pub fn token_address(&self) -> Address { - match &self.transfer.kind { - TransferToEthereumKind::Erc20 => { - erc20_token_address(&self.transfer.asset) - } - TransferToEthereumKind::Nut => { - erc20_nut_address(&self.transfer.asset) - } - } - } - - /// Retrieve a reference to the appendix of this [`PendingTransfer`]. - #[inline] - pub fn appendix(&self) -> PendingTransferAppendix<'_> { - self.into() - } - - /// Retrieve the owned appendix of this [`PendingTransfer`]. - #[inline] - pub fn into_appendix(self) -> PendingTransferAppendix<'static> { - self.into() - } - - /// Craft a [`PendingTransfer`] from its constituents. - pub fn from_parts( - event: &TransferToEthereumEvent, - appendix: PendingTransferAppendix<'_>, - ) -> Self { - let transfer = TransferToEthereum { - kind: *appendix.kind, - asset: event.asset, - recipient: event.receiver, - sender: (*appendix.sender).clone(), - amount: event.amount, - }; - let gas_fee = (*appendix.gas_fee).clone(); - Self { transfer, gas_fee } - } -} - -impl From<&PendingTransfer> for ethbridge_structs::Erc20Transfer { - fn from(pending: &PendingTransfer) -> Self { - let HashDigest(data_digest) = pending.appendix().checksum(); - Self { - from: pending.transfer.asset.0.into(), - to: pending.transfer.recipient.0.into(), - amount: pending.transfer.amount.into(), - data_digest, - } - } -} - -impl From<&PendingTransfer> for TransferToEthereumEvent { - fn from(pending: &PendingTransfer) -> Self { - Self { - amount: pending.transfer.amount, - asset: pending.transfer.asset, - receiver: pending.transfer.recipient, - checksum: pending.appendix().checksum(), - } - } -} - -impl Encode<6> for PendingTransfer { - fn tokenize(&self) -> [Token; 6] { - // TODO(namada#249): This version should be looked up from storage - let version = Token::Uint(VERSION.into()); - let namespace = Token::String(NAMESPACE.into()); - let from = Token::Address(self.transfer.asset.0.into()); - let to = Token::Address(self.transfer.recipient.0.into()); - let amount = Token::Uint(self.transfer.amount.into()); - let checksum = Token::FixedBytes(self.appendix().checksum().0.into()); - [version, namespace, from, to, amount, checksum] - } -} - -impl Encode<6> for TransferToEthereumEvent { - fn tokenize(&self) -> [Token; 6] { - // TODO(namada#249): This version should be looked up from storage - let version = Token::Uint(VERSION.into()); - let namespace = Token::String(NAMESPACE.into()); - let from = Token::Address(self.asset.0.into()); - let to = Token::Address(self.receiver.0.into()); - let amount = Token::Uint(self.amount.into()); - let checksum = Token::FixedBytes(self.checksum.0.into()); - [version, namespace, from, to, amount, checksum] - } -} - -impl From<&PendingTransfer> for Key { - fn from(transfer: &PendingTransfer) -> Self { - Key { - segments: vec![DbKeySeg::StringSeg( - transfer.keccak256().to_string(), - )], - } - } -} - -/// The amount of fees to be paid, in Namada, to the relayer -/// of a transfer across the Ethereum Bridge, compensating -/// for Ethereum gas costs. -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Debug, - Clone, - Hash, - PartialOrd, - PartialEq, - Ord, - Eq, - Serialize, - Deserialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct GasFee { - /// The amount of fees. - pub amount: Amount, - /// The account of fee payer. - pub payer: Address, - /// The address of the fungible token to draw - /// gas fees from. - pub token: Address, -} - -#[cfg(any(test, feature = "testing"))] -/// Testing helpers and strategies for the Ethereum bridge pool -pub mod testing { - use proptest::prop_compose; - use proptest::strategy::Strategy; - - use super::*; - use crate::address::testing::{ - arb_established_address, arb_non_internal_address, - }; - use crate::ethereum_events::testing::arb_eth_address; - use crate::token::testing::arb_amount; - - prop_compose! { - /// Generate an arbitrary pending transfer - pub fn arb_pending_transfer()( - transfer in arb_transfer_to_ethereum(), - gas_fee in arb_gas_fee(), - ) -> PendingTransfer { - PendingTransfer { - transfer, - gas_fee, - } - } - } - - prop_compose! { - /// Generate an arbitrary Ethereum gas fee - pub fn arb_gas_fee()( - amount in arb_amount(), - payer in arb_non_internal_address(), - token in arb_established_address().prop_map(Address::Established), - ) -> GasFee { - GasFee { - amount, - payer, - token, - } - } - } - - prop_compose! { - /// Generate the kind of a transfer to ethereum - pub fn arb_transfer_to_ethereum_kind()( - discriminant in 0..2, - ) -> TransferToEthereumKind { - match discriminant { - 0 => TransferToEthereumKind::Erc20, - 1 => TransferToEthereumKind::Nut, - _ => unreachable!(), - } - } - } - - prop_compose! { - /// Generate an arbitrary transfer to Ethereum - pub fn arb_transfer_to_ethereum()( - kind in arb_transfer_to_ethereum_kind(), - asset in arb_eth_address(), - recipient in arb_eth_address(), - sender in arb_non_internal_address(), - amount in arb_amount(), - ) -> TransferToEthereum { - TransferToEthereum { - kind, - asset, - recipient, - sender, - amount, - } - } - } -} - -#[cfg(test)] -mod test_eth_bridge_pool_types { - use super::*; - use crate::address::testing::{established_address_1, nam}; - - /// Test that [`PendingTransfer`] and [`TransferToEthereum`] - /// have the same keccak hash, after being ABI encoded. - #[test] - fn test_same_keccak_hash() { - let pending = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - amount: 10u64.into(), - asset: EthAddress([0xaa; 20]), - recipient: EthAddress([0xbb; 20]), - sender: established_address_1(), - }, - gas_fee: GasFee { - token: nam(), - amount: 10u64.into(), - payer: established_address_1(), - }, - }; - let event: TransferToEthereumEvent = (&pending).into(); - assert_eq!(pending.keccak256(), event.keccak256()); - } -} diff --git a/crates/core/src/ethereum_events.rs b/crates/core/src/ethereum_events.rs deleted file mode 100644 index b4873c695e0..00000000000 --- a/crates/core/src/ethereum_events.rs +++ /dev/null @@ -1,514 +0,0 @@ -//! Types representing data intended for Namada via Ethereum events - -use std::cmp::Ordering; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use ethabi::Token; -use ethabi::ethereum_types::{H160, U256 as ethUint}; -use eyre::{Context, eyre}; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use serde::{Deserialize, Serialize}; - -use crate::address::Address; -use crate::borsh::BorshSerializeExt; -use crate::eth_abi::Encode; -use crate::ethereum_structs::Erc20Transfer; -use crate::hash::Hash; -use crate::keccak::KeccakHash; -use crate::storage::{DbKeySeg, KeySeg}; -use crate::token::Amount; - -/// Namada native type to replace the ethabi::Uint type -#[derive( - Copy, - Clone, - Debug, - Default, - Hash, - PartialEq, - Eq, - Serialize, - Deserialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -#[repr(align(32))] -pub struct Uint(pub [u64; 4]); - -impl PartialOrd for Uint { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Uint { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - ethUint(self.0).cmp(ðUint(other.0)) - } -} - -impl Uint { - /// Convert to an Ethereum-compatible byte representation. - /// - /// The Ethereum virtual machine employs big-endian integers - /// (Wood, 2014), therefore the returned byte array has the - /// same endianness. - pub fn to_bytes(self) -> [u8; 32] { - let mut bytes = [0; 32]; - ethUint(self.0).to_big_endian(&mut bytes); - bytes - } - - /// Try to increment this [`Uint`], whilst checking - /// for overflows. - pub fn checked_increment(self) -> Option { - ethUint::from(self).checked_add(1.into()).map(Self::from) - } -} - -impl Display for Uint { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - ethUint(self.0).fmt(f) - } -} - -impl Encode<1> for Uint { - fn tokenize(&self) -> [Token; 1] { - [Token::Uint(self.into())] - } -} - -impl From for Uint { - fn from(value: ethUint) -> Self { - Self(value.0) - } -} - -impl From for ethUint { - fn from(value: Uint) -> Self { - Self(value.0) - } -} - -impl From<&Uint> for ethUint { - fn from(value: &Uint) -> Self { - Self(value.0) - } -} - -impl From for Uint { - fn from(value: u64) -> Self { - ethUint::from(value).into() - } -} - -/// Representation of address on Ethereum. The inner value is the last 20 bytes -/// of the public key that controls the account. -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -#[serde(try_from = "String")] -#[serde(into = "String")] -pub struct EthAddress(pub [u8; 20]); - -impl EthAddress { - /// The canonical way we represent an [`EthAddress`] in storage keys. A - /// 40-character lower case hexadecimal address prefixed by '0x'. - /// e.g. "0x6b175474e89094c44da98b954eedeac495271d0f" - pub fn to_canonical(&self) -> String { - format!("{:?}", ethabi::ethereum_types::Address::from(&self.0)) - } -} - -impl From for EthAddress { - fn from(H160(addr): H160) -> Self { - Self(addr) - } -} - -impl From for H160 { - fn from(EthAddress(addr): EthAddress) -> Self { - Self(addr) - } -} - -impl Display for EthAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.to_canonical()) - } -} - -impl FromStr for EthAddress { - type Err = eyre::Error; - - /// Parses an [`EthAddress`] from a standard hex-encoded Ethereum address - /// string. e.g. "0x6B175474E89094C44Da98b954EedeAC495271d0F" - fn from_str(s: &str) -> Result { - let h160 = ethabi::ethereum_types::Address::from_str(s) - .wrap_err_with(|| eyre!("couldn't parse Ethereum address {}", s))?; - Ok(Self(h160.into())) - } -} - -impl TryFrom for EthAddress { - type Error = eyre::Error; - - fn try_from(string: String) -> Result { - Self::from_str(string.as_ref()) - } -} - -impl From for String { - fn from(addr: EthAddress) -> Self { - addr.to_string() - } -} - -impl KeySeg for EthAddress { - fn parse(string: String) -> crate::storage::Result { - Self::from_str(string.as_str()) - .map_err(|_| crate::storage::Error::ParseKeySeg(string)) - } - - fn raw(&self) -> String { - self.to_canonical() - } - - fn to_db_key(&self) -> DbKeySeg { - DbKeySeg::StringSeg(self.raw()) - } -} - -/// Nonces of Ethereum events. -pub trait GetEventNonce { - /// Returns the nonce of an Ethereum event. - fn get_event_nonce(&self) -> Uint; -} - -/// Event transferring batches of ether or Ethereum based ERC20 tokens -/// from Ethereum to wrapped assets on Namada -#[derive( - PartialEq, - Eq, - PartialOrd, - Hash, - Ord, - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct TransfersToNamada { - /// Monotonically increasing nonce - pub nonce: Uint, - /// The batch of transfers - pub transfers: Vec, -} - -impl GetEventNonce for TransfersToNamada { - #[inline] - fn get_event_nonce(&self) -> Uint { - self.nonce - } -} - -impl From for EthereumEvent { - #[inline] - fn from(event: TransfersToNamada) -> Self { - let TransfersToNamada { nonce, transfers } = event; - Self::TransfersToNamada { nonce, transfers } - } -} - -/// An Ethereum event to be processed by the Namada ledger -#[derive( - PartialEq, - Eq, - PartialOrd, - Hash, - Ord, - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -// NOTE: Avoid changing the order of the elements in this struct, -// to maintain compatibility between Namada versions. -pub enum EthereumEvent { - /// Event transferring batches of ether or Ethereum based ERC20 tokens - /// from Ethereum to wrapped assets on Namada - TransfersToNamada { - /// Monotonically increasing nonce - nonce: Uint, - /// The batch of transfers - transfers: Vec, - }, - /// A confirmation event that a batch of transfers have been made - /// from Namada to Ethereum - TransfersToEthereum { - /// Monotonically increasing nonce - nonce: Uint, - /// The batch of transfers - transfers: Vec, - /// The Namada address that receives the gas fees - /// for relaying a batch of transfers - relayer: Address, - }, - /// Event indication that the validator set has been updated - /// in the governance contract - ValidatorSetUpdate { - /// Monotonically increasing nonce - nonce: Uint, - /// Hash of the validators in the bridge contract - bridge_validator_hash: KeccakHash, - /// Hash of the validators in the governance contract - governance_validator_hash: KeccakHash, - }, -} - -impl EthereumEvent { - /// SHA256 of the Borsh serialization of the [`EthereumEvent`]. - pub fn hash(&self) -> Result { - let bytes = self.serialize_to_vec(); - Ok(Hash::sha256(bytes)) - } -} - -/// An event transferring some kind of value from Ethereum to Namada -#[derive( - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Hash, - Ord, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct TransferToNamada { - /// Quantity of the ERC20 token in the transfer - pub amount: Amount, - /// Address of the smart contract issuing the token - pub asset: EthAddress, - /// The address receiving wrapped assets on Namada - pub receiver: Address, -} - -/// An event transferring some kind of value from Namada to Ethereum -#[derive( - Clone, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, -)] -pub struct TransferToEthereum { - /// Quantity of wrapped Asset in the transfer - pub amount: Amount, - /// Address of the smart contract issuing the token - pub asset: EthAddress, - /// The address receiving assets on Ethereum - pub receiver: EthAddress, - /// Checksum of all Namada specific fields, including, - /// but not limited to, whether it is a NUT transfer, - /// the address of the sender, etc - /// - /// It serves to uniquely identify an event stored under - /// the Bridge pool, in Namada - pub checksum: Hash, -} - -impl From for TransferToEthereum { - #[inline] - fn from(transfer: Erc20Transfer) -> Self { - Self { - amount: { - let uint = { - use crate::uint::Uint as NamadaUint; - let mut num_buf = [0; 32]; - transfer.amount.to_little_endian(&mut num_buf); - NamadaUint::from_little_endian(&num_buf) - }; - // this is infallible for a denom of 0 - Amount::from_uint(uint, 0).unwrap() - }, - asset: EthAddress(transfer.from.0), - receiver: EthAddress(transfer.to.0), - checksum: Hash(transfer.data_digest), - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_eth_address_to_canonical() { - let canonical = testing::DAI_ERC20_ETH_ADDRESS.to_canonical(); - - assert_eq!( - testing::DAI_ERC20_ETH_ADDRESS_CHECKSUMMED.to_ascii_lowercase(), - canonical, - ); - } - - #[test] - fn test_eth_address_from_str() { - let addr = - EthAddress::from_str(testing::DAI_ERC20_ETH_ADDRESS_CHECKSUMMED) - .unwrap(); - - assert_eq!(testing::DAI_ERC20_ETH_ADDRESS, addr); - } - - #[test] - fn test_eth_address_from_str_error() { - let result = EthAddress::from_str( - "arbitrary string which isn't an Ethereum address", - ); - - assert!(result.is_err()); - } - - /// Test that serde correct serializes EthAddress types to/from lowercase - /// hex encodings - #[test] - fn test_eth_address_serde_roundtrip() { - let addr = - EthAddress::from_str(testing::DAI_ERC20_ETH_ADDRESS_CHECKSUMMED) - .unwrap(); - let serialized = serde_json::to_string(&addr).expect("Test failed"); - assert_eq!( - serialized, - format!( - r#""{}""#, - testing::DAI_ERC20_ETH_ADDRESS_CHECKSUMMED.to_lowercase() - ) - ); - let deserialized: EthAddress = - serde_json::from_str(&serialized).expect("Test failed"); - assert_eq!(addr, deserialized); - } -} - -#[allow(missing_docs)] -#[allow(clippy::arithmetic_side_effects)] -/// Test helpers -#[cfg(any(test, feature = "testing", feature = "benches"))] -pub mod testing { - use proptest::prop_compose; - - use super::*; - use crate::token; - - pub const DAI_ERC20_ETH_ADDRESS_CHECKSUMMED: &str = - "0x6B175474E89094C44Da98b954EedeAC495271d0F"; - pub const DAI_ERC20_ETH_ADDRESS: EthAddress = EthAddress([ - 107, 23, 84, 116, 232, 144, 148, 196, 77, 169, 139, 149, 78, 237, 234, - 196, 149, 39, 29, 15, - ]); - pub const USDC_ERC20_ETH_ADDRESS_CHECKSUMMED: &str = - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - pub const USDC_ERC20_ETH_ADDRESS: EthAddress = EthAddress([ - 160, 184, 105, 145, 198, 33, 139, 54, 193, 209, 157, 74, 46, 158, 176, - 206, 54, 6, 235, 72, - ]); - - impl std::ops::Add for Uint { - type Output = Self; - - fn add(self, rhs: u64) -> Self::Output { - (ethUint(self.0) + rhs).into() - } - } - - impl std::ops::Sub for Uint { - type Output = Self; - - fn sub(self, rhs: u64) -> Self::Output { - (ethUint(self.0) - rhs).into() - } - } - - pub fn arbitrary_eth_address() -> EthAddress { - DAI_ERC20_ETH_ADDRESS - } - - pub fn arbitrary_nonce() -> Uint { - 0.into() - } - - pub fn arbitrary_keccak_hash() -> KeccakHash { - KeccakHash([0; 32]) - } - - pub fn arbitrary_amount() -> Amount { - Amount::from(1_000) - } - - pub fn arbitrary_bonded_stake() -> token::Amount { - token::Amount::from(1_000) - } - - /// A [`EthereumEvent::TransfersToNamada`] containing a single transfer of - /// some arbitrary ERC20 - pub fn arbitrary_single_transfer( - nonce: Uint, - receiver: Address, - ) -> EthereumEvent { - EthereumEvent::TransfersToNamada { - nonce, - transfers: vec![TransferToNamada { - amount: arbitrary_amount(), - asset: arbitrary_eth_address(), - receiver, - }], - } - } - - prop_compose! { - // Generate an arbitrary Ethereum address - pub fn arb_eth_address()(bytes: [u8; 20]) -> EthAddress { - EthAddress(bytes) - } - } -} diff --git a/crates/core/src/ethereum_structs.rs b/crates/core/src/ethereum_structs.rs deleted file mode 100644 index 8f1641c9ba0..00000000000 --- a/crates/core/src/ethereum_structs.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Ethereum bridge struct re-exports and types to do with ethereum. - -use std::fmt; -use std::io::Read; -use std::num::NonZeroU64; -use std::ops::Deref; - -use borsh::{BorshDeserialize, BorshSerialize}; -pub use ethbridge_structs::*; -use num256::Uint256; -use serde::{Deserialize, Serialize}; - -/// This type must be able to represent any valid Ethereum block height. It must -/// also be Borsh serializeable, so that it can be stored in blockchain storage. -/// -/// In Ethereum, the type for block height is an arbitrary precision integer - see . -#[derive( - Default, - Debug, - Clone, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, - Serialize, - Deserialize, -)] -#[repr(transparent)] -pub struct BlockHeight(Uint256); - -impl BlockHeight { - /// Get the next block height. - /// - /// # Panic - /// - /// Panics on overflow. - pub fn next(&self) -> Self { - self.unchecked_add(1_u64) - } - - /// Unchecked epoch addition. - /// - /// # Panic - /// - /// Panics on overflow. Care must be taken to only use this with trusted - /// values that are known to be in a limited range (e.g. system parameters - /// but not e.g. transaction variables). - pub fn unchecked_add(&self, rhs: impl Into) -> Self { - use num_traits::CheckedAdd; - Self( - self.0 - .checked_add(&rhs.into()) - .expect("Block height addition shouldn't overflow"), - ) - } -} - -impl fmt::Display for BlockHeight { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for BlockHeight { - fn from(value: u64) -> Self { - Self(Uint256::from(value)) - } -} - -impl From for BlockHeight { - fn from(value: NonZeroU64) -> Self { - Self(Uint256::from(value.get())) - } -} - -impl From for BlockHeight { - fn from(value: Uint256) -> Self { - Self(value) - } -} - -impl From for Uint256 { - fn from(BlockHeight(value): BlockHeight) -> Self { - value - } -} - -impl<'a> From<&'a BlockHeight> for &'a Uint256 { - fn from(BlockHeight(height): &'a BlockHeight) -> Self { - height - } -} - -impl Deref for BlockHeight { - type Target = Uint256; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl BorshSerialize for BlockHeight { - fn serialize( - &self, - writer: &mut W, - ) -> std::io::Result<()> { - let be = self.0.to_be_bytes(); - BorshSerialize::serialize(&be, writer) - } -} - -impl BorshDeserialize for BlockHeight { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let be: Vec = BorshDeserialize::deserialize_reader(reader)?; - Ok(Self(Uint256::from_be_bytes(&be))) - } -} diff --git a/crates/core/src/keccak.rs b/crates/core/src/keccak.rs deleted file mode 100644 index 147a5bcbb70..00000000000 --- a/crates/core/src/keccak.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! This module is for hashing Namada types using the keccak256 -//! hash function in a way that is compatible with smart contracts -//! on Ethereum. - -use std::fmt; -use std::str::FromStr; - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use data_encoding::HEXUPPER; -use ethabi::Token; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; -use thiserror::Error; -pub use tiny_keccak::{Hasher, Keccak}; - -use crate::eth_abi::Encode; -use crate::hash::{HASH_LENGTH, Hash}; - -/// Errors for converting / parsing Keccak hashes -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum TryFromError { - #[error("Unexpected tx hash length {0}, expected {1}")] - WrongLength(usize, usize), - #[error("Failed trying to convert slice to a hash: {0}")] - ConversionFailed(std::array::TryFromSliceError), - #[error("Failed to convert string into a hash: {0}")] - FromStringError(data_encoding::DecodeError), -} - -/// Represents a Keccak hash. -#[derive( - Clone, - Debug, - Default, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct KeccakHash(pub [u8; 32]); - -impl KeccakHash { - /// Check if this [`KeccakHash`] is comprised solely of bytes with - /// a value of zero. - #[inline] - pub fn is_zero(&self) -> bool { - self.0 == [0; 32] - } -} - -impl fmt::Display for KeccakHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in &self.0 { - write!(f, "{:02X}", byte)?; - } - Ok(()) - } -} -impl From for Hash { - fn from(hash: KeccakHash) -> Self { - Hash(hash.0) - } -} - -impl From for KeccakHash { - fn from(hash: Hash) -> Self { - KeccakHash(hash.0) - } -} - -impl TryFrom<&[u8]> for KeccakHash { - type Error = TryFromError; - - fn try_from(value: &[u8]) -> Result { - if value.len() != HASH_LENGTH { - return Err(TryFromError::WrongLength(value.len(), HASH_LENGTH)); - } - let hash: [u8; HASH_LENGTH] = - TryFrom::try_from(value).map_err(TryFromError::ConversionFailed)?; - Ok(KeccakHash(hash)) - } -} - -impl TryFrom for KeccakHash { - type Error = TryFromError; - - fn try_from(string: String) -> Result { - string.as_str().try_into() - } -} - -impl TryFrom<&str> for KeccakHash { - type Error = TryFromError; - - fn try_from(string: &str) -> Result { - let bytes: Vec = HEXUPPER - .decode(string.as_bytes()) - .map_err(TryFromError::FromStringError)?; - Self::try_from(bytes.as_slice()) - } -} - -impl FromStr for KeccakHash { - type Err = TryFromError; - - fn from_str(s: &str) -> Result { - s.try_into() - } -} - -impl AsRef<[u8]> for KeccakHash { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl Serialize for KeccakHash { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for KeccakHash { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct KeccakVisitor; - - impl de::Visitor<'_> for KeccakVisitor { - type Value = KeccakHash; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "a string containing a keccak hash") - } - - fn visit_str(self, s: &str) -> Result - where - E: de::Error, - { - KeccakHash::try_from(s).map_err(|_| { - de::Error::invalid_value(de::Unexpected::Str(s), &self) - }) - } - } - - deserializer.deserialize_str(KeccakVisitor) - } -} - -/// Hash bytes using Keccak -pub fn keccak_hash>(bytes: T) -> KeccakHash { - let mut output = [0; 32]; - - let mut hasher = Keccak::v256(); - hasher.update(bytes.as_ref()); - hasher.finalize(&mut output); - - KeccakHash(output) -} - -impl Encode<1> for KeccakHash { - fn tokenize(&self) -> [Token; 1] { - [Token::FixedBytes(self.0.to_vec())] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_keccak_serde_roundtrip() { - let mut hash = KeccakHash([0; 32]); - - for i in 0..32 { - hash.0[i] = u8::try_from(i).unwrap(); - } - - let serialized = serde_json::to_string(&hash).unwrap(); - let deserialized: KeccakHash = - serde_json::from_str(&serialized).unwrap(); - - assert_eq!(deserialized, hash); - } -} diff --git a/crates/core/src/key/common.rs b/crates/core/src/key/common.rs index 020930bb429..50de6f68ed8 100644 --- a/crates/core/src/key/common.rs +++ b/crates/core/src/key/common.rs @@ -11,7 +11,6 @@ use namada_migrations::*; use rand::{CryptoRng, RngCore}; use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use thiserror::Error; use super::{ ParsePublicKeyError, ParseSecretKeyError, ParseSignatureError, RefTo, @@ -19,7 +18,6 @@ use super::{ secp256k1, }; use crate::borsh::BorshSerializeExt; -use crate::ethereum_events::EthAddress; use crate::key::{SignableBytes, StorageHasher}; use crate::{impl_display_and_from_str_via_format, string_encoding}; @@ -159,24 +157,6 @@ impl From for crate::tendermint::PublicKey { } } -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum EthAddressConvError { - #[error("Eth key cannot be ed25519, only secp256k1")] - CannotBeEd25519, -} - -impl TryFrom<&PublicKey> for EthAddress { - type Error = EthAddressConvError; - - fn try_from(value: &PublicKey) -> Result { - match value { - PublicKey::Ed25519(_) => Err(EthAddressConvError::CannotBeEd25519), - PublicKey::Secp256k1(pk) => Ok(EthAddress::from(pk)), - } - } -} - /// Secret key #[derive( Debug, diff --git a/crates/core/src/key/mod.rs b/crates/core/src/key/mod.rs index 447cc6a68fd..b71ca2e6200 100644 --- a/crates/core/src/key/mod.rs +++ b/crates/core/src/key/mod.rs @@ -21,8 +21,7 @@ use thiserror::Error; use crate::address; use crate::borsh::BorshSerializeExt; -use crate::hash::{KeccakHasher, Sha256Hasher, StorageHasher}; -use crate::keccak::{KeccakHash, keccak_hash}; +use crate::hash::{Sha256Hasher, StorageHasher}; /// Represents an error in signature verification #[allow(missing_docs)] @@ -420,11 +419,6 @@ pub trait Signable { #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] pub struct SerializeWithBorsh; -/// Tag type that indicates we should use ABI serialization -/// to sign data in a [`Signable`] wrapper. -#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] -pub struct SignableEthMessage; - impl Signable for SerializeWithBorsh { type Hasher = Sha256Hasher; type Output = Vec; @@ -434,19 +428,6 @@ impl Signable for SerializeWithBorsh { } } -impl Signable for SignableEthMessage { - type Hasher = KeccakHasher; - type Output = KeccakHash; - - fn as_signable(hash: &KeccakHash) -> KeccakHash { - keccak_hash({ - let mut eth_message = Vec::from("\x19Ethereum Signed Message:\n32"); - eth_message.extend_from_slice(hash.as_ref()); - eth_message - }) - } -} - /// Helper trait to compress arbitrary bytes to a hash value, /// which can be signed over. pub trait SignableBytes: Sized + AsRef<[u8]> { @@ -474,18 +455,6 @@ impl SignableBytes for &crate::hash::Hash { } } -impl SignableBytes for crate::keccak::KeccakHash { - fn signable_hash(&self) -> [u8; 32] { - self.0 - } -} - -impl SignableBytes for &crate::keccak::KeccakHash { - fn signable_hash(&self) -> [u8; 32] { - self.0 - } -} - /// Helpers for testing with keys. #[cfg(any(test, feature = "testing", feature = "benches"))] pub mod testing { diff --git a/crates/core/src/key/secp256k1.rs b/crates/core/src/key/secp256k1.rs index d02f455af35..16cf0b54e17 100644 --- a/crates/core/src/key/secp256k1.rs +++ b/crates/core/src/key/secp256k1.rs @@ -10,9 +10,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use data_encoding::HEXLOWER; -use ethabi::Token; use k256::ecdsa::RecoveryId; -use k256::elliptic_curve::sec1::ToEncodedPoint; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; @@ -27,8 +25,6 @@ use super::{ SchemeType, SigScheme as SigSchemeTrait, SignableBytes, VerifySigError, }; use crate::borsh::BorshSerializeExt; -use crate::eth_abi::Encode; -use crate::ethereum_events::EthAddress; use crate::key::StorageHasher; /// The provided constant is for a traditional @@ -157,21 +153,6 @@ impl From for PublicKey { } } -impl From<&PublicKey> for EthAddress { - fn from(pk: &PublicKey) -> Self { - use tiny_keccak::Hasher; - - let mut hasher = tiny_keccak::Keccak::v256(); - let pk_bytes = &pk.0.to_encoded_point(false).to_bytes()[1..]; - hasher.update(pk_bytes); - let mut output = [0_u8; 32]; - hasher.finalize(&mut output); - let mut addr = [0; 20]; - addr.copy_from_slice(&output[12..]); - EthAddress(addr) - } -} - /// Secp256k1 secret key #[derive(Debug, Clone, BorshDeserializer)] pub struct SecretKey(pub Box); @@ -470,16 +451,6 @@ impl Signature { } } -impl Encode<1> for Signature { - fn tokenize(&self) -> [Token; 1] { - let (r, s, v) = self.clone().into_eth_rsv(); - let r = Token::FixedBytes(r.to_vec()); - let s = Token::FixedBytes(s.to_vec()); - let v = Token::Uint(v.into()); - [Token::Tuple(vec![r, s, v])] - } -} - #[allow(clippy::derived_hash_with_manual_eq)] impl Hash for Signature { fn hash(&self, state: &mut H) { @@ -665,26 +636,6 @@ mod test { const SECRET_KEY_HEX: &str = "c2c72dfbff11dfb4e9d5b0a20c620c58b15bb7552753601f043db91331b0db15"; - /// Test that we can recover an Ethereum address from - /// a public secp key. - #[test] - fn test_eth_address_from_secp() { - let expected_pk_hex = "a225bf565ff4ea039bccba3e26456e910cd74e4616d67ee0a166e26da6e5e55a08d0fa1659b4b547ba7139ca531f62907b9c2e72b80712f1c81ece43c33f4b8b"; - let expected_eth_addr_hex = "6ea27154616a29708dce7650b475dd6b82eba6a3"; - - let sk_bytes = HEXLOWER.decode(SECRET_KEY_HEX.as_bytes()).unwrap(); - let sk = SecretKey::try_from_slice(&sk_bytes[..]).unwrap(); - let pk: PublicKey = sk.ref_to(); - // We're removing the first byte with tag - let pk_hex = - HEXLOWER.encode(&pk.0.to_encoded_point(false).to_bytes()[1..]); - assert_eq!(expected_pk_hex, pk_hex); - - let eth_addr: EthAddress = (&pk).into(); - let eth_addr_hex = HEXLOWER.encode(ð_addr.0[..]); - assert_eq!(expected_eth_addr_hex, eth_addr_hex); - } - /// Test serializing and then de-serializing a signature /// with Serde is idempotent. #[test] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 2dd5c72b46c..81de0969279 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -26,22 +26,22 @@ pub mod control_flow; pub mod hints; pub use masp_primitives; -/// Re-export of tendermint v0.37 +/// Re-export of tendermint v0.38 pub mod tendermint { - /// Re-export of tendermint v0.37 ABCI + /// Re-export of tendermint v0.38 ABCI pub mod abci { pub use tendermint::abci::response::ApplySnapshotChunkResult; pub use tendermint::abci::{ Code, Event, EventAttribute, MethodKind, types, }; - pub use tendermint::v0_37::abci::*; + pub use tendermint::v0_38::abci::*; } pub use tendermint::*; } -/// Re-export of tendermint-proto v0.37 +/// Re-export of tendermint-proto v0.38 pub mod tendermint_proto { pub use tendermint_proto::google; // 💩 - pub use tendermint_proto::v0_37::*; + pub use tendermint_proto::v0_38::*; } #[allow(missing_docs)] @@ -64,14 +64,9 @@ pub mod address; pub mod booleans; pub mod chain; pub mod dec; -pub mod eth_abi; -pub mod eth_bridge_pool; -pub mod ethereum_events; -pub mod ethereum_structs; pub mod hash; pub mod ibc; pub mod internal; -pub mod keccak; pub mod key; pub mod masp; pub mod parameters; diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index 2ac9959df1f..7547440ded0 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -1,5 +1,4 @@ //! Storage types -use std::collections::VecDeque; use std::fmt::Display; use std::io::{Read, Write}; use std::ops::Deref; @@ -19,10 +18,7 @@ use usize_set::vec::VecIndexSet; use super::key::common; use crate::address::{self, Address, PARAMETERS}; use crate::chain::{BlockHeight, Epoch}; -use crate::ethereum_events::{GetEventNonce, TransfersToNamada, Uint}; use crate::hash::Hash; -use crate::hints; -use crate::keccak::{KeccakHash, TryFromError}; /// The maximum size of an IBC key (in bytes) allowed in merkle-ized storage pub const IBC_KEY_LIMIT: usize = 240; @@ -838,21 +834,6 @@ impl KeySeg for Hash { } } -impl KeySeg for KeccakHash { - fn parse(seg: String) -> Result { - seg.try_into() - .map_err(|e: TryFromError| Error::ParseKeySeg(e.to_string())) - } - - fn raw(&self) -> String { - self.to_string() - } - - fn to_db_key(&self) -> DbKeySeg { - DbKeySeg::StringSeg(self.raw()) - } -} - /// Implement [`KeySeg`] for a type via base32hex of its BE bytes (using /// `to_le_bytes()` and `from_le_bytes` methods) that maintains sort order of /// the original data. @@ -969,166 +950,6 @@ pub struct PrefixValue { pub value: Vec, } -/// Container of all Ethereum event queues. -#[derive( - Default, Debug, BorshSerialize, BorshDeserialize, BorshDeserializer, -)] -pub struct EthEventsQueue { - /// Queue of transfer to Namada events. - pub transfers_to_namada: InnerEthEventsQueue, -} - -/// A queue of confirmed Ethereum events of type `E`. -/// -/// __INVARIANT:__ At any given moment, the queue holds the nonce `N` -/// of the next confirmed event to be processed by the ledger, and any -/// number of events that have been confirmed with a nonce greater than -/// or equal to `N`. Events in the queue must be returned in ascending -/// order of their nonce. -#[derive(Debug, BorshSerialize, BorshDeserialize)] -pub struct InnerEthEventsQueue { - next_nonce_to_process: Uint, - inner: VecDeque, -} - -impl InnerEthEventsQueue { - /// Return an Ethereum events queue starting at the specified nonce. - pub fn new_at(next_nonce_to_process: Uint) -> Self { - Self { - next_nonce_to_process, - ..Default::default() - } - } -} - -impl Default for InnerEthEventsQueue { - fn default() -> Self { - Self { - next_nonce_to_process: 0u64.into(), - inner: Default::default(), - } - } -} - -/// Draining iterator over a queue of Ethereum events, -/// -/// At each iteration step, we peek into the head of the -/// queue, and if an event is present with a nonce equal -/// to the local nonce maintained by the iterator object, -/// we pop it and increment the local nonce. Otherwise, -/// iteration stops. -/// -/// Upon being dropped, the iterator object updates the -/// nonce of the next event of type `E` to be processed -/// by the ledger (stored in an [`InnerEthEventsQueue`]), -/// if the iterator's nonce was incremented. -pub struct EthEventsQueueIter<'queue, E> { - current_nonce: Uint, - queue: &'queue mut InnerEthEventsQueue, -} - -impl Drop for EthEventsQueueIter<'_, E> { - fn drop(&mut self) { - // on drop, we commit the nonce of the next event to process - if self.queue.next_nonce_to_process < self.current_nonce { - self.queue.next_nonce_to_process = self.current_nonce; - } - } -} - -impl Iterator for EthEventsQueueIter<'_, E> { - type Item = E; - - fn next(&mut self) -> Option { - let nonce_in_queue = self.queue.peek_event_nonce()?; - if nonce_in_queue == self.current_nonce { - self.current_nonce = self - .current_nonce - .checked_increment() - .expect("Nonce overflow"); - self.queue.pop_event() - } else { - None - } - } -} - -impl InnerEthEventsQueue { - /// Push a new Ethereum event of type `E` into the queue, - /// and return a draining iterator over the next events to - /// be processed, if any. - pub fn push_and_iter( - &mut self, - latest_event: E, - ) -> EthEventsQueueIter<'_, E> - where - E: std::fmt::Debug, - { - let event_nonce = latest_event.get_event_nonce(); - if hints::unlikely(self.next_nonce_to_process > event_nonce) { - unreachable!( - "Attempted to replay an Ethereum event: {latest_event:#?}" - ); - } - - self.try_push_event(latest_event); - - EthEventsQueueIter { - current_nonce: self.next_nonce_to_process, - queue: self, - } - } - - /// Provide a reference to the earliest event stored in the queue. - #[inline] - fn peek_event_nonce(&self) -> Option { - self.inner.front().map(GetEventNonce::get_event_nonce) - } - - /// Attempt to push a new Ethereum event to the queue. - /// - /// This operation may panic if a confirmed event is - /// already present in the queue. - #[inline] - fn try_push_event(&mut self, new_event: E) - where - E: std::fmt::Debug, - { - self.inner - .binary_search_by_key( - &new_event.get_event_nonce(), - |event_in_queue| event_in_queue.get_event_nonce(), - ) - .map_or_else( - |insert_at| { - tracing::debug!(?new_event, "Queueing Ethereum event"); - self.inner.insert(insert_at, new_event) - }, - // the event is already present in the queue... this is - // certainly a protocol error - |_| { - hints::cold(); - unreachable!( - "An event with an identical nonce was already present \ - in the EthEventsQueue" - ) - }, - ) - } - - /// Pop a transfer to Namada event from the queue. - #[inline] - fn pop_event(&mut self) -> Option { - self.inner.pop_front() - } -} - -impl GetEventNonce for InnerEthEventsQueue { - fn get_event_nonce(&self) -> Uint { - self.next_nonce_to_process - } -} - #[cfg(test)] /// Tests and strategies for storage pub mod tests { @@ -1173,126 +994,6 @@ pub mod tests { } } - /// Test that providing an [`EthEventsQueue`] with an event containing - /// a nonce identical to the next expected nonce in Namada yields the - /// event itself. - #[test] - fn test_eth_events_queue_equal_nonces() { - let mut queue = EthEventsQueue::default(); - queue.transfers_to_namada.next_nonce_to_process = 2u64.into(); - let new_event = TransfersToNamada { - transfers: vec![], - nonce: 2u64.into(), - }; - let next_event = queue - .transfers_to_namada - .push_and_iter(new_event.clone()) - .next(); - assert_eq!(next_event, Some(new_event)); - } - - /// Test that providing an [`EthEventsQueue`] with an event containing - /// a nonce lower than the next expected nonce in Namada results in a - /// panic. - #[test] - #[should_panic = "Attempted to replay an Ethereum event"] - fn test_eth_events_queue_panic_on_invalid_nonce() { - let mut queue = EthEventsQueue::default(); - queue.transfers_to_namada.next_nonce_to_process = 3u64.into(); - let new_event = TransfersToNamada { - transfers: vec![], - nonce: 2u64.into(), - }; - _ = queue.transfers_to_namada.push_and_iter(new_event); - } - - /// Test enqueueing transfer to Namada events to - /// an [`EthEventsQueue`]. - #[test] - fn test_eth_events_queue_enqueue() { - let mut queue = EthEventsQueue::default(); - queue.transfers_to_namada.next_nonce_to_process = 1u64.into(); - - let new_event_1 = TransfersToNamada { - transfers: vec![], - nonce: 1u64.into(), - }; - let new_event_2 = TransfersToNamada { - transfers: vec![], - nonce: 2u64.into(), - }; - let new_event_3 = TransfersToNamada { - transfers: vec![], - nonce: 3u64.into(), - }; - let new_event_4 = TransfersToNamada { - transfers: vec![], - nonce: 4u64.into(), - }; - let new_event_7 = TransfersToNamada { - transfers: vec![], - nonce: 7u64.into(), - }; - - // enqueue events - assert!( - queue - .transfers_to_namada - .push_and_iter(new_event_4.clone()) - .next() - .is_none() - ); - assert!( - queue - .transfers_to_namada - .push_and_iter(new_event_2.clone()) - .next() - .is_none() - ); - assert!( - queue - .transfers_to_namada - .push_and_iter(new_event_3.clone()) - .next() - .is_none() - ); - assert!( - queue - .transfers_to_namada - .push_and_iter(new_event_7.clone()) - .next() - .is_none() - ); - assert_eq!( - &queue.transfers_to_namada.inner, - &[ - new_event_2.clone(), - new_event_3.clone(), - new_event_4.clone(), - new_event_7.clone() - ] - ); - - // start dequeueing events - assert_eq!( - vec![new_event_1.clone(), new_event_2, new_event_3, new_event_4], - queue - .transfers_to_namada - .push_and_iter(new_event_1) - .collect::>() - ); - - // check the next nonce to process - assert_eq!(queue.transfers_to_namada.get_event_nonce(), 5u64.into()); - - // one remaining event with nonce 7 - assert_eq!( - queue.transfers_to_namada.pop_event().expect("Test failed"), - new_event_7 - ); - assert!(queue.transfers_to_namada.pop_event().is_none()); - } - #[test] fn test_key_parse_valid() { let addr = address::testing::established_address_1(); diff --git a/crates/ethereum_bridge/Cargo.toml b/crates/ethereum_bridge/Cargo.toml deleted file mode 100644 index a1758617587..00000000000 --- a/crates/ethereum_bridge/Cargo.toml +++ /dev/null @@ -1,72 +0,0 @@ -[package] -name = "namada_ethereum_bridge" -description = "The Namada Ethereum bridge library crate" -resolver = "2" -authors.workspace = true -edition.workspace = true -documentation.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true -rust-version.workspace = true - -[features] -default = [] -namada-eth-bridge = [] -testing = [ - "namada_account", - "namada_core/testing", - "namada_state/testing", - "namada_governance", -] -migrations = ["namada_migrations", "linkme"] - -[dependencies] -namada_account = { workspace = true, optional = true } -namada_core = { workspace = true, features = ["ethers-derive"] } -namada_events.workspace = true -namada_governance = { workspace = true, optional = true } -namada_macros.workspace = true -namada_migrations = { workspace = true, optional = true } -namada_parameters.workspace = true -namada_proof_of_stake.workspace = true -namada_state.workspace = true -namada_storage.workspace = true -namada_systems.workspace = true -namada_trans_token.workspace = true -namada_tx.workspace = true -namada_vote_ext.workspace = true -namada_vp_env.workspace = true - -borsh.workspace = true -ethers.workspace = true -eyre.workspace = true -itertools.workspace = true -konst.workspace = true -linkme = { workspace = true, optional = true } -serde.workspace = true -smooth-operator.workspace = true -thiserror.workspace = true -tracing.workspace = true - -[dev-dependencies] -namada_account.path = "../account" -namada_core = { path = "../core", features = ["ethers-derive", "testing"] } -namada_gas.path = "../gas" -namada_governance.path = "../governance" -namada_proof_of_stake = { path = "../proof_of_stake", features = ["testing"] } -namada_state = { path = "../state", features = ["testing"] } -namada_token = { path = "../token", features = ["testing"] } -namada_tx = { path = "../tx", features = ["testing"] } -namada_vm = { path = "../vm", default-features = true, features = ["testing"] } -namada_vp.workspace = true - -assert_matches.workspace = true -data-encoding.workspace = true -ethabi.workspace = true -proptest.workspace = true -rand.workspace = true -toml.workspace = true diff --git a/crates/ethereum_bridge/src/event.rs b/crates/ethereum_bridge/src/event.rs deleted file mode 100644 index a706340570a..00000000000 --- a/crates/ethereum_bridge/src/event.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! Ethereum Bridge transaction events. - -use namada_core::borsh::{BorshDeserialize, BorshSerialize}; -use namada_core::keccak::KeccakHash; -use namada_events::extend::{ComposeEvent, EventAttributeEntry}; -use namada_events::{Event, EventError, EventLevel, EventToEmit, EventType}; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use serde::{Deserialize, Serialize}; - -pub mod types { - //! Ethereum bridge event types. - - use namada_events::{EventType, event_type}; - - use super::EthBridgeEvent; - - /// Bridge pool relay event. - pub const BRIDGE_POOL_RELAYED: EventType = - event_type!(EthBridgeEvent, "bridge-pool", "relayed"); - - /// Bridge pool expiration event. - pub const BRIDGE_POOL_EXPIRED: EventType = - event_type!(EthBridgeEvent, "bridge-pool", "expired"); -} - -/// Status of some Bridge pool transfer. -#[derive( - Hash, - Clone, - Debug, - Eq, - PartialEq, - Ord, - PartialOrd, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - Serialize, - Deserialize, -)] -pub enum BpTransferStatus { - /// The transfer has been relayed. - Relayed, - /// The transfer has expired. - Expired, -} - -impl From for EventType { - fn from(transfer_status: BpTransferStatus) -> Self { - (&transfer_status).into() - } -} - -impl From<&BpTransferStatus> for EventType { - fn from(transfer_status: &BpTransferStatus) -> Self { - match transfer_status { - BpTransferStatus::Relayed => types::BRIDGE_POOL_RELAYED, - BpTransferStatus::Expired => types::BRIDGE_POOL_EXPIRED, - } - } -} - -impl TryFrom for BpTransferStatus { - type Error = EventError; - - fn try_from(event_type: EventType) -> Result { - (&event_type).try_into() - } -} - -impl TryFrom<&EventType> for BpTransferStatus { - type Error = EventError; - - fn try_from(event_type: &EventType) -> Result { - if *event_type == types::BRIDGE_POOL_RELAYED { - Ok(BpTransferStatus::Relayed) - } else if *event_type == types::BRIDGE_POOL_EXPIRED { - Ok(BpTransferStatus::Expired) - } else { - Err(EventError::InvalidEventType) - } - } -} - -/// Ethereum bridge events on Namada's event log. -#[derive( - Hash, - Clone, - Debug, - Eq, - PartialEq, - Ord, - PartialOrd, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - Serialize, - Deserialize, -)] -pub enum EthBridgeEvent { - /// Bridge pool transfer status update event. - BridgePool { - /// Hash of the Bridge pool transfer. - tx_hash: KeccakHash, - /// Status of the Bridge pool transfer. - status: BpTransferStatus, - }, -} - -impl EthBridgeEvent { - /// Return a new Bridge pool expired transfer event. - pub const fn new_bridge_pool_expired(tx_hash: KeccakHash) -> Self { - Self::BridgePool { - tx_hash, - status: BpTransferStatus::Expired, - } - } - - /// Return a new Bridge pool relayed transfer event. - pub const fn new_bridge_pool_relayed(tx_hash: KeccakHash) -> Self { - Self::BridgePool { - tx_hash, - status: BpTransferStatus::Relayed, - } - } -} - -impl From for Event { - #[inline] - fn from(event: EthBridgeEvent) -> Event { - Self::from(&event) - } -} - -impl From<&EthBridgeEvent> for Event { - fn from(event: &EthBridgeEvent) -> Event { - match event { - EthBridgeEvent::BridgePool { tx_hash, status } => { - Event::new(status.into(), EventLevel::Tx) - .with(BridgePoolTxHash(tx_hash)) - .into() - } - } - } -} - -impl EventToEmit for EthBridgeEvent { - const DOMAIN: &'static str = "eth-bridge"; -} - -/// Hash of bridge pool transaction -pub struct BridgePoolTxHash<'tx>(pub &'tx KeccakHash); - -impl<'tx> EventAttributeEntry<'tx> for BridgePoolTxHash<'tx> { - type Value = &'tx KeccakHash; - type ValueOwned = KeccakHash; - - const KEY: &'static str = "bridge_pool_tx_hash"; - - fn into_value(self) -> Self::Value { - self.0 - } -} diff --git a/crates/ethereum_bridge/src/lib.rs b/crates/ethereum_bridge/src/lib.rs deleted file mode 100644 index f2da36edcd9..00000000000 --- a/crates/ethereum_bridge/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Ethereum bridge - -#![doc(html_favicon_url = "https://dev.namada.net/master/favicon.png")] -#![doc(html_logo_url = "https://dev.namada.net/master/rustdoc-logo.png")] -#![deny(rustdoc::broken_intra_doc_links)] -#![deny(rustdoc::private_intra_doc_links)] -#![warn( - missing_docs, - rust_2018_idioms, - clippy::cast_sign_loss, - clippy::cast_possible_truncation, - clippy::cast_possible_wrap, - clippy::cast_lossless, - clippy::arithmetic_side_effects, - clippy::dbg_macro, - clippy::print_stdout, - clippy::print_stderr -)] - -pub mod event; -pub mod oracle; -pub mod protocol; -pub mod storage; -#[cfg(any(test, feature = "testing"))] -pub mod test_utils; -pub mod vp; - -pub use namada_core::address::ETH_BRIDGE as ADDRESS; -pub use namada_trans_token as token; diff --git a/crates/ethereum_bridge/src/oracle.rs b/crates/ethereum_bridge/src/oracle.rs deleted file mode 100644 index 3bc1cb1e49c..00000000000 --- a/crates/ethereum_bridge/src/oracle.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Ethereum bridge oracle - -pub mod config; diff --git a/crates/ethereum_bridge/src/oracle/config.rs b/crates/ethereum_bridge/src/oracle/config.rs deleted file mode 100644 index bfd43787885..00000000000 --- a/crates/ethereum_bridge/src/oracle/config.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Configuration for an oracle. -use std::num::NonZeroU64; - -use namada_core::ethereum_events::EthAddress; -use namada_core::ethereum_structs; - -/// Configuration for an oracle. -#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] -pub struct Config { - /// The minimum number of block confirmations an Ethereum block must have - /// before it will be checked for bridge events. - pub min_confirmations: NonZeroU64, - /// The Ethereum address of the current bridge contract. - pub bridge_contract: EthAddress, - /// The earliest Ethereum block from which events may be processed. - pub start_block: ethereum_structs::BlockHeight, - /// The status of the Ethereum bridge (active / inactive) - pub active: bool, -} - -#[cfg(any(test, feature = "testing"))] -impl std::default::Default for Config { - fn default() -> Self { - Self { - // SAFETY: we must always call NonZeroU64::new_unchecked here with a - // value that is >= 1 - min_confirmations: unsafe { NonZeroU64::new_unchecked(100) }, - bridge_contract: EthAddress([0; 20]), - start_block: 0.into(), - active: true, - } - } -} diff --git a/crates/ethereum_bridge/src/protocol/mod.rs b/crates/ethereum_bridge/src/protocol/mod.rs deleted file mode 100644 index ec47d370e39..00000000000 --- a/crates/ethereum_bridge/src/protocol/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Ethereum bridge protocol transactions and validation. - -pub mod transactions; -pub mod validation; diff --git a/crates/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/crates/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs deleted file mode 100644 index 5e3c189708d..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ /dev/null @@ -1,952 +0,0 @@ -//! Functions dealing with bridge pool root hash. - -use eyre::Result; -use namada_core::address::Address; -use namada_core::chain::BlockHeight; -use namada_core::collections::{HashMap, HashSet}; -use namada_core::keccak::keccak_hash; -use namada_core::key::{SignableEthMessage, common}; -use namada_core::token::Amount; -use namada_state::{DB, DBIter, StorageHasher, WlState}; -use namada_storage::{StorageRead, StorageWrite}; -use namada_systems::governance; -use namada_tx::Signed; -use namada_tx::data::BatchedTxResult; -use namada_vote_ext::bridge_pool_roots::{self, MultiSignedVext, SignedVext}; - -use crate::protocol::transactions::utils::GetVoters; -use crate::protocol::transactions::votes::update::NewVotes; -use crate::protocol::transactions::votes::{Votes, calculate_new}; -use crate::protocol::transactions::{ChangedKeys, utils, votes}; -use crate::storage::bridge_pool::get_signed_root_key; -use crate::storage::eth_bridge_queries::EthBridgeQueries; -use crate::storage::proof::BridgePoolRootProof; -use crate::storage::vote_tallies::{self, BridgePoolRoot}; - -/// Sign the latest Bridge pool root, and return the associated -/// vote extension protocol transaction. -pub fn sign_bridge_pool_root( - state: &WlState, - validator_addr: &Address, - eth_hot_key: &common::SecretKey, - protocol_key: &common::SecretKey, -) -> Option -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - if !state.ethbridge_queries().is_bridge_active() { - return None; - } - let bp_root = state.ethbridge_queries().get_bridge_pool_root().0; - let nonce = state.ethbridge_queries().get_bridge_pool_nonce().to_bytes(); - let to_sign = keccak_hash([bp_root.as_slice(), nonce.as_slice()].concat()); - let signed = Signed::<_, SignableEthMessage>::new(eth_hot_key, to_sign); - let ext = bridge_pool_roots::Vext { - block_height: state.in_mem().get_last_block_height(), - validator_addr: validator_addr.clone(), - sig: signed.sig, - }; - Some(ext.sign(protocol_key)) -} - -/// Applies a tally of signatures on over the Ethereum -/// bridge pool root and nonce. Note that every signature -/// passed into this function will be for the same -/// root and nonce. -/// -/// For roots + nonces which have been seen by a quorum of -/// validators, the signature is made available for bridge -/// pool proofs. -pub fn apply_derived_tx( - state: &mut WlState, - vext: MultiSignedVext, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - if vext.is_empty() { - return Ok(BatchedTxResult::default()); - } - tracing::info!( - bp_root_sigs = vext.len(), - "Applying state updates derived from signatures of the Ethereum \ - bridge pool root and nonce." - ); - let voting_powers = utils::get_voting_powers(state, &vext)?; - let root_height = vext.iter().next().unwrap().data.block_height; - let (partial_proof, seen_by) = parse_vexts::(state, vext); - - // return immediately if a complete proof has already been acquired - let bp_key = vote_tallies::Keys::from((&partial_proof, root_height)); - let seen = - votes::storage::maybe_read_seen(state, &bp_key)?.unwrap_or(false); - if seen { - tracing::debug!( - ?root_height, - ?partial_proof, - "Bridge pool root tally is already complete" - ); - return Ok(BatchedTxResult::default()); - } - - // apply updates to the bridge pool root. - let (mut changed, confirmed_update) = apply_update::( - state, - bp_key, - partial_proof, - seen_by, - &voting_powers, - )?; - - // if the root is confirmed, update storage and add - // relevant key to changed. - if let Some(proof) = confirmed_update { - let signed_root_key = get_signed_root_key(); - let should_write_root = state - .read::<(BridgePoolRoot, BlockHeight)>(&signed_root_key) - .expect( - "Reading a signed Bridge pool root from storage should not \ - fail", - ) - .map(|(_, existing_root_height)| { - // only write the newly confirmed signed root if - // it is more recent than the existing root in - // storage - existing_root_height < root_height - }) - .unwrap_or({ - // if no signed root was present in storage, write the new one - true - }); - if should_write_root { - tracing::debug!( - ?root_height, - "New Bridge pool root proof acquired" - ); - state.write(&signed_root_key, (proof, root_height)).expect( - "Writing a signed Bridge pool root to storage should not fail.", - ); - changed.insert(get_signed_root_key()); - } else { - tracing::debug!( - ?root_height, - "Discarding outdated Bridge pool root proof" - ); - } - } - - Ok(BatchedTxResult { - changed_keys: changed, - ..Default::default() - }) -} - -impl GetVoters for &MultiSignedVext { - fn get_voters(self) -> HashSet<(Address, BlockHeight)> { - self.iter() - .map(|signed| { - (signed.data.validator_addr.clone(), signed.data.block_height) - }) - .collect() - } -} - -/// Convert a set of signatures over bridge pool roots and nonces (at a certain -/// height) into a partial proof and a new set of votes. -fn parse_vexts( - state: &WlState, - multisigned: MultiSignedVext, -) -> (BridgePoolRoot, Votes) -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - let height = multisigned.iter().next().unwrap().data.block_height; - let epoch = state.get_epoch_at_height(height).unwrap(); - let root = state - .ethbridge_queries() - .get_bridge_pool_root_at_height(height) - .expect("A BP root should be available at the given height"); - let nonce = state - .ethbridge_queries() - .get_bridge_pool_nonce_at_height(height); - let mut partial_proof = BridgePoolRootProof::new((root, nonce)); - partial_proof.attach_signature_batch(multisigned.clone().into_iter().map( - |SignedVext(signed)| { - ( - state - .ethbridge_queries() - .get_eth_addr_book::( - &signed.data.validator_addr, - epoch, - ) - .unwrap(), - signed.data.sig, - ) - }, - )); - - let seen_by: Votes = multisigned - .0 - .into_iter() - .map(|SignedVext(signed)| { - (signed.data.validator_addr, signed.data.block_height) - }) - .collect(); - (BridgePoolRoot(partial_proof), seen_by) -} - -/// This vote updates the voting power backing a bridge pool root / nonce in -/// storage. If a quorum backs the root / nonce, a boolean is returned -/// indicating that it has been confirmed. -/// -/// In all instances, the changed storage keys are returned. -fn apply_update( - state: &mut WlState, - bp_key: vote_tallies::Keys, - mut update: BridgePoolRoot, - seen_by: Votes, - voting_powers: &HashMap<(Address, BlockHeight), Amount>, -) -> Result<(ChangedKeys, Option)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - let partial_proof = votes::storage::read_body(state, &bp_key); - let (vote_tracking, changed, confirmed, already_present) = if let Ok( - partial, - ) = - partial_proof - { - tracing::debug!( - %bp_key.prefix, - "Signatures for this Bridge pool update already exists in storage", - ); - update.0.attach_signature_batch(partial.0.signatures); - let new_votes = NewVotes::new(seen_by, voting_powers)?; - let (vote_tracking, changed) = votes::update::calculate::( - state, &bp_key, new_votes, - )?; - if changed.is_empty() { - return Ok((changed, None)); - } - let confirmed = vote_tracking.seen && changed.contains(&bp_key.seen()); - (vote_tracking, changed, confirmed, true) - } else { - tracing::debug!(%bp_key.prefix, "No validator has signed this bridge pool update before."); - let vote_tracking = - calculate_new::(state, seen_by, voting_powers)?; - let changed = bp_key.into_iter().collect(); - let confirmed = vote_tracking.seen; - (vote_tracking, changed, confirmed, false) - }; - - votes::storage::write( - state, - &bp_key, - &update, - &vote_tracking, - already_present, - )?; - Ok((changed, confirmed.then_some(update))) -} - -#[cfg(test)] -mod test_apply_bp_roots_to_storage { - - use std::collections::BTreeSet; - - use assert_matches::assert_matches; - use namada_core::address; - use namada_core::ethereum_events::Uint; - use namada_core::keccak::KeccakHash; - use namada_core::storage::Key; - use namada_core::voting_power::FractionalVotingPower; - use namada_proof_of_stake::parameters::OwnedPosParams; - use namada_proof_of_stake::queries::get_total_voting_power; - use namada_proof_of_stake::storage::{ - read_consensus_validator_set_addresses_with_stake, write_pos_params, - }; - use namada_state::testing::TestState; - - use super::*; - use crate::protocol::transactions::votes::{ - EpochedVotingPower, EpochedVotingPowerExt, - }; - use crate::storage::bridge_pool::{get_key_from_hash, get_nonce_key}; - use crate::storage::vp; - use crate::test_utils::{self, GovStore}; - - /// The data needed to run a test. - struct TestPackage { - /// Two validators - validators: [Address; 3], - /// The validator keys. - keys: HashMap, - /// Storage. - state: TestState, - } - - /// Setup storage for tests. - /// - /// * Creates three validators with equal voting power. - /// * Makes sure that a bridge pool nonce and root key are initialized. - /// * Commits a bridge pool merkle tree at height 100. - fn setup() -> TestPackage { - let validator_a = address::testing::established_address_2(); - let validator_b = address::testing::established_address_3(); - let validator_c = address::testing::established_address_4(); - let (mut state, keys) = test_utils::setup_storage_with_validators( - HashMap::from_iter(vec![ - (validator_a.clone(), Amount::native_whole(100)), - (validator_b.clone(), Amount::native_whole(100)), - (validator_c.clone(), Amount::native_whole(40)), - ]), - ); - // First commit - state.in_mem_mut().block.height = 1.into(); - state.commit_block().unwrap(); - - vp::bridge_pool::init_storage(&mut state); - test_utils::commit_bridge_pool_root_at_height( - &mut state, - &KeccakHash([1; 32]), - 99.into(), - ); - test_utils::commit_bridge_pool_root_at_height( - &mut state, - &KeccakHash([1; 32]), - 100.into(), - ); - state - .write(&get_key_from_hash(&KeccakHash([1; 32])), BlockHeight(101)) - .expect("Test failed"); - state - .write(&get_nonce_key(), Uint::from(42)) - .expect("Test failed"); - state.commit_block().unwrap(); - TestPackage { - validators: [validator_a, validator_b, validator_c], - keys, - state, - } - } - - #[test] - /// Test that applying a tx changes the expected keys - /// if a quorum is not present. - /// - /// There are two code paths to test: If the key existed in - /// storage previously or not. - fn test_update_changed_keys_not_quorum() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let hot_key = &keys[&validators[0]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign.clone()) - .sig, - } - .sign(&keys[&validators[0]].protocol); - let BatchedTxResult { changed_keys, .. } = - apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - let bp_root_key = vote_tallies::Keys::from(( - &BridgePoolRoot(BridgePoolRootProof::new((root, nonce))), - 100.into(), - )); - let expected: BTreeSet = bp_root_key.into_iter().collect(); - assert_eq!(expected, changed_keys); - - let hot_key = &keys[&validators[2]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[2].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - } - .sign(&keys[&validators[2]].protocol); - - let BatchedTxResult { changed_keys, .. } = - apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - - let expected: BTreeSet = - [bp_root_key.seen_by(), bp_root_key.voting_power()] - .into_iter() - .collect(); - assert_eq!(expected, changed_keys); - } - - #[test] - /// Test that applying a tx changes the expected keys - /// if a quorum is present and the tallies were not - /// present in storage. - fn test_update_changed_keys_quorum_not_in_storage() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let hot_key = &keys[&validators[0]].eth_bridge; - let mut vexts: MultiSignedVext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign.clone()) - .sig, - } - .sign(&keys[&validators[0]].protocol) - .into(); - let hot_key = &keys[&validators[1]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[1].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - } - .sign(&keys[&validators[1]].protocol); - vexts.insert(vext); - let BatchedTxResult { changed_keys, .. } = - apply_derived_tx::<_, _, GovStore<_>>(&mut state, vexts) - .expect("Test failed"); - let bp_root_key = vote_tallies::Keys::from(( - &BridgePoolRoot(BridgePoolRootProof::new((root, nonce))), - 100.into(), - )); - - let mut expected: BTreeSet = bp_root_key.into_iter().collect(); - expected.insert(get_signed_root_key()); - assert_eq!(expected, changed_keys); - } - - #[test] - /// Test that applying a tx changes the expected keys - /// if quorum is present and a partial tally already existed - /// in storage. - fn test_update_changed_keys_quorum_in_storage() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let hot_key = &keys[&validators[0]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign.clone()) - .sig, - } - .sign(&keys[&validators[0]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - - let hot_key = &keys[&validators[1]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[1].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - } - .sign(&keys[&validators[1]].protocol); - let BatchedTxResult { changed_keys, .. } = - apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - let bp_root_key = vote_tallies::Keys::from(( - &BridgePoolRoot(BridgePoolRootProof::new((root, nonce))), - 100.into(), - )); - let expected: BTreeSet = [ - bp_root_key.seen(), - bp_root_key.seen_by(), - bp_root_key.voting_power(), - get_signed_root_key(), - ] - .into_iter() - .collect(); - assert_eq!(expected, changed_keys); - } - - #[test] - /// Test that the voting power key is updated correctly. - fn test_voting_power() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let bp_root_key = vote_tallies::Keys::from(( - &BridgePoolRoot(BridgePoolRootProof::new((root, nonce))), - 100.into(), - )); - - let hot_key = &keys[&validators[0]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign.clone()) - .sig, - } - .sign(&keys[&validators[0]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - let voting_power = state - .read::(&bp_root_key.voting_power()) - .expect("Test failed") - .expect("Test failed") - .fractional_stake::<_, _, GovStore<_>>(&state); - assert_eq!( - voting_power, - FractionalVotingPower::new_u64(5, 12).unwrap() - ); - - let hot_key = &keys[&validators[1]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[1].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - } - .sign(&keys[&validators[1]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - let voting_power = state - .read::(&bp_root_key.voting_power()) - .expect("Test failed") - .expect("Test failed") - .fractional_stake::<_, _, GovStore<_>>(&state); - assert_eq!(voting_power, FractionalVotingPower::new_u64(5, 6).unwrap()); - } - - #[test] - /// Test that the seen storage key is updated correctly. - fn test_seen() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let hot_key = &keys[&validators[0]].eth_bridge; - - let bp_root_key = vote_tallies::Keys::from(( - &BridgePoolRoot(BridgePoolRootProof::new((root, nonce))), - 100.into(), - )); - - let vext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign.clone()) - .sig, - } - .sign(&keys[&validators[0]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - - let seen: bool = state - .read(&bp_root_key.seen()) - .expect("Test failed") - .expect("Test failed"); - assert!(!seen); - - let hot_key = &keys[&validators[1]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[1].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - } - .sign(&keys[&validators[1]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - - let seen: bool = state - .read(&bp_root_key.seen()) - .expect("Test failed") - .expect("Test failed"); - assert!(seen); - } - - #[test] - /// Test that the seen by keys is updated correctly. - fn test_seen_by() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let hot_key = &keys[&validators[0]].eth_bridge; - - let bp_root_key = vote_tallies::Keys::from(( - &BridgePoolRoot(BridgePoolRootProof::new((root, nonce))), - 100.into(), - )); - - let vext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign.clone()) - .sig, - } - .sign(&keys[&validators[0]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - - let expected = Votes::from([(validators[0].clone(), 100.into())]); - let seen_by: Votes = state - .read(&bp_root_key.seen_by()) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(seen_by, expected); - - let hot_key = &keys[&validators[1]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[1].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - } - .sign(&keys[&validators[1]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - - let expected = Votes::from([ - (validators[0].clone(), 100.into()), - (validators[1].clone(), 100.into()), - ]); - let seen_by: Votes = state - .read(&bp_root_key.seen_by()) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(seen_by, expected); - } - - #[test] - /// Test that the root and nonce are stored correctly. - fn test_body() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let hot_key = &keys[&validators[0]].eth_bridge; - let mut expected = - BridgePoolRoot(BridgePoolRootProof::new((root, nonce))); - let bp_root_key = vote_tallies::Keys::from((&expected, 100.into())); - - let vext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - }; - expected.0.attach_signature( - state - .ethbridge_queries() - .get_eth_addr_book::>( - &validators[0], - state.get_epoch_at_height(100.into()).unwrap(), - ) - .expect("Test failed"), - vext.sig.clone(), - ); - let vext = vext.sign(&keys[&validators[0]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - - let proof: BridgePoolRootProof = state - .read(&bp_root_key.body()) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(proof.data, expected.0.data); - assert_eq!(proof.signatures, expected.0.signatures); - } - - #[test] - /// Test that we update the bridge pool storage once a quorum - /// backs the new nonce and root. - fn test_quorum() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - - assert!( - state - .read::<(BridgePoolRoot, BlockHeight)>(&get_signed_root_key()) - .expect("Test failed") - .is_none() - ); - - let hot_key = &keys[&validators[0]].eth_bridge; - let mut vexts: MultiSignedVext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign.clone()) - .sig, - } - .sign(&keys[&validators[0]].protocol) - .into(); - - let hot_key = &keys[&validators[1]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[1].clone(), - block_height: 100.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - } - .sign(&keys[&validators[1]].protocol); - - vexts.insert(vext); - let epoch = state.get_epoch_at_height(100.into()).unwrap(); - let sigs: Vec<_> = vexts - .iter() - .map(|s| { - ( - state - .ethbridge_queries() - .get_eth_addr_book::>( - &s.data.validator_addr, - epoch, - ) - .expect("Test failed"), - s.data.sig.clone(), - ) - }) - .collect(); - - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vexts) - .expect("Test failed"); - let (proof, _): (BridgePoolRootProof, BlockHeight) = state - .read(&get_signed_root_key()) - .expect("Test failed") - .expect("Test failed"); - let mut expected = BridgePoolRootProof::new((root, nonce)); - expected.attach_signature_batch(sigs); - assert_eq!(proof.signatures, expected.signatures); - assert_eq!(proof.data, expected.data); - } - - /// Test that when we acquire a complete BP roots proof, - /// the block height stored in storage is that of the - /// tree root that was decided. - #[test] - fn test_bp_roots_across_epoch_boundaries() { - // the validators that will vote in the tally - let validator_1 = address::testing::established_address_1(); - let validator_1_stake = Amount::native_whole(100); - - let validator_2 = address::testing::established_address_2(); - let validator_2_stake = Amount::native_whole(100); - - let validator_3 = address::testing::established_address_3(); - let validator_3_stake = Amount::native_whole(100); - - // start epoch 0 with validator 1 - let (mut state, keys) = test_utils::setup_storage_with_validators( - HashMap::from([(validator_1.clone(), validator_1_stake)]), - ); - - // update the pos params - let params = OwnedPosParams { - pipeline_len: 1, - ..Default::default() - }; - write_pos_params(&mut state, ¶ms).expect("Test failed"); - - // insert validators 2 and 3 at epoch 1 - test_utils::append_validators_to_storage( - &mut state, - HashMap::from([ - (validator_2.clone(), validator_2_stake), - (validator_3.clone(), validator_3_stake), - ]), - ); - - // query validators to make sure they were inserted correctly - macro_rules! query_validators { - () => { - |epoch: u64| { - read_consensus_validator_set_addresses_with_stake( - &state, - epoch.into(), - ) - .unwrap() - .into_iter() - .map(|validator| { - (validator.address, validator.bonded_stake) - }) - .collect::>() - } - }; - } - let query_validators = query_validators!(); - let epoch_0_validators = query_validators(0); - let epoch_1_validators = query_validators(1); - _ = query_validators; - assert_eq!( - epoch_0_validators, - HashMap::from([(validator_1.clone(), validator_1_stake)]) - ); - assert_eq!( - get_total_voting_power::<_, GovStore<_>>(&state, 0.into()), - validator_1_stake, - ); - assert_eq!( - epoch_1_validators, - HashMap::from([ - (validator_1.clone(), validator_1_stake), - (validator_2, validator_2_stake), - (validator_3, validator_3_stake), - ]) - ); - assert_eq!( - get_total_voting_power::<_, GovStore<_>>(&state, 1.into()), - validator_1_stake + validator_2_stake + validator_3_stake, - ); - - // set up the bridge pool's storage - vp::bridge_pool::init_storage(&mut state); - test_utils::commit_bridge_pool_root_at_height( - &mut state, - &KeccakHash([1; 32]), - 3.into(), - ); - - // construct proof - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let hot_key = &keys[&validator_1].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validator_1.clone(), - block_height: 3.into(), - sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, - } - .sign(&keys[&validator_1].protocol); - - _ = apply_derived_tx::<_, _, GovStore<_>>(&mut state, vext.into()) - .expect("Test failed"); - - // query validator set of the proof - // (should be the one from epoch 0) - let (_, root_height) = state - .ethbridge_queries() - .get_signed_bridge_pool_root() - .expect("Test failed"); - let root_epoch = state - .get_epoch_at_height(root_height) - .unwrap() - .expect("Test failed"); - - let query_validators = query_validators!(); - let root_epoch_validators = query_validators(root_epoch.0); - assert_eq!(epoch_0_validators, root_epoch_validators); - } - - #[test] - /// Test that a signed root is not overwritten in storage - /// if a signed root is decided that had been signed at a - /// less recent block height. - fn test_more_recent_signed_root_not_overwritten() { - let TestPackage { - validators, - keys, - mut state, - } = setup(); - - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - - macro_rules! decide_at_height { - ($block_height:expr) => { - let hot_key = &keys[&validators[0]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[0].clone(), - block_height: $block_height.into(), - sig: Signed::<_, SignableEthMessage>::new( - hot_key, - to_sign.clone(), - ) - .sig, - } - .sign(&keys[&validators[0]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vext.into(), - ) - .expect("Test failed"); - let hot_key = &keys[&validators[1]].eth_bridge; - let vext = bridge_pool_roots::Vext { - validator_addr: validators[1].clone(), - block_height: $block_height.into(), - sig: Signed::<_, SignableEthMessage>::new( - hot_key, - to_sign.clone(), - ) - .sig, - } - .sign(&keys[&validators[1]].protocol); - _ = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vext.into(), - ) - .expect("Test failed"); - }; - } - - // decide bridge pool root signed at block height 100 - decide_at_height!(100); - - // check the signed root in storage - let root_in_storage = state - .read::<(BridgePoolRoot, BlockHeight)>(&get_signed_root_key()) - .expect("Test failed - storage read failed") - .expect("Test failed - no signed root in storage"); - assert_matches!( - root_in_storage, - (BridgePoolRoot(r), BlockHeight(100)) - if r.data.0 == root && r.data.1 == nonce - ); - - // decide bridge pool root signed at block height 99 - decide_at_height!(99); - - // check the signed root in storage is unchanged - let root_in_storage = state - .read::<(BridgePoolRoot, BlockHeight)>(&get_signed_root_key()) - .expect("Test failed - storage read failed") - .expect("Test failed - no signed root in storage"); - assert_matches!( - root_in_storage, - (BridgePoolRoot(r), BlockHeight(100)) - if r.data.0 == root && r.data.1 == nonce - ); - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/eth_msgs.rs b/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/eth_msgs.rs deleted file mode 100644 index 24f66a45a09..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/eth_msgs.rs +++ /dev/null @@ -1,128 +0,0 @@ -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use namada_core::ethereum_events::EthereumEvent; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_vote_ext::ethereum_events::MultiSignedEthEvent; - -use crate::protocol::transactions::votes::{Tally, Votes, dedupe}; - -/// Represents an Ethereum event being seen by some validators -#[derive( - Debug, - Clone, - Ord, - PartialOrd, - PartialEq, - Eq, - Hash, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -pub struct EthMsgUpdate { - /// The event being seen. - pub body: EthereumEvent, - /// New votes for this event. - // NOTE(feature = "abcipp"): This can just become BTreeSet
because - // BlockHeight will always be the previous block - pub seen_by: Votes, -} - -impl From for EthMsgUpdate { - fn from( - MultiSignedEthEvent { event, signers }: MultiSignedEthEvent, - ) -> Self { - Self { - body: event, - seen_by: dedupe(signers), - } - } -} - -/// Represents an event stored under `eth_msgs` -#[derive( - Clone, - Debug, - PartialEq, - Eq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct EthMsg { - /// The event being stored - pub body: EthereumEvent, - /// Tallying of votes for this event - pub votes: Tally, -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeSet; - - use namada_core::address; - use namada_core::chain::BlockHeight; - use namada_core::ethereum_events::testing::{ - arbitrary_nonce, arbitrary_single_transfer, - }; - - use super::*; - - #[test] - /// Tests [`From`] for [`EthMsgUpdate`] - fn test_from_multi_signed_eth_event_for_eth_msg_update() { - let sole_validator = address::testing::established_address_1(); - let receiver = address::testing::established_address_2(); - let event = arbitrary_single_transfer(arbitrary_nonce(), receiver); - let with_signers = MultiSignedEthEvent { - event: event.clone(), - signers: BTreeSet::from([( - sole_validator.clone(), - BlockHeight(100), - )]), - }; - let expected = EthMsgUpdate { - body: event, - seen_by: Votes::from([(sole_validator, BlockHeight(100))]), - }; - - let update: EthMsgUpdate = with_signers.into(); - - assert_eq!(update, expected); - } - - #[test] - /// Test that `From` for `EthMsgUpdate` does in fact - /// dedupe votes - fn test_from_multi_signed_eth_event_for_eth_msg_update_dedupes() { - let validator_1 = address::testing::established_address_1(); - let validator_2 = address::testing::established_address_2(); - let signers = BTreeSet::from([ - (validator_1.clone(), BlockHeight(100)), - (validator_2.clone(), BlockHeight(200)), - (validator_1, BlockHeight(300)), - (validator_2, BlockHeight(400)), - ]); - - let event = arbitrary_single_transfer( - arbitrary_nonce(), - address::testing::established_address_3(), - ); - let with_signers = MultiSignedEthEvent { - event: event.clone(), - signers: signers.clone(), - }; - - let update: EthMsgUpdate = with_signers.into(); - - assert_eq!( - update, - EthMsgUpdate { - body: event, - seen_by: dedupe(signers), - } - ); - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs deleted file mode 100644 index 3536c8931e9..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ /dev/null @@ -1,1464 +0,0 @@ -//! Logic for acting on events - -use std::collections::BTreeSet; -use std::str::FromStr; - -use borsh::BorshDeserialize; -use eyre::{Result, WrapErr}; -use namada_core::address::Address; -use namada_core::chain::BlockHeight; -use namada_core::collections::HashSet; -use namada_core::eth_abi::Encode; -use namada_core::eth_bridge_pool::{ - PendingTransfer, TransferToEthereumKind, erc20_nut_address, - erc20_token_address, -}; -use namada_core::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, - TransfersToNamada, -}; -use namada_core::hints; -use namada_core::storage::{Key, KeySeg}; -use namada_core::uint::Uint; -use namada_parameters::read_epoch_duration_parameter; -use namada_state::{DB, DBIter, StorageHasher, WlState}; -use namada_storage::{StorageRead, StorageWrite}; -use namada_trans_token::denominated; -use namada_trans_token::storage_key::{balance_key, minted_balance_key}; -use token::{burn_tokens, decrement_total_supply, increment_total_supply}; - -use crate::event::EthBridgeEvent; -use crate::storage::bridge_pool::{ - BRIDGE_POOL_ADDRESS, get_nonce_key, is_pending_transfer_key, -}; -use crate::storage::eth_bridge_queries::{EthAssetMint, EthBridgeQueries}; -use crate::storage::parameters::read_native_erc20_address; -use crate::storage::{self as bridge_storage}; -use crate::{ADDRESS as BRIDGE_ADDRESS, token}; - -/// Updates storage based on the given confirmed `event`. For example, for a -/// confirmed [`EthereumEvent::TransfersToNamada`], mint the corresponding -/// transferred assets to the appropriate receiver addresses. -pub(super) fn act_on( - state: &mut WlState, - event: EthereumEvent, -) -> Result<(BTreeSet, BTreeSet)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - match event { - EthereumEvent::TransfersToNamada { transfers, nonce } => { - act_on_transfers_to_namada( - state, - TransfersToNamada { transfers, nonce }, - ) - } - EthereumEvent::TransfersToEthereum { - ref transfers, - ref relayer, - .. - } => act_on_transfers_to_eth(state, transfers, relayer), - _ => { - tracing::debug!(?event, "No actions taken for Ethereum event"); - Ok(Default::default()) - } - } -} - -fn act_on_transfers_to_namada<'tx, D, H>( - state: &mut WlState, - transfer_event: TransfersToNamada, -) -> Result<(BTreeSet, BTreeSet)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - tracing::debug!(?transfer_event, "Acting on transfers to Namada"); - let mut changed_keys = BTreeSet::new(); - // we need to collect the events into a separate - // buffer because of rust's borrowing rules :| - let confirmed_events: Vec<_> = state - .in_mem_mut() - .eth_events_queue - .transfers_to_namada - .push_and_iter(transfer_event) - .collect(); - for TransfersToNamada { transfers, .. } in confirmed_events { - update_transfers_to_namada_state( - state, - &mut changed_keys, - transfers.iter(), - )?; - } - Ok(( - changed_keys, - // no tx events when we get a transfer to namada - BTreeSet::new(), - )) -} - -fn update_transfers_to_namada_state<'tx, D, H>( - state: &mut WlState, - changed_keys: &mut BTreeSet, - transfers: impl IntoIterator, -) -> Result<()> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let wrapped_native_erc20 = read_native_erc20_address(state)?; - for transfer in transfers { - tracing::debug!( - ?transfer, - "Applying state updates derived from a transfer to Namada event" - ); - let TransferToNamada { - amount, - asset, - receiver, - } = transfer; - let mut changed = if asset != &wrapped_native_erc20 { - let (asset_count, changed) = - mint_eth_assets(state, asset, receiver, amount)?; - if asset_count.should_mint_erc20s() { - let denominated_amount = denominated( - asset_count.erc20_amount, - &erc20_token_address(asset), - state, - ) - .expect("The ERC20 token should have been whitelisted"); - - tracing::info!( - %asset, - %receiver, - %denominated_amount, - "Minted wrapped ERC20s", - ); - } - if asset_count.should_mint_nuts() { - tracing::info!( - %asset, - %receiver, - undenominated_amount = %Uint::from(asset_count.nut_amount), - "Minted NUTs", - ); - } - changed - } else { - redeem_native_token(state, &wrapped_native_erc20, receiver, amount)? - }; - changed_keys.append(&mut changed) - } - Ok(()) -} - -/// Redeems `amount` of the native token for `receiver` from escrow. -fn redeem_native_token( - state: &mut WlState, - native_erc20: &EthAddress, - receiver: &Address, - amount: &token::Amount, -) -> Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let eth_bridge_native_token_balance_key = - balance_key(&state.in_mem().native_token, &BRIDGE_ADDRESS); - let receiver_native_token_balance_key = - balance_key(&state.in_mem().native_token, receiver); - let native_werc20_supply_key = - minted_balance_key(&erc20_token_address(native_erc20)); - - let native_token = state.in_mem().native_token.clone(); - token::transfer(state, &native_token, &BRIDGE_ADDRESS, receiver, *amount)?; - decrement_total_supply(state, &erc20_token_address(native_erc20), *amount)?; - - tracing::info!( - amount = %amount.to_string_native(), - %receiver, - "Redeemed native token for wrapped ERC20 token" - ); - Ok(BTreeSet::from([ - eth_bridge_native_token_balance_key, - receiver_native_token_balance_key, - native_werc20_supply_key, - ])) -} - -/// Helper function to mint assets originating from Ethereum -/// on Namada. -/// -/// Mints `amount` of a wrapped ERC20 `asset` for `receiver`. -/// If the given asset is not whitelisted or has exceeded the -/// token caps, mint NUTs, too. -fn mint_eth_assets( - state: &mut WlState, - asset: &EthAddress, - receiver: &Address, - &amount: &token::Amount, -) -> Result<(EthAssetMint, BTreeSet)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut changed_keys = BTreeSet::default(); - - let asset_count = state - .ethbridge_queries() - .get_eth_assets_to_mint(asset, amount); - - let assets_to_mint = [ - // check if we should mint nuts - asset_count - .should_mint_nuts() - .then(|| (erc20_nut_address(asset), asset_count.nut_amount)), - // check if we should mint erc20s - asset_count - .should_mint_erc20s() - .then(|| (erc20_token_address(asset), asset_count.erc20_amount)), - ] - .into_iter() - // remove assets that do not need to be - // minted from the iterator - .flatten(); - - for (token, ref amount) in assets_to_mint { - token::credit_tokens(state, &token, receiver, *amount)?; - - let balance_key = balance_key(&token, receiver); - let supply_key = minted_balance_key(&token); - _ = changed_keys.insert(balance_key); - _ = changed_keys.insert(supply_key); - } - - Ok((asset_count, changed_keys)) -} - -fn act_on_transfers_to_eth( - state: &mut WlState, - transfers: &[TransferToEthereum], - relayer: &Address, -) -> Result<(BTreeSet, BTreeSet)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - tracing::debug!(?transfers, "Acting on transfers to Ethereum"); - let mut changed_keys = BTreeSet::default(); - let mut tx_events = BTreeSet::default(); - - // the BP nonce should always be incremented, even if no valid - // transfers to Ethereum were relayed. failing to do this - // halts the Ethereum bridge, since nonces will fall out - // of sync between Namada and Ethereum - let nonce_key = get_nonce_key(); - increment_bp_nonce(&nonce_key, state)?; - changed_keys.insert(nonce_key); - - // all keys of pending transfers - let prefix = BRIDGE_POOL_ADDRESS.to_db_key().into(); - let mut pending_keys: HashSet = state - .iter_prefix(&prefix) - .context("Failed to iterate over storage")? - .map(|(k, _, _)| { - Key::from_str(k.as_str()).expect("Key should be parsable") - }) - .filter(is_pending_transfer_key) - .collect(); - // Remove the completed transfers from the bridge pool - for event in transfers { - let (pending_transfer, key) = if let Some((pending, key)) = - state.ethbridge_queries().lookup_transfer_to_eth(event) - { - (pending, key) - } else { - hints::cold(); - unreachable!("The transfer should exist in the bridge pool"); - }; - tracing::debug!( - ?pending_transfer, - "Valid transfer to Ethereum detected, compensating the relayer \ - and burning any Ethereum assets in Namada" - ); - changed_keys.append(&mut update_transferred_asset_balances( - state, - &pending_transfer, - )?); - let pool_balance_key = - balance_key(&pending_transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); - let relayer_rewards_key = - balance_key(&pending_transfer.gas_fee.token, relayer); - // give the relayer the gas fee for this transfer and remove it from - // escrow. - token::transfer( - state, - &pending_transfer.gas_fee.token, - &BRIDGE_POOL_ADDRESS, - relayer, - pending_transfer.gas_fee.amount, - )?; - - state.delete(&key)?; - _ = pending_keys.swap_remove(&key); - _ = changed_keys.insert(key); - _ = changed_keys.insert(pool_balance_key); - _ = changed_keys.insert(relayer_rewards_key); - _ = tx_events.insert(EthBridgeEvent::new_bridge_pool_relayed( - pending_transfer.keccak256(), - )); - } - - if pending_keys.is_empty() { - return Ok((changed_keys, tx_events)); - } - - // NB: the timeout height was chosen as the minimum number of - // blocks of an epoch. transfers that reside in the Bridge pool - // for a period longer than this number of blocks will be removed - // and refunded. - let epoch_duration = read_epoch_duration_parameter(state)?; - let timeout_offset = epoch_duration.min_num_of_blocks; - - // Check time out and refund - if state.in_mem().block.height.0 > timeout_offset { - let timeout_height = BlockHeight( - state - .in_mem() - .block - .height - .0 - .checked_sub(timeout_offset) - .expect("Cannot underflow - checked above"), - ); - for key in pending_keys { - let inserted_height = BlockHeight::try_from_slice( - &state.in_mem().block.tree.get(&key)?, - ) - .expect("BlockHeight should be decoded"); - if inserted_height <= timeout_height { - let (mut keys, mut new_tx_events) = - refund_transfer(state, key)?; - changed_keys.append(&mut keys); - tx_events.append(&mut new_tx_events); - } - } - } - - Ok((changed_keys, tx_events)) -} - -fn increment_bp_nonce( - nonce_key: &Key, - state: &mut WlState, -) -> Result<()> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let next_nonce = state - .ethbridge_queries() - .get_bridge_pool_nonce() - .checked_increment() - .expect("Bridge pool nonce has overflowed"); - state.write(nonce_key, next_nonce)?; - Ok(()) -} - -fn refund_transfer( - state: &mut WlState, - key: Key, -) -> Result<(BTreeSet, BTreeSet)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut changed_keys = BTreeSet::default(); - let mut tx_events = BTreeSet::default(); - - let transfer = state.read(&key)?.expect("No PendingTransfer"); - changed_keys.append(&mut refund_transfer_fees(state, &transfer)?); - changed_keys.append(&mut refund_transferred_assets(state, &transfer)?); - - // Delete the key from the bridge pool - state.delete(&key)?; - _ = changed_keys.insert(key); - - // Emit expiration event - _ = tx_events.insert(EthBridgeEvent::new_bridge_pool_expired( - transfer.keccak256(), - )); - - Ok((changed_keys, tx_events)) -} - -fn refund_transfer_fees( - state: &mut WlState, - transfer: &PendingTransfer, -) -> Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut changed_keys = BTreeSet::default(); - - let payer_balance_key = - balance_key(&transfer.gas_fee.token, &transfer.gas_fee.payer); - let pool_balance_key = - balance_key(&transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); - - token::transfer( - state, - &transfer.gas_fee.token, - &BRIDGE_POOL_ADDRESS, - &transfer.gas_fee.payer, - transfer.gas_fee.amount, - )?; - - tracing::debug!(?transfer, "Refunded Bridge pool transfer fees"); - _ = changed_keys.insert(payer_balance_key); - _ = changed_keys.insert(pool_balance_key); - Ok(changed_keys) -} - -fn refund_transferred_assets( - state: &mut WlState, - transfer: &PendingTransfer, -) -> Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut changed_keys = BTreeSet::default(); - - let native_erc20_addr = state - .read(&bridge_storage::native_erc20_key())? - .ok_or_else(|| eyre::eyre!("Could not read wNam key from storage"))?; - let (source, target, token) = if transfer.transfer.asset - == native_erc20_addr - { - let escrow_balance_key = - balance_key(&state.in_mem().native_token, &BRIDGE_ADDRESS); - let sender_balance_key = balance_key( - &state.in_mem().native_token, - &transfer.transfer.sender, - ); - ( - escrow_balance_key, - sender_balance_key, - state.in_mem().native_token.clone(), - ) - } else { - let token = transfer.token_address(); - let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); - let sender_balance_key = balance_key(&token, &transfer.transfer.sender); - (escrow_balance_key, sender_balance_key, token) - }; - token::transfer( - state, - &token, - &BRIDGE_POOL_ADDRESS, - &transfer.transfer.sender, - transfer.transfer.amount, - )?; - - tracing::debug!(?transfer, "Refunded Bridge pool transferred assets"); - _ = changed_keys.insert(source); - _ = changed_keys.insert(target); - Ok(changed_keys) -} - -/// Burns any transferred ERC20s other than wNAM. If NAM is transferred, -/// update the wNAM supply key. -fn update_transferred_asset_balances( - state: &mut WlState, - transfer: &PendingTransfer, -) -> Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut changed_keys = BTreeSet::default(); - - let maybe_addr = state.read(&bridge_storage::native_erc20_key())?; - let Some(native_erc20_addr) = maybe_addr else { - return Err(eyre::eyre!("Could not read wNam key from storage")); - }; - - let token = transfer.token_address(); - - // the wrapped NAM supply increases when we transfer to Ethereum - if transfer.transfer.asset == native_erc20_addr { - if hints::unlikely(matches!( - &transfer.transfer.kind, - TransferToEthereumKind::Nut - )) { - unreachable!("Attempted to mint wNAM NUTs!"); - } - let supply_key = minted_balance_key(&token); - increment_total_supply(state, &token, transfer.transfer.amount)?; - _ = changed_keys.insert(supply_key); - tracing::debug!(?transfer, "Updated wrapped NAM supply"); - return Ok(changed_keys); - } - - // other asset kinds must be burned - burn_tokens( - state, - &token, - &BRIDGE_POOL_ADDRESS, - transfer.transfer.amount, - )?; - - let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); - let supply_key = minted_balance_key(&token); - _ = changed_keys.insert(escrow_balance_key); - _ = changed_keys.insert(supply_key); - - tracing::debug!(?transfer, "Burned wrapped ERC20 tokens"); - Ok(changed_keys) -} - -#[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use namada_core::address::gen_established_address; - use namada_core::address::testing::{gen_implicit_address, nam, wnam}; - use namada_core::collections::HashMap; - use namada_core::eth_bridge_pool::GasFee; - use namada_core::ethereum_events::testing::{ - DAI_ERC20_ETH_ADDRESS, arbitrary_keccak_hash, arbitrary_nonce, - }; - use namada_core::time::DurationSecs; - use namada_core::token::Amount; - use namada_core::{address, eth_bridge_pool}; - use namada_parameters::{EpochDuration, update_epoch_parameter}; - use namada_state::testing::TestState; - use token::increment_balance; - - use super::*; - use crate::storage::bridge_pool::get_pending_key; - use crate::storage::wrapped_erc20s; - use crate::test_utils::{self, stored_keys_count}; - - fn init_storage(state: &mut TestState) { - // set the timeout height offset - let timeout_offset = 10; - let epoch_duration = EpochDuration { - min_num_of_blocks: timeout_offset, - min_duration: DurationSecs(5), - }; - update_epoch_parameter(state, &epoch_duration).expect("Test failed"); - // set native ERC20 token - state - .write(&bridge_storage::native_erc20_key(), wnam()) - .expect("Test failed"); - } - - /// Helper data structure to feed to [`init_bridge_pool_transfers`]. - struct TransferData { - kind: eth_bridge_pool::TransferToEthereumKind, - gas_token: Address, - } - - impl Default for TransferData { - fn default() -> Self { - Self { - kind: eth_bridge_pool::TransferToEthereumKind::Erc20, - gas_token: nam(), - } - } - } - - /// Build [`TransferData`] values. - struct TransferDataBuilder { - kind: Option, - gas_token: Option
, - } - - #[allow(dead_code)] - impl TransferDataBuilder { - fn new() -> Self { - Self { - kind: None, - gas_token: None, - } - } - - fn kind( - mut self, - kind: eth_bridge_pool::TransferToEthereumKind, - ) -> Self { - self.kind = Some(kind); - self - } - - fn kind_erc20(self) -> Self { - self.kind(eth_bridge_pool::TransferToEthereumKind::Erc20) - } - - fn kind_nut(self) -> Self { - self.kind(eth_bridge_pool::TransferToEthereumKind::Nut) - } - - fn gas_token(mut self, address: Address) -> Self { - self.gas_token = Some(address); - self - } - - fn gas_erc20(self, address: &EthAddress) -> Self { - self.gas_token(wrapped_erc20s::token(address)) - } - - fn gas_nut(self, address: &EthAddress) -> Self { - self.gas_token(wrapped_erc20s::nut(address)) - } - - fn build(self) -> TransferData { - TransferData { - kind: self.kind.unwrap_or_else(|| TransferData::default().kind), - gas_token: self - .gas_token - .unwrap_or_else(|| TransferData::default().gas_token), - } - } - } - - fn init_bridge_pool_transfers( - state: &mut TestState, - assets_transferred: A, - ) -> Vec - where - A: Into>, - { - let sender = address::testing::established_address_1(); - let payer = address::testing::established_address_2(); - - // set pending transfers - let mut pending_transfers = vec![]; - for (i, (asset, TransferData { kind, gas_token })) in - assets_transferred.into().into_iter().enumerate() - { - let transfer = PendingTransfer { - transfer: eth_bridge_pool::TransferToEthereum { - asset, - sender: sender.clone(), - recipient: EthAddress([i as u8 + 1; 20]), - amount: Amount::from(10), - kind, - }, - gas_fee: GasFee { - token: gas_token, - amount: Amount::from(1), - payer: payer.clone(), - }, - }; - let key = get_pending_key(&transfer); - state.write(&key, &transfer).expect("Test failed"); - - pending_transfers.push(transfer); - } - pending_transfers - } - - #[inline] - fn init_bridge_pool(state: &mut TestState) -> Vec { - init_bridge_pool_transfers( - state, - (0..2) - .map(|i| { - ( - EthAddress([i; 20]), - TransferDataBuilder::new() - .kind(if i & 1 == 0 { - eth_bridge_pool::TransferToEthereumKind::Erc20 - } else { - eth_bridge_pool::TransferToEthereumKind::Nut - }) - .build(), - ) - }) - .collect::>(), - ) - } - - fn init_balance( - state: &mut TestState, - pending_transfers: &Vec, - ) { - for transfer in pending_transfers { - // Gas - let payer = address::testing::established_address_2(); - let payer_key = balance_key(&transfer.gas_fee.token, &payer); - let payer_balance = Amount::from(0); - state.write(&payer_key, payer_balance).expect("Test failed"); - increment_balance( - state, - &transfer.gas_fee.token, - &BRIDGE_POOL_ADDRESS, - Amount::from_u64(1), - ) - .expect("Test failed"); - - if transfer.transfer.asset == wnam() { - // native ERC20 - let sender_key = balance_key(&nam(), &transfer.transfer.sender); - let sender_balance = Amount::from(0); - state - .write(&sender_key, sender_balance) - .expect("Test failed"); - let escrow_key = balance_key(&nam(), &BRIDGE_ADDRESS); - let escrow_balance = Amount::from(10); - state - .write(&escrow_key, escrow_balance) - .expect("Test failed"); - } else { - let token = transfer.token_address(); - let sender_key = balance_key(&token, &transfer.transfer.sender); - let sender_balance = Amount::from(0); - state - .write(&sender_key, sender_balance) - .expect("Test failed"); - let escrow_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); - let escrow_balance = Amount::from(10); - state - .write(&escrow_key, escrow_balance) - .expect("Test failed"); - increment_total_supply(state, &token, transfer.transfer.amount) - .expect("Test failed"); - }; - } - } - - #[test] - /// Test that we do not make any changes to state when acting on most - /// events - fn test_act_on_does_nothing_for_other_events() { - let mut state = TestState::default(); - test_utils::bootstrap_ethereum_bridge(&mut state); - let initial_stored_keys_count = stored_keys_count(&state); - let events = vec![EthereumEvent::ValidatorSetUpdate { - nonce: arbitrary_nonce(), - bridge_validator_hash: arbitrary_keccak_hash(), - governance_validator_hash: arbitrary_keccak_hash(), - }]; - - for event in events { - act_on(&mut state, event.clone()).unwrap(); - assert_eq!( - stored_keys_count(&state), - initial_stored_keys_count, - "storage changed unexpectedly while acting on event: {:#?}", - event - ); - } - } - - #[test] - /// Test that state is indeed changed when we act on a non-empty - /// TransfersToNamada batch - fn test_act_on_changes_storage_for_transfers_to_namada() { - let mut state = TestState::default(); - test_utils::bootstrap_ethereum_bridge(&mut state); - state.commit_block().expect("Test failed"); - let initial_stored_keys_count = stored_keys_count(&state); - let amount = Amount::from(100); - let receiver = address::testing::established_address_1(); - let transfers = vec![TransferToNamada { - amount, - asset: DAI_ERC20_ETH_ADDRESS, - receiver, - }]; - let event = EthereumEvent::TransfersToNamada { - nonce: arbitrary_nonce(), - transfers, - }; - - act_on(&mut state, event).unwrap(); - - assert_eq!(stored_keys_count(&state), initial_stored_keys_count + 2); - } - - /// Parameters to test minting DAI in Namada. - struct TestMintDai { - /// The token cap of DAI. - /// - /// If the token is not whitelisted, this value - /// is not set. - dai_token_cap: Option, - /// The transferred amount of DAI. - transferred_amount: token::Amount, - } - - impl TestMintDai { - /// Execute a test with the given parameters. - fn run_test(self) { - let dai_token_cap = self.dai_token_cap.unwrap_or_default(); - - let (erc20_amount, nut_amount) = - if dai_token_cap > self.transferred_amount { - (self.transferred_amount, token::Amount::zero()) - } else { - (dai_token_cap, self.transferred_amount - dai_token_cap) - }; - assert_eq!(self.transferred_amount, nut_amount + erc20_amount); - - let mut state = TestState::default(); - test_utils::bootstrap_ethereum_bridge(&mut state); - if !dai_token_cap.is_zero() { - test_utils::whitelist_tokens( - &mut state, - [( - DAI_ERC20_ETH_ADDRESS, - test_utils::WhitelistMeta { - cap: dai_token_cap, - denom: 18, - }, - )], - ); - } - - let receiver = address::testing::established_address_1(); - let transfers = vec![TransferToNamada { - amount: self.transferred_amount, - asset: DAI_ERC20_ETH_ADDRESS, - receiver: receiver.clone(), - }]; - - update_transfers_to_namada_state( - &mut state, - &mut BTreeSet::new(), - &transfers, - ) - .unwrap(); - - for is_nut in [false, true] { - let wdai = if is_nut { - wrapped_erc20s::nut(&DAI_ERC20_ETH_ADDRESS) - } else { - wrapped_erc20s::token(&DAI_ERC20_ETH_ADDRESS) - }; - let expected_amount = - if is_nut { nut_amount } else { erc20_amount }; - - let receiver_balance_key = balance_key(&wdai, &receiver); - let wdai_supply_key = minted_balance_key(&wdai); - - for key in [receiver_balance_key, wdai_supply_key] { - let value: Option = - state.read(&key).unwrap(); - if expected_amount.is_zero() { - assert_matches!(value, None); - } else { - assert_matches!(value, Some(amount) if amount == expected_amount); - } - } - } - } - } - - /// Test that if DAI is never whitelisted, we only mint NUTs. - #[test] - fn test_minting_dai_when_not_whitelisted() { - TestMintDai { - dai_token_cap: None, - transferred_amount: Amount::from(100), - } - .run_test(); - } - - /// Test that overrunning the token caps results in minting DAI NUTs, - /// along with wDAI. - #[test] - fn test_minting_dai_on_cap_overrun() { - TestMintDai { - dai_token_cap: Some(Amount::from(80)), - transferred_amount: Amount::from(100), - } - .run_test(); - } - - /// Test acting on a single "transfer to Namada" Ethereum event - /// and minting the first ever wDAI. - #[test] - fn test_minting_dai_wrapped() { - TestMintDai { - dai_token_cap: Some(Amount::max()), - transferred_amount: Amount::from(100), - } - .run_test(); - } - - #[test] - /// When we act on an [`EthereumEvent::TransfersToEthereum`], test - /// that pending transfers are deleted from the Bridge pool, the - /// Bridge pool nonce is updated and escrowed assets are burned. - fn test_act_on_changes_storage_for_transfers_to_eth() { - let mut state = TestState::default(); - test_utils::bootstrap_ethereum_bridge(&mut state); - state.commit_block().expect("Test failed"); - init_storage(&mut state); - let native_erc20 = - read_native_erc20_address(&state).expect("Test failed"); - let random_erc20 = EthAddress([0xff; 20]); - let random_erc20_token = wrapped_erc20s::nut(&random_erc20); - let random_erc20_2 = EthAddress([0xee; 20]); - let random_erc20_token_2 = wrapped_erc20s::token(&random_erc20_2); - let random_erc20_3 = EthAddress([0xdd; 20]); - let random_erc20_token_3 = wrapped_erc20s::token(&random_erc20_3); - let random_erc20_4 = EthAddress([0xcc; 20]); - let random_erc20_token_4 = wrapped_erc20s::nut(&random_erc20_4); - let erc20_gas_addr = EthAddress([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, - 19, - ]); - let pending_transfers = init_bridge_pool_transfers( - &mut state, - [ - (native_erc20, TransferData::default()), - (random_erc20, TransferDataBuilder::new().kind_nut().build()), - ( - random_erc20_2, - TransferDataBuilder::new().kind_erc20().build(), - ), - ( - random_erc20_3, - TransferDataBuilder::new() - .kind_erc20() - .gas_erc20(&erc20_gas_addr) - .build(), - ), - ( - random_erc20_4, - TransferDataBuilder::new() - .kind_nut() - .gas_erc20(&erc20_gas_addr) - .build(), - ), - ], - ); - init_balance(&mut state, &pending_transfers); - let pending_keys: HashSet = - pending_transfers.iter().map(get_pending_key).collect(); - let relayer = gen_established_address("random"); - let transfers: Vec<_> = pending_transfers - .iter() - .map(TransferToEthereum::from) - .collect(); - let event = EthereumEvent::TransfersToEthereum { - nonce: arbitrary_nonce(), - transfers, - relayer: relayer.clone(), - }; - let payer_nam_balance_key = balance_key(&nam(), &relayer); - let payer_erc_balance_key = - balance_key(&wrapped_erc20s::token(&erc20_gas_addr), &relayer); - let pool_nam_balance_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - let pool_erc_balance_key = balance_key( - &wrapped_erc20s::token(&erc20_gas_addr), - &BRIDGE_POOL_ADDRESS, - ); - let mut bp_nam_balance_pre: Amount = state - .read(&pool_nam_balance_key) - .expect("Test failed") - .expect("Test failed"); - let mut bp_erc_balance_pre: Amount = state - .read(&pool_erc_balance_key) - .expect("Test failed") - .expect("Test failed"); - let (mut changed_keys, _) = act_on(&mut state, event).unwrap(); - - for erc20 in [ - random_erc20_token, - random_erc20_token_2, - random_erc20_token_3, - random_erc20_token_4, - ] { - assert!( - changed_keys.remove(&balance_key(&erc20, &BRIDGE_POOL_ADDRESS)), - "Expected {erc20:?} Bridge pool balance to change" - ); - assert!( - changed_keys.remove(&minted_balance_key(&erc20)), - "Expected {erc20:?} minted supply to change" - ); - } - assert!( - changed_keys - .remove(&minted_balance_key(&wrapped_erc20s::token(&wnam()))) - ); - assert!(changed_keys.remove(&payer_nam_balance_key)); - assert!(changed_keys.remove(&payer_erc_balance_key)); - assert!(changed_keys.remove(&pool_nam_balance_key)); - assert!(changed_keys.remove(&pool_erc_balance_key)); - assert!(changed_keys.remove(&get_nonce_key())); - assert!(changed_keys.iter().all(|k| pending_keys.contains(k))); - - let prefix = BRIDGE_POOL_ADDRESS.to_db_key().into(); - assert_eq!( - state.iter_prefix(&prefix).expect("Test failed").count(), - // NOTE: we should have one write -- the bridge pool nonce update - 1 - ); - let relayer_nam_balance: Amount = state - .read(&payer_nam_balance_key) - .expect("Test failed: read error") - .expect("Test failed: no value in storage"); - assert_eq!(relayer_nam_balance, Amount::from(3)); - let relayer_erc_balance: Amount = state - .read(&payer_erc_balance_key) - .expect("Test failed: read error") - .expect("Test failed: no value in storage"); - assert_eq!(relayer_erc_balance, Amount::from(2)); - - let bp_nam_balance_post = state - .read(&pool_nam_balance_key) - .expect("Test failed: read error") - .expect("Test failed: no value in storage"); - let bp_erc_balance_post = state - .read(&pool_erc_balance_key) - .expect("Test failed: read error") - .expect("Test failed: no value in storage"); - - bp_nam_balance_pre.spend(&bp_nam_balance_post).unwrap(); - assert_eq!(bp_nam_balance_pre, Amount::from(3)); - assert_eq!(bp_nam_balance_post, Amount::from(0)); - - bp_erc_balance_pre.spend(&bp_erc_balance_post).unwrap(); - assert_eq!(bp_erc_balance_pre, Amount::from(2)); - assert_eq!(bp_erc_balance_post, Amount::from(0)); - } - - #[test] - /// Test that the transfers time out in the bridge pool then the refund when - /// we act on a TransfersToEthereum - fn test_act_on_timeout_for_transfers_to_eth() { - let mut state = TestState::default(); - test_utils::bootstrap_ethereum_bridge(&mut state); - state.commit_block().expect("Test failed"); - init_storage(&mut state); - // Height 0 - let pending_transfers = init_bridge_pool(&mut state); - init_balance(&mut state, &pending_transfers); - state.commit_block().expect("Test failed"); - // pending transfers time out - state.in_mem_mut().block.height += 10 + 1; - // new pending transfer - let transfer = PendingTransfer { - transfer: eth_bridge_pool::TransferToEthereum { - asset: EthAddress([4; 20]), - sender: address::testing::established_address_1(), - recipient: EthAddress([5; 20]), - amount: Amount::from(10), - kind: eth_bridge_pool::TransferToEthereumKind::Erc20, - }, - gas_fee: GasFee { - token: nam(), - amount: Amount::from(1), - payer: address::testing::established_address_1(), - }, - }; - let key = get_pending_key(&transfer); - state.write(&key, transfer).expect("Test failed"); - state.commit_block().expect("Test failed"); - state.in_mem_mut().block.height += 1; - - // This should only refund - let event = EthereumEvent::TransfersToEthereum { - nonce: arbitrary_nonce(), - transfers: vec![], - relayer: gen_implicit_address(), - }; - let _ = act_on(&mut state, event).unwrap(); - - // The latest transfer is still pending - let prefix = BRIDGE_POOL_ADDRESS.to_db_key().into(); - assert_eq!( - state.iter_prefix(&prefix).expect("Test failed").count(), - // NOTE: we should have two writes -- one of them being - // the bridge pool nonce update - 2 - ); - - // Check the gas fee - let expected = pending_transfers - .iter() - .fold(Amount::from(0), |acc, t| acc + t.gas_fee.amount); - let payer = address::testing::established_address_2(); - let payer_key = balance_key(&nam(), &payer); - let payer_balance: Amount = state - .read(&payer_key) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(payer_balance, expected); - let pool_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - let pool_balance: Amount = state - .read(&pool_key) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(pool_balance, Amount::from(0)); - - // Check the balances - for transfer in pending_transfers { - if transfer.transfer.asset == wnam() { - let sender_key = balance_key(&nam(), &transfer.transfer.sender); - let sender_balance: Amount = state - .read(&sender_key) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(sender_balance, transfer.transfer.amount); - let escrow_key = balance_key(&nam(), &BRIDGE_ADDRESS); - let escrow_balance: Amount = state - .read(&escrow_key) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(escrow_balance, Amount::from(0)); - } else { - let token = transfer.token_address(); - let sender_key = balance_key(&token, &transfer.transfer.sender); - let sender_balance: Amount = state - .read(&sender_key) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(sender_balance, transfer.transfer.amount); - let escrow_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); - let escrow_balance: Amount = state - .read(&escrow_key) - .expect("Test failed") - .expect("Test failed"); - assert_eq!(escrow_balance, Amount::from(0)); - } - } - } - - #[test] - fn test_redeem_native_token() -> Result<()> { - let mut state = TestState::default(); - test_utils::bootstrap_ethereum_bridge(&mut state); - let receiver = address::testing::established_address_1(); - let amount = Amount::from(100); - - // pre wNAM balance - 0 - let receiver_wnam_balance_key = token::storage_key::balance_key( - &wrapped_erc20s::token(&wnam()), - &receiver, - ); - assert!( - state - .read::(&receiver_wnam_balance_key) - .unwrap() - .is_none() - ); - - let bridge_pool_initial_balance = Amount::from(100_000_000); - let bridge_pool_native_token_balance_key = - token::storage_key::balance_key( - &state.in_mem().native_token, - &BRIDGE_ADDRESS, - ); - let bridge_pool_native_erc20_supply_key = - minted_balance_key(&wrapped_erc20s::token(&wnam())); - StorageWrite::write( - &mut state, - &bridge_pool_native_token_balance_key, - bridge_pool_initial_balance, - )?; - StorageWrite::write( - &mut state, - &bridge_pool_native_erc20_supply_key, - amount, - )?; - let receiver_native_token_balance_key = token::storage_key::balance_key( - &state.in_mem().native_token, - &receiver, - ); - - let changed_keys = - redeem_native_token(&mut state, &wnam(), &receiver, &amount)?; - - assert_eq!( - changed_keys, - BTreeSet::from([ - bridge_pool_native_token_balance_key.clone(), - receiver_native_token_balance_key.clone(), - bridge_pool_native_erc20_supply_key.clone(), - ]) - ); - assert_eq!( - StorageRead::read(&state, &bridge_pool_native_token_balance_key)?, - Some(bridge_pool_initial_balance - amount) - ); - assert_eq!( - StorageRead::read(&state, &receiver_native_token_balance_key)?, - Some(amount) - ); - assert_eq!( - StorageRead::read(&state, &bridge_pool_native_erc20_supply_key)?, - Some(Amount::zero()) - ); - - // post wNAM balance - 0 - // - // wNAM is never minted, it's converted back to NAM - assert!( - state - .read::(&receiver_wnam_balance_key) - .unwrap() - .is_none() - ); - - Ok(()) - } - - /// Auxiliary function to test wrapped Ethereum ERC20s functionality. - fn test_wrapped_erc20s_aux(mut f: F) - where - F: FnMut(&mut TestState, EthereumEvent), - { - let mut state = TestState::default(); - test_utils::bootstrap_ethereum_bridge(&mut state); - state.commit_block().expect("Test failed"); - init_storage(&mut state); - let native_erc20 = - read_native_erc20_address(&state).expect("Test failed"); - let pending_transfers = init_bridge_pool_transfers( - &mut state, - [ - (native_erc20, TransferData::default()), - ( - EthAddress([0xaa; 20]), - TransferDataBuilder::new().kind_erc20().build(), - ), - ( - EthAddress([0xbb; 20]), - TransferDataBuilder::new().kind_nut().build(), - ), - ( - EthAddress([0xcc; 20]), - TransferDataBuilder::new().kind_erc20().build(), - ), - ( - EthAddress([0xdd; 20]), - TransferDataBuilder::new().kind_nut().build(), - ), - ( - EthAddress([0xee; 20]), - TransferDataBuilder::new().kind_erc20().build(), - ), - ( - EthAddress([0xff; 20]), - TransferDataBuilder::new().kind_nut().build(), - ), - ], - ); - init_balance(&mut state, &pending_transfers); - let transfers = pending_transfers - .into_iter() - .map(|ref transfer| { - let transfer_to_eth: TransferToEthereum = transfer.into(); - transfer_to_eth - }) - .collect(); - let relayer = gen_established_address("random"); - let event = EthereumEvent::TransfersToEthereum { - nonce: arbitrary_nonce(), - transfers, - relayer, - }; - f(&mut state, event) - } - - #[test] - /// When we act on an [`EthereumEvent::TransfersToEthereum`], test - /// that the transferred wrapped ERC20 tokens are burned in Namada. - fn test_wrapped_erc20s_are_burned() { - struct Delta { - asset: EthAddress, - sent_amount: token::Amount, - prev_balance: Option, - prev_supply: Option, - kind: eth_bridge_pool::TransferToEthereumKind, - } - - test_wrapped_erc20s_aux(|state, event| { - let transfers = match &event { - EthereumEvent::TransfersToEthereum { transfers, .. } => { - transfers.iter() - } - _ => panic!("Test failed"), - }; - let native_erc20 = - read_native_erc20_address(state).expect("Test failed"); - let deltas = transfers - .filter_map( - |event @ TransferToEthereum { asset, amount, .. }| { - if asset == &native_erc20 { - return None; - } - let kind = { - let (pending, _) = state - .ethbridge_queries() - .lookup_transfer_to_eth(event) - .expect("Test failed"); - pending.transfer.kind - }; - let erc20_token = match &kind { - eth_bridge_pool::TransferToEthereumKind::Erc20 => { - wrapped_erc20s::token(asset) - } - eth_bridge_pool::TransferToEthereumKind::Nut => { - wrapped_erc20s::nut(asset) - } - }; - let prev_balance = state - .read(&balance_key( - &erc20_token, - &BRIDGE_POOL_ADDRESS, - )) - .expect("Test failed"); - let prev_supply = state - .read(&minted_balance_key(&erc20_token)) - .expect("Test failed"); - Some(Delta { - kind, - asset: *asset, - sent_amount: *amount, - prev_balance, - prev_supply, - }) - }, - ) - .collect::>(); - - _ = act_on(state, event).unwrap(); - - for Delta { - kind, - ref asset, - sent_amount, - prev_balance, - prev_supply, - } in deltas - { - let burn_balance = prev_balance - .unwrap_or_default() - .checked_sub(sent_amount) - .expect("Test failed"); - let burn_supply = prev_supply - .unwrap_or_default() - .checked_sub(sent_amount) - .expect("Test failed"); - - let erc20_token = match kind { - eth_bridge_pool::TransferToEthereumKind::Erc20 => { - wrapped_erc20s::token(asset) - } - eth_bridge_pool::TransferToEthereumKind::Nut => { - wrapped_erc20s::nut(asset) - } - }; - - let balance: token::Amount = state - .read(&balance_key(&erc20_token, &BRIDGE_POOL_ADDRESS)) - .expect("Read must succeed") - .expect("Balance must exist"); - let supply: token::Amount = state - .read(&minted_balance_key(&erc20_token)) - .expect("Read must succeed") - .expect("Balance must exist"); - - assert_eq!(balance, burn_balance); - assert_eq!(supply, burn_supply); - } - }) - } - - #[test] - /// When we act on an [`EthereumEvent::TransfersToEthereum`], test - /// that the transferred wrapped NAM tokens are not burned in - /// Namada and instead are kept in escrow, under the Ethereum bridge - /// account. - fn test_wrapped_nam_not_burned() { - test_wrapped_erc20s_aux(|state, event| { - let native_erc20 = - read_native_erc20_address(state).expect("Test failed"); - let wnam = wrapped_erc20s::token(&native_erc20); - let escrow_balance_key = balance_key(&nam(), &BRIDGE_ADDRESS); - - // check pre supply - assert!( - state - .read::(&balance_key(&wnam, &BRIDGE_POOL_ADDRESS)) - .expect("Test failed") - .is_none() - ); - assert!( - state - .read::(&minted_balance_key(&wnam)) - .expect("Test failed") - .is_none() - ); - - // check pre balance - let pre_escrowed_balance: token::Amount = state - .read(&escrow_balance_key) - .expect("Read must succeed") - .expect("Balance must exist"); - - _ = act_on(state, event).unwrap(); - - // check post supply - the wNAM minted supply should increase - // by the transferred amount - assert!( - state - .read::(&balance_key(&wnam, &BRIDGE_POOL_ADDRESS)) - .expect("Test failed") - .is_none() - ); - assert_eq!( - state - .read::(&minted_balance_key(&wnam)) - .expect("Reading from storage should not fail") - .expect("The wNAM supply should have been updated"), - Amount::from_u64(10), - ); - - // check post balance - let post_escrowed_balance: token::Amount = state - .read(&escrow_balance_key) - .expect("Read must succeed") - .expect("Balance must exist"); - - assert_eq!(pre_escrowed_balance, post_escrowed_balance); - }) - } - - /// Test that the ledger appropriately panics when we try to mint - /// wrapped NAM NUTs. Under normal circumstances, this should never - /// happen. - #[test] - #[should_panic(expected = "Attempted to mint wNAM NUTs!")] - fn test_wnam_doesnt_mint_nuts() { - let mut state = TestState::default(); - test_utils::bootstrap_ethereum_bridge(&mut state); - - let transfer = PendingTransfer { - transfer: eth_bridge_pool::TransferToEthereum { - asset: wnam(), - sender: address::testing::established_address_1(), - recipient: EthAddress([5; 20]), - amount: Amount::from(10), - kind: eth_bridge_pool::TransferToEthereumKind::Nut, - }, - gas_fee: GasFee { - token: nam(), - amount: Amount::from(1), - payer: address::testing::established_address_1(), - }, - }; - - _ = update_transferred_asset_balances(&mut state, &transfer); - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs deleted file mode 100644 index 21408ccded9..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs +++ /dev/null @@ -1,1016 +0,0 @@ -//! Code for handling Ethereum events protocol txs. - -mod eth_msgs; -mod events; - -use std::collections::BTreeSet; - -use borsh::BorshDeserialize; -use eth_msgs::EthMsgUpdate; -use eyre::Result; -use namada_core::address::Address; -use namada_core::chain::{BlockHeight, Epoch}; -use namada_core::collections::{HashMap, HashSet}; -use namada_core::ethereum_events::EthereumEvent; -use namada_core::key::common; -use namada_core::storage::Key; -use namada_core::token::Amount; -use namada_proof_of_stake::storage::read_owned_pos_params; -use namada_state::tx_queue::ExpiredTx; -use namada_state::{DB, DBIter, StorageHasher, WlState}; -use namada_systems::governance; -use namada_tx::data::BatchedTxResult; -use namada_vote_ext::ethereum_events::{MultiSignedEthEvent, SignedVext, Vext}; - -use super::ChangedKeys; -use crate::event::EthBridgeEvent; -use crate::protocol::transactions::utils; -use crate::protocol::transactions::votes::update::NewVotes; -use crate::protocol::transactions::votes::{self, calculate_new}; -use crate::storage::eth_bridge_queries::EthBridgeQueries; -use crate::storage::vote_tallies::{self, Keys}; - -impl utils::GetVoters for &HashSet { - #[inline] - fn get_voters(self) -> HashSet<(Address, BlockHeight)> { - self.iter().fold(HashSet::new(), |mut voters, update| { - voters.extend(update.seen_by.clone()); - voters - }) - } -} - -/// Sign the given Ethereum events, and return the associated -/// vote extension protocol transaction. -/// -/// __INVARIANT__: Assume `ethereum_events` are sorted in ascending -/// order. -pub fn sign_ethereum_events( - state: &WlState, - validator_addr: &Address, - protocol_key: &common::SecretKey, - ethereum_events: Vec, -) -> Option -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - if !state.ethbridge_queries().is_bridge_active() { - return None; - } - - let ext = Vext { - block_height: state.in_mem().get_last_block_height(), - validator_addr: validator_addr.clone(), - ethereum_events, - }; - if !ext.ethereum_events.is_empty() { - tracing::info!( - new_ethereum_events.len = ext.ethereum_events.len(), - ?ext.block_height, - "Voting for new Ethereum events" - ); - tracing::debug!("New Ethereum events - {:#?}", ext.ethereum_events); - } - - Some(ext.sign(protocol_key).into()) -} - -/// Applies derived state changes to storage, based on Ethereum `events` which -/// were newly seen by some consensus validator(s). -/// -/// For `events` which have been seen by enough voting power (`>= 2/3`), extra -/// state changes may take place, such as minting of wrapped ERC20s. -/// -/// This function is deterministic based on some existing blockchain state and -/// the passed `events`. -pub fn apply_derived_tx( - state: &mut WlState, - events: Vec, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - let mut changed_keys = timeout_events::(state)?; - if events.is_empty() { - return Ok(BatchedTxResult { - changed_keys, - ..Default::default() - }); - } - tracing::info!( - ethereum_events = events.len(), - "Applying state updates derived from Ethereum events found in \ - protocol transaction" - ); - - let updates = events - .into_iter() - .filter_map(|multisigned| { - // NB: discard events with outdated nonces - state - .ethbridge_queries() - .validate_eth_event_nonce(&multisigned.event) - .then(|| EthMsgUpdate::from(multisigned)) - }) - .collect(); - - let voting_powers = utils::get_voting_powers(state, &updates)?; - - let (mut apply_updates_keys, eth_bridge_events) = - apply_updates::(state, updates, voting_powers)?; - changed_keys.append(&mut apply_updates_keys); - - Ok(BatchedTxResult { - changed_keys, - events: eth_bridge_events - .into_iter() - .map(|event| event.into()) - .collect(), - ..Default::default() - }) -} - -/// Apply votes to Ethereum events in storage and act on any events which are -/// confirmed. -/// -/// The `voting_powers` map must contain a voting power for all -/// `(Address, BlockHeight)`s that occur in any of the `updates`. -pub(super) fn apply_updates( - state: &mut WlState, - updates: HashSet, - voting_powers: HashMap<(Address, BlockHeight), Amount>, -) -> Result<(ChangedKeys, BTreeSet)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - tracing::debug!( - updates.len = updates.len(), - ?voting_powers, - "Applying Ethereum state update transaction" - ); - - let mut changed_keys = BTreeSet::default(); - let mut tx_events = BTreeSet::default(); - let mut confirmed = vec![]; - for update in updates { - // The order in which updates are applied to storage does not matter. - // The final storage state will be the same regardless. - let (mut changed, newly_confirmed) = - apply_update::(state, update.clone(), &voting_powers)?; - changed_keys.append(&mut changed); - if newly_confirmed { - confirmed.push(update.body); - } - } - if confirmed.is_empty() { - tracing::debug!("No events were newly confirmed"); - return Ok((changed_keys, tx_events)); - } - tracing::debug!(n = confirmed.len(), "Events were newly confirmed",); - - // Right now, the order in which events are acted on does not matter. - // For `TransfersToNamada` events, they can happen in any order. - for event in confirmed { - let (mut changed, mut new_tx_events) = events::act_on(state, event)?; - changed_keys.append(&mut changed); - tx_events.append(&mut new_tx_events); - } - Ok((changed_keys, tx_events)) -} - -/// Apply an [`EthMsgUpdate`] to storage. Returns any keys changed and whether -/// the event was newly seen. -/// -/// The `voting_powers` map must contain a voting power for all -/// `(Address, BlockHeight)`s that occur in `update`. -fn apply_update( - state: &mut WlState, - update: EthMsgUpdate, - voting_powers: &HashMap<(Address, BlockHeight), Amount>, -) -> Result<(ChangedKeys, bool)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - let eth_msg_keys = vote_tallies::Keys::from(&update.body); - let exists_in_storage = if let Some(seen) = - votes::storage::maybe_read_seen(state, ð_msg_keys)? - { - if seen { - tracing::debug!(?update, "Ethereum event is already seen"); - return Ok((ChangedKeys::default(), false)); - } - true - } else { - false - }; - - let (vote_tracking, changed, confirmed, already_present) = - if !exists_in_storage { - tracing::debug!(%eth_msg_keys.prefix, "Ethereum event not seen before by any validator"); - let vote_tracking = calculate_new::( - state, - update.seen_by, - voting_powers, - )?; - let changed = eth_msg_keys.into_iter().collect(); - let confirmed = vote_tracking.seen; - (vote_tracking, changed, confirmed, false) - } else { - tracing::debug!( - %eth_msg_keys.prefix, - "Ethereum event already exists in storage", - ); - let new_votes = - NewVotes::new(update.seen_by.clone(), voting_powers)?; - let (vote_tracking, changed) = - votes::update::calculate::( - state, - ð_msg_keys, - new_votes, - )?; - if changed.is_empty() { - return Ok((changed, false)); - } - let confirmed = - vote_tracking.seen && changed.contains(ð_msg_keys.seen()); - (vote_tracking, changed, confirmed, true) - }; - - votes::storage::write( - state, - ð_msg_keys, - &update.body, - &vote_tracking, - already_present, - )?; - - Ok((changed, confirmed)) -} - -fn timeout_events(state: &mut WlState) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - let mut changed = ChangedKeys::new(); - for keys in get_timed_out_eth_events(state)? { - tracing::debug!( - %keys.prefix, - "Ethereum event timed out", - ); - if let Some(event) = - votes::storage::delete::(state, &keys)? - { - tracing::debug!( - %keys.prefix, - "Queueing Ethereum event for retransmission", - ); - // NOTE: if we error out in the `ethereum_bridge` crate, - // currently there is no way to reset the expired txs queue - // to its previous state. this shouldn't be a big deal, as - // replaying ethereum events has no effect on the ledger. - // however, we may need to revisit this code if we ever - // implement slashing on double voting of ethereum events. - state - .in_mem_mut() - .expired_txs_queue - .push(ExpiredTx::EthereumEvent(event)); - } - changed.extend(keys.clone().into_iter()); - } - - Ok(changed) -} - -fn get_timed_out_eth_events( - state: &mut WlState, -) -> Result>> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let unbonding_len = read_owned_pos_params(state)?.unbonding_len; - let current_epoch = state.in_mem().last_epoch; - if current_epoch.0 <= unbonding_len { - return Ok(Vec::new()); - } - - let timeout_epoch = Epoch( - current_epoch - .0 - .checked_sub(unbonding_len) - .expect("Cannot underflow - checked above"), - ); - let prefix = vote_tallies::eth_msgs_prefix(); - let mut cur_keys: Option> = None; - let mut is_timed_out = false; - let mut is_seen = false; - let mut results = Vec::new(); - for (key, val, _) in votes::storage::iter_prefix(state, &prefix)? { - let key = Key::parse(key).expect("The key should be parsable"); - if let Some(keys) = vote_tallies::eth_event_keys(&key) { - match &cur_keys { - Some(prev_keys) => { - if *prev_keys != keys { - // check the previous keys since we found new keys - if is_timed_out && !is_seen { - results.push(prev_keys.clone()); - } - is_timed_out = false; - is_seen = false; - cur_keys = Some(keys); - } - } - None => cur_keys = Some(keys), - } - - if vote_tallies::is_epoch_key(&key) { - let inserted_epoch = Epoch::try_from_slice(&val[..]) - .expect("Decoding Epoch failed"); - if inserted_epoch <= timeout_epoch { - is_timed_out = true; - } - } - - if vote_tallies::is_seen_key(&key) { - is_seen = bool::try_from_slice(&val[..]) - .expect("Decoding boolean failed"); - } - } - } - // check the last one - if let Some(cur_keys) = cur_keys { - if is_timed_out && !is_seen { - results.push(cur_keys); - } - } - - Ok(results) -} - -#[cfg(test)] -mod tests { - use namada_core::address; - use namada_core::ethereum_events::TransferToNamada; - use namada_core::ethereum_events::testing::{ - DAI_ERC20_ETH_ADDRESS, arbitrary_amount, arbitrary_eth_address, - arbitrary_nonce, arbitrary_single_transfer, - }; - use namada_core::voting_power::FractionalVotingPower; - use namada_state::testing::TestState; - use namada_storage::StorageRead; - - use super::*; - use crate::protocol::transactions::utils::GetVoters; - use crate::protocol::transactions::votes::{ - EpochedVotingPower, EpochedVotingPowerExt, Votes, - }; - use crate::storage::wrapped_erc20s; - use crate::test_utils::{self, GovStore}; - use crate::token::storage_key::{balance_key, minted_balance_key}; - - /// All kinds of [`Keys`]. - enum KeyKind { - Body, - Seen, - SeenBy, - VotingPower, - Epoch, - } - - #[test] - /// Test applying a `TransfersToNamada` batch containing a single transfer - fn test_apply_single_transfer() -> Result<()> { - let (sole_validator, validator_stake) = test_utils::default_validator(); - let receiver = address::testing::established_address_2(); - - let amount = arbitrary_amount(); - let asset = arbitrary_eth_address(); - let body = EthereumEvent::TransfersToNamada { - nonce: arbitrary_nonce(), - transfers: vec![TransferToNamada { - amount, - asset, - receiver: receiver.clone(), - }], - }; - let update = EthMsgUpdate { - body: body.clone(), - seen_by: Votes::from([(sole_validator.clone(), BlockHeight(100))]), - }; - let updates = HashSet::from_iter(vec![update]); - let voting_powers = HashMap::from_iter(vec![( - (sole_validator.clone(), BlockHeight(100)), - validator_stake, - )]); - let (mut state, _) = test_utils::setup_default_storage(); - test_utils::whitelist_tokens( - &mut state, - [( - DAI_ERC20_ETH_ADDRESS, - test_utils::WhitelistMeta { - cap: Amount::max(), - denom: 18, - }, - )], - ); - - let (changed_keys, _) = apply_updates::<_, _, GovStore<_>>( - &mut state, - updates, - voting_powers, - )?; - - let eth_msg_keys: vote_tallies::Keys = (&body).into(); - let wrapped_erc20_token = wrapped_erc20s::token(&asset); - assert_eq!( - BTreeSet::from_iter(vec![ - eth_msg_keys.body(), - eth_msg_keys.seen(), - eth_msg_keys.seen_by(), - eth_msg_keys.voting_power(), - eth_msg_keys.voting_started_epoch(), - balance_key(&wrapped_erc20_token, &receiver), - minted_balance_key(&wrapped_erc20_token), - ]), - changed_keys - ); - - let stored_body: EthereumEvent = - state.read(ð_msg_keys.body())?.expect("Test failed"); - assert_eq!(stored_body, body); - - let seen: bool = - state.read(ð_msg_keys.seen())?.expect("Test failed"); - assert!(seen); - - let seen_by: Votes = - state.read(ð_msg_keys.seen_by())?.expect("Test failed"); - assert_eq!(seen_by, Votes::from([(sole_validator, BlockHeight(100))])); - - let voting_power = state - .read::(ð_msg_keys.voting_power())? - .expect("Test failed") - .fractional_stake::<_, _, GovStore<_>>(&state); - assert_eq!(voting_power, FractionalVotingPower::WHOLE); - - let epoch: Epoch = state - .read(ð_msg_keys.voting_started_epoch())? - .expect("Test failed"); - assert_eq!(epoch, Epoch(0)); - - let wrapped_erc20_balance: Amount = state - .read(&balance_key(&wrapped_erc20_token, &receiver))? - .expect("Test failed"); - assert_eq!(wrapped_erc20_balance, amount); - - let wrapped_erc20_supply: Amount = state - .read(&minted_balance_key(&wrapped_erc20_token))? - .expect("Test failed"); - assert_eq!(wrapped_erc20_supply, amount); - - Ok(()) - } - - #[test] - /// Test applying a single transfer via `apply_derived_tx`, where an event - /// has enough voting power behind it for it to be applied at the same time - /// that it is recorded in storage - fn test_apply_derived_tx_new_event_mint_immediately() { - let sole_validator = address::testing::established_address_2(); - let (mut state, _) = - test_utils::setup_storage_with_validators(HashMap::from_iter( - vec![(sole_validator.clone(), Amount::native_whole(100))], - )); - test_utils::whitelist_tokens( - &mut state, - [( - DAI_ERC20_ETH_ADDRESS, - test_utils::WhitelistMeta { - cap: Amount::max(), - denom: 18, - }, - )], - ); - let receiver = address::testing::established_address_1(); - - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![TransferToNamada { - amount: Amount::from(100), - asset: DAI_ERC20_ETH_ADDRESS, - receiver: receiver.clone(), - }], - }; - - let result = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vec![MultiSignedEthEvent { - event: event.clone(), - signers: BTreeSet::from([(sole_validator, BlockHeight(100))]), - }], - ); - - let tx_result = match result { - Ok(tx_result) => tx_result, - Err(err) => panic!("unexpected error: {:#?}", err), - }; - - let eth_msg_keys = vote_tallies::Keys::from(&event); - let dai_token = wrapped_erc20s::token(&DAI_ERC20_ETH_ADDRESS); - assert_eq!( - tx_result.changed_keys, - BTreeSet::from_iter(vec![ - eth_msg_keys.body(), - eth_msg_keys.seen(), - eth_msg_keys.seen_by(), - eth_msg_keys.voting_power(), - eth_msg_keys.voting_started_epoch(), - balance_key(&dai_token, &receiver), - minted_balance_key(&dai_token), - ]) - ); - assert!(tx_result.vps_result.accepted_vps.is_empty()); - assert!(tx_result.vps_result.rejected_vps.is_empty()); - assert!(tx_result.vps_result.errors.is_empty()); - assert!(tx_result.initialized_accounts.is_empty()); - } - - /// Test calling apply_derived_tx for an event that isn't backed by enough - /// voting power to be acted on immediately - #[test] - fn test_apply_derived_tx_new_event_dont_mint() { - let validator_a = address::testing::established_address_2(); - let validator_b = address::testing::established_address_3(); - let (mut state, _) = test_utils::setup_storage_with_validators( - HashMap::from_iter(vec![ - (validator_a.clone(), Amount::native_whole(100)), - (validator_b, Amount::native_whole(100)), - ]), - ); - let receiver = address::testing::established_address_1(); - - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![TransferToNamada { - amount: Amount::from(100), - asset: DAI_ERC20_ETH_ADDRESS, - receiver, - }], - }; - - let result = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vec![MultiSignedEthEvent { - event: event.clone(), - signers: BTreeSet::from([(validator_a, BlockHeight(100))]), - }], - ); - let tx_result = match result { - Ok(tx_result) => tx_result, - Err(err) => panic!("unexpected error: {:#?}", err), - }; - - let eth_msg_keys = vote_tallies::Keys::from(&event); - assert_eq!( - tx_result.changed_keys, - BTreeSet::from_iter(vec![ - eth_msg_keys.body(), - eth_msg_keys.seen(), - eth_msg_keys.seen_by(), - eth_msg_keys.voting_power(), - eth_msg_keys.voting_started_epoch(), - ]), - "The Ethereum event should have been recorded, but no minting \ - should have happened yet as it has only been seen by 1/2 the \ - voting power so far" - ); - } - - #[test] - /// Test that attempts made to apply duplicate - /// [`MultiSignedEthEvent`]s in a single [`apply_derived_tx`] call don't - /// result in duplicate votes in storage. - pub fn test_apply_derived_tx_duplicates() -> Result<()> { - let validator_a = address::testing::established_address_2(); - let validator_b = address::testing::established_address_3(); - let (mut state, _) = test_utils::setup_storage_with_validators( - HashMap::from_iter(vec![ - (validator_a.clone(), Amount::native_whole(100)), - (validator_b, Amount::native_whole(100)), - ]), - ); - - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![TransferToNamada { - amount: Amount::from(100), - asset: DAI_ERC20_ETH_ADDRESS, - receiver: address::testing::established_address_1(), - }], - }; - // two votes for the same event from validator A - let signers = BTreeSet::from([(validator_a.clone(), BlockHeight(100))]); - let multisigned = MultiSignedEthEvent { - event: event.clone(), - signers, - }; - - let multisigneds = vec![multisigned.clone(), multisigned]; - - let result = - apply_derived_tx::<_, _, GovStore<_>>(&mut state, multisigneds); - let tx_result = match result { - Ok(tx_result) => tx_result, - Err(err) => panic!("unexpected error: {:#?}", err), - }; - - let eth_msg_keys = vote_tallies::Keys::from(&event); - assert_eq!( - tx_result.changed_keys, - BTreeSet::from_iter(vec![ - eth_msg_keys.body(), - eth_msg_keys.seen(), - eth_msg_keys.seen_by(), - eth_msg_keys.voting_power(), - eth_msg_keys.voting_started_epoch(), - ]), - "One vote for the Ethereum event should have been recorded", - ); - - let seen_by: Votes = - state.read(ð_msg_keys.seen_by())?.expect("Test failed"); - assert_eq!(seen_by, Votes::from([(validator_a, BlockHeight(100))])); - - let voting_power = state - .read::(ð_msg_keys.voting_power())? - .expect("Test failed") - .fractional_stake::<_, _, GovStore<_>>(&state); - assert_eq!(voting_power, FractionalVotingPower::HALF); - - Ok(()) - } - - #[test] - /// Assert we don't return anything if we try to get the votes for an empty - /// set of updates - pub fn test_get_votes_for_updates_empty() { - let updates = HashSet::new(); - assert!(updates.get_voters().is_empty()); - } - - #[test] - /// Test that we correctly get the votes from a set of updates - pub fn test_get_votes_for_events() { - let updates = HashSet::from([ - EthMsgUpdate { - body: arbitrary_single_transfer( - 1.into(), - address::testing::established_address_1(), - ), - seen_by: Votes::from([ - ( - address::testing::established_address_1(), - BlockHeight(100), - ), - ( - address::testing::established_address_2(), - BlockHeight(102), - ), - ]), - }, - EthMsgUpdate { - body: arbitrary_single_transfer( - 2.into(), - address::testing::established_address_2(), - ), - seen_by: Votes::from([ - ( - address::testing::established_address_1(), - BlockHeight(101), - ), - ( - address::testing::established_address_3(), - BlockHeight(100), - ), - ]), - }, - ]); - let voters = updates.get_voters(); - assert_eq!( - voters, - HashSet::from([ - (address::testing::established_address_1(), BlockHeight(100)), - (address::testing::established_address_1(), BlockHeight(101)), - (address::testing::established_address_2(), BlockHeight(102)), - (address::testing::established_address_3(), BlockHeight(100)) - ]) - ) - } - - #[test] - /// Test that timed out events are deleted - pub fn test_timeout_events() { - let validator_a = address::testing::established_address_2(); - let validator_b = address::testing::established_address_3(); - let (mut state, _) = test_utils::setup_storage_with_validators( - HashMap::from_iter(vec![ - (validator_a.clone(), Amount::native_whole(100)), - (validator_b, Amount::native_whole(100)), - ]), - ); - let receiver = address::testing::established_address_1(); - - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![TransferToNamada { - amount: Amount::from(100), - asset: DAI_ERC20_ETH_ADDRESS, - receiver: receiver.clone(), - }], - }; - let _result = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vec![MultiSignedEthEvent { - event: event.clone(), - signers: BTreeSet::from([( - validator_a.clone(), - BlockHeight(100), - )]), - }], - ); - let prev_keys = vote_tallies::Keys::from(&event); - - // commit then update the epoch - state.commit_block().unwrap(); - let unbonding_len = namada_proof_of_stake::storage::read_pos_params::< - _, - GovStore<_>, - >(&state) - .expect("Test failed") - .unbonding_len - + 1; - state.in_mem_mut().last_epoch = - state.in_mem().last_epoch + unbonding_len; - state.in_mem_mut().block.epoch = state.in_mem().last_epoch + 1_u64; - - let new_event = EthereumEvent::TransfersToNamada { - nonce: 1.into(), - transfers: vec![TransferToNamada { - amount: Amount::from(100), - asset: DAI_ERC20_ETH_ADDRESS, - receiver, - }], - }; - let result = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vec![MultiSignedEthEvent { - event: new_event.clone(), - signers: BTreeSet::from([(validator_a, BlockHeight(100))]), - }], - ); - let tx_result = match result { - Ok(tx_result) => tx_result, - Err(err) => panic!("unexpected error: {:#?}", err), - }; - - let new_keys = vote_tallies::Keys::from(&new_event); - assert_eq!( - tx_result.changed_keys, - BTreeSet::from_iter(vec![ - prev_keys.body(), - prev_keys.seen(), - prev_keys.seen_by(), - prev_keys.voting_power(), - prev_keys.voting_started_epoch(), - new_keys.body(), - new_keys.seen(), - new_keys.seen_by(), - new_keys.voting_power(), - new_keys.voting_started_epoch(), - ]), - "New event should be inserted and the previous one should be \ - deleted", - ); - assert!( - state - .read::(&prev_keys.body()) - .unwrap() - .is_none() - ); - assert!( - state - .read::(&new_keys.body()) - .unwrap() - .is_some() - ); - } - - /// Helper fn to [`test_timeout_events_before_state_upds`]. - fn check_event_keys( - keys: &Keys, - state: &TestState, - result: Result, - mut assert: F, - ) where - F: FnMut(KeyKind, Option>), - { - let tx_result = match result { - Ok(tx_result) => tx_result, - Err(err) => panic!("unexpected error: {:#?}", err), - }; - assert(KeyKind::Body, state.read_bytes(&keys.body()).unwrap()); - assert(KeyKind::Seen, state.read_bytes(&keys.seen()).unwrap()); - assert(KeyKind::SeenBy, state.read_bytes(&keys.seen_by()).unwrap()); - assert( - KeyKind::VotingPower, - state.read_bytes(&keys.voting_power()).unwrap(), - ); - assert( - KeyKind::Epoch, - state.read_bytes(&keys.voting_started_epoch()).unwrap(), - ); - assert_eq!( - tx_result.changed_keys, - BTreeSet::from_iter([ - keys.body(), - keys.seen(), - keys.seen_by(), - keys.voting_power(), - keys.voting_started_epoch(), - ]), - ); - } - - /// Test that we time out events before we do any state update - /// on them. This should prevent double voting from rebonded - /// validators. - #[test] - fn test_timeout_events_before_state_upds() { - let validator_a = address::testing::established_address_2(); - let validator_b = address::testing::established_address_3(); - let (mut state, _) = test_utils::setup_storage_with_validators( - HashMap::from_iter(vec![ - (validator_a.clone(), Amount::native_whole(100)), - (validator_b.clone(), Amount::native_whole(100)), - ]), - ); - - let receiver = address::testing::established_address_1(); - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![TransferToNamada { - amount: Amount::from(100), - asset: DAI_ERC20_ETH_ADDRESS, - receiver, - }], - }; - let keys = vote_tallies::Keys::from(&event); - - let result = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vec![MultiSignedEthEvent { - event: event.clone(), - signers: BTreeSet::from([(validator_a, BlockHeight(100))]), - }], - ); - check_event_keys(&keys, &state, result, |key_kind, value| { - match (key_kind, value) { - (_, None) => panic!("Test failed"), - (KeyKind::VotingPower, Some(power)) => { - let power = EpochedVotingPower::try_from_slice(&power) - .expect("Test failed") - .fractional_stake::<_, _, GovStore<_>>(&state); - assert_eq!(power, FractionalVotingPower::HALF); - } - (_, Some(_)) => {} - } - }); - - // commit then update the epoch - state.commit_block().unwrap(); - let unbonding_len = namada_proof_of_stake::storage::read_pos_params::< - _, - GovStore<_>, - >(&state) - .expect("Test failed") - .unbonding_len - + 1; - state.in_mem_mut().last_epoch = - state.in_mem().last_epoch + unbonding_len; - state.in_mem_mut().block.epoch = state.in_mem().last_epoch + 1_u64; - - let result = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vec![MultiSignedEthEvent { - event, - signers: BTreeSet::from([(validator_b, BlockHeight(100))]), - }], - ); - check_event_keys(&keys, &state, result, |key_kind, value| { - match (key_kind, value) { - (_, None) => panic!("Test failed"), - (KeyKind::VotingPower, Some(power)) => { - let power = EpochedVotingPower::try_from_slice(&power) - .expect("Test failed") - .fractional_stake::<_, _, GovStore<_>>(&state); - assert_eq!(power, FractionalVotingPower::HALF); - } - (_, Some(_)) => {} - } - }); - } - - /// Test that [`MultiSignedEthEvent`]s with outdated nonces do - /// not result in votes in storage. - #[test] - fn test_apply_derived_tx_outdated_nonce() -> Result<()> { - let (mut state, _) = test_utils::setup_default_storage(); - - let new_multisigned = |nonce: u64| { - let (validator, _) = test_utils::default_validator(); - let event = EthereumEvent::TransfersToNamada { - nonce: nonce.into(), - transfers: vec![TransferToNamada { - amount: Amount::from(100), - asset: DAI_ERC20_ETH_ADDRESS, - receiver: validator.clone(), - }], - }; - let signers = BTreeSet::from([(validator, BlockHeight(100))]); - ( - MultiSignedEthEvent { - event: event.clone(), - signers, - }, - event, - ) - }; - macro_rules! nonce_ok { - ($nonce:expr) => { - let (multisigned, event) = new_multisigned($nonce); - let tx_result = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vec![multisigned], - )?; - - let eth_msg_keys = vote_tallies::Keys::from(&event); - assert!( - tx_result.changed_keys.contains(ð_msg_keys.seen()), - "The Ethereum event should have been seen", - ); - assert_eq!( - state.ethbridge_queries().get_next_nam_transfers_nonce(), - ($nonce + 1).into(), - "The transfers to Namada nonce should have been \ - incremented", - ); - }; - } - macro_rules! nonce_err { - ($nonce:expr) => { - let (multisigned, event) = new_multisigned($nonce); - let tx_result = apply_derived_tx::<_, _, GovStore<_>>( - &mut state, - vec![multisigned], - )?; - - let eth_msg_keys = vote_tallies::Keys::from(&event); - assert!( - !tx_result.changed_keys.contains(ð_msg_keys.seen()), - "The Ethereum event should have been ignored", - ); - assert_eq!( - state.ethbridge_queries().get_next_nam_transfers_nonce(), - NEXT_NONCE_TO_PROCESS.into(), - "The transfers to Namada nonce should not have changed", - ); - }; - } - - // update storage with valid events - const NEXT_NONCE_TO_PROCESS: u64 = 3; - for nonce in 0..NEXT_NONCE_TO_PROCESS { - nonce_ok!(nonce); - } - - // attempts to replay events with older nonces should - // result in the events getting ignored - for nonce in 0..NEXT_NONCE_TO_PROCESS { - nonce_err!(nonce); - } - - // process new valid event - nonce_ok!(NEXT_NONCE_TO_PROCESS); - - Ok(()) - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/mod.rs b/crates/ethereum_bridge/src/protocol/transactions/mod.rs deleted file mode 100644 index a6b646e5e92..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! This module contains functionality for handling protocol transactions. -//! -//! When a protocol transaction is included in a block, we may expect all nodes -//! to update their blockchain state in a deterministic way. This can be done -//! natively rather than via the wasm environment as happens with regular -//! transactions. - -pub mod bridge_pool_roots; -pub mod ethereum_events; -mod read; -mod update; -mod utils; -pub mod validator_set_update; -pub mod votes; - -use std::collections::BTreeSet; - -use namada_core::storage; - -/// The keys changed while applying a protocol transaction. -pub type ChangedKeys = BTreeSet; diff --git a/crates/ethereum_bridge/src/protocol/transactions/read.rs b/crates/ethereum_bridge/src/protocol/transactions/read.rs deleted file mode 100644 index a6804d1e3b8..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/read.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Helpers for reading from storage -use borsh::BorshDeserialize; -use eyre::{Result, eyre}; -use namada_core::storage; -#[cfg(test)] -use namada_core::token::Amount; -use namada_state::{DB, DBIter, StorageHasher, WlState}; -use namada_storage::StorageRead; - -/// Returns the stored Amount, or 0 if not stored -#[cfg(test)] -pub(super) fn amount_or_default( - state: &WlState, - key: &storage::Key, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - Ok(maybe_value(state, key)?.unwrap_or_default()) -} - -/// Read some arbitrary value from storage, erroring if it's not found -pub(super) fn value( - state: &WlState, - key: &storage::Key, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - maybe_value(state, key)?.ok_or_else(|| eyre!("no value found at {}", key)) -} - -/// Try to read some arbitrary value from storage, returning `None` if nothing -/// is read. This will still error if there is data stored at `key` but it is -/// not deserializable to `T`. -pub(super) fn maybe_value( - state: &WlState, - key: &storage::Key, -) -> Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - Ok(state.read(key)?) -} - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use namada_core::storage; - use namada_core::token::Amount; - use namada_state::testing::TestState; - use namada_storage::StorageWrite; - - use crate::protocol::transactions::read; - - #[test] - fn test_amount_returns_zero_for_uninitialized_storage() { - let fake_storage = TestState::default(); - let amt = read::amount_or_default( - &fake_storage, - &storage::Key::parse("some arbitrary key with no stored value") - .unwrap(), - ) - .unwrap(); - assert_eq!(amt, Amount::from(0)); - } - - #[test] - fn test_amount_returns_stored_amount() { - let key = storage::Key::parse("some arbitrary key").unwrap(); - let amount = Amount::from(1_000_000); - let mut fake_storage = TestState::default(); - fake_storage.write(&key, amount).unwrap(); - - let amt = read::amount_or_default(&fake_storage, &key).unwrap(); - assert_eq!(amt, amount); - } - - #[test] - fn test_amount_errors_if_not_amount() { - let key = storage::Key::parse("some arbitrary key").unwrap(); - let amount = "not an Amount type"; - let mut fake_storage = TestState::default(); - fake_storage.write(&key, amount).unwrap(); - - assert_matches!(read::amount_or_default(&fake_storage, &key), Err(_)); - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/update.rs b/crates/ethereum_bridge/src/protocol/transactions/update.rs deleted file mode 100644 index 06508502a7a..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/update.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Helpers for writing to storage -use eyre::Result; -use namada_core::borsh::{BorshDeserialize, BorshSerialize}; -use namada_core::hash::StorageHasher; -use namada_core::storage; -use namada_state::{DB, DBIter, WlState}; -use namada_storage::StorageWrite; - -#[allow(dead_code)] -/// Reads an arbitrary value, applies update then writes it back -pub fn value( - state: &mut WlState, - key: &storage::Key, - update: impl FnOnce(&mut T), -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut value = super::read::value(state, key)?; - update(&mut value); - state.write(key, &value)?; - Ok(value) -} - -#[cfg(test)] -mod tests { - use eyre::eyre; - use namada_state::testing::TestState; - use namada_storage::StorageRead; - - use super::*; - - #[test] - /// Test updating a value - fn test_value() -> Result<()> { - let key = storage::Key::parse("some arbitrary key") - .expect("could not set up test"); - let value = 21i32; - let mut state = TestState::default(); - state.write(&key, value).expect("could not set up test"); - - super::value(&mut state, &key, |v: &mut i32| *v *= 2)?; - - let new_val = state - .read::(&key)? - .ok_or_else(|| eyre!("no value found"))?; - assert_eq!(new_val, 42); - Ok(()) - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/utils.rs b/crates/ethereum_bridge/src/protocol/transactions/utils.rs deleted file mode 100644 index e21aad80cf4..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/utils.rs +++ /dev/null @@ -1,252 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; - -use eyre::eyre; -use itertools::Itertools; -use namada_core::address::Address; -use namada_core::chain::BlockHeight; -use namada_core::collections::{HashMap, HashSet}; -use namada_core::token; -use namada_proof_of_stake::storage::read_consensus_validator_set_addresses_with_stake; -use namada_proof_of_stake::types::WeightedValidator; -use namada_state::{DB, DBIter, StorageHasher, StorageRead, WlState}; - -/// Proof of some arbitrary tally whose voters can be queried. -pub(super) trait GetVoters { - /// Extract all the voters and the block heights at which they voted from - /// the given proof. - // TODO(feature = "abcipp"): we do not need to return block heights - // anymore. votes will always be from `storage.last_height`. - fn get_voters(self) -> HashSet<(Address, BlockHeight)>; -} - -/// Returns a map whose keys are addresses of validators and the block height at -/// which they signed some arbitrary object, and whose values are the voting -/// powers of these validators at the key's given block height. -pub(super) fn get_voting_powers( - state: &WlState, - proof: P, -) -> eyre::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - P: GetVoters, -{ - let voters = proof.get_voters(); - tracing::debug!(?voters, "Got validators who voted on at least one event"); - - let consensus_validators = get_consensus_validators( - state, - voters.iter().map(|(_, h)| h.to_owned()).collect(), - ); - tracing::debug!( - n = consensus_validators.len(), - ?consensus_validators, - "Got consensus validators" - ); - - let voting_powers = - get_voting_powers_for_selected(&consensus_validators, voters)?; - tracing::debug!( - ?voting_powers, - "Got voting powers for relevant validators" - ); - - Ok(voting_powers) -} - -pub(super) fn get_consensus_validators( - state: &WlState, - block_heights: HashSet, -) -> BTreeMap> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut consensus_validators = BTreeMap::default(); - for height in block_heights.into_iter() { - let epoch = state.get_epoch_at_height(height).unwrap().expect( - "The epoch of the last block height should always be known", - ); - _ = consensus_validators.insert( - height, - read_consensus_validator_set_addresses_with_stake(state, epoch) - .unwrap(), - ); - } - consensus_validators -} - -/// Gets the voting power of `selected` from `all_consensus`. Errors if a -/// `selected` validator is not found in `all_consensus`. -pub(super) fn get_voting_powers_for_selected( - all_consensus: &BTreeMap>, - selected: HashSet<(Address, BlockHeight)>, -) -> eyre::Result> { - let voting_powers = selected - .into_iter() - .map( - |(addr, height)| -> eyre::Result<( - (Address, BlockHeight), - token::Amount, - )> { - let consensus_validators = - all_consensus.get(&height).ok_or_else(|| { - eyre!( - "No consensus validators found for height {height}" - ) - })?; - let voting_power = consensus_validators - .iter() - .find(|&v| v.address == addr) - .ok_or_else(|| { - eyre!( - "No consensus validator found with address {addr} \ - for height {height}" - ) - })? - .bonded_stake; - Ok(( - (addr, height), - voting_power, - )) - }, - ) - .try_collect()?; - Ok(voting_powers) -} - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use namada_core::address; - use namada_core::ethereum_events::testing::arbitrary_bonded_stake; - use namada_core::voting_power::FractionalVotingPower; - - use super::*; - - #[test] - /// Test getting the voting power for the sole consensus validator from the - /// set of consensus validators - fn test_get_voting_powers_for_selected_sole_validator() { - let sole_validator = address::testing::established_address_1(); - let bonded_stake = arbitrary_bonded_stake(); - let weighted_sole_validator = WeightedValidator { - bonded_stake, - address: sole_validator.clone(), - }; - let validators = HashSet::from_iter(vec![( - sole_validator.clone(), - BlockHeight(100), - )]); - let consensus_validators = BTreeMap::from_iter(vec![( - BlockHeight(100), - BTreeSet::from_iter(vec![weighted_sole_validator]), - )]); - - let result = - get_voting_powers_for_selected(&consensus_validators, validators); - - let voting_powers = match result { - Ok(voting_powers) => voting_powers, - Err(error) => panic!("error: {:?}", error), - }; - assert_eq!(voting_powers.len(), 1); - assert_matches!( - voting_powers.get(&(sole_validator, BlockHeight(100))), - Some(v) if *v == bonded_stake - ); - } - - #[test] - /// Test that an error is returned if a validator is not found in the set of - /// consensus validators - fn test_get_voting_powers_for_selected_missing_validator() { - let present_validator = address::testing::established_address_1(); - let missing_validator = address::testing::established_address_2(); - let bonded_stake = arbitrary_bonded_stake(); - let weighted_present_validator = WeightedValidator { - bonded_stake, - address: present_validator.clone(), - }; - let validators = HashSet::from_iter(vec![ - (present_validator, BlockHeight(100)), - (missing_validator, BlockHeight(100)), - ]); - let consensus_validators = BTreeMap::from_iter(vec![( - BlockHeight(100), - BTreeSet::from_iter(vec![weighted_present_validator]), - )]); - - let result = - get_voting_powers_for_selected(&consensus_validators, validators); - - assert!(result.is_err()); - } - - #[test] - /// Assert we error if we are passed an `(Address, BlockHeight)` but are not - /// given a corresponding set of validators for the block height - fn test_get_voting_powers_for_selected_no_consensus_validators_for_height() - { - let all_consensus = BTreeMap::default(); - let selected = HashSet::from_iter(vec![( - address::testing::established_address_1(), - BlockHeight(100), - )]); - - let result = get_voting_powers_for_selected(&all_consensus, selected); - - assert!(result.is_err()); - } - - #[test] - /// Test getting the voting powers for two consensus validators from the set - /// of consensus validators - fn test_get_voting_powers_for_selected_two_validators() { - let validator_1 = address::testing::established_address_1(); - let validator_2 = address::testing::established_address_2(); - let bonded_stake_1 = token::Amount::from(100); - let bonded_stake_2 = token::Amount::from(200); - let weighted_validator_1 = WeightedValidator { - bonded_stake: bonded_stake_1, - address: validator_1.clone(), - }; - let weighted_validator_2 = WeightedValidator { - bonded_stake: bonded_stake_2, - address: validator_2.clone(), - }; - let validators = HashSet::from_iter(vec![ - (validator_1.clone(), BlockHeight(100)), - (validator_2.clone(), BlockHeight(100)), - ]); - let consensus_validators = BTreeMap::from_iter(vec![( - BlockHeight(100), - BTreeSet::from_iter(vec![ - weighted_validator_1, - weighted_validator_2, - ]), - )]); - let bonded_stake = bonded_stake_1 + bonded_stake_2; - - let result = - get_voting_powers_for_selected(&consensus_validators, validators); - - let voting_powers = match result { - Ok(voting_powers) => voting_powers, - Err(error) => panic!("error: {:?}", error), - }; - assert_eq!(voting_powers.len(), 2); - let expected_stake = - FractionalVotingPower::new_u64(100, 300).unwrap() * bonded_stake; - assert_matches!( - voting_powers.get(&(validator_1, BlockHeight(100))), - Some(v) if *v == expected_stake - ); - let expected_stake = - FractionalVotingPower::new_u64(200, 300).unwrap() * bonded_stake; - assert_matches!( - voting_powers.get(&(validator_2, BlockHeight(100))), - Some(v) if *v == expected_stake - ); - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs b/crates/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs deleted file mode 100644 index 251f05a508b..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs +++ /dev/null @@ -1,450 +0,0 @@ -//! Code for handling validator set update protocol txs. - -use eyre::Result; -use namada_core::address::Address; -use namada_core::chain::{BlockHeight, Epoch}; -use namada_core::collections::{HashMap, HashSet}; -use namada_core::key::common; -use namada_core::token::Amount; -use namada_state::{DB, DBIter, StorageHasher, WlState}; -use namada_systems::governance; -use namada_tx::data::BatchedTxResult; -use namada_vote_ext::validator_set_update; - -use super::ChangedKeys; -use crate::protocol::transactions::utils; -use crate::protocol::transactions::votes::update::NewVotes; -use crate::protocol::transactions::votes::{self, Votes}; -use crate::storage::eth_bridge_queries::{EthBridgeQueries, SendValsetUpd}; -use crate::storage::proof::EthereumProof; -use crate::storage::vote_tallies; - -impl utils::GetVoters for (&validator_set_update::VextDigest, BlockHeight) { - #[inline] - fn get_voters(self) -> HashSet<(Address, BlockHeight)> { - // votes were cast at the 2nd block height of the ext's signing epoch - let (ext, epoch_2nd_height) = self; - ext.signatures - .keys() - .cloned() - .zip(std::iter::repeat(epoch_2nd_height)) - .collect() - } -} - -/// Sign the next set of validators, and return the associated -/// vote extension protocol transaction. -pub fn sign_validator_set_update( - state: &WlState, - validator_addr: &Address, - eth_hot_key: &common::SecretKey, -) -> Option -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - state - .ethbridge_queries() - .must_send_valset_upd(SendValsetUpd::Now) - .then(|| { - let next_epoch = state.in_mem().get_current_epoch().0.next(); - - let voting_powers = state - .ethbridge_queries() - .get_consensus_eth_addresses::(next_epoch) - .map(|(eth_addr_book, _, voting_power)| { - (eth_addr_book, voting_power) - }) - .collect(); - - let ext = validator_set_update::Vext { - voting_powers, - validator_addr: validator_addr.clone(), - signing_epoch: state.in_mem().get_current_epoch().0, - }; - - ext.sign(eth_hot_key) - }) -} - -/// Aggregate validators' votes -pub fn aggregate_votes( - state: &mut WlState, - ext: validator_set_update::VextDigest, - signing_epoch: Epoch, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - if ext.signatures.is_empty() { - tracing::debug!("Ignoring empty validator set update"); - return Ok(Default::default()); - } - - tracing::info!( - num_votes = ext.signatures.len(), - "Aggregating new votes for validator set update" - ); - - let epoch_2nd_height = state - .in_mem() - .block - .pred_epochs - .get_start_height_of_epoch(signing_epoch) - // NOTE: The only way this can fail is if validator set updates do not - // reach a `seen` state before the relevant epoch data is purged from - // Namada. In most scenarios, we should reach a complete proof before - // the end of an epoch, and even if we cross an epoch boundary without - // a complete proof, we should get one shortly after. - .expect("The first block height of the signing epoch should be known") - .next_height(); - let voting_powers = - utils::get_voting_powers(state, (&ext, epoch_2nd_height))?; - let changed_keys = apply_update::( - state, - ext, - signing_epoch, - epoch_2nd_height, - voting_powers, - )?; - - Ok(BatchedTxResult { - changed_keys, - ..Default::default() - }) -} - -fn apply_update( - state: &mut WlState, - ext: validator_set_update::VextDigest, - signing_epoch: Epoch, - epoch_2nd_height: BlockHeight, - voting_powers: HashMap<(Address, BlockHeight), Amount>, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - let next_epoch = { - // proofs should be written to the sub-key space of the next epoch. - // this way, we do, for instance, an RPC call to `E=2` to query a - // validator set proof for epoch 2 signed by validators of epoch 1. - signing_epoch.next() - }; - let valset_upd_keys = vote_tallies::Keys::from(&next_epoch); - let maybe_proof = 'check_storage: { - let Some(seen) = - votes::storage::maybe_read_seen(state, &valset_upd_keys)? - else { - break 'check_storage None; - }; - if seen { - tracing::debug!("Validator set update tally is already seen"); - return Ok(ChangedKeys::default()); - } - let proof = votes::storage::read_body(state, &valset_upd_keys)?; - Some(proof) - }; - - let mut seen_by = Votes::default(); - for address in ext.signatures.keys().cloned() { - if let Some(present) = seen_by.insert(address, epoch_2nd_height) { - // TODO(namada#770): this shouldn't be happening in any case and we - // should be refactoring to get rid of `BlockHeight` - tracing::warn!(?present, "Duplicate vote in digest"); - } - } - - let (tally, proof, changed, confirmed, already_present) = - if let Some(mut proof) = maybe_proof { - tracing::debug!( - %valset_upd_keys.prefix, - "Validator set update votes already in storage", - ); - let new_votes = NewVotes::new(seen_by, &voting_powers)?; - let (tally, changed) = votes::update::calculate::<_, _, Gov, _>( - state, - &valset_upd_keys, - new_votes, - )?; - if changed.is_empty() { - return Ok(changed); - } - let confirmed = - tally.seen && changed.contains(&valset_upd_keys.seen()); - proof.attach_signature_batch(ext.signatures.into_iter().map( - |(addr, sig)| { - ( - state - .ethbridge_queries() - .get_eth_addr_book::( - &addr, - Some(signing_epoch), - ) - .expect("All validators should have eth keys"), - sig, - ) - }, - )); - (tally, proof, changed, confirmed, true) - } else { - tracing::debug!( - %valset_upd_keys.prefix, - ?ext.voting_powers, - "New validator set update vote aggregation started" - ); - let tally = votes::calculate_new::( - state, - seen_by, - &voting_powers, - )?; - let mut proof = EthereumProof::new(ext.voting_powers); - proof.attach_signature_batch(ext.signatures.into_iter().map( - |(addr, sig)| { - ( - state - .ethbridge_queries() - .get_eth_addr_book::( - &addr, - Some(signing_epoch), - ) - .expect("All validators should have eth keys"), - sig, - ) - }, - )); - let changed = valset_upd_keys.into_iter().collect(); - let confirmed = tally.seen; - (tally, proof, changed, confirmed, false) - }; - - tracing::debug!( - ?tally, - ?proof, - "Applying validator set update state changes" - ); - votes::storage::write( - state, - &valset_upd_keys, - &proof, - &tally, - already_present, - )?; - - if confirmed { - tracing::debug!( - %valset_upd_keys.prefix, - "Acquired complete proof on validator set update" - ); - } - - Ok(changed) -} - -#[cfg(test)] -mod test_valset_upd_state_changes { - use namada_core::address; - use namada_core::voting_power::FractionalVotingPower; - use namada_proof_of_stake::queries::{ - get_total_voting_power, read_validator_stake, - }; - use namada_state::StorageRead; - use namada_vote_ext::validator_set_update::VotingPowersMap; - - use super::*; - use crate::test_utils::{self, GovStore}; - - /// Test that if a validator set update becomes "seen", then - /// it should have a complete proof backing it up in storage. - #[test] - fn test_seen_has_complete_proof() { - let (mut state, keys) = test_utils::setup_default_storage(); - - let last_height = state.in_mem().get_last_block_height(); - let signing_epoch = state - .get_epoch_at_height(last_height) - .unwrap() - .expect("The epoch of the last block height should be known"); - - let tx_result = aggregate_votes::<_, _, GovStore<_>>( - &mut state, - validator_set_update::VextDigest::singleton( - validator_set_update::Vext { - voting_powers: VotingPowersMap::new(), - validator_addr: address::testing::established_address_1(), - signing_epoch, - } - .sign( - &keys - .get(&address::testing::established_address_1()) - .expect("Test failed") - .eth_bridge, - ), - ), - signing_epoch, - ) - .expect("Test failed"); - - // let's make sure we updated storage - assert!(!tx_result.changed_keys.is_empty()); - - let valset_upd_keys = vote_tallies::Keys::from(&signing_epoch.next()); - - assert!(tx_result.changed_keys.contains(&valset_upd_keys.body())); - assert!(tx_result.changed_keys.contains(&valset_upd_keys.seen())); - assert!(tx_result.changed_keys.contains(&valset_upd_keys.seen_by())); - assert!( - tx_result - .changed_keys - .contains(&valset_upd_keys.voting_power()) - ); - - // check if the valset upd is marked as "seen" - let tally = votes::storage::read(&state, &valset_upd_keys) - .expect("Test failed"); - assert!(tally.seen); - - // read the proof in storage and make sure its signature is - // from the configured validator - let proof = votes::storage::read_body(&state, &valset_upd_keys) - .expect("Test failed"); - assert_eq!(proof.data, VotingPowersMap::new()); - - let mut proof_sigs: Vec<_> = proof.signatures.into_keys().collect(); - assert_eq!(proof_sigs.len(), 1); - - let addr_book = proof_sigs.pop().expect("Test failed"); - assert_eq!( - addr_book, - state - .ethbridge_queries() - .get_eth_addr_book::>( - &address::testing::established_address_1(), - Some(signing_epoch) - ) - .expect("Test failed") - ); - - // since only one validator is configured, we should - // have reached a complete proof - let total_voting_power = - get_total_voting_power::<_, GovStore<_>>(&state, signing_epoch); - let validator_voting_power = read_validator_stake::<_, GovStore<_>>( - &state, - &address::testing::established_address_1(), - signing_epoch, - ) - .expect("Test failed"); - let voting_power = FractionalVotingPower::new( - validator_voting_power.into(), - total_voting_power.into(), - ) - .expect("Test failed"); - - assert!(voting_power > FractionalVotingPower::TWO_THIRDS); - } - - /// Test that if a validator set update is not "seen" yet, then - /// it should never have a complete proof backing it up in storage. - #[test] - fn test_not_seen_has_incomplete_proof() { - let (mut state, keys) = - test_utils::setup_storage_with_validators(HashMap::from_iter([ - // the first validator has exactly 2/3 of the total stake - ( - address::testing::established_address_1(), - Amount::native_whole(50_000), - ), - ( - address::testing::established_address_2(), - Amount::native_whole(25_000), - ), - ])); - - let last_height = state.in_mem().get_last_block_height(); - let signing_epoch = state - .get_epoch_at_height(last_height) - .unwrap() - .expect("The epoch of the last block height should be known"); - - let tx_result = aggregate_votes::<_, _, GovStore<_>>( - &mut state, - validator_set_update::VextDigest::singleton( - validator_set_update::Vext { - voting_powers: VotingPowersMap::new(), - validator_addr: address::testing::established_address_1(), - signing_epoch, - } - .sign( - &keys - .get(&address::testing::established_address_1()) - .expect("Test failed") - .eth_bridge, - ), - ), - signing_epoch, - ) - .expect("Test failed"); - - // let's make sure we updated storage - assert!(!tx_result.changed_keys.is_empty()); - - let valset_upd_keys = vote_tallies::Keys::from(&signing_epoch.next()); - - assert!(tx_result.changed_keys.contains(&valset_upd_keys.body())); - assert!(tx_result.changed_keys.contains(&valset_upd_keys.seen())); - assert!(tx_result.changed_keys.contains(&valset_upd_keys.seen_by())); - assert!( - tx_result - .changed_keys - .contains(&valset_upd_keys.voting_power()) - ); - - // assert the validator set update is not "seen" yet - let tally = votes::storage::read(&state, &valset_upd_keys) - .expect("Test failed"); - assert!(!tally.seen); - - // read the proof in storage and make sure its signature is - // from the configured validator - let proof = votes::storage::read_body(&state, &valset_upd_keys) - .expect("Test failed"); - assert_eq!(proof.data, VotingPowersMap::new()); - - let mut proof_sigs: Vec<_> = proof.signatures.into_keys().collect(); - assert_eq!(proof_sigs.len(), 1); - - let addr_book = proof_sigs.pop().expect("Test failed"); - assert_eq!( - addr_book, - state - .ethbridge_queries() - .get_eth_addr_book::>( - &address::testing::established_address_1(), - Some(signing_epoch) - ) - .expect("Test failed") - ); - - // make sure we do not have a complete proof yet - let total_voting_power = - get_total_voting_power::<_, GovStore<_>>(&state, signing_epoch); - let validator_voting_power = read_validator_stake::<_, GovStore<_>>( - &state, - &address::testing::established_address_1(), - signing_epoch, - ) - .expect("Test failed"); - let voting_power = FractionalVotingPower::new( - validator_voting_power.into(), - total_voting_power.into(), - ) - .expect("Test failed"); - - assert!(voting_power <= FractionalVotingPower::TWO_THIRDS); - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/votes.rs b/crates/ethereum_bridge/src/protocol/transactions/votes.rs deleted file mode 100644 index e310ca5c4e9..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/votes.rs +++ /dev/null @@ -1,411 +0,0 @@ -//! Logic and data types relating to tallying validators' votes for pieces of -//! data stored in the ledger, where those pieces of data should only be acted -//! on once they have received enough votes -use std::collections::{BTreeMap, BTreeSet}; - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use eyre::{Result, eyre}; -use namada_core::address::Address; -use namada_core::chain::{BlockHeight, Epoch}; -use namada_core::collections::HashMap; -use namada_core::token; -use namada_core::voting_power::FractionalVotingPower; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_proof_of_stake::queries::get_total_voting_power; -use namada_state::{DB, DBIter, StorageHasher, StorageRead, WlState}; -use namada_systems::governance; - -use super::{ChangedKeys, read}; - -pub(super) mod storage; -pub(super) mod update; - -/// The addresses of validators that voted for something, and the block -/// heights at which they voted. -/// -/// We use a [`BTreeMap`] to enforce that a validator (as uniquely identified by -/// an [`Address`]) may vote at most once, their vote must be associated with a -/// specific [`BlockHeight`]. Their voting power at that block height is what is -/// used when calculating whether something has enough voting power behind it or -/// not. -pub type Votes = BTreeMap; - -/// The voting power behind a tally aggregated over multiple epochs. -pub type EpochedVotingPower = BTreeMap; - -/// Extension methods for [`EpochedVotingPower`] instances. -pub trait EpochedVotingPowerExt { - /// Query the stake of the most secure [`Epoch`] referenced by an - /// [`EpochedVotingPower`]. This translates to the [`Epoch`] with - /// the most staked tokens. - fn epoch_max_voting_power( - &self, - state: &WlState, - ) -> Option - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>; - - /// Fetch the sum of the stake tallied on an - /// [`EpochedVotingPower`]. - fn tallied_stake(&self) -> token::Amount; - - /// Fetch the sum of the stake tallied on an - /// [`EpochedVotingPower`], as a fraction over - /// the maximum stake seen in the epochs voted on. - #[inline] - fn fractional_stake( - &self, - state: &WlState, - ) -> FractionalVotingPower - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, - { - let Some(max_voting_power) = - self.epoch_max_voting_power::<_, _, Gov>(state) - else { - return FractionalVotingPower::NULL; - }; - FractionalVotingPower::new( - self.tallied_stake().into(), - max_voting_power.into(), - ) - .unwrap() - } - - /// Check if the [`Tally`] associated with an [`EpochedVotingPower`] - /// can be considered `seen`. - #[inline] - fn has_majority_quorum(&self, state: &WlState) -> bool - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, - { - let Some(max_voting_power) = - self.epoch_max_voting_power::<_, _, Gov>(state) - else { - return false; - }; - // NB: Preserve the safety property of the Tendermint protocol across - // all the epochs we vote on. - // - // PROOF: We calculate the maximum amount of tokens S_max staked on - // one of the epochs the tally occurred in. At most F = 1/3 * S_max - // of the combined stake can be Byzantine, for the protocol to uphold - // its linearizability property whilst remaining "secure" against - // arbitrarily faulty nodes. Therefore, we can consider a tally secure - // if has accumulated an amount of stake greater than the threshold - // stake of S_max - F = 2/3 S_max. - let threshold = FractionalVotingPower::TWO_THIRDS - .checked_mul_amount(max_voting_power) - .expect("Cannot overflow"); - self.tallied_stake() > threshold - } -} - -impl EpochedVotingPowerExt for EpochedVotingPower { - fn epoch_max_voting_power( - &self, - state: &WlState, - ) -> Option - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, - { - self.keys() - .copied() - .map(|epoch| get_total_voting_power::<_, Gov>(state, epoch)) - .max() - } - - fn tallied_stake(&self) -> token::Amount { - token::Amount::sum(self.values().copied()) - .expect("Talling stake shouldn't overflow") - } -} - -#[derive( - Clone, - Debug, - PartialEq, - Eq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -/// Represents all the information needed to tally a piece of data that may be -/// voted for over multiple epochs -pub struct Tally { - /// The total voting power that's voted for this event across all epochs. - pub voting_power: EpochedVotingPower, - /// The votes which have been counted towards `voting_power`. Note that - /// validators may submit multiple votes at different block heights for - /// the same thing, but ultimately only one vote per validator will be - /// used when tallying voting power. - pub seen_by: Votes, - /// Whether this event has been acted on or not - this should only ever - /// transition from `false` to `true`, once there is enough voting power. - pub seen: bool, -} - -/// Calculate a new [`Tally`] based on some validators' fractional voting powers -/// as specific block heights -pub fn calculate_new( - state: &WlState, - seen_by: Votes, - voting_powers: &HashMap<(Address, BlockHeight), token::Amount>, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - let mut seen_by_voting_power = EpochedVotingPower::new(); - for (validator, block_height) in seen_by.iter() { - match voting_powers - .get(&(validator.to_owned(), block_height.to_owned())) - { - Some(&voting_power) => { - let epoch = state - .get_epoch_at_height(*block_height) - .unwrap() - .expect("The queried epoch should be known"); - let aggregated = seen_by_voting_power - .entry(epoch) - .or_insert_with(token::Amount::zero); - *aggregated = aggregated - .checked_add(voting_power) - .ok_or_else(|| eyre!("Aggregated voting power overflow"))?; - } - None => { - return Err(eyre!( - "voting power was not provided for validator {}", - validator - )); - } - }; - } - - let newly_confirmed = - seen_by_voting_power.has_majority_quorum::(state); - Ok(Tally { - voting_power: seen_by_voting_power, - seen_by, - seen: newly_confirmed, - }) -} - -/// Deterministically constructs a [`Votes`] map from a set of validator -/// addresses and the block heights they signed something at. -/// -/// We arbitrarily take the earliest block height for each validator address -/// encountered. -pub fn dedupe(signers: BTreeSet<(Address, BlockHeight)>) -> Votes { - signers.into_iter().rev().collect() -} - -#[cfg(test)] -mod tests { - use namada_core::address; - use namada_proof_of_stake::parameters::OwnedPosParams; - use namada_proof_of_stake::storage::{ - read_consensus_validator_set_addresses_with_stake, write_pos_params, - }; - - use super::*; - use crate::test_utils::{self, GovStore}; - - #[test] - fn test_dedupe_empty() { - let signers = BTreeSet::new(); - - let deduped = dedupe(signers); - - assert_eq!(deduped, Votes::new()); - } - - #[test] - fn test_dedupe_single_vote() { - let sole_validator = address::testing::established_address_1(); - let votes = [(sole_validator, BlockHeight(100))]; - let signers = BTreeSet::from(votes.clone()); - - let deduped = dedupe(signers); - - assert_eq!(deduped, Votes::from(votes)); - } - - #[test] - fn test_dedupe_multiple_votes_same_voter() { - let sole_validator = address::testing::established_address_1(); - let earliest_vote_height = 100; - let earliest_vote = - (sole_validator.clone(), BlockHeight(earliest_vote_height)); - let votes = [ - earliest_vote.clone(), - ( - sole_validator.clone(), - BlockHeight(earliest_vote_height + 1), - ), - (sole_validator, BlockHeight(earliest_vote_height + 100)), - ]; - let signers = BTreeSet::from(votes); - - let deduped = dedupe(signers); - - assert_eq!(deduped, Votes::from([earliest_vote])); - } - - #[test] - fn test_dedupe_multiple_votes_multiple_voters() { - let validator_1 = address::testing::established_address_1(); - let validator_2 = address::testing::established_address_2(); - let validator_1_earliest_vote_height = 100; - let validator_1_earliest_vote = ( - validator_1.clone(), - BlockHeight(validator_1_earliest_vote_height), - ); - let validator_2_earliest_vote_height = 200; - let validator_2_earliest_vote = ( - validator_2.clone(), - BlockHeight(validator_2_earliest_vote_height), - ); - let votes = [ - validator_1_earliest_vote.clone(), - ( - validator_1.clone(), - BlockHeight(validator_1_earliest_vote_height + 1), - ), - ( - validator_1, - BlockHeight(validator_1_earliest_vote_height + 100), - ), - validator_2_earliest_vote.clone(), - ( - validator_2.clone(), - BlockHeight(validator_2_earliest_vote_height + 1), - ), - ( - validator_2, - BlockHeight(validator_2_earliest_vote_height + 100), - ), - ]; - let signers = BTreeSet::from(votes); - - let deduped = dedupe(signers); - - assert_eq!( - deduped, - Votes::from([validator_1_earliest_vote, validator_2_earliest_vote]) - ); - } - - /// Test that voting on a tally during a single epoch does - /// not require any storage reads, and goes through the - /// fast path of the algorithm. - #[test] - fn test_tally_vote_single_epoch() { - let (_, dummy_validator_stake) = test_utils::default_validator(); - let (dummy_storage, _) = test_utils::setup_default_storage(); - - let aggregated = EpochedVotingPower::from([( - 0.into(), - FractionalVotingPower::HALF * dummy_validator_stake, - )]); - assert_eq!( - aggregated.fractional_stake::<_, _, GovStore<_>>(&dummy_storage), - FractionalVotingPower::HALF - ); - } - - /// Test that voting on a tally across epoch boundaries accounts - /// for the maximum voting power attained along those epochs. - #[test] - fn test_voting_across_epoch_boundaries() { - // the validators that will vote in the tally - let validator_1 = address::testing::established_address_1(); - let validator_1_stake = token::Amount::native_whole(100); - - let validator_2 = address::testing::established_address_2(); - let validator_2_stake = token::Amount::native_whole(100); - - let validator_3 = address::testing::established_address_3(); - let validator_3_stake = token::Amount::native_whole(100); - - let total_stake = - validator_1_stake + validator_2_stake + validator_3_stake; - - // start epoch 0 with validator 1 - let (mut state, _) = test_utils::setup_storage_with_validators( - HashMap::from([(validator_1.clone(), validator_1_stake)]), - ); - - // update the pos params - let params = OwnedPosParams { - pipeline_len: 1, - ..Default::default() - }; - write_pos_params(&mut state, ¶ms).expect("Test failed"); - - // insert validators 2 and 3 at epoch 1 - test_utils::append_validators_to_storage( - &mut state, - HashMap::from([ - (validator_2.clone(), validator_2_stake), - (validator_3.clone(), validator_3_stake), - ]), - ); - - // query validators to make sure they were inserted correctly - let query_validators = |epoch: u64| { - read_consensus_validator_set_addresses_with_stake( - &state, - epoch.into(), - ) - .unwrap() - .into_iter() - .map(|validator| (validator.address, validator.bonded_stake)) - .collect::>() - }; - let epoch_0_validators = query_validators(0); - let epoch_1_validators = query_validators(1); - assert_eq!( - epoch_0_validators, - HashMap::from([(validator_1.clone(), validator_1_stake)]) - ); - assert_eq!( - get_total_voting_power::<_, GovStore<_>>(&state, 0.into()), - validator_1_stake, - ); - assert_eq!( - epoch_1_validators, - HashMap::from([ - (validator_1, validator_1_stake), - (validator_2, validator_2_stake), - (validator_3, validator_3_stake), - ]) - ); - assert_eq!( - get_total_voting_power::<_, GovStore<_>>(&state, 1.into()), - total_stake, - ); - - // check that voting works as expected - let aggregated = EpochedVotingPower::from([ - (0.into(), FractionalVotingPower::ONE_THIRD * total_stake), - (1.into(), FractionalVotingPower::ONE_THIRD * total_stake), - ]); - assert_eq!( - aggregated.fractional_stake::<_, _, GovStore<_>>(&state), - FractionalVotingPower::TWO_THIRDS - ); - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/votes/storage.rs b/crates/ethereum_bridge/src/protocol/transactions/votes/storage.rs deleted file mode 100644 index a20675d58be..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/votes/storage.rs +++ /dev/null @@ -1,256 +0,0 @@ -use eyre::{Result, WrapErr}; -use namada_core::borsh::{BorshDeserialize, BorshSerialize}; -use namada_core::hints; -use namada_core::storage::Key; -use namada_core::voting_power::FractionalVotingPower; -use namada_state::{DB, DBIter, PrefixIter, StorageHasher, WlState}; -use namada_storage::{StorageRead, StorageWrite}; -use namada_systems::governance; - -use super::{EpochedVotingPower, EpochedVotingPowerExt, Tally, Votes}; -use crate::storage::vote_tallies; - -pub fn write( - state: &mut WlState, - keys: &vote_tallies::Keys, - body: &T, - tally: &Tally, - already_present: bool, -) -> Result<()> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - T: BorshSerialize, -{ - state.write(&keys.body(), body)?; - state.write(&keys.seen(), tally.seen)?; - state.write(&keys.seen_by(), tally.seen_by.clone())?; - state.write(&keys.voting_power(), tally.voting_power.clone())?; - if !already_present { - // add the current epoch for the inserted event - state.write( - &keys.voting_started_epoch(), - state.in_mem().get_current_epoch().0, - )?; - } - Ok(()) -} - -/// Delete a tally from storage, and return the associated value of -/// type `T` being voted on, in case it has accumulated more than 1/3 -/// of fractional voting power behind it. -#[must_use = "The storage value returned by this function must be used"] -pub fn delete( - state: &mut WlState, - keys: &vote_tallies::Keys, -) -> Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, - T: BorshDeserialize, -{ - let opt_body = { - let voting_power: EpochedVotingPower = - super::read::value(state, &keys.voting_power())?; - - if hints::unlikely( - voting_power.fractional_stake::(state) - > FractionalVotingPower::ONE_THIRD, - ) { - let body: T = super::read::value(state, &keys.body())?; - Some(body) - } else { - None - } - }; - state.delete(&keys.body())?; - state.delete(&keys.seen())?; - state.delete(&keys.seen_by())?; - state.delete(&keys.voting_power())?; - state.delete(&keys.voting_started_epoch())?; - Ok(opt_body) -} - -pub fn read( - state: &WlState, - keys: &vote_tallies::Keys, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let seen: bool = super::read::value(state, &keys.seen())?; - let seen_by: Votes = super::read::value(state, &keys.seen_by())?; - let voting_power: EpochedVotingPower = - super::read::value(state, &keys.voting_power())?; - - Ok(Tally { - voting_power, - seen_by, - seen, - }) -} - -pub fn iter_prefix<'a, D, H>( - state: &'a WlState, - prefix: &Key, -) -> Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - state - .iter_prefix(prefix) - .context("Failed to iterate over the given storage prefix") -} - -#[inline] -pub fn read_body( - state: &WlState, - keys: &vote_tallies::Keys, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - T: BorshDeserialize, -{ - super::read::value(state, &keys.body()) -} - -#[inline] -pub fn maybe_read_seen( - state: &WlState, - keys: &vote_tallies::Keys, -) -> Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - T: BorshDeserialize, -{ - super::read::maybe_value(state, &keys.seen()) -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use assert_matches::assert_matches; - use namada_core::ethereum_events::EthereumEvent; - - use super::*; - use crate::test_utils::{self, GovStore}; - - #[test] - fn test_delete_expired_tally() { - let (mut state, _) = test_utils::setup_default_storage(); - let (validator, validator_voting_power) = - test_utils::default_validator(); - - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![], - }; - let keys = vote_tallies::Keys::from(&event); - - // write some random ethereum event's tally to storage - // with >1/3 voting power behind it - let mut tally = Tally { - voting_power: EpochedVotingPower::from([( - 0.into(), - // store only half of the available voting power, - // which is >1/3 but <=2/3 - FractionalVotingPower::HALF * validator_voting_power, - )]), - seen_by: BTreeMap::from([(validator, 1.into())]), - seen: false, - }; - assert!(write(&mut state, &keys, &event, &tally, false).is_ok()); - - // delete the tally and check that the body is returned - let opt_body = - delete::<_, _, GovStore<_>, _>(&mut state, &keys).unwrap(); - assert_matches!(opt_body, Some(e) if e == event); - - // now, we write another tally, with <=1/3 voting power - tally.voting_power = - EpochedVotingPower::from([(0.into(), 1u64.into())]); - assert!(write(&mut state, &keys, &event, &tally, false).is_ok()); - - // delete the tally and check that no body is returned - let opt_body = - delete::<_, _, GovStore<_>, _>(&mut state, &keys).unwrap(); - assert_matches!(opt_body, None); - } - - #[test] - fn test_write_tally() { - let (mut state, _) = test_utils::setup_default_storage(); - let (validator, validator_voting_power) = - test_utils::default_validator(); - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![], - }; - let keys = vote_tallies::Keys::from(&event); - let tally = Tally { - voting_power: EpochedVotingPower::from([( - 0.into(), - validator_voting_power, - )]), - seen_by: BTreeMap::from([(validator, 10.into())]), - seen: false, - }; - - let result = write(&mut state, &keys, &event, &tally, false); - - assert!(result.is_ok()); - let body = state.read(&keys.body()).unwrap(); - assert_eq!(body, Some(event)); - let seen = state.read(&keys.seen()).unwrap(); - assert_eq!(seen, Some(tally.seen)); - let seen_by = state.read(&keys.seen_by()).unwrap(); - assert_eq!(seen_by, Some(tally.seen_by)); - let voting_power = state.read(&keys.voting_power()).unwrap(); - assert_eq!(voting_power, Some(tally.voting_power)); - let epoch = state.read(&keys.voting_started_epoch()).unwrap(); - assert_eq!(epoch, Some(state.in_mem().get_current_epoch().0)); - } - - #[test] - fn test_read_tally() { - let (mut state, _) = test_utils::setup_default_storage(); - let (validator, validator_voting_power) = - test_utils::default_validator(); - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![], - }; - let keys = vote_tallies::Keys::from(&event); - let tally = Tally { - voting_power: EpochedVotingPower::from([( - 0.into(), - validator_voting_power, - )]), - seen_by: BTreeMap::from([(validator, 10.into())]), - seen: false, - }; - state.write(&keys.body(), &event).unwrap(); - state.write(&keys.seen(), tally.seen).unwrap(); - state.write(&keys.seen_by(), &tally.seen_by).unwrap(); - state - .write(&keys.voting_power(), &tally.voting_power) - .unwrap(); - state - .write( - &keys.voting_started_epoch(), - state.in_mem().get_block_height().0, - ) - .unwrap(); - - let result = read(&state, &keys); - - assert!(result.is_ok()); - assert_eq!(result.unwrap(), tally); - } -} diff --git a/crates/ethereum_bridge/src/protocol/transactions/votes/update.rs b/crates/ethereum_bridge/src/protocol/transactions/votes/update.rs deleted file mode 100644 index 081288a2dbb..00000000000 --- a/crates/ethereum_bridge/src/protocol/transactions/votes/update.rs +++ /dev/null @@ -1,657 +0,0 @@ -use std::collections::BTreeSet; - -use borsh::BorshDeserialize; -use eyre::{Result, eyre}; -use namada_core::address::Address; -use namada_core::chain::BlockHeight; -use namada_core::collections::{HashMap, HashSet}; -use namada_core::token; -use namada_state::{DB, DBIter, StorageHasher, StorageRead, WlState}; -use namada_systems::governance; - -use super::{ChangedKeys, EpochedVotingPowerExt, Tally, Votes}; -use crate::storage::vote_tallies; - -/// Wraps all the information about new votes to be applied to some existing -/// tally in storage. -pub(in super::super) struct NewVotes { - inner: HashMap, -} - -impl NewVotes { - /// Constructs a new [`NewVotes`]. - /// - /// For all `votes` provided, a corresponding [`token::Amount`] must - /// be provided in `voting_powers` also, otherwise an error will be - /// returned. - pub fn new( - votes: Votes, - voting_powers: &HashMap<(Address, BlockHeight), token::Amount>, - ) -> Result { - let mut inner = HashMap::default(); - for vote in votes { - let voting_power = match voting_powers.get(&vote) { - Some(voting_power) => voting_power, - None => { - let (address, block_height) = vote; - return Err(eyre!( - "No voting power provided for vote by validator \ - {address} at block height {block_height}" - )); - } - }; - let (address, block_height) = vote; - _ = inner.insert(address, (block_height, voting_power.to_owned())); - } - Ok(Self { inner }) - } - - pub fn voters(&self) -> BTreeSet
{ - self.inner.keys().cloned().collect() - } - - /// Consumes `self` and returns a [`NewVotes`] with any addresses from - /// `voters` removed, as well as the set of addresses that were actually - /// removed. Useful for removing voters who have already voted for - /// something. - pub fn without_voters<'a>( - self, - voters: impl IntoIterator, - ) -> (Self, HashSet<&'a Address>) { - let mut inner = self.inner; - let mut removed = HashSet::default(); - for voter in voters { - if inner.swap_remove(voter).is_some() { - removed.insert(voter); - } - } - (Self { inner }, removed) - } -} - -impl IntoIterator for NewVotes { - type IntoIter = namada_core::collections::hash_set::IntoIter; - type Item = (Address, BlockHeight, token::Amount); - - fn into_iter(self) -> Self::IntoIter { - let items: HashSet<_> = self - .inner - .into_iter() - .map(|(address, (block_height, stake))| { - (address, block_height, stake) - }) - .collect(); - items.into_iter() - } -} - -/// Calculate an updated [`Tally`] based on one that is in storage under `keys`, -/// with new votes from `vote_info` applied, as well as the storage keys that -/// would change. If [`Tally`] is already `seen = true` in storage, then no -/// votes from `vote_info` should be applied, and the returned changed keys will -/// be empty. -pub(in super::super) fn calculate( - state: &mut WlState, - keys: &vote_tallies::Keys, - vote_info: NewVotes, -) -> Result<(Tally, ChangedKeys)> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, - T: BorshDeserialize, -{ - tracing::info!( - ?keys.prefix, - validators = ?vote_info.voters(), - "Calculating validators' votes applied to an existing tally" - ); - let tally_pre = super::storage::read(state, keys)?; - if tally_pre.seen { - return Ok((tally_pre, ChangedKeys::default())); - } - - let (vote_info, duplicate_voters) = - vote_info.without_voters(tally_pre.seen_by.keys()); - for voter in duplicate_voters { - tracing::info!( - ?keys.prefix, - ?voter, - "Ignoring duplicate voter" - ); - } - let tally_post = apply::(state, &tally_pre, vote_info) - .expect("We deduplicated voters already, so this should never error"); - - let changed_keys = keys_changed(keys, &tally_pre, &tally_post); - - if tally_post.seen { - tracing::info!( - ?keys.prefix, - "Tally has been seen by a quorum of validators", - ); - } else { - tracing::debug!( - ?keys.prefix, - "Tally is not yet seen by a quorum of validators", - ); - }; - - tracing::debug!( - ?tally_pre, - ?tally_post, - "Calculated and validated vote tracking updates", - ); - Ok((tally_post, changed_keys)) -} - -/// Takes an existing [`Tally`] and calculates the new [`Tally`] based on new -/// voters from `vote_info`. An error is returned if any validator which -/// previously voted is present in `vote_info`. -fn apply( - state: &WlState, - tally: &Tally, - vote_info: NewVotes, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - Gov: governance::Read>, -{ - // TODO(namada#1305): remove the clone here - let mut voting_power_post = tally.voting_power.clone(); - let mut seen_by_post = tally.seen_by.clone(); - for (validator, vote_height, voting_power) in vote_info { - if let Some(already_voted_height) = - seen_by_post.insert(validator.clone(), vote_height) - { - return Err(eyre!( - "Validator {validator} had already voted at height \ - {already_voted_height}", - )); - }; - let epoch = state - .get_epoch_at_height(vote_height) - .unwrap() - .expect("The queried epoch should be known"); - let aggregated = voting_power_post - .entry(epoch) - .or_insert_with(token::Amount::zero); - *aggregated = aggregated - .checked_add(voting_power) - .ok_or_else(|| eyre!("Aggregated voting power overflow"))?; - } - - let seen_post = voting_power_post.has_majority_quorum::(state); - - Ok(Tally { - voting_power: voting_power_post, - seen_by: seen_by_post, - seen: seen_post, - }) -} - -/// Straightforwardly calculates the keys that changed between `pre` and `post`. -fn keys_changed( - keys: &vote_tallies::Keys, - pre: &Tally, - post: &Tally, -) -> ChangedKeys { - let mut changed_keys = ChangedKeys::default(); - if pre.seen != post.seen { - changed_keys.insert(keys.seen()); - }; - if pre.voting_power != post.voting_power { - changed_keys.insert(keys.voting_power()); - }; - if pre.seen_by != post.seen_by { - changed_keys.insert(keys.seen_by()); - }; - changed_keys -} - -#[allow(clippy::arithmetic_side_effects)] -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use namada_core::address; - use namada_core::ethereum_events::EthereumEvent; - use namada_core::voting_power::FractionalVotingPower; - use namada_state::testing::TestState; - - use self::helpers::{TallyParams, default_event, default_total_stake}; - use super::*; - use crate::protocol::transactions::votes::{self, EpochedVotingPower}; - use crate::test_utils::{self, GovStore}; - - mod helpers { - use namada_proof_of_stake::storage::total_consensus_stake_handle; - use test_utils::GovStore; - - use super::*; - - /// Default amount of staked NAM to be used in tests. - pub(super) fn default_total_stake() -> token::Amount { - // 1000 NAM - token::Amount::native_whole(1_000) - } - - /// Returns an arbitrary piece of data that can have votes tallied - /// against it. - pub(super) fn default_event() -> EthereumEvent { - EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![], - } - } - - /// Parameters to construct a test [`Tally`]. - pub(super) struct TallyParams<'a> { - /// Handle to storage. - pub state: &'a mut TestState, - /// The event to be voted on. - pub event: &'a EthereumEvent, - /// Votes from the given validators at the given block height. - /// - /// The voting power of each validator is expressed as a fraction - /// of the provided `total_stake` parameter. - pub votes: HashSet<(Address, BlockHeight, token::Amount)>, - /// The [`token::Amount`] staked at epoch 0. - pub total_stake: token::Amount, - } - - impl TallyParams<'_> { - /// Write an initial [`Tally`] to storage. - pub(super) fn setup(self) -> Result { - let Self { - state, - event, - votes, - total_stake, - } = self; - - let keys = vote_tallies::Keys::from(event); - let seen_voting_power: token::Amount = votes - .iter() - .map(|(_, _, voting_power)| *voting_power) - .sum(); - let tally = Tally { - voting_power: get_epoched_voting_power(seen_voting_power), - seen_by: votes - .into_iter() - .map(|(addr, height, _)| (addr, height)) - .collect(), - seen: seen_voting_power - > FractionalVotingPower::TWO_THIRDS * total_stake, - }; - votes::storage::write(state, &keys, event, &tally, false)?; - total_consensus_stake_handle().set::<_, GovStore<_>>( - state, - total_stake, - 0u64.into(), - 0, - )?; - Ok(tally) - } - } - } - - #[test] - fn test_vote_info_new_empty() -> Result<()> { - let voting_powers = HashMap::default(); - - let vote_info = NewVotes::new(Votes::default(), &voting_powers)?; - - assert!(vote_info.voters().is_empty()); - assert_eq!(vote_info.into_iter().count(), 0); - Ok(()) - } - - #[test] - fn test_vote_info_new_single_voter() -> Result<()> { - let validator = address::testing::established_address_1(); - let vote_height = BlockHeight(100); - let voting_power = - FractionalVotingPower::ONE_THIRD * default_total_stake(); - let vote = (validator.clone(), vote_height); - let votes = Votes::from([vote.clone()]); - let voting_powers = HashMap::from([(vote, voting_power)]); - - let vote_info = NewVotes::new(votes, &voting_powers)?; - - assert_eq!(vote_info.voters(), BTreeSet::from([validator.clone()])); - let votes: BTreeSet<_> = vote_info.into_iter().collect(); - assert_eq!( - votes, - BTreeSet::from([(validator, vote_height, voting_power)]), - ); - Ok(()) - } - - #[test] - fn test_vote_info_new_error() -> Result<()> { - let votes = Votes::from([( - address::testing::established_address_1(), - BlockHeight(100), - )]); - let voting_powers = HashMap::default(); - - let result = NewVotes::new(votes, &voting_powers); - - assert!(result.is_err()); - Ok(()) - } - - #[test] - fn test_vote_info_without_voters() -> Result<()> { - let validator = address::testing::established_address_1(); - let vote_height = BlockHeight(100); - let voting_power = - FractionalVotingPower::ONE_THIRD * default_total_stake(); - let vote = (validator.clone(), vote_height); - let votes = Votes::from([vote.clone()]); - let voting_powers = HashMap::from([(vote, voting_power)]); - let vote_info = NewVotes::new(votes, &voting_powers)?; - - let (vote_info, removed) = vote_info.without_voters(vec![&validator]); - - assert!(vote_info.voters().is_empty()); - assert_eq!(removed, HashSet::from([&validator])); - Ok(()) - } - - #[test] - fn test_vote_info_remove_non_dupe() -> Result<()> { - let validator = address::testing::established_address_1(); - let new_validator = address::testing::established_address_2(); - let vote_height = BlockHeight(100); - let voting_power = - FractionalVotingPower::ONE_THIRD * default_total_stake(); - let vote = (validator.clone(), vote_height); - let votes = Votes::from([vote.clone()]); - let voting_powers = HashMap::from([(vote, voting_power)]); - let vote_info = NewVotes::new(votes, &voting_powers)?; - - let (vote_info, removed) = - vote_info.without_voters(vec![&new_validator]); - - assert!(removed.is_empty()); - assert_eq!(vote_info.voters(), BTreeSet::from([validator])); - Ok(()) - } - - #[test] - fn test_apply_duplicate_votes() -> Result<()> { - let mut state = TestState::default(); - test_utils::init_default_storage(&mut state); - - let validator = address::testing::established_address_1(); - let already_voted_height = BlockHeight(100); - - let event = default_event(); - let tally_pre = TallyParams { - total_stake: default_total_stake(), - state: &mut state, - event: &event, - votes: HashSet::from([( - validator.clone(), - already_voted_height, - FractionalVotingPower::ONE_THIRD * default_total_stake(), - )]), - } - .setup()?; - - let votes = Votes::from([(validator.clone(), BlockHeight(1000))]); - let voting_powers = HashMap::from([( - (validator, BlockHeight(1000)), - FractionalVotingPower::ONE_THIRD * default_total_stake(), - )]); - let vote_info = NewVotes::new(votes, &voting_powers)?; - - let result = apply::<_, _, GovStore<_>>(&state, &tally_pre, vote_info); - - assert!(result.is_err()); - Ok(()) - } - - /// Tests that an unchanged tally is returned if the tally as in storage is - /// already recorded as having been seen. - #[test] - fn test_calculate_already_seen() -> Result<()> { - let mut state = TestState::default(); - test_utils::init_default_storage(&mut state); - let event = default_event(); - let keys = vote_tallies::Keys::from(&event); - let tally_pre = TallyParams { - total_stake: default_total_stake(), - state: &mut state, - event: &event, - votes: HashSet::from([( - address::testing::established_address_1(), - BlockHeight(10), - // this is > 2/3 - FractionalVotingPower::new_u64(3, 4)? * default_total_stake(), - )]), - } - .setup()?; - - let validator = address::testing::established_address_2(); - let vote_height = BlockHeight(100); - let voting_power = - FractionalVotingPower::new_u64(1, 4)? * default_total_stake(); - let vote = (validator, vote_height); - let votes = Votes::from([vote.clone()]); - let voting_powers = HashMap::from([(vote, voting_power)]); - let vote_info = NewVotes::new(votes, &voting_powers)?; - - let (tally_post, changed_keys) = - calculate::<_, _, GovStore<_>, _>(&mut state, &keys, vote_info)?; - - assert_eq!(tally_post, tally_pre); - assert!(changed_keys.is_empty()); - Ok(()) - } - - /// Tests that an unchanged tally is returned if no votes are passed. - #[test] - fn test_calculate_empty() -> Result<()> { - let (mut state, _) = test_utils::setup_default_storage(); - let event = default_event(); - let keys = vote_tallies::Keys::from(&event); - let tally_pre = TallyParams { - total_stake: default_total_stake(), - state: &mut state, - event: &event, - votes: HashSet::from([( - address::testing::established_address_1(), - BlockHeight(10), - FractionalVotingPower::ONE_THIRD * default_total_stake(), - )]), - } - .setup()?; - let vote_info = NewVotes::new(Votes::default(), &HashMap::default())?; - - let (tally_post, changed_keys) = - calculate::<_, _, GovStore<_>, _>(&mut state, &keys, vote_info)?; - - assert_eq!(tally_post, tally_pre); - assert!(changed_keys.is_empty()); - Ok(()) - } - - /// Tests the case where a single vote is applied, and the tally is still - /// not yet seen. - #[test] - fn test_calculate_one_vote_not_seen() -> Result<()> { - let (mut state, _) = test_utils::setup_default_storage(); - - let event = default_event(); - let keys = vote_tallies::Keys::from(&event); - let _tally_pre = TallyParams { - total_stake: default_total_stake(), - state: &mut state, - event: &event, - votes: HashSet::from([( - address::testing::established_address_1(), - BlockHeight(10), - FractionalVotingPower::ONE_THIRD * default_total_stake(), - )]), - } - .setup()?; - - let validator = address::testing::established_address_2(); - let vote_height = BlockHeight(100); - let voting_power = - FractionalVotingPower::ONE_THIRD * default_total_stake(); - let vote = (validator, vote_height); - let votes = Votes::from([vote.clone()]); - let voting_powers = HashMap::from([(vote.clone(), voting_power)]); - let vote_info = NewVotes::new(votes, &voting_powers)?; - - let (tally_post, changed_keys) = - calculate::<_, _, GovStore<_>, _>(&mut state, &keys, vote_info)?; - - assert_eq!( - tally_post, - Tally { - voting_power: get_epoched_voting_power( - FractionalVotingPower::TWO_THIRDS * default_total_stake(), - ), - seen_by: BTreeMap::from([ - (address::testing::established_address_1(), 10.into()), - vote, - ]), - seen: false, - } - ); - assert_eq!( - changed_keys, - BTreeSet::from([keys.voting_power(), keys.seen_by()]) - ); - Ok(()) - } - - /// Tests the case where a single vote is applied, and the tally is now - /// seen. - #[test] - fn test_calculate_one_vote_seen() -> Result<()> { - let (mut state, _) = test_utils::setup_default_storage(); - - let first_vote_stake = - FractionalVotingPower::ONE_THIRD * default_total_stake(); - let second_vote_stake = - FractionalVotingPower::ONE_THIRD * default_total_stake(); - let total_stake = first_vote_stake + second_vote_stake; - - let event = default_event(); - let keys = vote_tallies::Keys::from(&event); - let _tally_pre = TallyParams { - total_stake, - state: &mut state, - event: &event, - votes: HashSet::from([( - address::testing::established_address_1(), - BlockHeight(10), - first_vote_stake, - )]), - } - .setup()?; - - let validator = address::testing::established_address_2(); - let vote_height = BlockHeight(100); - let vote = (validator, vote_height); - let votes = Votes::from([vote.clone()]); - let voting_powers = HashMap::from([(vote.clone(), second_vote_stake)]); - let vote_info = NewVotes::new(votes, &voting_powers)?; - - let (tally_post, changed_keys) = - calculate::<_, _, GovStore<_>, _>(&mut state, &keys, vote_info)?; - - assert_eq!( - tally_post, - Tally { - voting_power: get_epoched_voting_power(total_stake), - seen_by: BTreeMap::from([ - (address::testing::established_address_1(), 10.into()), - vote, - ]), - seen: true, - } - ); - assert_eq!( - changed_keys, - BTreeSet::from([keys.voting_power(), keys.seen_by(), keys.seen()]) - ); - Ok(()) - } - - #[test] - fn test_keys_changed_all() -> Result<()> { - let voting_power_a = - FractionalVotingPower::ONE_THIRD * default_total_stake(); - let voting_power_b = - FractionalVotingPower::TWO_THIRDS * default_total_stake(); - - let seen_a = false; - let seen_b = true; - - let seen_by_a = BTreeMap::from([( - address::testing::established_address_1(), - BlockHeight(10), - )]); - let seen_by_b = BTreeMap::from([( - address::testing::established_address_2(), - BlockHeight(20), - )]); - - let event = default_event(); - let keys = vote_tallies::Keys::from(&event); - let pre = Tally { - voting_power: get_epoched_voting_power(voting_power_a), - seen: seen_a, - seen_by: seen_by_a, - }; - let post = Tally { - voting_power: get_epoched_voting_power(voting_power_b), - seen: seen_b, - seen_by: seen_by_b, - }; - let changed_keys = keys_changed(&keys, &pre, &post); - - assert_eq!( - changed_keys, - BTreeSet::from([keys.seen(), keys.seen_by(), keys.voting_power()]) - ); - Ok(()) - } - - #[test] - fn test_keys_changed_none() -> Result<()> { - let seen = false; - let seen_by = BTreeMap::from([( - address::testing::established_address_1(), - BlockHeight(10), - )]); - - let event = default_event(); - let keys = vote_tallies::Keys::from(&event); - let pre = Tally { - voting_power: get_epoched_voting_power( - FractionalVotingPower::ONE_THIRD * default_total_stake(), - ), - seen, - seen_by, - }; - #[allow(clippy::redundant_clone)] - let post = pre.clone(); - let changed_keys = keys_changed(&keys, &pre, &post); - - assert!(changed_keys.is_empty()); - Ok(()) - } - - fn get_epoched_voting_power(thus_far: token::Amount) -> EpochedVotingPower { - EpochedVotingPower::from([(0.into(), thus_far)]) - } -} diff --git a/crates/ethereum_bridge/src/protocol/validation.rs b/crates/ethereum_bridge/src/protocol/validation.rs deleted file mode 100644 index 3d8242dde07..00000000000 --- a/crates/ethereum_bridge/src/protocol/validation.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Validation logic for Ethereum bridge protocol actions. - -pub mod bridge_pool_roots; -pub mod ethereum_events; -pub mod validator_set_update; - -use thiserror::Error; - -/// The error yielded from validating faulty vote extensions. -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum VoteExtensionError { - #[error( - "A validator set update proof is already available in storage for the \ - given epoch" - )] - ValsetUpdProofAvailable, - #[error("The nonce in the Ethereum event is invalid")] - InvalidEthEventNonce, - #[error("The vote extension was issued for an unexpected block height")] - UnexpectedBlockHeight, - #[error("The vote extension was issued for an unexpected epoch")] - UnexpectedEpoch, - #[error( - "The vote extension contains duplicate or non-sorted Ethereum events" - )] - HaveDupesOrNonSorted, - #[error( - "The public key of the vote extension's associated validator could \ - not be found in storage" - )] - PubKeyNotInStorage, - #[error("The vote extension's signature is invalid")] - VerifySigFailed, - #[error( - "Validator is missing from an expected field in the vote extension" - )] - ValidatorMissingFromExtension, - #[error( - "Vote extension provides a superset of the available validators in \ - storage" - )] - ExtraValidatorsInExtension, - #[error( - "Found value for a field in the vote extension diverging from the \ - equivalent field in storage" - )] - DivergesFromStorage, - #[error("The signature of the Bridge pool root is invalid")] - InvalidBPRootSig, - #[error( - "Received a vote extension for the Ethereum bridge which is currently \ - not active" - )] - EthereumBridgeInactive, -} diff --git a/crates/ethereum_bridge/src/protocol/validation/bridge_pool_roots.rs b/crates/ethereum_bridge/src/protocol/validation/bridge_pool_roots.rs deleted file mode 100644 index 0ba794c96a0..00000000000 --- a/crates/ethereum_bridge/src/protocol/validation/bridge_pool_roots.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! Bridge pool roots validation. - -use namada_core::chain::BlockHeight; -use namada_core::keccak::keccak_hash; -use namada_proof_of_stake::queries::{ - get_validator_eth_hot_key, get_validator_protocol_key, -}; -use namada_state::{DB, DBIter, StorageHasher, StorageRead, WlState}; -use namada_systems::governance; -use namada_tx::{SignableEthMessage, Signed}; -use namada_vote_ext::bridge_pool_roots; - -use super::VoteExtensionError; -use crate::storage::eth_bridge_queries::EthBridgeQueries; - -/// Validates a vote extension issued at the provided -/// block height signing over the latest Ethereum bridge -/// pool root and nonce. -/// -/// Checks that at epoch of the provided height: -/// * The inner Namada address corresponds to a consensus validator. -/// * Check that the root and nonce are correct. -/// * The validator correctly signed the extension. -/// * The validator signed over the correct height inside of the extension. -/// * Check that the inner signature is valid. -pub fn validate_bp_roots_vext( - state: &WlState, - ext: &Signed, - last_height: BlockHeight, -) -> Result<(), VoteExtensionError> -where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, - Gov: governance::Read>, -{ - // NOTE: for ABCI++, we should pass - // `last_height` here, instead of `ext.data.block_height` - let ext_height_epoch = - match state.get_epoch_at_height(ext.data.block_height).unwrap() { - Some(epoch) => epoch, - _ => { - tracing::debug!( - block_height = ?ext.data.block_height, - "The epoch of the Bridge pool root's vote extension's \ - block height should always be known", - ); - return Err(VoteExtensionError::UnexpectedEpoch); - } - }; - if !state - .ethbridge_queries() - .is_bridge_active_at(ext_height_epoch) - { - tracing::debug!( - vext_epoch = ?ext_height_epoch, - "The Ethereum bridge was not enabled when the pool - root's vote extension was cast", - ); - return Err(VoteExtensionError::EthereumBridgeInactive); - } - - if ext.data.block_height > last_height { - tracing::debug!( - ext_height = ?ext.data.block_height, - ?last_height, - "Bridge pool root's vote extension issued for a block height \ - higher than the chain's last height." - ); - return Err(VoteExtensionError::UnexpectedBlockHeight); - } - if ext.data.block_height.0 == 0 { - tracing::debug!("Dropping vote extension issued at genesis"); - return Err(VoteExtensionError::UnexpectedBlockHeight); - } - - // get the public key associated with this validator - let validator = &ext.data.validator_addr; - let pk = get_validator_protocol_key::<_, Gov>( - state, - validator, - ext_height_epoch, - ) - .ok() - .flatten() - .ok_or_else(|| { - tracing::debug!( - %validator, - "Could not get public key from Storage for some validator, \ - while validating Bridge pool root's vote extension" - ); - VoteExtensionError::PubKeyNotInStorage - })?; - // verify the signature of the vote extension - ext.verify(&pk).map_err(|err| { - tracing::debug!( - ?err, - ?ext.sig, - ?pk, - %validator, - "Failed to verify the signature of an Bridge pool root's vote \ - extension issued by some validator" - ); - VoteExtensionError::VerifySigFailed - })?; - - let bp_root = state - .ethbridge_queries() - .get_bridge_pool_root_at_height(ext.data.block_height) - .expect("We asserted that the queried height is correct") - .0; - let nonce = state - .ethbridge_queries() - .get_bridge_pool_nonce_at_height(ext.data.block_height) - .to_bytes(); - let signed = Signed::<_, SignableEthMessage>::new_from( - keccak_hash([bp_root, nonce].concat()), - ext.data.sig.clone(), - ); - let pk = - get_validator_eth_hot_key::<_, Gov>(state, validator, ext_height_epoch) - .ok() - .flatten() - .expect("A validator should have an Ethereum hot key in storage."); - signed.verify(&pk).map_err(|err| { - tracing::debug!( - ?err, - ?signed.sig, - ?pk, - %validator, - "Failed to verify the signature of an Bridge pool root \ - issued by some validator." - ); - VoteExtensionError::InvalidBPRootSig - })?; - Ok(()) -} diff --git a/crates/ethereum_bridge/src/protocol/validation/ethereum_events.rs b/crates/ethereum_bridge/src/protocol/validation/ethereum_events.rs deleted file mode 100644 index 64a81f63bca..00000000000 --- a/crates/ethereum_bridge/src/protocol/validation/ethereum_events.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! Ethereum events validation. - -use namada_core::chain::BlockHeight; -use namada_proof_of_stake::queries::get_validator_protocol_key; -use namada_state::{DB, DBIter, StorageHasher, StorageRead, WlState}; -use namada_systems::governance; -use namada_tx::Signed; -use namada_vote_ext::ethereum_events; - -use super::VoteExtensionError; -use crate::storage::eth_bridge_queries::EthBridgeQueries; - -/// Validates an Ethereum events vote extension issued at the provided -/// block height. -/// -/// Checks that at epoch of the provided height: -/// * The inner Namada address corresponds to a consensus validator. -/// * The validator correctly signed the extension. -/// * The validator signed over the correct height inside of the extension. -/// * There are no duplicate Ethereum events in this vote extension, and the -/// events are sorted in ascending order. -pub fn validate_eth_events_vext( - state: &WlState, - ext: &Signed, - last_height: BlockHeight, -) -> Result<(), VoteExtensionError> -where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, - Gov: governance::Read>, -{ - // NOTE: for ABCI++, we should pass - // `last_height` here, instead of `ext.data.block_height` - let ext_height_epoch = - match state.get_epoch_at_height(ext.data.block_height).unwrap() { - Some(epoch) => epoch, - _ => { - tracing::debug!( - block_height = ?ext.data.block_height, - "The epoch of the Ethereum events vote extension's \ - block height should always be known", - ); - return Err(VoteExtensionError::UnexpectedEpoch); - } - }; - if !state - .ethbridge_queries() - .is_bridge_active_at(ext_height_epoch) - { - tracing::debug!( - vext_epoch = ?ext_height_epoch, - "The Ethereum bridge was not enabled when the Ethereum - events' vote extension was cast", - ); - return Err(VoteExtensionError::EthereumBridgeInactive); - } - if ext.data.block_height > last_height { - tracing::debug!( - ext_height = ?ext.data.block_height, - ?last_height, - "Ethereum events vote extension issued for a block height \ - higher than the chain's last height." - ); - return Err(VoteExtensionError::UnexpectedBlockHeight); - } - if ext.data.block_height.0 == 0 { - tracing::debug!("Dropping vote extension issued at genesis"); - return Err(VoteExtensionError::UnexpectedBlockHeight); - } - validate_eth_events(state, &ext.data)?; - // get the public key associated with this validator - let validator = &ext.data.validator_addr; - let pk = get_validator_protocol_key::<_, Gov>( - state, - validator, - ext_height_epoch, - ) - .ok() - .flatten() - .ok_or_else(|| { - tracing::debug!( - %validator, - "Could not get public key from Storage for some validator, \ - while validating Ethereum events vote extension" - ); - VoteExtensionError::PubKeyNotInStorage - })?; - // verify the signature of the vote extension - ext.verify(&pk).map_err(|err| { - tracing::debug!( - ?err, - ?ext.sig, - ?pk, - %validator, - "Failed to verify the signature of an Ethereum events vote \ - extension issued by some validator" - ); - VoteExtensionError::VerifySigFailed - })?; - Ok(()) -} - -/// Validate a batch of Ethereum events contained in -/// an [`ethereum_events::Vext`]. -/// -/// The supplied Ethereum events must be ordered in -/// ascending ordering, must not contain any dupes -/// and must have valid nonces. -fn validate_eth_events( - state: &WlState, - ext: ðereum_events::Vext, -) -> Result<(), VoteExtensionError> -where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, -{ - // verify if we have any duplicate Ethereum events, - // and if these are sorted in ascending order - let have_dupes_or_non_sorted = { - !ext.ethereum_events - // TODO(rust-lang/rust#75027): move to `array_windows` when it - // reaches Rust stable - .windows(2) - .all(|evs| evs[0] < evs[1]) - }; - let validator = &ext.validator_addr; - if have_dupes_or_non_sorted { - tracing::debug!( - %validator, - "Found duplicate or non-sorted Ethereum events in a vote extension from \ - some validator" - ); - return Err(VoteExtensionError::HaveDupesOrNonSorted); - } - // for the proposal to be valid, at least one of the - // event's nonces must be valid - if ext - .ethereum_events - .iter() - .any(|event| state.ethbridge_queries().validate_eth_event_nonce(event)) - { - Ok(()) - } else { - Err(VoteExtensionError::InvalidEthEventNonce) - } -} diff --git a/crates/ethereum_bridge/src/protocol/validation/validator_set_update.rs b/crates/ethereum_bridge/src/protocol/validation/validator_set_update.rs deleted file mode 100644 index 99c5d8370a5..00000000000 --- a/crates/ethereum_bridge/src/protocol/validation/validator_set_update.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! Validator set update validation. - -use namada_core::chain::Epoch; -use namada_proof_of_stake::queries::get_validator_eth_hot_key; -use namada_state::{DB, DBIter, StorageHasher, WlState}; -use namada_systems::governance; -use namada_vote_ext::validator_set_update; - -use super::VoteExtensionError; -use crate::storage::eth_bridge_queries::{ - EthBridgeQueries, is_bridge_comptime_enabled, -}; - -/// Validates a validator set update vote extension issued at the -/// epoch provided as an argument. -/// -/// # Validation checks -/// -/// To validate a [`validator_set_update::SignedVext`], Namada nodes -/// check if: -/// -/// * The signing validator is a consensus validator during the epoch -/// `signing_epoch` inside the extension. -/// * A validator set update proof is not available yet for `signing_epoch`. -/// * The validator correctly signed the extension, with its Ethereum hot key. -/// * The validator signed over the epoch inside of the extension, whose value -/// should not be greater than `last_epoch`. -/// * The voting powers in the vote extension correspond to the voting powers -/// of the validators of `signing_epoch + 1`. -/// * The voting powers signed over were Ethereum ABI encoded, normalized to -/// `2^32`, and sorted in descending order. -pub fn validate_valset_upd_vext( - state: &WlState, - ext: &validator_set_update::SignedVext, - last_epoch: Epoch, -) -> Result<(), VoteExtensionError> -where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, - Gov: governance::Read>, -{ - let signing_epoch = ext.data.signing_epoch; - if !is_bridge_comptime_enabled() { - tracing::debug!( - vext_epoch = ?signing_epoch, - "The Ethereum bridge was not enabled when the validator set \ - update's vote extension was cast", - ); - return Err(VoteExtensionError::EthereumBridgeInactive); - } - if state.in_mem().last_block.is_none() { - tracing::debug!( - "Dropping validator set update vote extension issued at genesis" - ); - return Err(VoteExtensionError::UnexpectedBlockHeight); - } - if signing_epoch > last_epoch { - tracing::debug!( - vext_epoch = ?signing_epoch, - ?last_epoch, - "Validator set update vote extension issued for an epoch \ - greater than the last one.", - ); - return Err(VoteExtensionError::UnexpectedEpoch); - } - if state - .ethbridge_queries() - .valset_upd_seen(signing_epoch.next()) - { - let err = VoteExtensionError::ValsetUpdProofAvailable; - tracing::debug!( - proof_epoch = ?signing_epoch.next(), - "{err}" - ); - return Err(err); - } - // verify if the new epoch validators' voting powers in storage match - // the voting powers in the vote extension - let mut no_local_consensus_eth_addresses = 0; - for (eth_addr_book, namada_addr, namada_power) in - state - .ethbridge_queries() - .get_consensus_eth_addresses::(signing_epoch.next()) - { - let &ext_power = match ext.data.voting_powers.get(ð_addr_book) { - Some(voting_power) => voting_power, - _ => { - tracing::debug!( - ?eth_addr_book, - "Could not find expected Ethereum addresses in valset upd \ - vote extension", - ); - return Err(VoteExtensionError::ValidatorMissingFromExtension); - } - }; - if namada_power != ext_power { - tracing::debug!( - validator = %namada_addr, - expected = ?namada_power, - got = ?ext_power, - "Found unexpected voting power value in valset upd vote extension", - ); - return Err(VoteExtensionError::DivergesFromStorage); - } - // At most the number of consensus validator addresses - cannot overflow - #[allow(clippy::arithmetic_side_effects)] - { - no_local_consensus_eth_addresses += 1; - } - } - if no_local_consensus_eth_addresses != ext.data.voting_powers.len() { - tracing::debug!( - no_ext_consensus_eth_addresses = ext.data.voting_powers.len(), - no_local_consensus_eth_addresses, - "Superset of the next validator set was included in the validator \ - set update vote extension", - ); - return Err(VoteExtensionError::ExtraValidatorsInExtension); - } - // get the public key associated with this validator - let validator = &ext.data.validator_addr; - let pk = get_validator_eth_hot_key::<_, Gov>( - state, - validator, - signing_epoch, - ) - .ok() - .flatten() - .ok_or_else(|| { - tracing::debug!( - %validator, - "Could not get Ethereum hot key from Storage for some validator, \ - while validating valset upd vote extension" - ); - VoteExtensionError::PubKeyNotInStorage - })?; - // verify the signature of the vote extension - ext.verify(&pk).map_err(|err| { - tracing::debug!( - ?err, - ?ext.sig, - ?pk, - %validator, - "Failed to verify the signature of a valset upd vote \ - extension issued by some validator" - ); - VoteExtensionError::VerifySigFailed - })?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use namada_core::ethereum_events::EthAddress; - use namada_core::key::{RefTo, common}; - use namada_vote_ext::validator_set_update::{EthAddrBook, VotingPowersMap}; - - use super::*; - use crate::storage::eth_bridge_queries::is_bridge_comptime_enabled; - use crate::test_utils::{self, GovStore}; - - /// Test that we reject vote extensions containing a superset of the - /// next validator set in storage. - #[test] - fn test_superset_valsetupd_rejected() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - - let (state, keys) = test_utils::setup_default_storage(); - let (validator, validator_stake) = test_utils::default_validator(); - - let hot_key_addr = { - let hot_key = &keys - .get(&validator) - .expect("Test failed") - .eth_bridge - .ref_to(); - - match hot_key { - common::PublicKey::Secp256k1(k) => k.into(), - _ => panic!("Test failed"), - } - }; - let cold_key_addr = { - let cold_key = - &keys.get(&validator).expect("Test failed").eth_gov.ref_to(); - - match cold_key { - common::PublicKey::Secp256k1(k) => k.into(), - _ => panic!("Test failed"), - } - }; - - let voting_powers = { - let mut map = VotingPowersMap::new(); - map.insert( - EthAddrBook { - hot_key_addr, - cold_key_addr, - }, - validator_stake, - ); - map.insert( - EthAddrBook { - hot_key_addr: EthAddress([0; 20]), - cold_key_addr: EthAddress([0xff; 20]), - }, - validator_stake, - ); - map - }; - - let ext = validator_set_update::Vext { - voting_powers, - signing_epoch: 0.into(), - validator_addr: validator.clone(), - } - .sign(&keys.get(&validator).expect("Test failed").eth_bridge); - - let result = validate_valset_upd_vext::<_, _, GovStore<_>>( - &state, - &ext, - 0.into(), - ); - assert_matches!( - result, - Err(VoteExtensionError::ExtraValidatorsInExtension) - ); - } -} diff --git a/crates/ethereum_bridge/src/storage/bridge_pool.rs b/crates/ethereum_bridge/src/storage/bridge_pool.rs deleted file mode 100644 index 88f4263382f..00000000000 --- a/crates/ethereum_bridge/src/storage/bridge_pool.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Tools for accessing the storage subspaces of the Ethereum -//! bridge pool - -use namada_core::eth_bridge_pool::Segments; -pub use namada_core::eth_bridge_pool::{ - BRIDGE_POOL_ADDRESS, get_key_from_hash, get_pending_key, - is_pending_transfer_key, -}; -use namada_core::storage::{DbKeySeg, Key}; -pub use namada_state::merkle_tree::eth_bridge_pool::BridgePoolTree; - -/// Get the storage key for the root of the Merkle tree -/// containing the transfers in the pool -pub fn get_signed_root_key() -> Key { - Key { - segments: vec![ - DbKeySeg::AddressSeg(BRIDGE_POOL_ADDRESS), - DbKeySeg::StringSeg(Segments::VALUES.signed_root.into()), - ], - } -} - -/// Get the storage key for the batch nonce of -/// the bridge pool. Used for replay protection. -pub fn get_nonce_key() -> Key { - Key { - segments: vec![ - DbKeySeg::AddressSeg(BRIDGE_POOL_ADDRESS), - DbKeySeg::StringSeg(Segments::VALUES.bridge_pool_nonce.into()), - ], - } -} - -/// Check if a key belongs to the bridge pools sub-storage -pub fn is_bridge_pool_key(key: &Key) -> bool { - matches!(&key.segments[0], DbKeySeg::AddressSeg(addr) if addr == &BRIDGE_POOL_ADDRESS) -} diff --git a/crates/ethereum_bridge/src/storage/eth_bridge_queries.rs b/crates/ethereum_bridge/src/storage/eth_bridge_queries.rs deleted file mode 100644 index 660f2f1e7d1..00000000000 --- a/crates/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ /dev/null @@ -1,657 +0,0 @@ -//! Storage queries for ethereum bridge. - -use borsh::{BorshDeserialize, BorshSerialize}; -use namada_core::address::Address; -use namada_core::chain::{BlockHeight, Epoch}; -use namada_core::eth_abi::Encode; -use namada_core::eth_bridge_pool::PendingTransfer; -use namada_core::ethereum_events::{ - EthAddress, EthereumEvent, GetEventNonce, TransferToEthereum, Uint, -}; -use namada_core::keccak::KeccakHash; -use namada_core::storage::Key as StorageKey; -use namada_core::voting_power::{EthBridgeVotingPower, FractionalVotingPower}; -use namada_core::{hints, token}; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_proof_of_stake::queries::get_total_voting_power; -use namada_proof_of_stake::storage::{ - read_consensus_validator_set_addresses_with_stake, read_pos_params, - validator_eth_cold_key_handle, validator_eth_hot_key_handle, -}; -use namada_state::{DB, DBIter, StorageHasher, StoreType, WlState}; -use namada_storage::StorageRead; -use namada_systems::governance; -use namada_vote_ext::validator_set_update::{ - EthAddrBook, ValidatorSetArgs, VotingPowersMap, VotingPowersMapExt, -}; - -use crate::storage::proof::BridgePoolRootProof; -use crate::storage::{active_key, bridge_pool, vote_tallies, whitelist}; - -/// Check if the Ethereum Bridge has been enabled at compile time. -pub const fn is_bridge_comptime_enabled() -> bool { - cfg!(feature = "namada-eth-bridge") -} - -/// Check if the bridge is disabled, enabled, or scheduled to be -/// enabled at a specified [`Epoch`]. -pub fn check_bridge_status( - storage: &S, -) -> namada_storage::Result { - #[cfg(not(test))] - if !is_bridge_comptime_enabled() { - return Ok(EthBridgeStatus::Disabled); - } - let status = storage - .read(&active_key())? - .expect("The Ethereum bridge active key should be in storage"); - Ok(status) -} - -/// Returns a boolean indicating whether the bridge is -/// currently active at the specified [`Epoch`]. -pub fn is_bridge_active_at( - storage: &S, - queried_epoch: Epoch, -) -> namada_storage::Result { - Ok(match check_bridge_status(storage)? { - EthBridgeStatus::Disabled => false, - EthBridgeStatus::Enabled(EthBridgeEnabled::AtGenesis) => true, - EthBridgeStatus::Enabled(EthBridgeEnabled::AtEpoch(enabled_epoch)) => { - queried_epoch >= enabled_epoch - } - }) -} - -/// This enum is used as a parameter to -/// [`EthBridgeQueriesHook::must_send_valset_upd`]. -pub enum SendValsetUpd { - /// Check if it is possible to send a validator set update - /// vote extension at the current block height. - Now, - /// Check if it is possible to send a validator set update - /// vote extension at the previous block height. - AtPrevHeight, -} - -#[derive( - Debug, - Clone, - BorshDeserialize, - BorshDeserializer, - BorshSerialize, - PartialEq, - Eq, - Hash, - Ord, - PartialOrd, -)] -/// An enum indicating if the Ethereum bridge is enabled. -pub enum EthBridgeStatus { - /// The bridge is disabled - Disabled, - /// The bridge is enabled - Enabled(EthBridgeEnabled), -} - -#[derive( - Debug, - Clone, - BorshDeserialize, - BorshDeserializer, - BorshSerialize, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, -)] -/// Enum indicating if the bridge was initialized at genesis -/// or a later epoch. -pub enum EthBridgeEnabled { - /// Bridge is enabled from genesis - AtGenesis, - /// Bridge is enabled from this epoch - /// onwards. a validator set proof must - /// exist for this epoch. - AtEpoch(Epoch), -} - -/// Methods used to query blockchain Ethereum bridge related state. -pub trait EthBridgeQueries { - /// The underlying storage type. - type Storage; - - /// Return a handle to [`EthBridgeQueries`]. - fn ethbridge_queries(&self) -> EthBridgeQueriesHook<'_, Self::Storage>; -} - -impl EthBridgeQueries for WlState -where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, -{ - type Storage = Self; - - #[inline] - fn ethbridge_queries(&self) -> EthBridgeQueriesHook<'_, Self> { - EthBridgeQueriesHook { state: self } - } -} - -/// A handle to [`EthBridgeQueries`]. -/// -/// This type is a wrapper around a pointer to a [`WlState`]. -#[derive(Debug)] -#[repr(transparent)] -pub struct EthBridgeQueriesHook<'db, S> { - state: &'db S, -} - -impl Clone for EthBridgeQueriesHook<'_, S> { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for EthBridgeQueriesHook<'_, S> {} - -impl<'db, D, H> EthBridgeQueriesHook<'db, WlState> -where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, -{ - /// Return a handle to the inner [`WlState`]. - #[inline] - pub fn state(self) -> &'db WlState { - self.state - } - - /// Check if a validator set update proof is available for - /// the given [`Epoch`]. - pub fn valset_upd_seen(self, epoch: Epoch) -> bool { - if hints::unlikely(epoch.0 == 0) { - unreachable!( - "There are no validator set update proofs for the first epoch" - ); - } - let valset_upd_keys = vote_tallies::Keys::from(&epoch); - self.state - .read(&valset_upd_keys.seen()) - .expect("Reading a value from storage should not fail") - .unwrap_or(false) - } - - /// Check if the bridge is disabled, enabled, or - /// scheduled to be enabled at a specified epoch. - #[inline] - pub fn check_bridge_status(self) -> EthBridgeStatus { - check_bridge_status(self.state).expect( - "Failed to read Ethereum bridge activation status from storage", - ) - } - - /// Returns a boolean indicating whether the bridge is - /// currently active. - #[inline] - pub fn is_bridge_active(self) -> bool { - is_bridge_active_at( - self.state, - self.state.in_mem().get_current_epoch().0, - ) - .expect("Failed to read Ethereum bridge activation status from storage") - } - - /// Behaves exactly like [`Self::is_bridge_active`], but performs - /// the check at the given [`Epoch`]. - #[inline] - pub fn is_bridge_active_at(self, queried_epoch: Epoch) -> bool { - is_bridge_active_at(self.state, queried_epoch).expect( - "Failed to read Ethereum bridge activation status from storage", - ) - } - - /// Get the nonce of the next transfers to Namada event to be processed. - pub fn get_next_nam_transfers_nonce(self) -> Uint { - self.state - .in_mem() - .eth_events_queue - .transfers_to_namada - .get_event_nonce() - } - - /// Get the latest nonce for the Ethereum bridge - /// pool. - pub fn get_bridge_pool_nonce(self) -> Uint { - self.state - .read(&bridge_pool::get_nonce_key()) - .expect("Reading Bridge pool nonce shouldn't fail.") - .expect("Bridge pool nonce must be present.") - } - - /// Get the nonce at a particular block height. - pub fn get_bridge_pool_nonce_at_height(self, height: BlockHeight) -> Uint { - Uint::try_from_slice( - &self - .state - .db() - .read_subspace_val_with_height( - &bridge_pool::get_nonce_key(), - height, - self.state.in_mem().get_last_block_height(), - ) - .expect("Reading signed Bridge pool nonce shouldn't fail.") - .expect("Reading signed Bridge pool nonce shouldn't fail."), - ) - .expect("Deserializing the signed nonce from storage should not fail.") - } - - /// Get the latest root of the Ethereum bridge - /// pool Merkle tree. - pub fn get_bridge_pool_root(self) -> KeccakHash { - self.state - .in_mem() - .block - .tree - .sub_root(&StoreType::BridgePool) - .into() - } - - /// Get a quorum of validator signatures over - /// the concatenation of the latest bridge pool - /// root and nonce. - /// - /// Also returns the block height at which the - /// Bridge pool root was originally signed. - /// - /// No value exists when the bridge if first - /// started. - pub fn get_signed_bridge_pool_root( - self, - ) -> Option<(BridgePoolRootProof, BlockHeight)> { - self.state - .read(&bridge_pool::get_signed_root_key()) - .expect("Reading signed Bridge pool root shouldn't fail.") - } - - /// Get the root of the Ethereum bridge - /// pool Merkle tree at a given height. - pub fn get_bridge_pool_root_at_height( - self, - height: BlockHeight, - ) -> Option { - let base_tree = self - .state - .get_merkle_tree(height, Some(StoreType::BridgePool)) - .ok()?; - Some(base_tree.sub_root(&StoreType::BridgePool).into()) - } - - /// Determines if it is possible to send a validator set update vote - /// extension at the provided [`BlockHeight`] in [`SendValsetUpd`]. - #[inline] - pub fn must_send_valset_upd(self, can_send: SendValsetUpd) -> bool { - if !is_bridge_comptime_enabled() { - // the bridge is disabled at compile time, therefore - // we must never submit validator set updates - false - } else if matches!(can_send, SendValsetUpd::AtPrevHeight) { - // when checking vote extensions in Prepare - // and ProcessProposal, we simply return true - true - } else { - // offset of 1 => are we at the 2nd - // block within the epoch? - self.state.is_deciding_offset_within_epoch(1) - } - } - - /// For a given Namada validator, return its corresponding Ethereum bridge - /// address. - #[inline] - pub fn get_ethbridge_from_namada_addr( - self, - validator: &Address, - epoch: Option, - ) -> Option - where - Gov: governance::Read>, - { - let epoch = - epoch.unwrap_or_else(|| self.state.in_mem().get_current_epoch().0); - let params = read_pos_params::<_, Gov>(self.state).unwrap(); - validator_eth_hot_key_handle(validator) - .get(self.state, epoch, ¶ms) - .expect("Should be able to read eth hot key from storage") - .and_then(|ref pk| pk.try_into().ok()) - } - - /// For a given Namada validator, return its corresponding Ethereum - /// governance address. - #[inline] - pub fn get_ethgov_from_namada_addr( - self, - validator: &Address, - epoch: Option, - ) -> Option - where - Gov: governance::Read>, - { - let epoch = - epoch.unwrap_or_else(|| self.state.in_mem().get_current_epoch().0); - let params = read_pos_params::<_, Gov>(self.state).unwrap(); - validator_eth_cold_key_handle(validator) - .get(self.state, epoch, ¶ms) - .expect("Should be able to read eth cold key from storage") - .and_then(|ref pk| pk.try_into().ok()) - } - - /// For a given Namada validator, return its corresponding Ethereum - /// address book. - #[inline] - pub fn get_eth_addr_book( - self, - validator: &Address, - epoch: Option, - ) -> Option - where - Gov: governance::Read>, - { - let bridge = - self.get_ethbridge_from_namada_addr::(validator, epoch)?; - let governance = - self.get_ethgov_from_namada_addr::(validator, epoch)?; - Some(EthAddrBook { - hot_key_addr: bridge, - cold_key_addr: governance, - }) - } - - /// Extension of [`read_consensus_validator_set_addresses_with_stake`], - /// which additionally returns all Ethereum addresses of some validator. - #[inline] - pub fn get_consensus_eth_addresses( - self, - epoch: Epoch, - ) -> impl Iterator + 'db - where - Gov: governance::Read>, - { - read_consensus_validator_set_addresses_with_stake(self.state, epoch) - .unwrap() - .into_iter() - .map(move |validator| { - let eth_addr_book = self - .state - .ethbridge_queries() - .get_eth_addr_book::(&validator.address, Some(epoch)) - .expect("All Namada validators should have Ethereum keys"); - (eth_addr_book, validator.address, validator.bonded_stake) - }) - } - - /// Query a chosen [`ValidatorSetArgs`] at the given [`Epoch`]. - /// Also returns a map of each validator's voting power. - fn get_validator_set_args( - self, - epoch: Option, - mut select_validator: F, - ) -> (ValidatorSetArgs, VotingPowersMap) - where - Gov: governance::Read>, - F: FnMut(&EthAddrBook) -> EthAddress, - { - let epoch = - epoch.unwrap_or_else(|| self.state.in_mem().get_current_epoch().0); - - let voting_powers_map: VotingPowersMap = self - .get_consensus_eth_addresses::(epoch) - .map(|(addr_book, _, power)| (addr_book, power)) - .collect(); - - let total_power = - get_total_voting_power::<_, Gov>(self.state, epoch).into(); - let (validators, voting_powers) = voting_powers_map - .get_sorted() - .into_iter() - .map(|(addr_book, &power)| { - let voting_power: EthBridgeVotingPower = - FractionalVotingPower::new(power.into(), total_power) - .expect("Fractional voting power should be >1") - .try_into() - .unwrap(); - (select_validator(addr_book), voting_power) - }) - .unzip(); - - ( - ValidatorSetArgs { - epoch, - validators, - voting_powers, - }, - voting_powers_map, - ) - } - - /// Query the Bridge [`ValidatorSetArgs`] at the given [`Epoch`]. - /// Also returns a map of each validator's voting power. - #[inline] - pub fn get_bridge_validator_set( - self, - epoch: Option, - ) -> (ValidatorSetArgs, VotingPowersMap) - where - Gov: governance::Read>, - { - self.get_validator_set_args::( - epoch, - |&EthAddrBook { hot_key_addr, .. }| hot_key_addr, - ) - } - - /// Query the Governance [`ValidatorSetArgs`] at the given [`Epoch`]. - /// Also returns a map of each validator's voting power. - #[inline] - pub fn get_governance_validator_set( - self, - epoch: Option, - ) -> (ValidatorSetArgs, VotingPowersMap) - where - Gov: governance::Read>, - { - self.get_validator_set_args::( - epoch, - |&EthAddrBook { cold_key_addr, .. }| cold_key_addr, - ) - } - - /// Check if the token at the given [`EthAddress`] is whitelisted. - pub fn is_token_whitelisted(self, &token: &EthAddress) -> bool { - let key = whitelist::Key { - asset: token, - suffix: whitelist::KeyType::Whitelisted, - } - .into(); - - self.state - .read(&key) - .expect("Reading from storage should not fail") - .unwrap_or(false) - } - - /// Fetch the token cap of the asset associated with the given - /// [`EthAddress`]. - /// - /// If the asset has never been whitelisted, return [`None`]. - pub fn get_token_cap(self, &token: &EthAddress) -> Option { - let key = whitelist::Key { - asset: token, - suffix: whitelist::KeyType::Cap, - } - .into(); - - self.state - .read(&key) - .expect("Reading from storage should not fail") - } - - /// Fetch the token supply of the asset associated with the given - /// [`EthAddress`]. - /// - /// If the asset has never been minted, return [`None`]. - pub fn get_token_supply( - self, - &token: &EthAddress, - ) -> Option { - let key = whitelist::Key { - asset: token, - suffix: whitelist::KeyType::WrappedSupply, - } - .into(); - - self.state - .read(&key) - .expect("Reading from storage should not fail") - } - - /// Return the number of ERC20 and NUT assets to be minted, - /// after receiving a "transfer to Namada" Ethereum event. - /// - /// NUTs are minted when: - /// - /// 1. `token` is not whitelisted. - /// 2. `token` has exceeded the configured token caps, after minting - /// `amount_to_mint`. - pub fn get_eth_assets_to_mint( - self, - token: &EthAddress, - amount_to_mint: token::Amount, - ) -> EthAssetMint { - if !self.is_token_whitelisted(token) { - return EthAssetMint { - nut_amount: amount_to_mint, - erc20_amount: token::Amount::zero(), - }; - } - - let supply = self.get_token_supply(token).unwrap_or_default(); - let cap = self.get_token_cap(token).unwrap_or_default(); - - if hints::unlikely(cap < supply) { - panic!( - "Namada's state is faulty! The Ethereum ERC20 asset {token} \ - has a higher minted supply than the configured token cap: \ - cap:{cap:?} < supply:{supply:?}" - ); - } - - if amount_to_mint - .checked_add(supply) - .expect("Token amount shouldn't overflow") - > cap - { - let erc20_amount = - cap.checked_sub(supply).expect("Cannot underflow"); - let nut_amount = amount_to_mint - .checked_sub(erc20_amount) - .expect("Cannot underflow"); - - return EthAssetMint { - nut_amount, - erc20_amount, - }; - } - - EthAssetMint { - erc20_amount: amount_to_mint, - nut_amount: token::Amount::zero(), - } - } - - /// Given a [`TransferToEthereum`] event, look-up the corresponding - /// [`PendingTransfer`]. - pub fn lookup_transfer_to_eth( - self, - transfer: &TransferToEthereum, - ) -> Option<(PendingTransfer, StorageKey)> { - let pending_key = bridge_pool::get_key_from_hash(&transfer.keccak256()); - self.state - .read(&pending_key) - .expect("Reading from storage should not fail") - .zip(Some(pending_key)) - } - - /// Valdidate an [`EthereumEvent`]'s nonce against the current - /// state of the ledger. - /// - /// # Event kinds - /// - /// In this section, we shall describe the checks perform for - /// each kind of relevant Ethereum event. - /// - /// ## Transfers to Ethereum - /// - /// We need to check if the nonce in the event corresponds to - /// the most recent bridge pool nonce. Unless the nonces match, - /// no state updates derived from the event should be applied. - /// In case the nonces are different, we reject the event, and - /// thus the inclusion of its container Ethereum events vote - /// extension. - /// - /// ## Transfers to Namada - /// - /// For a transfers to Namada event to be considered valid, - /// the nonce of this kind of event must not be lower than - /// the one stored in Namada. - pub fn validate_eth_event_nonce(&self, event: &EthereumEvent) -> bool { - match event { - EthereumEvent::TransfersToEthereum { - nonce: ext_nonce, .. - } => { - let current_bp_nonce = self.get_bridge_pool_nonce(); - if ¤t_bp_nonce != ext_nonce { - return false; - } - } - EthereumEvent::TransfersToNamada { - nonce: ext_nonce, .. - } => { - let next_nam_transfers_nonce = - self.get_next_nam_transfers_nonce(); - if &next_nam_transfers_nonce > ext_nonce { - return false; - } - } - // consider other ethereum event kinds valid - _ => {} - } - true - } -} - -/// Number of tokens to mint after receiving a "transfer -/// to Namada" Ethereum event. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct EthAssetMint { - /// Amount of NUTs to mint. - pub nut_amount: token::Amount, - /// Amount of wrapped ERC20s to mint. - pub erc20_amount: token::Amount, -} - -impl EthAssetMint { - /// Check if NUTs should be minted. - #[inline] - pub fn should_mint_nuts(&self) -> bool { - !self.nut_amount.is_zero() - } - - /// Check if ERC20s should be minted. - #[inline] - pub fn should_mint_erc20s(&self) -> bool { - !self.erc20_amount.is_zero() - } -} diff --git a/crates/ethereum_bridge/src/storage/mod.rs b/crates/ethereum_bridge/src/storage/mod.rs deleted file mode 100644 index 7b42284e1a6..00000000000 --- a/crates/ethereum_bridge/src/storage/mod.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! Functionality for accessing the storage subspace - -pub mod bridge_pool; -pub mod eth_bridge_queries; -pub mod parameters; -pub mod proof; -pub mod vote_tallies; -pub mod vp; -pub mod whitelist; -pub mod wrapped_erc20s; - -use namada_core::address::Address; -use namada_core::storage::{DbKeySeg, Key, KeySeg}; -use namada_parameters::ADDRESS as PARAM_ADDRESS; -pub use namada_parameters::native_erc20_key; -use namada_parameters::storage::*; -use namada_trans_token::storage_key::balance_key; - -use crate::ADDRESS; - -/// Key prefix for the storage subspace -pub fn prefix() -> Key { - Key::from(ADDRESS.to_db_key()) -} - -/// Key for storing the initial Ethereum block height when -/// events will first be extracted from. -pub fn eth_start_height_key() -> Key { - get_eth_start_height_key_at_addr(PARAM_ADDRESS) -} - -/// The key to the escrow of the VP. -pub fn escrow_key(nam_addr: &Address) -> Key { - balance_key(nam_addr, &ADDRESS) -} - -/// Check if the given `key` contains an Ethereum -/// bridge address segment. -#[inline] -pub fn has_eth_addr_segment(key: &Key) -> bool { - key.segments - .iter() - .any(|s| matches!(s, DbKeySeg::AddressSeg(ADDRESS))) -} - -/// Returns whether a key belongs to this account or not -pub fn is_eth_bridge_key(nam_addr: &Address, key: &Key) -> bool { - key == &escrow_key(nam_addr) - || matches!(key.segments.first(), Some(first_segment) if first_segment == &ADDRESS.to_db_key()) - || wrapped_erc20s::has_erc20_segment(key) -} - -/// A key for storing the active / inactive status -/// of the Ethereum bridge. -pub fn active_key() -> Key { - get_active_status_key_at_addr(PARAM_ADDRESS) -} - -/// Storage key for the minimum confirmations parameter. -pub fn min_confirmations_key() -> Key { - get_min_confirmations_key_at_addr(PARAM_ADDRESS) -} - -/// Storage key for the Ethereum address of the bridge contract. -pub fn bridge_contract_key() -> Key { - get_bridge_contract_address_key_at_addr(PARAM_ADDRESS) -} - -#[cfg(test)] -mod test { - use namada_core::address; - use namada_core::address::testing::nam; - use namada_core::ethereum_events::testing::arbitrary_eth_address; - - use super::*; - - #[test] - fn test_is_eth_bridge_key_returns_true_for_eth_bridge_address() { - let key = Key::from(super::ADDRESS.to_db_key()); - assert!(is_eth_bridge_key(&nam(), &key)); - } - - #[test] - fn test_is_eth_bridge_key_returns_true_for_eth_bridge_subkey() { - let key = Key::from(super::ADDRESS.to_db_key()) - .push(&"arbitrary key segment".to_owned()) - .expect("Could not set up test"); - assert!(is_eth_bridge_key(&nam(), &key)); - } - - #[test] - fn test_is_eth_bridge_key_returns_true_for_eth_bridge_balance_key() { - let eth_addr = arbitrary_eth_address(); - let token = address::Address::Internal( - address::InternalAddress::Erc20(eth_addr), - ); - let key = - balance_key(&token, &address::testing::established_address_1()); - assert!(is_eth_bridge_key(&nam(), &key)); - } - - #[test] - fn test_is_eth_bridge_key_returns_false_for_different_address() { - let key = - Key::from(address::testing::established_address_1().to_db_key()); - assert!(!is_eth_bridge_key(&nam(), &key)); - } - - #[test] - fn test_is_eth_bridge_key_returns_false_for_different_address_subkey() { - let key = - Key::from(address::testing::established_address_1().to_db_key()) - .push(&"arbitrary key segment".to_owned()) - .expect("Could not set up test"); - assert!(!is_eth_bridge_key(&nam(), &key)); - } - - #[test] - fn test_is_eth_bridge_key_returns_false_for_non_eth_bridge_balance_key() { - let key = - balance_key(&nam(), &address::testing::established_address_1()); - assert!(!is_eth_bridge_key(&nam(), &key)); - } -} diff --git a/crates/ethereum_bridge/src/storage/parameters.rs b/crates/ethereum_bridge/src/storage/parameters.rs deleted file mode 100644 index 9af9cc5ad88..00000000000 --- a/crates/ethereum_bridge/src/storage/parameters.rs +++ /dev/null @@ -1,484 +0,0 @@ -//! Parameters for configuring the Ethereum bridge -use std::num::NonZeroU64; - -use namada_core::borsh::{BorshDeserialize, BorshSerialize}; -use namada_core::ethereum_events::EthAddress; -use namada_core::ethereum_structs; -use namada_core::storage::Key; -use namada_core::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_state::{DB, DBIter, StorageHasher, WlState}; -use namada_storage::{Error, Result, StorageRead, StorageWrite}; -use serde::{Deserialize, Serialize}; - -use super::whitelist; -use crate::storage as bridge_storage; -use crate::storage::eth_bridge_queries::{ - EthBridgeEnabled, EthBridgeQueries, EthBridgeStatus, -}; -use crate::storage::vp; - -/// An ERC20 token whitelist entry. -#[derive( - Clone, - Copy, - Eq, - PartialEq, - Debug, - Deserialize, - Serialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -pub struct Erc20WhitelistEntry { - /// The address of the whitelisted ERC20 token. - pub token_address: EthAddress, - /// The token cap of the whitelisted ERC20 token. - pub token_cap: DenominatedAmount, -} - -/// Represents a configuration value for the minimum number of -/// confirmations an Ethereum event must reach before it can be acted on. -#[derive( - Clone, - Copy, - Eq, - PartialEq, - Debug, - Deserialize, - Serialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -#[repr(transparent)] -pub struct MinimumConfirmations(NonZeroU64); - -impl Default for MinimumConfirmations { - fn default() -> Self { - // SAFETY: The only way the API contract of `NonZeroU64` can be violated - // is if we construct values of this type using 0 as argument. - Self(unsafe { NonZeroU64::new_unchecked(100) }) - } -} - -impl From for MinimumConfirmations { - fn from(value: NonZeroU64) -> Self { - Self(value) - } -} - -impl From for NonZeroU64 { - fn from(value: MinimumConfirmations) -> Self { - value.0 - } -} - -/// Represents a configuration value for the version of a contract that can be -/// upgraded. Starts from 1. -#[derive( - Clone, - Copy, - Eq, - PartialEq, - Debug, - Deserialize, - Serialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -#[repr(transparent)] -pub struct ContractVersion(NonZeroU64); - -impl Default for ContractVersion { - fn default() -> Self { - // SAFETY: The only way the API contract of `NonZeroU64` can be - // violated is if we construct values of this type using 0 as - // argument. - Self(unsafe { NonZeroU64::new_unchecked(1) }) - } -} - -/// Represents an Ethereum contract that may be upgraded. -#[derive( - Copy, - Clone, - Debug, - Eq, - PartialEq, - Deserialize, - Serialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -pub struct UpgradeableContract { - /// The Ethereum address of the contract. - pub address: EthAddress, - /// The version of the contract. Starts from 1. - pub version: ContractVersion, -} - -/// Represents all the Ethereum contracts that need to be directly know about by -/// validators. -#[derive( - Copy, - Clone, - Debug, - Eq, - PartialEq, - Deserialize, - Serialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -pub struct Contracts { - /// The Ethereum address of the ERC20 contract that represents this chain's - /// native token. - pub native_erc20: EthAddress, - /// The Ethereum address of the bridge contract. - pub bridge: UpgradeableContract, -} - -/// Represents chain parameters for the Ethereum bridge. -#[derive( - Clone, - Debug, - Eq, - PartialEq, - Deserialize, - Serialize, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -pub struct EthereumBridgeParams { - /// Initial Ethereum block height when events will first be extracted from. - pub eth_start_height: ethereum_structs::BlockHeight, - /// Minimum number of confirmations needed to trust an Ethereum branch. - /// This must be at least one. - pub min_confirmations: MinimumConfirmations, - /// List of ERC20 token types whitelisted at genesis time. - pub erc20_whitelist: Vec, - /// The addresses of the Ethereum contracts that need to be directly known - /// by validators. - pub contracts: Contracts, -} - -impl EthereumBridgeParams { - /// Initialize the Ethereum bridge parameters in storage. - /// - /// If these parameters are initialized, the storage subspaces - /// for the Ethereum bridge VPs are also initialized. - pub fn init_storage(&self, state: &mut WlState) - where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, - { - let Self { - erc20_whitelist, - eth_start_height, - min_confirmations, - contracts: - Contracts { - native_erc20, - bridge, - }, - } = self; - let active_key = bridge_storage::active_key(); - let min_confirmations_key = bridge_storage::min_confirmations_key(); - let native_erc20_key = bridge_storage::native_erc20_key(); - let bridge_contract_key = bridge_storage::bridge_contract_key(); - let eth_start_height_key = bridge_storage::eth_start_height_key(); - state - .write( - &active_key, - EthBridgeStatus::Enabled(EthBridgeEnabled::AtGenesis), - ) - .unwrap(); - state - .write(&min_confirmations_key, min_confirmations) - .unwrap(); - state.write(&native_erc20_key, native_erc20).unwrap(); - state.write(&bridge_contract_key, bridge).unwrap(); - state - .write(ð_start_height_key, eth_start_height) - .unwrap(); - for Erc20WhitelistEntry { - token_address: addr, - token_cap, - } in erc20_whitelist - { - let cap = token_cap.amount(); - let denom = token_cap.denom(); - if addr == native_erc20 && denom != NATIVE_MAX_DECIMAL_PLACES.into() - { - panic!( - "Error writing Ethereum bridge config: The native token \ - should have {NATIVE_MAX_DECIMAL_PLACES} decimal places" - ); - } - - let key = whitelist::Key { - asset: *addr, - suffix: whitelist::KeyType::Whitelisted, - } - .into(); - state.write(&key, true).unwrap(); - - let key = whitelist::Key { - asset: *addr, - suffix: whitelist::KeyType::Cap, - } - .into(); - state.write(&key, cap).unwrap(); - - let key = whitelist::Key { - asset: *addr, - suffix: whitelist::KeyType::Denomination, - } - .into(); - state.write(&key, denom).unwrap(); - } - // Initialize the storage for the Ethereum Bridge VP. - vp::ethereum_bridge::init_storage(state); - // Initialize the storage for the Bridge Pool VP. - vp::bridge_pool::init_storage(state); - } -} - -/// Subset of [`EthereumBridgeParams`], containing only Ethereum -/// oracle specific parameters. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct EthereumOracleConfig { - /// Initial Ethereum block height when events will first be extracted from. - pub eth_start_height: ethereum_structs::BlockHeight, - /// Minimum number of confirmations needed to trust an Ethereum branch. - /// This must be at least one. - pub min_confirmations: MinimumConfirmations, - /// The addresses of the Ethereum contracts that need to be directly known - /// by validators. - pub contracts: Contracts, -} - -impl From for EthereumOracleConfig { - fn from(config: EthereumBridgeParams) -> Self { - let EthereumBridgeParams { - eth_start_height, - min_confirmations, - contracts, - .. - } = config; - Self { - eth_start_height, - min_confirmations, - contracts, - } - } -} - -impl EthereumOracleConfig { - /// Reads the latest [`EthereumOracleConfig`] from storage. If it is not - /// present, `None` will be returned - this could be the case if the bridge - /// has not been bootstrapped yet. Panics if the storage appears to be - /// corrupt. - pub fn read(state: &WlState) -> Option - where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, - { - // TODO(namada#1720): remove present key check; `is_bridge_active` - // should not panic, when the active status key has not been - // written to; simply return bridge disabled instead - let has_active_key = - state.has_key(&bridge_storage::active_key()).unwrap(); - - if !has_active_key || !state.ethbridge_queries().is_bridge_active() { - return None; - } - - let min_confirmations_key = bridge_storage::min_confirmations_key(); - let native_erc20_key = bridge_storage::native_erc20_key(); - let bridge_contract_key = bridge_storage::bridge_contract_key(); - let eth_start_height_key = bridge_storage::eth_start_height_key(); - - // These reads must succeed otherwise the storage is corrupt or a - // read failed - let min_confirmations = must_read_key(state, &min_confirmations_key); - let native_erc20 = must_read_key(state, &native_erc20_key); - let bridge_contract = must_read_key(state, &bridge_contract_key); - let eth_start_height = must_read_key(state, ð_start_height_key); - - Some(Self { - eth_start_height, - min_confirmations, - contracts: Contracts { - native_erc20, - bridge: bridge_contract, - }, - }) - } -} - -/// Get the Ethereum address for wNam from storage, if possible -pub fn read_native_erc20_address(storage: &S) -> Result -where - S: StorageRead, -{ - let native_erc20 = bridge_storage::native_erc20_key(); - - storage.read(&native_erc20)?.ok_or_else(|| { - Error::SimpleMessage("The Ethereum bridge storage is not initialized") - }) -} - -/// Reads the value of `key` from `storage` and deserializes it, or panics -/// otherwise. -fn must_read_key( - state: &WlState, - key: &Key, -) -> T -where - D: 'static + DB + for<'iter> DBIter<'iter>, - H: 'static + StorageHasher, -{ - StorageRead::read::(state, key).map_or_else( - |err| panic!("Could not read {key}: {err:?}"), - |value| { - value.unwrap_or_else(|| { - panic!( - "Ethereum bridge appears to be only partially configured! \ - There was no value for {key}" - ) - }) - }, - ) -} - -#[cfg(test)] -mod tests { - use namada_state::testing::TestState; - use namada_storage::ResultExt; - - use super::*; - use crate::storage::eth_bridge_queries::is_bridge_comptime_enabled; - - /// Ensure we can serialize and deserialize a [`Config`] struct to and from - /// TOML. This can fail if complex fields are ordered before simple fields - /// in any of the config structs. - #[test] - fn test_round_trip_toml_serde() -> Result<()> { - let config = EthereumBridgeParams { - erc20_whitelist: vec![], - eth_start_height: Default::default(), - min_confirmations: MinimumConfirmations::default(), - contracts: Contracts { - native_erc20: EthAddress([42; 20]), - bridge: UpgradeableContract { - address: EthAddress([23; 20]), - version: ContractVersion::default(), - }, - }, - }; - let serialized = toml::to_string(&config).into_storage_result()?; - let deserialized: EthereumBridgeParams = - toml::from_str(&serialized).into_storage_result()?; - - assert_eq!(config, deserialized); - Ok(()) - } - - #[test] - fn test_ethereum_bridge_config_read_write_storage() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - - let mut state = TestState::default(); - let config = EthereumBridgeParams { - erc20_whitelist: vec![], - eth_start_height: Default::default(), - min_confirmations: MinimumConfirmations::default(), - contracts: Contracts { - native_erc20: EthAddress([42; 20]), - bridge: UpgradeableContract { - address: EthAddress([23; 20]), - version: ContractVersion::default(), - }, - }, - }; - config.init_storage(&mut state); - - let read = EthereumOracleConfig::read(&state).unwrap(); - let config = EthereumOracleConfig::from(config); - - assert_eq!(config, read); - } - - #[test] - fn test_ethereum_bridge_config_uninitialized() { - let state = TestState::default(); - let read = EthereumOracleConfig::read(&state); - - assert!(read.is_none()); - } - - #[test] - #[cfg_attr(not(feature = "namada-eth-bridge"), ignore)] - #[should_panic(expected = "Could not read")] - fn test_ethereum_bridge_config_storage_corrupt() { - let mut state = TestState::default(); - let config = EthereumBridgeParams { - erc20_whitelist: vec![], - eth_start_height: Default::default(), - min_confirmations: MinimumConfirmations::default(), - contracts: Contracts { - native_erc20: EthAddress([42; 20]), - bridge: UpgradeableContract { - address: EthAddress([23; 20]), - version: ContractVersion::default(), - }, - }, - }; - config.init_storage(&mut state); - let min_confirmations_key = bridge_storage::min_confirmations_key(); - state - .write(&min_confirmations_key, vec![42, 1, 2, 3, 4]) - .unwrap(); - - // This should panic because the min_confirmations value is not valid - EthereumOracleConfig::read(&state); - } - - #[test] - #[cfg_attr(not(feature = "namada-eth-bridge"), ignore)] - #[should_panic( - expected = "Ethereum bridge appears to be only partially configured!" - )] - fn test_ethereum_bridge_config_storage_partially_configured() { - let mut state = TestState::default(); - state - .write( - &bridge_storage::active_key(), - EthBridgeStatus::Enabled(EthBridgeEnabled::AtGenesis), - ) - .unwrap(); - // Write a valid min_confirmations value - state - .write( - &bridge_storage::min_confirmations_key(), - MinimumConfirmations::default(), - ) - .unwrap(); - - // This should panic as the other config values are not written - EthereumOracleConfig::read(&state); - } -} diff --git a/crates/ethereum_bridge/src/storage/proof.rs b/crates/ethereum_bridge/src/storage/proof.rs deleted file mode 100644 index f05b1485388..00000000000 --- a/crates/ethereum_bridge/src/storage/proof.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Proofs over some arbitrary data. - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use ethers::abi::Tokenizable; -use namada_core::chain::Epoch; -use namada_core::collections::HashMap; -use namada_core::eth_abi::Encode; -use namada_core::ethereum_events::Uint; -use namada_core::keccak::KeccakHash; -use namada_core::key::{common, secp256k1}; -use namada_core::{eth_abi, ethereum_structs}; -use namada_vote_ext::validator_set_update::{ - EthAddrBook, VotingPowersMap, VotingPowersMapExt, valset_upd_toks_to_hashes, -}; - -/// Ethereum proofs contain the [`secp256k1`] signatures of validators -/// over some data to be signed. -/// -/// At any given time, an [`EthereumProof`] will be considered -/// "complete" once a number of signatures pertaining to validators -/// reflecting more than 2/3 of the bonded stake on Namada is available. -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema)] -pub struct EthereumProof { - /// The signatures contained in the proof. - pub signatures: HashMap, - /// The signed data. - pub data: T, -} - -/// Ethereum bridge pool root proof. -pub type BridgePoolRootProof = EthereumProof<(KeccakHash, Uint)>; - -impl EthereumProof { - /// Return an incomplete [`EthereumProof`]. - pub fn new(data: T) -> Self { - Self { - data, - signatures: HashMap::new(), - } - } - - /// Map a function over the inner data of this [`EthereumProof`]. - #[inline] - pub fn map(self, mut f: F) -> EthereumProof - where - F: FnMut(T) -> R, - { - EthereumProof { - signatures: self.signatures, - data: f(self.data), - } - } - - /// Add a new signature to this [`EthereumProof`]. - pub fn attach_signature( - &mut self, - addr_book: EthAddrBook, - signature: common::Signature, - ) { - if let common::Signature::Secp256k1(sig) = signature { - self.signatures.insert(addr_book, sig); - } - } - - /// Add a new batch of signatures to this [`EthereumProof`]. - pub fn attach_signature_batch(&mut self, batch: I) - where - I: IntoIterator, - K: Into, - { - for (addr_book, signature) in batch { - self.attach_signature(addr_book, signature.into()); - } - } -} - -/// Sort signatures based on voting powers in descending order. -/// Puts a dummy signature in place of invalid or missing signatures. -pub fn sort_sigs( - voting_powers: &VotingPowersMap, - signatures: &HashMap, -) -> Vec { - voting_powers - .get_sorted() - .into_iter() - .map(|(addr_book, _)| { - signatures - .get(addr_book) - .map(|sig| { - let (r, s, v) = sig.clone().into_eth_rsv(); - ethereum_structs::Signature { r, s, v } - }) - .unwrap_or(ethereum_structs::Signature { - r: [0; 32], - s: [0; 32], - v: 0, - }) - }) - .collect() -} - -impl Encode<1> for EthereumProof<(Epoch, VotingPowersMap)> { - fn tokenize(&self) -> [eth_abi::Token; 1] { - let signatures = sort_sigs(&self.data.1, &self.signatures); - let (bridge_validators, governance_validators) = - self.data.1.get_abi_encoded(); - let (KeccakHash(bridge_hash), KeccakHash(gov_hash)) = - valset_upd_toks_to_hashes( - self.data.0, - bridge_validators, - governance_validators, - ); - [eth_abi::Token::Tuple(vec![ - eth_abi::Token::FixedBytes(bridge_hash.to_vec()), - eth_abi::Token::FixedBytes(gov_hash.to_vec()), - Tokenizable::into_token(signatures), - ])] - } -} - -#[cfg(test)] -mod test_ethbridge_proofs { - //! Test ethereum bridge proofs. - - use assert_matches::assert_matches; - use namada_core::ethereum_events::EthAddress; - use namada_core::key; - use namada_tx::Signed; - - use super::*; - - /// Test that adding a non-secp256k1 signature to an [`EthereumProof`] is a - /// NOOP. - #[test] - fn test_add_non_secp256k1_is_noop() { - let mut proof = EthereumProof::new(()); - assert!(proof.signatures.is_empty()); - let key = key::testing::keypair_1(); - assert_matches!(&key, common::SecretKey::Ed25519(_)); - let signed = Signed::<&'static str>::new(&key, ":)))))))"); - - proof.attach_signature( - EthAddrBook { - hot_key_addr: EthAddress([0; 20]), - cold_key_addr: EthAddress([0; 20]), - }, - signed.sig, - ); - assert!(proof.signatures.is_empty()); - } -} diff --git a/crates/ethereum_bridge/src/storage/vote_tallies.rs b/crates/ethereum_bridge/src/storage/vote_tallies.rs deleted file mode 100644 index 30ad96a2d8d..00000000000 --- a/crates/ethereum_bridge/src/storage/vote_tallies.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Functionality for accessing keys to do with tallying votes - -use std::io::{Read, Write}; -use std::str::FromStr; - -use borsh::{BorshDeserialize, BorshSerialize}; -use namada_core::address::Address; -use namada_core::chain::{BlockHeight, Epoch}; -use namada_core::ethereum_events::{EthereumEvent, Uint}; -use namada_core::hash::Hash; -use namada_core::keccak::{KeccakHash, keccak_hash}; -use namada_core::storage::{DbKeySeg, Key}; -use namada_macros::{BorshDeserializer, StorageKeys}; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_vote_ext::validator_set_update::VotingPowersMap; - -use crate::ADDRESS; -use crate::storage::proof::{BridgePoolRootProof, EthereumProof}; - -/// Storage sub-key space reserved to keeping track of the -/// voting power assigned to Ethereum events. -pub const ETH_MSGS_PREFIX_KEY_SEGMENT: &str = "eth_msgs"; - -/// Storage sub-key space reserved to keeping track of the -/// voting power assigned to Ethereum bridge pool roots and -/// nonces. -pub const BRIDGE_POOL_ROOT_PREFIX_KEY_SEGMENT: &str = "bp_root_and_nonce"; - -/// Storage sub-key space reserved to keeping track of the -/// voting power assigned to validator set updates. -pub const VALSET_UPDS_PREFIX_KEY_SEGMENT: &str = "validator_set_updates"; - -/// Storage segments of [`Keys`]. -#[derive(StorageKeys)] -pub struct KeysSegments { - /// The data being voted on, corresponding to the `T` type - /// argument in [`Keys`]. - pub body: &'static str, - /// Whether more than two thirds of voting power across different - /// epochs have voted on `body`. - pub seen: &'static str, - /// The validators who have voted on `body`. - pub seen_by: &'static str, - /// The total voting power behind `body`. - pub voting_power: &'static str, - /// The epoch when voting on `body` started. - pub voting_started_epoch: &'static str, -} - -/// Generator for the keys under which details of votes for some piece of data -/// is stored -#[derive(Clone, PartialEq)] -pub struct Keys { - /// The prefix under which the details of a piece of data for which we are - /// tallying votes is stored - pub prefix: Key, - _phantom: std::marker::PhantomData<*const T>, -} - -impl Keys<()> { - /// Return the storage key segments to be stored under [`Keys`]. - #[inline(always)] - pub fn segments() -> &'static KeysSegments { - &KeysSegments::VALUES - } -} - -impl Keys { - /// Get the `body` key - there should be a Borsh-serialized `T` stored - /// here. - pub fn body(&self) -> Key { - self.prefix - .push(&KeysSegments::VALUES.body.to_owned()) - .expect("should always be able to construct this key") - } - - /// Get the `seen` key - there should be a `bool` stored here. - pub fn seen(&self) -> Key { - self.prefix - .push(&KeysSegments::VALUES.seen.to_owned()) - .expect("should always be able to construct this key") - } - - /// Get the `seen_by` key - there should be a `BTreeSet
` stored - /// here. - pub fn seen_by(&self) -> Key { - self.prefix - .push(&KeysSegments::VALUES.seen_by.to_owned()) - .expect("should always be able to construct this key") - } - - /// Get the `voting_power` key - there should be an `EpochedVotingPower` - /// stored here. - pub fn voting_power(&self) -> Key { - self.prefix - .push(&KeysSegments::VALUES.voting_power.to_owned()) - .expect("should always be able to construct this key") - } - - /// Get the `voting_started_epoch` key - there should be an [`Epoch`] stored - /// here. - pub fn voting_started_epoch(&self) -> Key { - self.prefix - .push(&KeysSegments::VALUES.voting_started_epoch.to_owned()) - .expect("should always be able to construct this key") - } -} - -impl IntoIterator for &Keys { - type IntoIter = std::vec::IntoIter; - type Item = Key; - - fn into_iter(self) -> Self::IntoIter { - vec![ - self.body(), - self.seen(), - self.seen_by(), - self.voting_power(), - self.voting_started_epoch(), - ] - .into_iter() - } -} - -/// Get the key prefix corresponding to the storage location of -/// [`EthereumEvent`]s whose "seen" state is being tracked. -pub fn eth_msgs_prefix() -> Key { - super::prefix() - .push(Ð_MSGS_PREFIX_KEY_SEGMENT.to_owned()) - .expect("should always be able to construct this key") -} - -/// Get the Keys from the storage key. It returns None if the storage key isn't -/// for an Ethereum event. -pub fn eth_event_keys(storage_key: &Key) -> Option> { - match &storage_key.segments[..] { - [ - DbKeySeg::AddressSeg(ADDRESS), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(hash), - .., - ] if prefix == ETH_MSGS_PREFIX_KEY_SEGMENT => { - let hash = &Hash::from_str(hash).expect("Hash should be parsable"); - Some(hash.into()) - } - _ => None, - } -} - -/// Return true if the storage key is a key to store the epoch -pub fn is_epoch_key(key: &Key) -> bool { - matches!(&key.segments[..], [ - DbKeySeg::AddressSeg(ADDRESS), - DbKeySeg::StringSeg(_prefix), - DbKeySeg::StringSeg(_hash), - DbKeySeg::StringSeg(e), - ] if e == KeysSegments::VALUES.voting_started_epoch) -} - -/// Return true if the storage key is a key to store the `seen` -pub fn is_seen_key(key: &Key) -> bool { - matches!(&key.segments[..], [ - DbKeySeg::AddressSeg(ADDRESS), - DbKeySeg::StringSeg(_prefix), - DbKeySeg::StringSeg(_hash), - DbKeySeg::StringSeg(e), - ] if e == KeysSegments::VALUES.seen) -} - -impl From<&EthereumEvent> for Keys { - fn from(event: &EthereumEvent) -> Self { - let hash = event - .hash() - .expect("should always be able to hash Ethereum events"); - (&hash).into() - } -} - -impl From<&Hash> for Keys { - fn from(hash: &Hash) -> Self { - let hex = format!("{}", hash); - let prefix = eth_msgs_prefix() - .push(&hex) - .expect("should always be able to construct this key"); - Keys { - prefix, - _phantom: std::marker::PhantomData, - } - } -} - -/// A wrapper struct for managing keys related to -/// tracking signatures over bridge pool roots and nonces. -#[derive(Debug, Clone, BorshDeserializer)] -pub struct BridgePoolRoot(pub BridgePoolRootProof); - -impl BorshSerialize for BridgePoolRoot { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - BorshSerialize::serialize(&self.0, writer) - } -} - -impl BorshDeserialize for BridgePoolRoot { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - as BorshDeserialize>::deserialize_reader( - reader, - ) - .map(BridgePoolRoot) - } -} - -impl From<(&BridgePoolRoot, BlockHeight)> for Keys { - fn from( - (BridgePoolRoot(bp_root), root_height): (&BridgePoolRoot, BlockHeight), - ) -> Self { - let hash = { - let (KeccakHash(root), nonce) = &bp_root.data; - - let mut to_hash = [0u8; 64]; - to_hash[..32].copy_from_slice(root); - to_hash[32..].copy_from_slice(&nonce.to_bytes()); - - keccak_hash(to_hash).to_string() - }; - let prefix = super::prefix() - .with_segment(BRIDGE_POOL_ROOT_PREFIX_KEY_SEGMENT.to_owned()) - .with_segment(root_height) - .with_segment(hash); - Keys { - prefix, - _phantom: std::marker::PhantomData, - } - } -} - -/// Get the key prefix corresponding to the storage location of validator set -/// updates whose "seen" state is being tracked. -pub fn valset_upds_prefix() -> Key { - super::prefix() - .push(&VALSET_UPDS_PREFIX_KEY_SEGMENT.to_owned()) - .expect("should always be able to construct this key") -} - -impl From<&Epoch> for Keys> { - fn from(epoch: &Epoch) -> Self { - let prefix = valset_upds_prefix() - .push(epoch) - .expect("should always be able to construct this key"); - Keys { - prefix, - _phantom: std::marker::PhantomData, - } - } -} - -#[cfg(test)] -mod test { - use assert_matches::assert_matches; - - use super::*; - - mod helpers { - use super::*; - - pub(super) fn arbitrary_event_with_hash() -> (EthereumEvent, String) { - ( - EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![], - }, - "AB24A95F44CECA5D2AED4B6D056ADDDD8539F44C6CD6CA506534E830C82EA8A8" - .to_owned(), - ) - } - } - - #[test] - fn test_eth_msgs_prefix() { - assert_matches!(ð_msgs_prefix().segments[..], [ - DbKeySeg::AddressSeg(ADDRESS), - DbKeySeg::StringSeg(s), - ] if s == ETH_MSGS_PREFIX_KEY_SEGMENT) - } - - #[test] - fn test_ethereum_event_keys_all_keys() { - let (event, hash) = helpers::arbitrary_event_with_hash(); - let keys: Keys = (&event).into(); - let prefix = [ - DbKeySeg::AddressSeg(ADDRESS), - DbKeySeg::StringSeg(ETH_MSGS_PREFIX_KEY_SEGMENT.to_owned()), - DbKeySeg::StringSeg(hash), - ]; - let body_key = keys.body(); - assert_eq!(body_key.segments[..3], prefix[..]); - assert_eq!( - body_key.segments[3], - DbKeySeg::StringSeg(KeysSegments::VALUES.body.to_owned()) - ); - - let seen_key = keys.seen(); - assert_eq!(seen_key.segments[..3], prefix[..]); - assert_eq!( - seen_key.segments[3], - DbKeySeg::StringSeg(KeysSegments::VALUES.seen.to_owned()) - ); - - let seen_by_key = keys.seen_by(); - assert_eq!(seen_by_key.segments[..3], prefix[..]); - assert_eq!( - seen_by_key.segments[3], - DbKeySeg::StringSeg(KeysSegments::VALUES.seen_by.to_owned()) - ); - - let voting_power_key = keys.voting_power(); - assert_eq!(voting_power_key.segments[..3], prefix[..]); - assert_eq!( - voting_power_key.segments[3], - DbKeySeg::StringSeg(KeysSegments::VALUES.voting_power.to_owned()) - ); - } - - #[test] - fn test_ethereum_event_keys_into_iter() { - let (event, _) = helpers::arbitrary_event_with_hash(); - let keys: Keys = (&event).into(); - let as_keys: Vec<_> = keys.into_iter().collect(); - assert_eq!( - as_keys, - vec![ - keys.body(), - keys.seen(), - keys.seen_by(), - keys.voting_power(), - keys.voting_started_epoch(), - ] - ); - } - - #[test] - fn test_ethereum_event_keys_from_ethereum_event() { - let (event, hash) = helpers::arbitrary_event_with_hash(); - let keys: Keys = (&event).into(); - let expected = [ - DbKeySeg::AddressSeg(ADDRESS), - DbKeySeg::StringSeg(ETH_MSGS_PREFIX_KEY_SEGMENT.to_owned()), - DbKeySeg::StringSeg(hash), - ]; - assert_eq!(&keys.prefix.segments[..], &expected[..]); - } - - #[test] - fn test_ethereum_event_keys_from_hash() { - let (event, hash) = helpers::arbitrary_event_with_hash(); - let keys: Keys = (&event.hash().unwrap()).into(); - let expected = [ - DbKeySeg::AddressSeg(ADDRESS), - DbKeySeg::StringSeg(ETH_MSGS_PREFIX_KEY_SEGMENT.to_owned()), - DbKeySeg::StringSeg(hash), - ]; - assert_eq!(&keys.prefix.segments[..], &expected[..]); - } -} diff --git a/crates/ethereum_bridge/src/storage/vp.rs b/crates/ethereum_bridge/src/storage/vp.rs deleted file mode 100644 index 95abf8595ad..00000000000 --- a/crates/ethereum_bridge/src/storage/vp.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Validity predicate storage - -pub mod bridge_pool; -pub mod ethereum_bridge; diff --git a/crates/ethereum_bridge/src/storage/vp/bridge_pool.rs b/crates/ethereum_bridge/src/storage/vp/bridge_pool.rs deleted file mode 100644 index 6fee117951e..00000000000 --- a/crates/ethereum_bridge/src/storage/vp/bridge_pool.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Ethereum bridge pool VP storage - -use namada_core::ethereum_events::Uint; -use namada_storage::{StorageRead, StorageWrite}; -use namada_trans_token::Amount; -use namada_trans_token::storage_key::balance_key; - -use crate::storage::bridge_pool::{BRIDGE_POOL_ADDRESS, get_nonce_key}; - -/// Initialize the storage owned by the Bridge Pool VP. -/// -/// This means that the amount of escrowed gas fees is -/// initialized to 0. -pub fn init_storage(storage: &mut S) -where - S: StorageRead + StorageWrite, -{ - let escrow_key = - balance_key(&storage.get_native_token().unwrap(), &BRIDGE_POOL_ADDRESS); - storage.write(&escrow_key, Amount::default()).expect( - "Initializing the escrow balance of the Bridge pool VP shouldn't fail.", - ); - storage - .write(&get_nonce_key(), Uint::from(0)) - .expect("Initializing the Bridge pool nonce shouldn't fail."); -} diff --git a/crates/ethereum_bridge/src/storage/vp/ethereum_bridge.rs b/crates/ethereum_bridge/src/storage/vp/ethereum_bridge.rs deleted file mode 100644 index 4ae9627a6ef..00000000000 --- a/crates/ethereum_bridge/src/storage/vp/ethereum_bridge.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Ethereum bridge VP storage - -use namada_storage::{StorageRead, StorageWrite}; -use namada_trans_token::Amount; -use namada_trans_token::storage_key::balance_key; - -use crate::ADDRESS; - -/// Initialize the storage owned by the Ethereum Bridge VP. -/// -/// This means that the amount of escrowed Nam is -/// initialized to 0. -pub fn init_storage(storage: &mut S) -where - S: StorageRead + StorageWrite, -{ - let escrow_key = - balance_key(&storage.get_native_token().unwrap(), &ADDRESS); - storage.write(&escrow_key, Amount::default()).expect( - "Initializing the escrow balance of the Ethereum Bridge VP shouldn't \ - fail.", - ); -} diff --git a/crates/ethereum_bridge/src/storage/whitelist.rs b/crates/ethereum_bridge/src/storage/whitelist.rs deleted file mode 100644 index 349469a0d00..00000000000 --- a/crates/ethereum_bridge/src/storage/whitelist.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! ERC20 token whitelist storage data. -//! -//! These storage keys should only ever be written to by governance, -//! or `InitChain`. - -use std::str::FromStr; - -use namada_core::eth_bridge_pool::erc20_token_address; -use namada_core::ethereum_events::EthAddress; -use namada_core::storage; -use namada_core::storage::DbKeySeg; -use namada_trans_token::storage_key::{denom_key, minted_balance_key}; - -use super::prefix as ethbridge_key_prefix; -use crate::ADDRESS as BRIDGE_ADDRESS; - -mod segments { - //! Storage key segments under the token whitelist. - use namada_core::address::Address; - use namada_macros::StorageKeys; - - /// The name of the main storage segment. - pub(super) const MAIN_SEGMENT: &str = "whitelist"; - - /// Storage key segments under the token whitelist. - #[derive(StorageKeys)] - pub(super) struct Segments { - /// Whether an ERC20 asset is whitelisted or not. - pub whitelisted: &'static str, - /// The token cap of an ERC20 asset. - pub cap: &'static str, - } - - /// All the values of the generated [`Segments`]. - pub(super) const VALUES: Segments = Segments::VALUES; - - /// Listing of each of the generated [`Segments`]. - pub(super) const ALL: &[&str] = Segments::ALL; -} - -/// Represents the type of a key relating to whitelisted ERC20. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub enum KeyType { - /// Whether an ERC20 asset is whitelisted or not. - Whitelisted, - /// The token cap of an ERC20 asset. - Cap, - /// The current supply of a wrapped ERC20 asset, - /// circulating in Namada. - WrappedSupply, - /// The denomination of the ERC20 asset. - Denomination, -} - -/// Whitelisted ERC20 token storage sub-space. -pub struct Key { - /// The specific ERC20 as identified by its Ethereum address. - pub asset: EthAddress, - /// The type of this key. - pub suffix: KeyType, -} - -/// Return the whitelist storage key sub-space prefix. -fn whitelist_prefix(asset: &EthAddress) -> storage::Key { - ethbridge_key_prefix() - .push(&segments::MAIN_SEGMENT.to_owned()) - .expect("Should be able to push a storage key segment") - .push(&asset.to_canonical()) - .expect("Should be able to push a storage key segment") -} - -impl From for storage::Key { - #[inline] - fn from(key: Key) -> Self { - (&key).into() - } -} - -impl From<&Key> for storage::Key { - fn from(key: &Key) -> Self { - match &key.suffix { - KeyType::Whitelisted => whitelist_prefix(&key.asset) - .push(&segments::VALUES.whitelisted.to_owned()) - .expect("Should be able to push a storage key segment"), - KeyType::Cap => whitelist_prefix(&key.asset) - .push(&segments::VALUES.cap.to_owned()) - .expect("Should be able to push a storage key segment"), - KeyType::WrappedSupply => { - let token = erc20_token_address(&key.asset); - minted_balance_key(&token) - } - KeyType::Denomination => { - let token = erc20_token_address(&key.asset); - denom_key(&token) - } - } - } -} - -/// Check if some [`storage::Key`] is an Ethereum bridge whitelist key -/// of type [`KeyType::Cap`] or [`KeyType::Whitelisted`]. -pub fn is_cap_or_whitelisted_key(key: &storage::Key) -> bool { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(s1), - DbKeySeg::StringSeg(s2), - DbKeySeg::StringSeg(s3), - DbKeySeg::StringSeg(s4), - ] => { - s1 == &BRIDGE_ADDRESS - && s2 == segments::MAIN_SEGMENT - && EthAddress::from_str(s3).is_ok() - && segments::ALL.binary_search(&s4.as_str()).is_ok() - } - _ => false, - } -} - -#[cfg(test)] -mod tests { - use namada_core::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; - - use super::*; - - /// Test that storage key serialization yields the expected value. - #[test] - fn test_keys_whitelisted_to_string() { - let key: storage::Key = Key { - asset: DAI_ERC20_ETH_ADDRESS, - suffix: KeyType::Whitelisted, - } - .into(); - let expected = "#tnam1quqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqfgdmms/\ - whitelist/0x6b175474e89094c44da98b954eedeac495271d0f/\ - whitelisted"; - assert_eq!(expected, key.to_string()); - } - - /// Test that checking if a key is of type "cap" or "whitelisted" works. - #[test] - fn test_cap_or_whitelisted_key() { - let whitelisted_key: storage::Key = Key { - asset: DAI_ERC20_ETH_ADDRESS, - suffix: KeyType::Whitelisted, - } - .into(); - assert!(is_cap_or_whitelisted_key(&whitelisted_key)); - - let cap_key: storage::Key = Key { - asset: DAI_ERC20_ETH_ADDRESS, - suffix: KeyType::Cap, - } - .into(); - assert!(is_cap_or_whitelisted_key(&cap_key)); - - let unexpected_key = { - let mut k: storage::Key = Key { - asset: DAI_ERC20_ETH_ADDRESS, - suffix: KeyType::Cap, - } - .into(); - k.segments[3] = DbKeySeg::StringSeg("abc".to_owned()); - k - }; - assert!(!is_cap_or_whitelisted_key(&unexpected_key)); - } -} diff --git a/crates/ethereum_bridge/src/storage/wrapped_erc20s.rs b/crates/ethereum_bridge/src/storage/wrapped_erc20s.rs deleted file mode 100644 index 2d019fef002..00000000000 --- a/crates/ethereum_bridge/src/storage/wrapped_erc20s.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! Functionality for accessing the multitoken subspace - -use eyre::eyre; -use namada_core::address::{Address, InternalAddress}; -pub use namada_core::eth_bridge_pool::{ - erc20_nut_address as nut, erc20_token_address as token, -}; -use namada_core::ethereum_events::EthAddress; -use namada_core::storage::{self, DbKeySeg}; -use namada_trans_token::storage_key::{ - MINTED_STORAGE_KEY, balance_key, minted_balance_key, -}; - -/// Represents the type of a key relating to a wrapped ERC20 -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub enum KeyType { - /// The key holds a wrapped ERC20 balance - Balance { - /// The owner of the balance - owner: Address, - }, - /// A type of key which tracks the total supply of some wrapped ERC20 - Supply, -} - -/// Represents a key relating to a wrapped ERC20 -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub struct Key { - /// The specific ERC20 as identified by its Ethereum address - pub asset: EthAddress, - /// The type of this key - pub suffix: KeyType, -} - -impl From<&Key> for storage::Key { - fn from(mt_key: &Key) -> Self { - let token = token(&mt_key.asset); - match &mt_key.suffix { - KeyType::Balance { owner } => balance_key(&token, owner), - KeyType::Supply => minted_balance_key(&token), - } - } -} - -/// Returns true if the given key has an ERC20 token -pub fn has_erc20_segment(key: &storage::Key) -> bool { - matches!( - key.segments.get(1), - Some(DbKeySeg::AddressSeg(Address::Internal( - InternalAddress::Erc20(_addr), - ))) - ) -} - -impl TryFrom<(&Address, &storage::Key)> for Key { - type Error = eyre::Error; - - fn try_from( - (nam_addr, key): (&Address, &storage::Key), - ) -> Result { - if !super::is_eth_bridge_key(nam_addr, key) { - return Err(eyre!("key does not belong to the EthBridge")); - } - if !has_erc20_segment(key) { - return Err(eyre!("key does not have ERC20 segment")); - } - - let asset = if let Some(DbKeySeg::AddressSeg(Address::Internal( - InternalAddress::Erc20(addr), - ))) = key.segments.get(1) - { - *addr - } else { - return Err(eyre!( - "key has an incorrect segment at index #2, expected an \ - Ethereum address" - )); - }; - - match key.segments.get(3) { - Some(DbKeySeg::AddressSeg(owner)) => { - let balance_key = Key { - asset, - suffix: KeyType::Balance { - owner: owner.clone(), - }, - }; - Ok(balance_key) - } - Some(DbKeySeg::StringSeg(segment)) - if segment == MINTED_STORAGE_KEY => - { - let supply_key = Key { - asset, - suffix: KeyType::Supply, - }; - Ok(supply_key) - } - _ => Err(eyre!( - "key has an incorrect segment at index #3, expected a string \ - segment" - )), - } - } -} - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use assert_matches::assert_matches; - use namada_core::address::testing::nam; - use namada_core::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; - - use super::*; - use crate::ADDRESS; - use crate::token::storage_key::BALANCE_STORAGE_KEY; - - const MULTITOKEN_ADDRESS: Address = - Address::Internal(InternalAddress::Multitoken); - const ARBITRARY_OWNER_ADDRESS: &str = - "tnam1qqwuj7aart6ackjfkk7486jwm2ufr4t7cq4535u4"; - - fn dai_erc20_token() -> Address { - Address::Internal(InternalAddress::Erc20(DAI_ERC20_ETH_ADDRESS)) - } - - #[test] - fn test_keys_balance() { - let token = token(&DAI_ERC20_ETH_ADDRESS); - let key = balance_key( - &token, - &Address::from_str(ARBITRARY_OWNER_ADDRESS).unwrap(), - ); - assert_matches!( - &key.segments[..], - [ - DbKeySeg::AddressSeg(multitoken_addr), - DbKeySeg::AddressSeg(token_addr), - DbKeySeg::StringSeg(balance_key_seg), - DbKeySeg::AddressSeg(owner_addr), - ] if multitoken_addr == &MULTITOKEN_ADDRESS && - token_addr == &dai_erc20_token() && - balance_key_seg == BALANCE_STORAGE_KEY && - owner_addr == &Address::decode(ARBITRARY_OWNER_ADDRESS).unwrap() - ) - } - - #[test] - fn test_keys_balance_to_string() { - let token = token(&DAI_ERC20_ETH_ADDRESS); - let key = balance_key( - &token, - &Address::from_str(ARBITRARY_OWNER_ADDRESS).unwrap(), - ); - assert_eq!( - "#tnam1pyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqej6juv/#\ - tnam1pd43w4r5azgff3zd4x9e2nhdatzf2fcapusvp8s9/balance/#\ - tnam1qqwuj7aart6ackjfkk7486jwm2ufr4t7cq4535u4", - key.to_string() - ) - } - - #[test] - fn test_keys_supply() { - let token = token(&DAI_ERC20_ETH_ADDRESS); - let key = minted_balance_key(&token); - assert_matches!( - &key.segments[..], - [ - DbKeySeg::AddressSeg(multitoken_addr), - DbKeySeg::AddressSeg(token_addr), - DbKeySeg::StringSeg(balance_key_seg), - DbKeySeg::StringSeg(supply_key_seg), - ] if multitoken_addr == &MULTITOKEN_ADDRESS && - token_addr == &dai_erc20_token() && - balance_key_seg == BALANCE_STORAGE_KEY && - supply_key_seg == MINTED_STORAGE_KEY - ) - } - - #[test] - fn test_keys_supply_to_string() { - let token = token(&DAI_ERC20_ETH_ADDRESS); - let key = minted_balance_key(&token); - assert_eq!( - "#tnam1pyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqej6juv/#\ - tnam1pd43w4r5azgff3zd4x9e2nhdatzf2fcapusvp8s9/balance/minted", - key.to_string(), - ) - } - - #[test] - fn test_from_multitoken_key_for_key() { - // supply key - let wdai_supply = Key { - asset: DAI_ERC20_ETH_ADDRESS, - suffix: KeyType::Supply, - }; - let key: storage::Key = (&wdai_supply).into(); - assert_matches!( - &key.segments[..], - [ - DbKeySeg::AddressSeg(multitoken_addr), - DbKeySeg::AddressSeg(token_addr), - DbKeySeg::StringSeg(balance_key_seg), - DbKeySeg::StringSeg(supply_key_seg), - ] if multitoken_addr == &MULTITOKEN_ADDRESS && - token_addr == &dai_erc20_token() && - balance_key_seg == BALANCE_STORAGE_KEY && - supply_key_seg == MINTED_STORAGE_KEY - ); - - // balance key - let wdai_balance = Key { - asset: DAI_ERC20_ETH_ADDRESS, - suffix: KeyType::Balance { - owner: Address::from_str(ARBITRARY_OWNER_ADDRESS).unwrap(), - }, - }; - let key: storage::Key = (&wdai_balance).into(); - assert_matches!( - &key.segments[..], - [ - DbKeySeg::AddressSeg(multitoken_addr), - DbKeySeg::AddressSeg(token_addr), - DbKeySeg::StringSeg(balance_key_seg), - DbKeySeg::AddressSeg(owner_addr), - ] if multitoken_addr == &MULTITOKEN_ADDRESS && - token_addr == &dai_erc20_token() && - balance_key_seg == BALANCE_STORAGE_KEY && - owner_addr == &Address::decode(ARBITRARY_OWNER_ADDRESS).unwrap() - ); - } - - #[test] - fn test_try_from_key_for_multitoken_key_supply() { - // supply key - let key = storage::Key::from_str(&format!( - "#{}/#{}/balance/{}", - MULTITOKEN_ADDRESS, - dai_erc20_token(), - MINTED_STORAGE_KEY, - )) - .expect("Should be able to construct key for test"); - - let result: Result = Key::try_from((&nam(), &key)); - - let mt_key = match result { - Ok(mt_key) => mt_key, - Err(error) => { - panic!( - "Could not convert key {:?} to MultitokenKey: {:?}", - key, error - ) - } - }; - - assert_eq!(mt_key.asset, DAI_ERC20_ETH_ADDRESS); - assert_eq!(mt_key.suffix, KeyType::Supply); - } - - #[test] - fn test_try_from_key_for_multitoken_key_balance() { - // supply key - let key = storage::Key::from_str(&format!( - "#{}/#{}/balance/#{}", - ADDRESS, - dai_erc20_token(), - ARBITRARY_OWNER_ADDRESS - )) - .expect("Should be able to construct key for test"); - - let result: Result = Key::try_from((&nam(), &key)); - - let mt_key = match result { - Ok(mt_key) => mt_key, - Err(error) => { - panic!( - "Could not convert key {:?} to MultitokenKey: {:?}", - key, error - ) - } - }; - - assert_eq!(mt_key.asset, DAI_ERC20_ETH_ADDRESS); - assert_eq!( - mt_key.suffix, - KeyType::Balance { - owner: Address::from_str(ARBITRARY_OWNER_ADDRESS).unwrap() - } - ); - } - - #[test] - fn test_has_erc20_segment() { - let key = storage::Key::from_str(&format!( - "#{}/#{}/balance/#{}", - ADDRESS, - dai_erc20_token(), - ARBITRARY_OWNER_ADDRESS - )) - .expect("Should be able to construct key for test"); - - assert!(has_erc20_segment(&key)); - - let key = storage::Key::from_str(&format!( - "#{}/#{}/balance/{}", - ADDRESS, - dai_erc20_token(), - MINTED_STORAGE_KEY, - )) - .expect("Should be able to construct key for test"); - - assert!(has_erc20_segment(&key)); - - let key = storage::Key::from_str(&format!( - "#{}/#{}", - MULTITOKEN_ADDRESS, - dai_erc20_token() - )) - .expect("Should be able to construct key for test"); - - assert!(has_erc20_segment(&key)); - } -} diff --git a/crates/ethereum_bridge/src/test_utils.rs b/crates/ethereum_bridge/src/test_utils.rs deleted file mode 100644 index 572a3137ada..00000000000 --- a/crates/ethereum_bridge/src/test_utils.rs +++ /dev/null @@ -1,337 +0,0 @@ -//! Test utilities for the Ethereum bridge crate. - -#![allow(clippy::arithmetic_side_effects)] - -use std::num::NonZeroU64; - -use namada_account::protocol_pk_key; -use namada_core::address::testing::wnam; -use namada_core::address::{self, Address}; -use namada_core::chain::BlockHeight; -use namada_core::collections::HashMap; -use namada_core::dec::Dec; -use namada_core::ethereum_events::EthAddress; -use namada_core::keccak::KeccakHash; -use namada_core::key::{self, RefTo}; -use namada_core::storage::Key; -use namada_proof_of_stake::parameters::OwnedPosParams; -use namada_proof_of_stake::types::GenesisValidator; -use namada_proof_of_stake::{ - BecomeValidator, become_validator, bond_tokens, - compute_and_store_total_consensus_stake, staking_token_address, -}; -use namada_state::testing::TestState; -use namada_storage::{StorageRead, StorageWrite}; -use namada_trans_token as token; -use namada_trans_token::credit_tokens; - -use crate::storage::bridge_pool::get_key_from_hash; -use crate::storage::parameters::{ - ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, - UpgradeableContract, -}; -use crate::storage::whitelist; - -/// Validator keys used for testing purposes. -pub struct TestValidatorKeys { - /// Consensus keypair. - pub consensus: key::common::SecretKey, - /// Protocol keypair. - pub protocol: key::common::SecretKey, - /// Ethereum hot keypair. - pub eth_bridge: key::common::SecretKey, - /// Ethereum cold keypair. - pub eth_gov: key::common::SecretKey, -} - -impl TestValidatorKeys { - /// Generate a new test wallet. - #[inline] - pub fn generate() -> Self { - TestValidatorKeys { - consensus: key::common::SecretKey::Ed25519( - key::testing::gen_keypair::(), - ), - protocol: key::common::SecretKey::Ed25519( - key::testing::gen_keypair::(), - ), - eth_bridge: key::common::SecretKey::Secp256k1( - key::testing::gen_keypair::(), - ), - eth_gov: key::common::SecretKey::Secp256k1( - key::testing::gen_keypair::(), - ), - } - } -} - -/// Set up a [`TestState`] initialized at genesis with a single -/// validator. -/// -/// The validator's address is [`address::testing::established_address_1`]. -#[inline] -pub fn setup_default_storage() --> (TestState, HashMap) { - let mut state = TestState::default(); - let all_keys = init_default_storage(&mut state); - (state, all_keys) -} - -/// Set up a [`TestState`] initialized at genesis with -/// [`default_validator`]. -#[inline] -pub fn init_default_storage( - state: &mut TestState, -) -> HashMap { - init_storage_with_validators( - state, - HashMap::from_iter([default_validator()]), - ) -} - -/// Default validator used in tests. -/// -/// The validator's address is [`address::testing::established_address_1`], -/// and its voting power is proportional to the stake of 100 NAM. -#[inline] -pub fn default_validator() -> (Address, token::Amount) { - let addr = address::testing::established_address_1(); - let voting_power = token::Amount::native_whole(100); - (addr, voting_power) -} - -/// Writes a dummy [`EthereumBridgeParams`] to the given [`TestState`], and -/// returns it. -pub fn bootstrap_ethereum_bridge( - state: &mut TestState, -) -> EthereumBridgeParams { - let config = EthereumBridgeParams { - // start with empty erc20 whitelist - erc20_whitelist: vec![], - eth_start_height: Default::default(), - min_confirmations: MinimumConfirmations::from(unsafe { - // SAFETY: The only way the API contract of `NonZeroU64` can - // be violated is if we construct values - // of this type using 0 as argument. - NonZeroU64::new_unchecked(10) - }), - contracts: Contracts { - native_erc20: wnam(), - bridge: UpgradeableContract { - address: EthAddress([2; 20]), - version: ContractVersion::default(), - }, - }, - }; - config.init_storage(state); - config -} - -/// Whitelist metadata to pass to [`whitelist_tokens`]. -pub struct WhitelistMeta { - /// Token cap. - pub cap: token::Amount, - /// Token denomination. - pub denom: u8, -} - -/// Whitelist the given Ethereum tokens. -pub fn whitelist_tokens(state: &mut TestState, token_list: L) -where - L: Into>, -{ - for (asset, WhitelistMeta { cap, denom }) in token_list.into() { - let cap_key = whitelist::Key { - asset, - suffix: whitelist::KeyType::Cap, - } - .into(); - state.write(&cap_key, cap).expect("Test failed"); - - let whitelisted_key = whitelist::Key { - asset, - suffix: whitelist::KeyType::Whitelisted, - } - .into(); - state.write(&whitelisted_key, true).expect("Test failed"); - - let denom_key = whitelist::Key { - asset, - suffix: whitelist::KeyType::Denomination, - } - .into(); - state.write(&denom_key, denom).expect("Test failed"); - } -} - -/// Returns the number of keys in `storage` which have values present. -pub fn stored_keys_count(state: &TestState) -> usize { - let root = Key { segments: vec![] }; - state.iter_prefix(&root).expect("Test failed").count() -} - -/// Set up a [`TestState`] initialized at genesis with the given -/// validators. -pub fn setup_storage_with_validators( - consensus_validators: HashMap, -) -> (TestState, HashMap) { - let mut state = TestState::default(); - let all_keys = - init_storage_with_validators(&mut state, consensus_validators); - (state, all_keys) -} - -/// Set up a [`TestState`] initialized at genesis with the given -/// validators. -pub fn init_storage_with_validators( - state: &mut TestState, - consensus_validators: HashMap, -) -> HashMap { - // set last height to a reasonable value; - // it should allow vote extensions to be cast - state.in_mem_mut().block.height = 1.into(); - - let mut all_keys = HashMap::new(); - let validators: Vec<_> = consensus_validators - .into_iter() - .map(|(address, tokens)| { - let keys = TestValidatorKeys::generate(); - let consensus_key = keys.consensus.ref_to(); - let protocol_key = keys.protocol.ref_to(); - let eth_cold_key = keys.eth_gov.ref_to(); - let eth_hot_key = keys.eth_bridge.ref_to(); - all_keys.insert(address.clone(), keys); - GenesisValidator { - address, - tokens, - consensus_key, - protocol_key, - eth_cold_key, - eth_hot_key, - commission_rate: Dec::new(5, 2).unwrap(), - max_commission_rate_change: Dec::new(1, 2).unwrap(), - metadata: Default::default(), - } - }) - .collect(); - - namada_proof_of_stake::test_utils::test_init_genesis::< - _, - namada_parameters::Store<_>, - namada_governance::Store<_>, - namada_trans_token::Store<_>, - >( - state, - OwnedPosParams::default(), - validators.into_iter(), - 0.into(), - ) - .expect("Test failed"); - bootstrap_ethereum_bridge(state); - - for (validator, keys) in all_keys.iter() { - let protocol_key = keys.protocol.ref_to(); - state - .write(&protocol_pk_key(validator), protocol_key) - .expect("Test failed"); - } - // Initialize pred_epochs to the current height - let height = state.in_mem().block.height; - state.in_mem_mut().block.pred_epochs.new_epoch(height); - state.commit_block().expect("Test failed"); - state.in_mem_mut().block.height += 1; - - all_keys -} - -/// Commit a bridge pool root at a given height -/// to storage. -/// -/// N.B. assumes the bridge pool is empty. -pub fn commit_bridge_pool_root_at_height( - state: &mut TestState, - root: &KeccakHash, - height: BlockHeight, -) { - state.in_mem_mut().block.height = height; - state.write(&get_key_from_hash(root), height).unwrap(); - state.commit_block().unwrap(); - state.delete(&get_key_from_hash(root)).unwrap(); -} - -/// Append validators to storage at the current epoch -/// offset by pipeline length. -pub fn append_validators_to_storage( - state: &mut TestState, - consensus_validators: HashMap, -) -> HashMap { - let current_epoch = state.in_mem().get_current_epoch().0; - - let mut all_keys = HashMap::new(); - let params = namada_proof_of_stake::storage::read_pos_params::< - _, - namada_governance::Store<_>, - >(state) - .expect("Should be able to read PosParams from storage"); - - let staking_token = staking_token_address(state); - - for (validator, stake) in consensus_validators { - let keys = TestValidatorKeys::generate(); - - let consensus_key = &keys.consensus.ref_to(); - let protocol_key = &&keys.protocol.ref_to(); - let eth_cold_key = &keys.eth_gov.ref_to(); - let eth_hot_key = &keys.eth_bridge.ref_to(); - - become_validator::<_, GovStore<_>>( - state, - BecomeValidator { - params: ¶ms, - address: &validator, - consensus_key, - protocol_key, - eth_cold_key, - eth_hot_key, - current_epoch, - commission_rate: Dec::new(5, 2).unwrap(), - max_commission_rate_change: Dec::new(1, 2).unwrap(), - metadata: Default::default(), - offset_opt: Some(1), - }, - ) - .expect("Test failed"); - credit_tokens(state, &staking_token, &validator, stake) - .expect("Test failed"); - bond_tokens::<_, GovStore<_>, token::Store<_>>( - state, - None, - &validator, - stake, - current_epoch, - None, - ) - .expect("Test failed"); - - all_keys.insert(validator, keys); - } - - compute_and_store_total_consensus_stake::<_, GovStore<_>>( - state, - current_epoch + params.pipeline_len, - ) - .expect("Test failed"); - - for (validator, keys) in all_keys.iter() { - let protocol_key = keys.protocol.ref_to(); - state - .write(&protocol_pk_key(validator), protocol_key) - .expect("Test failed"); - } - state.commit_block().expect("Test failed"); - - all_keys -} - -/// Gov impl type -pub type GovStore = namada_governance::Store; diff --git a/crates/ethereum_bridge/src/vp/bridge_pool_vp.rs b/crates/ethereum_bridge/src/vp/bridge_pool_vp.rs deleted file mode 100644 index ffe10c8fde9..00000000000 --- a/crates/ethereum_bridge/src/vp/bridge_pool_vp.rs +++ /dev/null @@ -1,1911 +0,0 @@ -//! Validity predicate for the Ethereum bridge pool -//! -//! This pool holds user initiated transfers of value from -//! Namada to Ethereum. It is to act like a mempool: users -//! add in their desired transfers and their chosen amount -//! of NAM to cover Ethereum side gas fees. These transfers -//! can be relayed in batches along with Merkle proofs. -//! -//! This VP checks that additions to the pool are handled -//! correctly. This means that the appropriate data is -//! added to the pool and gas fees are submitted appropriately -//! and that tokens to be transferred are escrowed. - -use std::borrow::Cow; -use std::collections::BTreeSet; -use std::marker::PhantomData; - -use borsh::BorshDeserialize; -use namada_core::address::{Address, InternalAddress}; -use namada_core::arith::{CheckedAdd, CheckedNeg, CheckedSub, checked}; -use namada_core::booleans::BoolResultUnitExt; -use namada_core::eth_bridge_pool::{ - PendingTransfer, TransferToEthereumKind, erc20_token_address, -}; -use namada_core::ethereum_events::EthAddress; -use namada_core::hints; -use namada_core::storage::Key; -use namada_core::uint::I320; -use namada_state::ResultExt; -use namada_systems::trans_token::{self as token, Amount}; -use namada_tx::BatchedTxRef; -use namada_vp_env::{Error, Result, StorageRead, VpEnv}; - -use crate::ADDRESS as BRIDGE_ADDRESS; -use crate::storage::bridge_pool::{ - BRIDGE_POOL_ADDRESS, get_pending_key, is_bridge_pool_key, -}; -use crate::storage::eth_bridge_queries::is_bridge_active_at; -use crate::storage::parameters::read_native_erc20_address; -use crate::storage::whitelist; - -/// An [`Amount`] that has been updated with some delta value. -#[derive(Copy, Clone)] -struct AmountDelta { - /// The base [`Amount`], before applying the delta. - base: Amount, - /// The delta to be applied to the base amount. - delta: I320, -} - -impl AmountDelta { - /// Resolve the updated amount by applying the delta value. - #[inline] - fn resolve(self) -> Result { - checked!(self.delta + I320::from(self.base)).map_err(Into::into) - } -} - -/// Validity predicate for the Ethereum bridge -pub struct BridgePool<'ctx, CTX, TokenKeys> { - /// Generic types for DI - pub _marker: PhantomData<(&'ctx CTX, TokenKeys)>, -} - -impl<'ctx, CTX, TokenKeys> BridgePool<'ctx, CTX, TokenKeys> -where - CTX: VpEnv<'ctx> + namada_tx::action::Read, - TokenKeys: token::Keys, -{ - /// Run the validity predicate - pub fn validate_tx( - ctx: &'ctx CTX, - batched_tx: &BatchedTxRef<'_>, - keys_changed: &BTreeSet, - _verifiers: &BTreeSet
, - ) -> Result<()> { - tracing::debug!( - keys_changed_len = keys_changed.len(), - verifiers_len = _verifiers.len(), - "Ethereum Bridge Pool VP triggered", - ); - if !is_bridge_active_at(&ctx.pre(), ctx.get_block_epoch()?)? { - tracing::debug!( - "Rejecting transaction, since the Ethereum bridge is disabled." - ); - return Err(Error::new_const( - "Rejecting transaction, since the Ethereum bridge is disabled.", - )); - } - let Some(tx_data) = batched_tx.tx.data(batched_tx.cmt) else { - return Err(Error::new_const("No transaction data found")); - }; - let transfer: PendingTransfer = - BorshDeserialize::try_from_slice(&tx_data[..]) - .into_storage_result()?; - - let pending_key = get_pending_key(&transfer); - // check that transfer is not already in the pool - match ctx.pre().read::(&pending_key) { - Ok(Some(_)) => { - let error = Error::new_const( - "Rejecting transaction as the transfer is already in the \ - Ethereum bridge pool.", - ); - tracing::debug!("{error}"); - return Err(error); - } - // NOTE: make sure we don't erase storage errors returned by the - // ctx, as these may contain gas errors! - Err(e) => return Err(e), - _ => {} - } - for key in keys_changed.iter().filter(|k| is_bridge_pool_key(k)) { - if *key != pending_key { - let error = Error::new_alloc(format!( - "Rejecting transaction as it is attempting to change an \ - incorrect key in the Ethereum bridge pool: {key}.\n \ - Expected key: {pending_key}", - )); - tracing::debug!("{error}"); - return Err(error); - } - } - let pending: PendingTransfer = - ctx.post().read(&pending_key)?.ok_or_else(|| { - Error::new_const( - "Rejecting transaction as the transfer wasn't added to \ - the pool of pending transfers", - ) - })?; - if pending != transfer { - let error = Error::new_alloc(format!( - "An incorrect transfer was added to the Ethereum bridge pool: \ - {transfer:?}.\n Expected: {pending:?}", - )); - tracing::debug!("{error}"); - return Err(error); - } - // The deltas in the escrowed amounts we must check. - let wnam_address = read_native_erc20_address(&ctx.pre())?; - let escrow_checks = - Self::determine_escrow_checks(ctx, &wnam_address, &transfer)?; - if !escrow_checks.validate::(keys_changed) { - let error = Error::new_const( - // TODO(namada#3247): specify which storage changes are missing - // or which ones are invalid - "Invalid storage modifications in the Bridge pool", - ); - tracing::debug!("{error}"); - return Err(error); - } - // check that gas was correctly escrowed. - if !Self::check_gas_escrow( - ctx, - &wnam_address, - &transfer, - escrow_checks.gas_check, - )? { - return Err(Error::new_const( - "Gas was not correctly escrowed into the Bridge pool storage", - )); - } - // check the escrowed assets - if transfer.transfer.asset == wnam_address { - Self::check_wnam_escrow( - ctx, - &wnam_address, - &transfer, - escrow_checks.token_check, - )? - .ok_or_else(|| { - Error::new_const( - "The wrapped NAM tokens were not escrowed properly", - ) - }) - } else { - Self::check_escrowed_toks(ctx, escrow_checks.token_check)? - .ok_or_else(|| { - Error::new_alloc(format!( - "The {} tokens were not escrowed properly", - transfer.transfer.asset - )) - }) - } - .inspect(|_| { - tracing::info!( - "The Ethereum bridge pool VP accepted the transfer {:?}.", - transfer - ); - }) - .inspect_err(|err| { - tracing::debug!( - ?transfer, - reason = ?err, - "The assets of the transfer were not properly escrowed \ - into the Ethereum bridge pool." - ); - }) - } - - /// Get the change in the balance of an account - /// associated with an address - fn account_balance_delta( - ctx: &'ctx CTX, - token: &Address, - address: &Address, - ) -> Result> { - let account_key = TokenKeys::balance_key(token, address); - let before: Amount = ctx - .pre() - .read(&account_key) - .map_err(|error| { - tracing::warn!(?error, %account_key, "reading pre value"); - error - })? - // NB: the previous balance of the given account might - // have been null. this is valid if the account is - // being credited, such as when we escrow gas under - // the Bridge pool - .unwrap_or_default(); - let after: Amount = match ctx.post().read(&account_key)? { - Some(after) => after, - None => { - tracing::warn!(%account_key, "no post value"); - return Ok(None); - } - }; - Ok(Some(AmountDelta { - base: before, - delta: checked!(I320::from(after) - I320::from(before)).map_err( - |error| { - tracing::warn!(?error, %account_key, "reading pre value"); - error - }, - )?, - })) - } - - /// Check that the correct amount of tokens were sent - /// from the correct account into escrow. - #[inline] - fn check_escrowed_toks( - ctx: &'ctx CTX, - delta: EscrowDelta<'_, K>, - ) -> Result { - Self::check_escrowed_toks_balance(ctx, delta) - .map(|balance| balance.is_some()) - } - - /// Check that the correct amount of tokens were sent - /// from the correct account into escrow, and return - /// the updated escrow balance. - fn check_escrowed_toks_balance( - ctx: &'ctx CTX, - delta: EscrowDelta<'_, K>, - ) -> Result> { - let EscrowDelta { - token, - payer_account, - escrow_account, - expected_debit, - expected_credit, - .. - } = delta; - let debit = Self::account_balance_delta(ctx, &token, payer_account)?; - let credit = Self::account_balance_delta(ctx, &token, escrow_account)?; - - match (debit, credit) { - // success case - ( - Some(AmountDelta { delta: debit, .. }), - Some(escrow_balance @ AmountDelta { delta: credit, .. }), - ) if !debit.is_positive() && !credit.is_negative() => { - Ok((Some(debit) == I320::from(expected_debit).checked_neg() - && credit == I320::from(expected_credit)) - .then_some(escrow_balance)) - } - // user did not debit from their account - (Some(AmountDelta { delta, .. }), _) if !delta.is_negative() => { - tracing::debug!( - "The account {} was not debited.", - payer_account - ); - Ok(None) - } - // user did not credit escrow account - (_, Some(AmountDelta { delta, .. })) if !delta.is_positive() => { - tracing::debug!( - "The Ethereum bridge pool's escrow was not credited from \ - account {}.", - payer_account - ); - Ok(None) - } - // some other error occurred while calculating - // balance deltas - _ => Err(Error::AllocMessage(format!( - "Could not calculate the balance delta for {}", - payer_account - ))), - } - } - - /// Check that the gas was correctly escrowed. - fn check_gas_escrow( - ctx: &'ctx CTX, - wnam_address: &EthAddress, - transfer: &PendingTransfer, - gas_check: EscrowDelta<'_, GasCheck>, - ) -> Result { - if hints::unlikely( - *gas_check.token == erc20_token_address(wnam_address), - ) { - // NB: this should never be possible: protocol tx state updates - // never result in wNAM ERC20s being minted - tracing::error!( - ?transfer, - "Attempted to pay Bridge pool fees with wrapped NAM." - ); - return Ok(false); - } - if matches!( - &*gas_check.token, - Address::Internal(InternalAddress::Nut(_)) - ) { - tracing::debug!( - ?transfer, - "The gas fees of the transfer cannot be paid in NUTs." - ); - return Ok(false); - } - if !Self::check_escrowed_toks(ctx, gas_check)? { - tracing::debug!( - ?transfer, - "The gas fees of the transfer were not properly escrowed into \ - the Ethereum bridge pool." - ); - return Ok(false); - } - Ok(true) - } - - /// Validate a wrapped NAM transfer to Ethereum. - fn check_wnam_escrow( - ctx: &'ctx CTX, - &wnam_address: &EthAddress, - transfer: &PendingTransfer, - token_check: EscrowDelta<'_, TokenCheck>, - ) -> Result { - if hints::unlikely(matches!( - &transfer.transfer.kind, - TransferToEthereumKind::Nut - )) { - // NB: this should never be possible: protocol tx state updates - // never result in wNAM NUTs being minted. in turn, this means - // that users should never hold wNAM NUTs. doesn't hurt to add - // the extra check to the vp, though - tracing::error!( - ?transfer, - "Attempted to add a wNAM NUT transfer to the Bridge pool" - ); - return Ok(false); - } - - let wnam_whitelisted = { - let key = whitelist::Key { - asset: wnam_address, - suffix: whitelist::KeyType::Whitelisted, - } - .into(); - ctx.pre().read(&key)?.unwrap_or(false) - }; - if !wnam_whitelisted { - tracing::debug!( - ?transfer, - "Wrapped NAM transfers are currently disabled" - ); - return Ok(false); - } - - // if we are going to mint wNam on Ethereum, the appropriate - // amount of Nam must be escrowed in the Ethereum bridge VP's - // storage. - let escrowed_balance = - match Self::check_escrowed_toks_balance(ctx, token_check)? { - Some(balance) => balance.resolve()?, - None => return Ok(false), - }; - - let wnam_cap: Amount = { - let key = whitelist::Key { - asset: wnam_address, - suffix: whitelist::KeyType::Cap, - } - .into(); - ctx.pre().read(&key)?.unwrap_or_default() - }; - if escrowed_balance > I320::from(wnam_cap) { - tracing::debug!( - ?transfer, - escrowed_nam = %escrowed_balance.to_string_native(), - wnam_cap = %wnam_cap.to_string_native(), - "The balance of the escrow account exceeds the amount \ - of NAM that is allowed to cross the Ethereum bridge" - ); - return Ok(false); - } - - Ok(true) - } - - /// Determine the debit and credit amounts that should be checked. - fn determine_escrow_checks<'trans>( - ctx: &'ctx CTX, - wnam_address: &EthAddress, - transfer: &'trans PendingTransfer, - ) -> Result> { - let tok_is_native_asset = &transfer.transfer.asset == wnam_address; - - // NB: this comparison is not enough to check - // if NAM is being used for both tokens and gas - // fees, since wrapped NAM will have a different - // token address - let same_token_and_gas_erc20 = - transfer.token_address() == transfer.gas_fee.token; - - let (expected_gas_debit, expected_token_debit) = { - // NB: there is a corner case where the gas fees and escrowed - // tokens are debited from the same address, when the gas fee - // payer and token sender are the same, and the underlying - // transferred assets are the same - let same_sender_and_fee_payer = - transfer.gas_fee.payer == transfer.transfer.sender; - let gas_is_native_asset = - transfer.gas_fee.token == ctx.get_native_token()?; - let gas_and_token_is_native_asset = - gas_is_native_asset && tok_is_native_asset; - let same_token_and_gas_asset = - gas_and_token_is_native_asset || same_token_and_gas_erc20; - let same_debited_address = - same_sender_and_fee_payer && same_token_and_gas_asset; - - if same_debited_address { - let debit = sum_gas_and_token_amounts(transfer)?; - (debit, debit) - } else { - (transfer.gas_fee.amount, transfer.transfer.amount) - } - }; - let (expected_gas_credit, expected_token_credit) = { - // NB: there is a corner case where the gas fees and escrowed - // tokens are credited to the same address, when the underlying - // transferred assets are the same (unless the asset is NAM) - let same_credited_address = same_token_and_gas_erc20; - - if same_credited_address { - let credit = sum_gas_and_token_amounts(transfer)?; - (credit, credit) - } else { - (transfer.gas_fee.amount, transfer.transfer.amount) - } - }; - let (token_check_addr, token_check_escrow_acc) = if tok_is_native_asset - { - // when minting wrapped NAM on Ethereum, escrow to the Ethereum - // bridge address, and draw from NAM token accounts - let token = Cow::Owned(ctx.get_native_token()?); - let escrow_account = &BRIDGE_ADDRESS; - (token, escrow_account) - } else { - // otherwise, draw from ERC20/NUT wrapped asset token accounts, - // and escrow to the Bridge pool address - let token = Cow::Owned(transfer.token_address()); - let escrow_account = &BRIDGE_POOL_ADDRESS; - (token, escrow_account) - }; - - Ok(EscrowCheck { - gas_check: EscrowDelta { - // NB: it's fine to not check for wrapped NAM here, - // as users won't hold wrapped NAM tokens in practice, - // anyway - token: Cow::Borrowed(&transfer.gas_fee.token), - payer_account: &transfer.gas_fee.payer, - escrow_account: &BRIDGE_POOL_ADDRESS, - expected_debit: expected_gas_debit, - expected_credit: expected_gas_credit, - transferred_amount: &transfer.gas_fee.amount, - _kind: PhantomData, - }, - token_check: EscrowDelta { - token: token_check_addr, - payer_account: &transfer.transfer.sender, - escrow_account: token_check_escrow_acc, - expected_debit: expected_token_debit, - expected_credit: expected_token_credit, - transferred_amount: &transfer.transfer.amount, - _kind: PhantomData, - }, - }) - } -} - -/// Helper struct for handling the different escrow -/// checking scenarios. -struct EscrowDelta<'ctx, KIND> { - token: Cow<'ctx, Address>, - payer_account: &'ctx Address, - escrow_account: &'ctx Address, - expected_debit: Amount, - expected_credit: Amount, - transferred_amount: &'ctx Amount, - _kind: PhantomData<*const KIND>, -} - -impl EscrowDelta<'_, KIND> { - /// Validate an [`EscrowDelta`]. - /// - /// # Conditions for validation - /// - /// If the transferred amount in the [`EscrowDelta`] is nil, - /// then no keys could have been changed. If the transferred - /// amount is greater than zero, then the appropriate escrow - /// keys must have been written to by some wasm tx. - #[inline] - fn validate( - &self, - changed_keys: &BTreeSet, - ) -> bool { - if hints::unlikely(self.transferred_amount_is_nil()) { - self.check_escrow_keys_unchanged::(changed_keys) - } else { - self.check_escrow_keys_changed::(changed_keys) - } - } - - /// Check if all required escrow keys in `changed_keys` were modified. - #[inline] - fn check_escrow_keys_changed( - &self, - changed_keys: &BTreeSet, - ) -> bool { - let EscrowDelta { - token, - payer_account, - escrow_account, - .. - } = self; - - let owner_key = TokenKeys::balance_key(token, payer_account); - let escrow_key = TokenKeys::balance_key(token, escrow_account); - - changed_keys.contains(&owner_key) && changed_keys.contains(&escrow_key) - } - - /// Check if no escrow keys in `changed_keys` were modified. - #[inline] - fn check_escrow_keys_unchanged( - &self, - changed_keys: &BTreeSet, - ) -> bool { - let EscrowDelta { - token, - payer_account, - escrow_account, - .. - } = self; - - let owner_key = TokenKeys::balance_key(token, payer_account); - let escrow_key = TokenKeys::balance_key(token, escrow_account); - - !changed_keys.contains(&owner_key) - && !changed_keys.contains(&escrow_key) - } - - /// Check if the amount transferred to escrow is nil. - #[inline] - fn transferred_amount_is_nil(&self) -> bool { - let EscrowDelta { - transferred_amount, .. - } = self; - transferred_amount.is_zero() - } -} - -/// There are two checks we must do when minting wNam. -/// -/// 1. Check that gas fees were escrowed. -/// 2. Check that the Nam to back wNam was escrowed. -struct EscrowCheck<'ctx> { - gas_check: EscrowDelta<'ctx, GasCheck>, - token_check: EscrowDelta<'ctx, TokenCheck>, -} - -impl EscrowCheck<'_> { - #[inline] - fn validate( - &self, - changed_keys: &BTreeSet, - ) -> bool { - self.gas_check.validate::(changed_keys) - && self.token_check.validate::(changed_keys) - } -} - -/// Perform a gas check. -enum GasCheck {} - -/// Perform a token check. -enum TokenCheck {} - -/// Sum gas and token amounts on a pending transfer, checking for overflows. -#[inline] -fn sum_gas_and_token_amounts(transfer: &PendingTransfer) -> Result { - transfer - .gas_fee - .amount - .checked_add(transfer.transfer.amount) - .ok_or_else(|| { - Error::new_const( - "Addition overflowed adding gas fee + transfer amount.", - ) - }) -} - -#[allow(clippy::arithmetic_side_effects)] -#[cfg(test)] -mod test_bridge_pool_vp { - use std::cell::RefCell; - use std::env::temp_dir; - - use namada_core::address::testing::{nam, wnam}; - use namada_core::borsh::BorshSerializeExt; - use namada_core::eth_bridge_pool::{GasFee, TransferToEthereum}; - use namada_core::hash::Hash; - use namada_gas::{GasMeterKind, TxGasMeter, VpGasMeter}; - use namada_state::testing::TestState; - use namada_state::write_log::WriteLog; - use namada_state::{StateRead, StorageWrite, TxIndex}; - use namada_trans_token::storage_key::balance_key; - use namada_tx::Tx; - use namada_tx::data::TxType; - use namada_vm::WasmCacheRwAccess; - use namada_vm::wasm::VpCache; - use namada_vm::wasm::run::VpEvalWasm; - use namada_vp::native_vp; - - use super::*; - use crate::storage::bridge_pool::get_signed_root_key; - use crate::storage::parameters::{ - Contracts, EthereumBridgeParams, UpgradeableContract, - }; - use crate::storage::wrapped_erc20s; - - type CA = WasmCacheRwAccess; - type Eval = VpEvalWasm<::D, ::H, CA>; - type Ctx<'ctx, S> = native_vp::Ctx<'ctx, S, VpCache, Eval>; - type TokenKeys = namada_token::Store<()>; - type BridgePool<'ctx, S> = super::BridgePool<'ctx, Ctx<'ctx, S>, TokenKeys>; - - /// The amount of NAM Bertha has - const ASSET: EthAddress = EthAddress([0; 20]); - const BERTHA_WEALTH: u64 = 1_000_000; - const BERTHA_TOKENS: u64 = 10_000; - const DAES_NUTS: u64 = 10_000; - const DAEWONS_GAS: u64 = 1_000_000; - const ESCROWED_AMOUNT: u64 = 1_000; - const ESCROWED_TOKENS: u64 = 1_000; - const ESCROWED_NUTS: u64 = 1_000; - const GAS_FEE: u64 = 100; - const TOKENS: u64 = 100; - - /// A set of balances for an address - struct Balance { - /// The address of the Ethereum asset. - asset: EthAddress, - /// NUT or ERC20 Ethereum asset kind. - kind: TransferToEthereumKind, - /// The owner of the ERC20 assets. - owner: Address, - /// The gas to escrow under the Bridge pool. - gas: Amount, - /// The tokens to be sent across the Ethereum bridge, - /// escrowed to the Bridge pool account. - token: Amount, - } - - impl Balance { - fn new(kind: TransferToEthereumKind, address: Address) -> Self { - Self { - kind, - asset: ASSET, - owner: address, - gas: 0.into(), - token: 0.into(), - } - } - } - - /// An established user address for testing & development - fn bertha_address() -> Address { - Address::decode("tnam1qyctxtpnkhwaygye0sftkq28zedf774xc5a2m7st") - .expect("The token address decoding shouldn't fail") - } - - /// An implicit user address for testing & development - #[allow(dead_code)] - pub fn daewon_address() -> Address { - use namada_core::key::*; - pub fn daewon_keypair() -> common::SecretKey { - let bytes = [ - 235, 250, 15, 1, 145, 250, 172, 218, 247, 27, 63, 212, 60, 47, - 164, 57, 187, 156, 182, 144, 107, 174, 38, 81, 37, 40, 19, 142, - 68, 135, 57, 50, - ]; - let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); - ed_sk.try_to_sk().unwrap() - } - (&daewon_keypair().ref_to()).into() - } - - /// A sampled established address for tests - pub fn established_address_1() -> Address { - Address::decode("tnam1q8j5s6xp55p05yznwnftkv3kr9gjtsw3nq7x6tw5") - .expect("The token address decoding shouldn't fail") - } - - /// The bridge pool at the beginning of all tests - fn initial_pool() -> PendingTransfer { - PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: ASSET, - sender: bertha_address(), - recipient: EthAddress([0; 20]), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - } - } - - /// Create a write-log representing storage before a transfer is added to - /// the pool. - fn new_write_log(write_log: &mut WriteLog) { - *write_log = WriteLog::default(); - // setup the initial bridge pool storage - let _ = write_log - .write(&get_signed_root_key(), Hash([0; 32]).serialize_to_vec()) - .expect("Test failed"); - let transfer = initial_pool(); - let _ = write_log - .write(&get_pending_key(&transfer), transfer.serialize_to_vec()) - .expect("Test failed"); - // whitelist wnam - let key = whitelist::Key { - asset: wnam(), - suffix: whitelist::KeyType::Whitelisted, - } - .into(); - let _ = write_log - .write(&key, true.serialize_to_vec()) - .expect("Test failed"); - let key = whitelist::Key { - asset: wnam(), - suffix: whitelist::KeyType::Cap, - } - .into(); - let _ = write_log - .write(&key, Amount::max().serialize_to_vec()) - .expect("Test failed"); - // set up users with ERC20 and NUT balances - update_balances( - write_log, - Balance::new(TransferToEthereumKind::Erc20, bertha_address()), - I320::from(BERTHA_WEALTH), - I320::from(BERTHA_TOKENS), - ); - update_balances( - write_log, - Balance::new(TransferToEthereumKind::Nut, daewon_address()), - I320::from(DAEWONS_GAS), - I320::from(DAES_NUTS), - ); - // set up the initial balances of the bridge pool - update_balances( - write_log, - Balance::new(TransferToEthereumKind::Erc20, BRIDGE_POOL_ADDRESS), - I320::from(ESCROWED_AMOUNT), - I320::from(ESCROWED_TOKENS), - ); - update_balances( - write_log, - Balance::new(TransferToEthereumKind::Nut, BRIDGE_POOL_ADDRESS), - I320::from(ESCROWED_AMOUNT), - I320::from(ESCROWED_NUTS), - ); - // set up the initial balances of the ethereum bridge account - update_balances( - write_log, - Balance::new(TransferToEthereumKind::Erc20, BRIDGE_ADDRESS), - I320::from(ESCROWED_AMOUNT), - // we only care about escrowing NAM - I320::from(0), - ); - write_log.commit_tx_to_batch(); - } - - /// Update gas and token balances of an address and - /// return the keys changed - fn update_balances( - write_log: &mut WriteLog, - balance: Balance, - gas_delta: I320, - token_delta: I320, - ) -> BTreeSet { - // wnam is drawn from the same account - if balance.asset == wnam() - && !matches!(&balance.owner, Address::Internal(_)) - { - // update the balance of nam - let original_balance = std::cmp::max(balance.token, balance.gas); - let updated_balance: Amount = - (I320::from(original_balance) + gas_delta + token_delta) - .try_into() - .unwrap(); - - // write the changes to the log - let account_key = balance_key(&nam(), &balance.owner); - let _ = write_log - .write(&account_key, updated_balance.serialize_to_vec()) - .expect("Test failed"); - - // changed keys - [account_key].into() - } else { - // get the balance keys - let token_key = if balance.asset == wnam() { - // the match above guards against non-internal addresses, - // so the only logical owner here is the Ethereum bridge - // address, where we escrow NAM to, when minting wNAM on - // Ethereum - assert_eq!(balance.owner, BRIDGE_POOL_ADDRESS); - balance_key(&nam(), &BRIDGE_ADDRESS) - } else { - balance_key( - &match balance.kind { - TransferToEthereumKind::Erc20 => { - wrapped_erc20s::token(&balance.asset) - } - TransferToEthereumKind::Nut => { - wrapped_erc20s::nut(&balance.asset) - } - }, - &balance.owner, - ) - }; - let account_key = balance_key(&nam(), &balance.owner); - - // update the balance of nam - let new_gas_balance: Amount = - (I320::from(balance.gas) + gas_delta).try_into().unwrap(); - - // update the balance of tokens - let new_token_balance: Amount = (I320::from(balance.token) - + token_delta) - .try_into() - .unwrap(); - - // write the changes to the log - let _ = write_log - .write(&account_key, new_gas_balance.serialize_to_vec()) - .expect("Test failed"); - let _ = write_log - .write(&token_key, new_token_balance.serialize_to_vec()) - .expect("Test failed"); - - // return the keys changed - [account_key, token_key].into() - } - } - - /// Initialize some dummy storage for testing - fn setup_storage() -> TestState { - // a dummy config for testing - let config = EthereumBridgeParams { - erc20_whitelist: vec![], - eth_start_height: Default::default(), - min_confirmations: Default::default(), - contracts: Contracts { - native_erc20: wnam(), - bridge: UpgradeableContract { - address: EthAddress([42; 20]), - version: Default::default(), - }, - }, - }; - let mut state = TestState::default(); - config.init_storage(&mut state); - state.commit_block().expect("Test failed"); - new_write_log(state.write_log_mut()); - state.commit_block().expect("Test failed"); - state - } - - /// Setup a ctx for running native vps - fn setup_ctx<'ctx>( - tx: &'ctx Tx, - state: &'ctx TestState, - gas_meter: &'ctx RefCell, - keys_changed: &'ctx BTreeSet, - verifiers: &'ctx BTreeSet
, - ) -> Ctx<'ctx, TestState> { - let batched_tx = tx.batch_ref_first_tx().unwrap(); - Ctx::new( - &BRIDGE_POOL_ADDRESS, - state, - batched_tx.tx, - batched_tx.cmt, - &TxIndex(0), - gas_meter, - keys_changed, - verifiers, - VpCache::new(temp_dir(), 100usize), - GasMeterKind::MutGlobal, - ) - } - - enum Expect { - Accepted, - Rejected, - } - - /// Helper function that tests various ways gas can be escrowed, - /// either correctly or incorrectly, is handled appropriately - fn assert_bridge_pool( - payer_gas_delta: I320, - gas_escrow_delta: I320, - payer_delta: I320, - escrow_delta: I320, - insert_transfer: F, - expect: Expect, - ) where - F: FnOnce(&mut PendingTransfer, &mut WriteLog) -> BTreeSet, - { - // setup - let mut state = setup_storage(); - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - - // the transfer to be added to the pool - let mut transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: ASSET, - sender: bertha_address(), - recipient: EthAddress([1; 20]), - amount: TOKENS.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: GAS_FEE.into(), - payer: bertha_address(), - }, - }; - // add transfer to pool - let mut keys_changed = - insert_transfer(&mut transfer, state.write_log_mut()); - - // change Bertha's balances - let mut new_keys_changed = update_balances( - state.write_log_mut(), - Balance { - asset: transfer.transfer.asset, - kind: TransferToEthereumKind::Erc20, - owner: bertha_address(), - gas: BERTHA_WEALTH.into(), - token: BERTHA_TOKENS.into(), - }, - payer_gas_delta, - payer_delta, - ); - keys_changed.append(&mut new_keys_changed); - - // change the bridge pool balances - let mut new_keys_changed = update_balances( - state.write_log_mut(), - Balance { - asset: transfer.transfer.asset, - kind: TransferToEthereumKind::Erc20, - owner: BRIDGE_POOL_ADDRESS, - gas: ESCROWED_AMOUNT.into(), - token: ESCROWED_TOKENS.into(), - }, - gas_escrow_delta, - escrow_delta, - ); - keys_changed.append(&mut new_keys_changed); - let verifiers = BTreeSet::default(); - // create the data to be given to the vp - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = setup_ctx(&tx, &state, &gas_meter, &keys_changed, &verifiers); - - let mut tx = Tx::new(state.in_mem().chain_id.clone(), None); - tx.add_data(transfer); - - let tx = tx.batch_ref_first_tx().unwrap(); - let res = BridgePool::validate_tx(&ctx, &tx, &keys_changed, &verifiers); - match (expect, res) { - (Expect::Accepted, Ok(())) => (), - (Expect::Accepted, Err(err)) => { - panic!("Expected VP success, but got: {err}") - } - (Expect::Rejected, Err(_)) => (), - (Expect::Rejected, Ok(())) => { - panic!("Expected VP failure, but the tx was accepted") - } - } - } - - /// Test adding a transfer to the pool and escrowing gas passes vp - #[test] - fn test_happy_flow() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Accepted, - ); - } - - /// Test that if the balance for the gas payer - /// was not correctly adjusted, reject - #[test] - fn test_incorrect_gas_withdrawn() { - assert_bridge_pool( - -I320::from(10), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that if the gas payer's balance - /// does not decrease, we reject the tx - #[test] - fn test_payer_balance_must_decrease() { - assert_bridge_pool( - I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that if the gas amount escrowed is incorrect, - /// the tx is rejected - #[test] - fn test_incorrect_gas_deposited() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(10), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that if the number of tokens debited - /// from one account does not equal the amount - /// credited the other, the tx is rejected - #[test] - fn test_incorrect_token_deltas() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(10), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that if the number of tokens transferred - /// is incorrect, the tx is rejected - #[test] - fn test_incorrect_tokens_escrowed() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(10), - I320::from(10), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that the amount of gas escrowed increases, - /// otherwise the tx is rejected. - #[test] - fn test_escrowed_gas_must_increase() { - assert_bridge_pool( - -I320::from(GAS_FEE), - -I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that the amount of tokens escrowed in the - /// bridge pool is positive. - #[test] - fn test_escrowed_tokens_must_increase() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - I320::from(TOKENS), - -I320::from(TOKENS), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that if the transfer was not added to the - /// pool, the vp rejects - #[test] - fn test_not_adding_transfer_rejected() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, _| BTreeSet::from([get_pending_key(transfer)]), - Expect::Rejected, - ); - } - - /// Test that if the wrong transaction was added - /// to the pool, it is rejected. - #[test] - fn test_add_wrong_transfer() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - let t = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - sender: bertha_address(), - recipient: EthAddress([11; 20]), - amount: 100.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: GAS_FEE.into(), - payer: bertha_address(), - }, - }; - let _ = log - .write(&get_pending_key(transfer), t.serialize_to_vec()) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that if the wrong transaction was added - /// to the pool, it is rejected. - #[test] - fn test_add_wrong_key() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - let t = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - sender: bertha_address(), - recipient: EthAddress([11; 20]), - amount: 100.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: GAS_FEE.into(), - payer: bertha_address(), - }, - }; - let _ = log - .write(&get_pending_key(&t), transfer.serialize_to_vec()) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that no tx may alter the storage containing - /// the signed merkle root. - #[test] - fn test_signed_merkle_root_changes_rejected() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([ - get_pending_key(transfer), - get_signed_root_key(), - ]) - }, - Expect::Rejected, - ); - } - - /// Test that adding a transfer to the pool - /// that is already in the pool fails. - #[test] - fn test_adding_transfer_twice_fails() { - // setup - let mut state = setup_storage(); - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - - // the transfer to be added to the pool - let transfer = initial_pool(); - - // add transfer to pool - let mut keys_changed = { - let _ = state - .write_log_mut() - .write(&get_pending_key(&transfer), transfer.serialize_to_vec()) - .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) - }; - - // update Bertha's balances - let mut new_keys_changed = update_balances( - state.write_log_mut(), - Balance { - asset: ASSET, - kind: TransferToEthereumKind::Erc20, - owner: bertha_address(), - gas: BERTHA_WEALTH.into(), - token: BERTHA_TOKENS.into(), - }, - -I320::from(GAS_FEE), - -I320::from(TOKENS), - ); - keys_changed.append(&mut new_keys_changed); - - // update the bridge pool balances - let mut new_keys_changed = update_balances( - state.write_log_mut(), - Balance { - asset: ASSET, - kind: TransferToEthereumKind::Erc20, - owner: BRIDGE_POOL_ADDRESS, - gas: ESCROWED_AMOUNT.into(), - token: ESCROWED_TOKENS.into(), - }, - I320::from(GAS_FEE), - I320::from(TOKENS), - ); - keys_changed.append(&mut new_keys_changed); - let verifiers = BTreeSet::default(); - - // create the data to be given to the vp - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = setup_ctx(&tx, &state, &gas_meter, &keys_changed, &verifiers); - - let mut tx = Tx::new(state.in_mem().chain_id.clone(), None); - tx.add_data(transfer); - - let tx = tx.batch_ref_first_tx().unwrap(); - let res = BridgePool::validate_tx(&ctx, &tx, &keys_changed, &verifiers); - assert!(res.is_err()); - } - - /// Test that a transfer added to the pool with zero gas fees - /// is rejected. - #[test] - fn test_zero_gas_fees_rejected() { - // setup - let mut state = setup_storage(); - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - - // the transfer to be added to the pool - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: ASSET, - sender: bertha_address(), - recipient: EthAddress([1; 20]), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - - // add transfer to pool - let mut keys_changed = { - let _ = state - .write_log_mut() - .write(&get_pending_key(&transfer), transfer.serialize_to_vec()) - .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) - }; - // We escrow 0 tokens - keys_changed.insert(balance_key( - &wrapped_erc20s::token(&ASSET), - &bertha_address(), - )); - keys_changed.insert(balance_key( - &wrapped_erc20s::token(&ASSET), - &BRIDGE_POOL_ADDRESS, - )); - - let verifiers = BTreeSet::default(); - // create the data to be given to the vp - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = setup_ctx(&tx, &state, &gas_meter, &keys_changed, &verifiers); - - let mut tx = Tx::new(state.in_mem().chain_id.clone(), None); - tx.add_data(transfer); - - let tx = tx.batch_ref_first_tx().unwrap(); - let res = BridgePool::validate_tx(&ctx, &tx, &keys_changed, &verifiers); - assert!(res.is_err()); - } - - /// Test that we can escrow Nam if we - /// want to mint wNam on Ethereum. - #[test] - fn test_minting_wnam() { - // setup - let mut state = setup_storage(); - let eb_account_key = - balance_key(&nam(), &Address::Internal(InternalAddress::EthBridge)); - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - - // the transfer to be added to the pool - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - sender: bertha_address(), - recipient: EthAddress([1; 20]), - amount: 100.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 100.into(), - payer: bertha_address(), - }, - }; - - // add transfer to pool - let mut keys_changed = { - let _ = state - .write_log_mut() - .write(&get_pending_key(&transfer), transfer.serialize_to_vec()) - .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) - }; - // We escrow 100 Nam into the bridge pool VP - // and 100 Nam in the Eth bridge VP - let account_key = balance_key(&nam(), &bertha_address()); - let _ = state - .write_log_mut() - .write( - &account_key, - Amount::from(BERTHA_WEALTH - 200).serialize_to_vec(), - ) - .expect("Test failed"); - assert!(keys_changed.insert(account_key)); - let bp_account_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - let _ = state - .write_log_mut() - .write( - &bp_account_key, - Amount::from(ESCROWED_AMOUNT + 100).serialize_to_vec(), - ) - .expect("Test failed"); - assert!(keys_changed.insert(bp_account_key)); - let _ = state - .write_log_mut() - .write( - &eb_account_key, - Amount::from(ESCROWED_AMOUNT + 100).serialize_to_vec(), - ) - .expect("Test failed"); - assert!(keys_changed.insert(eb_account_key)); - - let verifiers = BTreeSet::default(); - // create the data to be given to the vp - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = setup_ctx(&tx, &state, &gas_meter, &keys_changed, &verifiers); - - let mut tx = Tx::new(state.in_mem().chain_id.clone(), None); - tx.add_data(transfer); - - let tx = tx.batch_ref_first_tx().unwrap(); - let res = BridgePool::validate_tx(&ctx, &tx, &keys_changed, &verifiers); - assert!(res.is_ok()); - } - - /// Test that we can reject a transfer that - /// mints wNam if we don't escrow the correct - /// amount of Nam. - #[test] - fn test_reject_mint_wnam() { - // setup - let mut state = setup_storage(); - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - let eb_account_key = - balance_key(&nam(), &Address::Internal(InternalAddress::EthBridge)); - - // the transfer to be added to the pool - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - sender: bertha_address(), - recipient: EthAddress([1; 20]), - amount: 100.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 100.into(), - payer: bertha_address(), - }, - }; - - // add transfer to pool - let keys_changed = { - let _ = state - .write_log_mut() - .write(&get_pending_key(&transfer), transfer.serialize_to_vec()) - .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) - }; - // We escrow 100 Nam into the bridge pool VP - // and 100 Nam in the Eth bridge VP - let account_key = balance_key(&nam(), &bertha_address()); - let _ = state - .write_log_mut() - .write( - &account_key, - Amount::from(BERTHA_WEALTH - 200).serialize_to_vec(), - ) - .expect("Test failed"); - let bp_account_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - let _ = state - .write_log_mut() - .write( - &bp_account_key, - Amount::from(ESCROWED_AMOUNT + 100).serialize_to_vec(), - ) - .expect("Test failed"); - let _ = state - .write_log_mut() - .write(&eb_account_key, Amount::from(10).serialize_to_vec()) - .expect("Test failed"); - let verifiers = BTreeSet::default(); - - // create the data to be given to the vp - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = setup_ctx(&tx, &state, &gas_meter, &keys_changed, &verifiers); - - let mut tx = Tx::new(state.in_mem().chain_id.clone(), None); - tx.add_data(transfer); - - let tx = tx.batch_ref_first_tx().unwrap(); - let res = BridgePool::validate_tx(&ctx, &tx, &keys_changed, &verifiers); - assert!(res.is_err()); - } - - /// Test that we check escrowing Nam correctly when minting wNam - /// and the gas payer account is different from the transferring - /// account. - #[test] - fn test_mint_wnam_separate_gas_payer() { - // setup - let mut state = setup_storage(); - // initialize the eth bridge balance to 0 - let eb_account_key = - balance_key(&nam(), &Address::Internal(InternalAddress::EthBridge)); - state - .write(&eb_account_key, Amount::default()) - .expect("Test failed"); - // initialize the gas payers account - let gas_payer_balance_key = - balance_key(&nam(), &established_address_1()); - state - .write(&gas_payer_balance_key, Amount::from(BERTHA_WEALTH)) - .expect("Test failed"); - state.write_log_mut().commit_tx_to_batch(); - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - - // the transfer to be added to the pool - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - sender: bertha_address(), - recipient: EthAddress([1; 20]), - amount: 100.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 100.into(), - payer: established_address_1(), - }, - }; - - // add transfer to pool - let keys_changed = { - let _ = state - .write_log_mut() - .write(&get_pending_key(&transfer), transfer.serialize_to_vec()) - .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) - }; - // We escrow 100 Nam into the bridge pool VP - // and 100 Nam in the Eth bridge VP - let account_key = balance_key(&nam(), &bertha_address()); - let _ = state - .write_log_mut() - .write( - &account_key, - Amount::from(BERTHA_WEALTH - 100).serialize_to_vec(), - ) - .expect("Test failed"); - let _ = state - .write_log_mut() - .write( - &gas_payer_balance_key, - Amount::from(BERTHA_WEALTH - 100).serialize_to_vec(), - ) - .expect("Test failed"); - let bp_account_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - let _ = state - .write_log_mut() - .write( - &bp_account_key, - Amount::from(ESCROWED_AMOUNT + 100).serialize_to_vec(), - ) - .expect("Test failed"); - let _ = state - .write_log_mut() - .write(&eb_account_key, Amount::from(10).serialize_to_vec()) - .expect("Test failed"); - let verifiers = BTreeSet::default(); - // create the data to be given to the vp - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = setup_ctx(&tx, &state, &gas_meter, &keys_changed, &verifiers); - - let mut tx = Tx::new(state.in_mem().chain_id.clone(), None); - tx.add_data(transfer); - - let tx = tx.batch_ref_first_tx().unwrap(); - let res = BridgePool::validate_tx(&ctx, &tx, &keys_changed, &verifiers); - assert!(res.is_err()); - } - - /// Auxiliary function to test NUT functionality. - fn test_nut_aux(kind: TransferToEthereumKind, expect: Expect) { - // setup - let mut state = setup_storage(); - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - - // the transfer to be added to the pool - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind, - asset: ASSET, - sender: daewon_address(), - recipient: EthAddress([1; 20]), - amount: TOKENS.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: GAS_FEE.into(), - payer: daewon_address(), - }, - }; - - // add transfer to pool - let mut keys_changed = { - let _ = state - .write_log_mut() - .write(&get_pending_key(&transfer), transfer.serialize_to_vec()) - .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) - }; - - // update Daewon's balances - let mut new_keys_changed = update_balances( - state.write_log_mut(), - Balance { - kind, - asset: ASSET, - owner: daewon_address(), - gas: DAEWONS_GAS.into(), - token: DAES_NUTS.into(), - }, - -I320::from(GAS_FEE), - -I320::from(TOKENS), - ); - keys_changed.append(&mut new_keys_changed); - - // change the bridge pool balances - let mut new_keys_changed = update_balances( - state.write_log_mut(), - Balance { - kind, - asset: ASSET, - owner: BRIDGE_POOL_ADDRESS, - gas: ESCROWED_AMOUNT.into(), - token: ESCROWED_NUTS.into(), - }, - I320::from(GAS_FEE), - I320::from(TOKENS), - ); - keys_changed.append(&mut new_keys_changed); - - // create the data to be given to the vp - let verifiers = BTreeSet::default(); - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let ctx = setup_ctx(&tx, &state, &gas_meter, &keys_changed, &verifiers); - - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - tx.add_data(transfer); - - let tx = tx.batch_ref_first_tx().unwrap(); - let res = BridgePool::validate_tx(&ctx, &tx, &keys_changed, &verifiers); - match (expect, res) { - (Expect::Accepted, Ok(())) => (), - (Expect::Accepted, Err(err)) => { - panic!("Expected VP success, but got: {err}") - } - (Expect::Rejected, Err(_)) => (), - (Expect::Rejected, Ok(())) => { - panic!("Expected VP failure, but the tx was accepted") - } - } - } - - /// Test that the Bridge pool VP rejects a tx based on the fact - /// that an account might hold NUTs of some arbitrary Ethereum - /// asset, but not hold ERC20s. - #[test] - fn test_reject_no_erc20_balance_despite_nut_balance() { - test_nut_aux(TransferToEthereumKind::Erc20, Expect::Rejected) - } - - /// Test the happy flow of escrowing NUTs. - #[test] - fn test_escrowing_nuts_happy_flow() { - test_nut_aux(TransferToEthereumKind::Nut, Expect::Accepted) - } - - /// Test that the Bridge pool VP rejects a wNAM NUT transfer. - #[test] - fn test_bridge_pool_vp_rejects_wnam_nut() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - transfer.transfer.kind = TransferToEthereumKind::Nut; - transfer.transfer.asset = wnam(); - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Rejected, - ); - } - - /// Test that the Bridge pool VP accepts a wNAM ERC20 transfer. - #[test] - fn test_bridge_pool_vp_accepts_wnam_erc20() { - assert_bridge_pool( - -I320::from(GAS_FEE), - I320::from(GAS_FEE), - -I320::from(TOKENS), - I320::from(TOKENS), - |transfer, log| { - transfer.transfer.kind = TransferToEthereumKind::Erc20; - transfer.transfer.asset = wnam(); - let _ = log - .write( - &get_pending_key(transfer), - transfer.serialize_to_vec(), - ) - .unwrap(); - BTreeSet::from([get_pending_key(transfer)]) - }, - Expect::Accepted, - ); - } - - /// Test that the Bridge pool native VP validates transfers that - /// do not contain gas fees and no associated changed keys. - #[test] - fn test_no_gas_fees_with_no_changed_keys() { - let nam_addr = nam(); - let delta = EscrowDelta { - token: Cow::Borrowed(&nam_addr), - payer_account: &bertha_address(), - escrow_account: &BRIDGE_ADDRESS, - expected_debit: Amount::zero(), - expected_credit: Amount::zero(), - // NOTE: testing 0 amount - transferred_amount: &Amount::zero(), - // NOTE: testing gas fees - _kind: PhantomData::<*const GasCheck>, - }; - // NOTE: testing no changed keys - let empty_keys = BTreeSet::new(); - - assert!(delta.validate::(&empty_keys)); - } - - /// Test that the Bridge pool native VP rejects transfers that - /// do not contain gas fees and has associated changed keys. - #[test] - fn test_no_gas_fees_with_changed_keys() { - let nam_addr = nam(); - let delta = EscrowDelta { - token: Cow::Borrowed(&nam_addr), - payer_account: &bertha_address(), - escrow_account: &BRIDGE_ADDRESS, - expected_debit: Amount::zero(), - expected_credit: Amount::zero(), - // NOTE: testing 0 amount - transferred_amount: &Amount::zero(), - // NOTE: testing gas fees - _kind: PhantomData::<*const GasCheck>, - }; - let owner_key = balance_key(&nam_addr, &bertha_address()); - // NOTE: testing changed keys - let some_changed_keys = BTreeSet::from([owner_key]); - - assert!(!delta.validate::(&some_changed_keys)); - } - - /// Test that the Bridge pool native VP validates transfers - /// moving no value and with no associated changed keys. - #[test] - fn test_no_amount_with_no_changed_keys() { - let nam_addr = nam(); - let delta = EscrowDelta { - token: Cow::Borrowed(&nam_addr), - payer_account: &bertha_address(), - escrow_account: &BRIDGE_ADDRESS, - expected_debit: Amount::zero(), - expected_credit: Amount::zero(), - // NOTE: testing 0 amount - transferred_amount: &Amount::zero(), - // NOTE: testing token transfers - _kind: PhantomData::<*const TokenCheck>, - }; - // NOTE: testing no changed keys - let empty_keys = BTreeSet::new(); - - assert!(delta.validate::(&empty_keys)); - } - - /// Test that the Bridge pool native VP rejects transfers - /// moving no value and with associated changed keys. - #[test] - fn test_no_amount_with_changed_keys() { - let nam_addr = nam(); - let delta = EscrowDelta { - token: Cow::Borrowed(&nam_addr), - payer_account: &bertha_address(), - escrow_account: &BRIDGE_ADDRESS, - expected_debit: Amount::zero(), - expected_credit: Amount::zero(), - // NOTE: testing 0 amount - transferred_amount: &Amount::zero(), - // NOTE: testing token transfers - _kind: PhantomData::<*const TokenCheck>, - }; - let owner_key = balance_key(&nam_addr, &bertha_address()); - // NOTE: testing changed keys - let some_changed_keys = BTreeSet::from([owner_key]); - - assert!(!delta.validate::(&some_changed_keys)); - } -} diff --git a/crates/ethereum_bridge/src/vp/eth_bridge_vp.rs b/crates/ethereum_bridge/src/vp/eth_bridge_vp.rs deleted file mode 100644 index eff83cac5b4..00000000000 --- a/crates/ethereum_bridge/src/vp/eth_bridge_vp.rs +++ /dev/null @@ -1,521 +0,0 @@ -//! Validity predicate for the Ethereum bridge - -use std::collections::BTreeSet; -use std::marker::PhantomData; - -use namada_core::address::Address; -use namada_core::booleans::BoolResultUnitExt; -use namada_core::collections::HashSet; -use namada_core::storage::Key; -use namada_systems::trans_token::{self as token, Amount}; -use namada_tx::BatchedTxRef; -use namada_vp_env::{Error, Result, StorageRead, VpEnv}; - -use crate::storage; -use crate::storage::escrow_key; - -/// Validity predicate for the Ethereum bridge -pub struct EthBridge<'ctx, CTX, TokenKeys> { - /// Generic types for DI - pub _marker: PhantomData<(&'ctx CTX, TokenKeys)>, -} - -impl<'ctx, CTX, TokenKeys> EthBridge<'ctx, CTX, TokenKeys> -where - CTX: VpEnv<'ctx> + namada_tx::action::Read, - TokenKeys: token::Keys, -{ - /// Validate that a wasm transaction is permitted to change keys under this - /// account. - /// - /// We only permit increasing the escrowed balance of NAM under the Ethereum - /// bridge address, when writing to storage from wasm transactions. - /// - /// Some other changes to the storage subspace of this account are expected - /// to happen natively i.e. bypassing this validity predicate. For example, - /// changes to the `eth_msgs/...` keys. For those cases, we reject here as - /// no wasm transactions should be able to modify those keys. - pub fn validate_tx( - ctx: &'ctx CTX, - _: &BatchedTxRef<'_>, - keys_changed: &BTreeSet, - verifiers: &BTreeSet
, - ) -> Result<()> { - tracing::debug!( - keys_changed_len = keys_changed.len(), - verifiers_len = verifiers.len(), - "Ethereum Bridge VP triggered", - ); - - let native_token = ctx.get_native_token()?; - validate_changed_keys::(&native_token, keys_changed)?; - - Self::check_escrow(ctx, &native_token, verifiers) - } - - /// If the Ethereum bridge's escrow key was written to, we check - /// that the NAM balance increased and that the Bridge pool VP has - /// been triggered. - fn check_escrow( - ctx: &'ctx CTX, - native_token: &Address, - verifiers: &BTreeSet
, - ) -> Result<()> { - let escrow_key = TokenKeys::balance_key(native_token, &crate::ADDRESS); - - let escrow_pre: Amount = - ctx.pre().read(&escrow_key)?.unwrap_or_default(); - let escrow_post: Amount = ctx - .post() - .read(&escrow_key)? - .ok_or_else(|| Error::new_const("Escrow must be present"))?; - - // The amount escrowed should increase. - if escrow_pre < escrow_post { - // NB: normally, we only escrow NAM under the Ethereum bridge - // address in the context of a Bridge pool transfer - let bridge_pool_is_verifier = - verifiers.contains(&storage::bridge_pool::BRIDGE_POOL_ADDRESS); - - bridge_pool_is_verifier.ok_or_else(|| { - Error::new_const( - "Bridge pool VP was not marked as a verifier of the \ - transaction", - ) - }) - } else { - Err(Error::new_const( - "User tx attempted to decrease the amount of native tokens \ - escrowed in the Ethereum Bridge's account", - )) - } - } -} - -/// Checks if `keys_changed` represents a valid set of changed keys. -/// -/// This implies checking if two distinct keys were changed: -/// -/// 1. The Ethereum bridge escrow account's NAM balance key. -/// 2. Another account's NAM balance key. -/// -/// Any other keys changed under the Ethereum bridge account -/// are rejected. -fn validate_changed_keys( - nam_addr: &Address, - keys_changed: &BTreeSet, -) -> Result<()> { - // acquire all keys that either changed our account, or that touched - // nam balances - let keys_changed: HashSet<_> = keys_changed - .iter() - .filter(|&key| { - let changes_eth_storage = storage::has_eth_addr_segment(key); - let changes_nam_balance = - TokenKeys::is_balance_key(nam_addr, key).is_some(); - changes_nam_balance || changes_eth_storage - }) - .collect(); - if keys_changed.is_empty() { - return Err(Error::SimpleMessage( - "No keys changed under our account so this validity predicate \ - shouldn't have been triggered", - )); - } - tracing::debug!( - relevant_keys.len = keys_changed.len(), - "Found keys changed under our account" - ); - let nam_escrow_addr_modified = keys_changed.contains(&escrow_key(nam_addr)); - if !nam_escrow_addr_modified { - let error = Error::new_const( - "The native token's escrow balance should have been modified", - ); - tracing::debug!("{error}"); - return Err(error); - } - let all_keys_are_nam_balance = keys_changed - .iter() - .all(|key| TokenKeys::is_balance_key(nam_addr, key).is_some()); - if !all_keys_are_nam_balance { - let error = Error::new_const( - "Some modified keys were not a native token's balance key", - ); - tracing::debug!("{error}"); - return Err(error); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use std::cell::RefCell; - use std::env::temp_dir; - - use namada_core::address::testing::{established_address_1, nam, wnam}; - use namada_core::borsh::BorshSerializeExt; - use namada_core::ethereum_events; - use namada_core::ethereum_events::EthAddress; - use namada_gas::{GasMeterKind, TxGasMeter, VpGasMeter}; - use namada_state::testing::TestState; - use namada_state::{StateRead, StorageWrite, TxIndex}; - use namada_trans_token::storage_key::{balance_key, minted_balance_key}; - use namada_tx::data::TxType; - use namada_tx::{Tx, TxCommitments}; - use namada_vm::WasmCacheRwAccess; - use namada_vm::wasm::VpCache; - use namada_vm::wasm::run::VpEvalWasm; - use namada_vp::native_vp; - use rand::Rng; - - use super::*; - use crate::storage::bridge_pool::BRIDGE_POOL_ADDRESS; - use crate::storage::parameters::{ - Contracts, EthereumBridgeParams, UpgradeableContract, - }; - use crate::storage::wrapped_erc20s; - - const ARBITRARY_OWNER_A_ADDRESS: &str = - "tnam1qqwuj7aart6ackjfkk7486jwm2ufr4t7cq4535u4"; - const ARBITRARY_OWNER_A_INITIAL_BALANCE: u64 = 100; - const ESCROW_AMOUNT: u64 = 100; - const BRIDGE_POOL_ESCROW_INITIAL_BALANCE: u64 = 0; - - type CA = WasmCacheRwAccess; - type Eval = VpEvalWasm<::D, ::H, CA>; - type Ctx<'ctx, S> = native_vp::Ctx<'ctx, S, VpCache, Eval>; - type TokenKeys = namada_token::Store<()>; - type EthBridge<'ctx, S> = super::EthBridge<'ctx, Ctx<'ctx, S>, TokenKeys>; - - /// Return some arbitrary random key belonging to this account - fn arbitrary_key() -> Key { - let mut rng = rand::thread_rng(); - let rn = rng.r#gen::(); - storage::prefix() - .push(&format!("arbitrary key segment {}", rn)) - .expect("should always be able to construct this key") - } - - /// Initialize some dummy storage for testing - fn setup_storage() -> TestState { - let mut state = TestState::default(); - - // setup a user with a balance - let balance_key = balance_key( - &nam(), - &Address::decode(ARBITRARY_OWNER_A_ADDRESS).expect("Test failed"), - ); - state - .write( - &balance_key, - Amount::from(ARBITRARY_OWNER_A_INITIAL_BALANCE), - ) - .expect("Test failed"); - - // a dummy config for testing - let config = EthereumBridgeParams { - erc20_whitelist: vec![], - eth_start_height: Default::default(), - min_confirmations: Default::default(), - contracts: Contracts { - native_erc20: wnam(), - bridge: UpgradeableContract { - address: EthAddress([42; 20]), - version: Default::default(), - }, - }, - }; - config.init_storage(&mut state); - state.commit_block().expect("Test failed"); - state - } - - /// Setup a ctx for running native vps - fn setup_ctx<'ctx>( - tx: &'ctx Tx, - cmt: &'ctx TxCommitments, - state: &'ctx TestState, - gas_meter: &'ctx RefCell, - keys_changed: &'ctx BTreeSet, - verifiers: &'ctx BTreeSet
, - ) -> Ctx<'ctx, TestState> { - Ctx::new( - &crate::ADDRESS, - state, - tx, - cmt, - &TxIndex(0), - gas_meter, - keys_changed, - verifiers, - VpCache::new(temp_dir(), 100usize), - GasMeterKind::MutGlobal, - ) - } - - #[test] - fn test_accepts_expected_keys_changed() { - let keys_changed = BTreeSet::from([ - balance_key(&nam(), &established_address_1()), - balance_key(&nam(), &crate::ADDRESS), - ]); - - let result = validate_changed_keys::(&nam(), &keys_changed); - - assert!(result.is_ok()); - } - - #[test] - fn test_error_if_triggered_without_keys_changed() { - let keys_changed = BTreeSet::new(); - - let result = validate_changed_keys::(&nam(), &keys_changed); - - assert!(result.is_err()); - } - - #[test] - fn test_rejects_if_not_two_keys_changed() { - { - let keys_changed = BTreeSet::from_iter(vec![arbitrary_key(); 3]); - - let result = - validate_changed_keys::(&nam(), &keys_changed); - - assert!(result.is_err()); - } - { - let keys_changed = BTreeSet::from_iter(vec![ - escrow_key(&nam()), - arbitrary_key(), - arbitrary_key(), - ]); - - let result = - validate_changed_keys::(&nam(), &keys_changed); - - assert!(result.is_err()); - } - } - - #[test] - fn test_rejects_if_not_two_multitoken_keys_changed() { - { - let keys_changed = - BTreeSet::from_iter(vec![arbitrary_key(), arbitrary_key()]); - - let result = - validate_changed_keys::(&nam(), &keys_changed); - - assert!(result.is_err()); - } - - { - let keys_changed = BTreeSet::from_iter(vec![ - arbitrary_key(), - minted_balance_key(&wrapped_erc20s::token( - ðereum_events::testing::DAI_ERC20_ETH_ADDRESS, - )), - ]); - - let result = - validate_changed_keys::(&nam(), &keys_changed); - - assert!(result.is_err()); - } - - { - let keys_changed = BTreeSet::from_iter(vec![ - arbitrary_key(), - balance_key( - &wrapped_erc20s::token( - ðereum_events::testing::DAI_ERC20_ETH_ADDRESS, - ), - &Address::decode(ARBITRARY_OWNER_A_ADDRESS) - .expect("Couldn't set up test"), - ), - ]); - - let result = - validate_changed_keys::(&nam(), &keys_changed); - - assert!(result.is_err()); - } - } - - /// Test that escrowing Nam is accepted. - #[test] - fn test_escrow_nam_accepted() { - let mut state = setup_storage(); - // debit the user's balance - let account_key = balance_key( - &nam(), - &Address::decode(ARBITRARY_OWNER_A_ADDRESS).expect("Test failed"), - ); - let _ = state - .write_log_mut() - .write( - &account_key, - Amount::from(ARBITRARY_OWNER_A_INITIAL_BALANCE - ESCROW_AMOUNT) - .serialize_to_vec(), - ) - .expect("Test failed"); - - // credit the balance to the escrow - let escrow_key = balance_key(&nam(), &crate::ADDRESS); - let _ = state - .write_log_mut() - .write( - &escrow_key, - Amount::from( - BRIDGE_POOL_ESCROW_INITIAL_BALANCE + ESCROW_AMOUNT, - ) - .serialize_to_vec(), - ) - .expect("Test failed"); - - let keys_changed = BTreeSet::from([account_key, escrow_key]); - let verifiers = BTreeSet::from([BRIDGE_POOL_ADDRESS]); - - // set up the VP - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let batched_tx = tx.batch_ref_first_tx().unwrap(); - let ctx = setup_ctx( - batched_tx.tx, - batched_tx.cmt, - &state, - &gas_meter, - &keys_changed, - &verifiers, - ); - - let res = EthBridge::validate_tx( - &ctx, - &batched_tx, - &keys_changed, - &verifiers, - ); - assert!(res.is_ok()); - } - - /// Test that escrowing must increase the balance - #[test] - fn test_escrowed_nam_must_increase() { - let mut state = setup_storage(); - // debit the user's balance - let account_key = balance_key( - &nam(), - &Address::decode(ARBITRARY_OWNER_A_ADDRESS).expect("Test failed"), - ); - let _ = state - .write_log_mut() - .write( - &account_key, - Amount::from(ARBITRARY_OWNER_A_INITIAL_BALANCE - ESCROW_AMOUNT) - .serialize_to_vec(), - ) - .expect("Test failed"); - - // do not credit the balance to the escrow - let escrow_key = balance_key(&nam(), &crate::ADDRESS); - let _ = state - .write_log_mut() - .write( - &escrow_key, - Amount::from(BRIDGE_POOL_ESCROW_INITIAL_BALANCE) - .serialize_to_vec(), - ) - .expect("Test failed"); - - let keys_changed = BTreeSet::from([account_key, escrow_key]); - let verifiers = BTreeSet::from([BRIDGE_POOL_ADDRESS]); - - // set up the VP - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let batched_tx = tx.batch_ref_first_tx().unwrap(); - let ctx = setup_ctx( - batched_tx.tx, - batched_tx.cmt, - &state, - &gas_meter, - &keys_changed, - &verifiers, - ); - - let res = EthBridge::validate_tx( - &ctx, - &batched_tx, - &keys_changed, - &verifiers, - ); - assert!(res.is_err()); - } - - /// Test that the VP checks that the bridge pool vp will - /// be triggered if escrowing occurs. - #[test] - fn test_escrowing_must_trigger_bridge_pool_vp() { - let mut state = setup_storage(); - // debit the user's balance - let account_key = balance_key( - &nam(), - &Address::decode(ARBITRARY_OWNER_A_ADDRESS).expect("Test failed"), - ); - let _ = state - .write_log_mut() - .write( - &account_key, - Amount::from(ARBITRARY_OWNER_A_INITIAL_BALANCE - ESCROW_AMOUNT) - .serialize_to_vec(), - ) - .expect("Test failed"); - - // credit the balance to the escrow - let escrow_key = balance_key(&nam(), &crate::ADDRESS); - let _ = state - .write_log_mut() - .write( - &escrow_key, - Amount::from( - BRIDGE_POOL_ESCROW_INITIAL_BALANCE + ESCROW_AMOUNT, - ) - .serialize_to_vec(), - ) - .expect("Test failed"); - - let keys_changed = BTreeSet::from([account_key, escrow_key]); - let verifiers = BTreeSet::from([]); - - // set up the VP - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let batched_tx = tx.batch_ref_first_tx().unwrap(); - let ctx = setup_ctx( - batched_tx.tx, - batched_tx.cmt, - &state, - &gas_meter, - &keys_changed, - &verifiers, - ); - - let res = EthBridge::validate_tx( - &ctx, - &batched_tx, - &keys_changed, - &verifiers, - ); - assert!(res.is_err()); - } -} diff --git a/crates/ethereum_bridge/src/vp/mod.rs b/crates/ethereum_bridge/src/vp/mod.rs deleted file mode 100644 index e16646f73ef..00000000000 --- a/crates/ethereum_bridge/src/vp/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Native validity predicates for the Namada Ethereum bridge. -//! This includes both the bridge vp and the vp for the bridge -//! pool. - -mod bridge_pool_vp; -mod eth_bridge_vp; -mod nut_vp; - -pub use bridge_pool_vp::BridgePool; -pub use eth_bridge_vp::EthBridge; -pub use nut_vp::NonUsableTokens; diff --git a/crates/ethereum_bridge/src/vp/nut_vp.rs b/crates/ethereum_bridge/src/vp/nut_vp.rs deleted file mode 100644 index 40f637acf68..00000000000 --- a/crates/ethereum_bridge/src/vp/nut_vp.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! Validity predicate for Non Usable Tokens (NUTs). - -use std::collections::BTreeSet; -use std::marker::PhantomData; - -use namada_core::address::{Address, InternalAddress}; -use namada_core::booleans::BoolResultUnitExt; -use namada_core::storage::Key; -use namada_systems::trans_token::{self as token, Amount}; -use namada_tx::BatchedTxRef; -use namada_vp_env::{Error, Result, VpEnv}; - -/// Validity predicate for non-usable tokens. -/// -/// All this VP does is reject NUT transfers whose destination -/// address is not the Bridge pool escrow address. -pub struct NonUsableTokens<'ctx, CTX, TokenKeys> { - /// Generic types for DI - pub _marker: PhantomData<(&'ctx CTX, TokenKeys)>, -} - -impl<'ctx, CTX, TokenKeys> NonUsableTokens<'ctx, CTX, TokenKeys> -where - CTX: VpEnv<'ctx> + namada_tx::action::Read, - TokenKeys: token::Keys, -{ - /// Run the validity predicate - pub fn validate_tx( - ctx: &'ctx CTX, - _: &BatchedTxRef<'_>, - keys_changed: &BTreeSet, - verifiers: &BTreeSet
, - ) -> Result<()> { - tracing::debug!( - keys_changed_len = keys_changed.len(), - verifiers_len = verifiers.len(), - "Non usable tokens VP triggered", - ); - - verifiers - .contains(&Address::Internal(InternalAddress::Multitoken)) - .ok_or_else(|| { - let error = - Error::new_const("Rejecting non-multitoken transfer tx"); - tracing::debug!("{error}"); - error - })?; - - let nut_owners = keys_changed.iter().filter_map(|key| { - match TokenKeys::is_any_token_balance_key(key) { - Some([Address::Internal(InternalAddress::Nut(_)), owner]) => { - Some((key, owner)) - } - _ => None, - } - }); - - for (changed_key, token_owner) in nut_owners { - let pre: Amount = ctx.read_pre(changed_key)?.unwrap_or_default(); - let post: Amount = ctx.read_post(changed_key)?.unwrap_or_default(); - - match token_owner { - // the NUT balance of the bridge pool should increase - Address::Internal(InternalAddress::EthBridgePool) => { - if post < pre { - tracing::debug!( - %changed_key, - pre_amount = ?pre, - post_amount = ?post, - "Bridge pool balance should have increased" - ); - return Err(Error::new_alloc(format!( - "Bridge pool balance should have increased. The \ - previous balance was {pre:?}, the post balance \ - is {post:?}.", - ))); - } - } - // arbitrary addresses should have their balance decrease - _addr => { - if post > pre { - tracing::debug!( - %changed_key, - pre_amount = ?pre, - post_amount = ?post, - "Balance should have decreased" - ); - return Err(Error::new_alloc(format!( - "Balance should have decreased. The previous \ - balance was {pre:?}, the post balance is \ - {post:?}." - ))); - } - } - } - } - - Ok(()) - } -} - -#[cfg(test)] -mod test_nuts { - use std::cell::RefCell; - use std::env::temp_dir; - - use namada_core::address::testing::arb_non_internal_address; - use namada_core::borsh::BorshSerializeExt; - use namada_core::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; - use namada_core::storage::TxIndex; - use namada_gas::{GasMeterKind, TxGasMeter, VpGasMeter}; - use namada_state::testing::TestState; - use namada_state::{StateRead, StorageWrite}; - use namada_trans_token::storage_key::balance_key; - use namada_tx::Tx; - use namada_tx::data::TxType; - use namada_vm::WasmCacheRwAccess; - use namada_vm::wasm::VpCache; - use namada_vm::wasm::run::VpEvalWasm; - use namada_vp::native_vp; - use proptest::prelude::*; - - use super::*; - use crate::storage::wrapped_erc20s; - - type CA = WasmCacheRwAccess; - type Eval = VpEvalWasm<::D, ::H, CA>; - type Ctx<'ctx, S> = native_vp::Ctx<'ctx, S, VpCache, Eval>; - type TokenKeys = namada_token::Store<()>; - type NonUsableTokens<'ctx, S> = - super::NonUsableTokens<'ctx, Ctx<'ctx, S>, TokenKeys>; - - /// Run a VP check on a NUT transfer between the two provided addresses. - fn check_nut_transfer(src: Address, dst: Address) -> bool { - let nut = wrapped_erc20s::nut(&DAI_ERC20_ETH_ADDRESS); - let src_balance_key = balance_key(&nut, &src); - let dst_balance_key = balance_key(&nut, &dst); - - let state = { - let mut state = TestState::default(); - - // write initial balances - state - .write(&src_balance_key, Amount::from(200_u64)) - .expect("Test failed"); - state - .write(&dst_balance_key, Amount::from(100_u64)) - .expect("Test failed"); - state.commit_block().expect("Test failed"); - - // write the updated balances - let _ = state - .write_log_mut() - .write( - &src_balance_key, - Amount::from(100_u64).serialize_to_vec(), - ) - .expect("Test failed"); - let _ = state - .write_log_mut() - .write( - &dst_balance_key, - Amount::from(200_u64).serialize_to_vec(), - ) - .expect("Test failed"); - - state - }; - - let keys_changed = { - let mut keys = BTreeSet::new(); - keys.insert(src_balance_key); - keys.insert(dst_balance_key); - keys - }; - let verifiers = { - let mut v = BTreeSet::new(); - v.insert(Address::Internal(InternalAddress::Multitoken)); - v - }; - - let mut tx = Tx::from_type(TxType::Raw); - tx.push_default_inner_tx(); - - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, 1), - )); - let batched_tx = tx.batch_ref_first_tx().unwrap(); - let ctx = Ctx::new( - &Address::Internal(InternalAddress::Nut(DAI_ERC20_ETH_ADDRESS)), - &state, - batched_tx.tx, - batched_tx.cmt, - &TxIndex(0), - &gas_meter, - &keys_changed, - &verifiers, - VpCache::new(temp_dir(), 100usize), - GasMeterKind::MutGlobal, - ); - - // print debug info in case we run into failures - for key in &keys_changed { - let pre: Amount = - ctx.read_pre(key).expect("Test failed").unwrap_or_default(); - let post: Amount = - ctx.read_post(key).expect("Test failed").unwrap_or_default(); - println!("{key}: PRE={pre:?} POST={post:?}"); - } - - NonUsableTokens::validate_tx( - &ctx, - &batched_tx, - &keys_changed, - &verifiers, - ) - .map_or_else(|_| false, |()| true) - } - - proptest! { - /// Test that transferring NUTs between two arbitrary addresses - /// will always fail. - #[test] - fn test_nut_transfer_rejected( - (src, dst) in (arb_non_internal_address(), arb_non_internal_address()) - ) { - assert!(!check_nut_transfer(src, dst)); - } - - /// Test that transferring NUTs from an arbitrary address to the - /// Bridge pool address passes. - #[test] - fn test_nut_transfer_passes(src in arb_non_internal_address()) { - assert!(check_nut_transfer( - src, - Address::Internal(InternalAddress::EthBridgePool), - )); - } - } -} diff --git a/crates/ibc/src/context/token_transfer.rs b/crates/ibc/src/context/token_transfer.rs index 1f273c84866..997cb02e117 100644 --- a/crates/ibc/src/context/token_transfer.rs +++ b/crates/ibc/src/context/token_transfer.rs @@ -279,13 +279,6 @@ where self.add_withdraw(&ibc_token, amount)?; - // A transfer of NUT tokens must be verified by their VP - if ibc_token.is_internal() - && matches!(ibc_token, Address::Internal(InternalAddress::Nut(_))) - { - self.insert_verifier(&ibc_token); - } - let from_account = if self.is_shielded { &MASP } else { @@ -331,13 +324,6 @@ where self.update_mint_amount(&ibc_token, amount, true)?; self.add_deposit(&ibc_token, amount)?; - // A transfer of NUT tokens must be verified by their VP - if ibc_token.is_internal() - && matches!(ibc_token, Address::Internal(InternalAddress::Nut(_))) - { - self.insert_verifier(&ibc_token); - } - // Store the IBC denom with the token hash to be able to retrieve it // later self.maybe_store_ibc_denom(account, coin)?; @@ -359,13 +345,6 @@ where self.update_mint_amount(&ibc_token, amount, false)?; self.add_withdraw(&ibc_token, amount)?; - // A transfer of NUT tokens must be verified by their VP - if ibc_token.is_internal() - && matches!(ibc_token, Address::Internal(InternalAddress::Nut(_))) - { - self.insert_verifier(&ibc_token); - } - let account = if self.is_shielded { &MASP } else { account }; // The burn is "unminting" from the minted balance diff --git a/crates/light_sdk/Cargo.toml b/crates/light_sdk/Cargo.toml deleted file mode 100644 index af34789edb3..00000000000 --- a/crates/light_sdk/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "namada_light_sdk" -description = "A higher level, user friendly version of the Namada SDK" -resolver = "2" -authors.workspace = true -edition.workspace = true -documentation.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true -rust-version.workspace = true - -[features] -blocking = ["tokio"] -namada-eth-bridge = ["namada_sdk/namada-eth-bridge"] - -[dependencies] -namada_sdk.workspace = true - -borsh.workspace = true -prost.workspace = true -tendermint-config.workspace = true -tendermint-rpc = { workspace = true, features = ["http-client"] } -tokio = { workspace = true, features = ["rt"], optional = true } -serde_json.workspace = true diff --git a/crates/light_sdk/src/lib.rs b/crates/light_sdk/src/lib.rs deleted file mode 100644 index a375a06f0bd..00000000000 --- a/crates/light_sdk/src/lib.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! The Namada light SDK is a simplified version of the SDK aimed at making -//! interaction with the protocol easier and faster. The library is developed -//! with ease-of-use and interoperability in mind so that it should be possible -//! to wrap it for usage in an FFI context. -//! -//! The [`namada_sdk`] crate of Namada is also re-exported to allow access -//! to its types. -//! -//! # Structure -//! -//! This SDK is divided into three modules: -//! -//! - [`transaction`]: contains functions to construct all the transactions -//! currently supported by the protocol -//! - [`reading`]: exposes queries to retrieve data from a Namada node -//! - [`writing`]: exposes functions to send data to a Namada node -//! -//! Both the [`reading`] and [`writing`] modules are further divided into a -//! blocking and asynchronous submodules. - -#![doc(html_favicon_url = "https://dev.namada.net/master/favicon.png")] -#![doc(html_logo_url = "https://dev.namada.net/master/rustdoc-logo.png")] -#![deny(rustdoc::broken_intra_doc_links)] -#![deny(rustdoc::private_intra_doc_links)] -#![warn( - rust_2018_idioms, - clippy::dbg_macro, - clippy::print_stdout, - clippy::print_stderr -)] - -pub mod reading; -pub mod transaction; -pub mod writing; -pub use namada_sdk; diff --git a/crates/light_sdk/src/reading/asynchronous/account.rs b/crates/light_sdk/src/reading/asynchronous/account.rs deleted file mode 100644 index eda5fd74641..00000000000 --- a/crates/light_sdk/src/reading/asynchronous/account.rs +++ /dev/null @@ -1,75 +0,0 @@ -use namada_sdk::account::Account; -use namada_sdk::key::common; -use namada_sdk::storage::BlockHeight; - -use super::*; - -/// Query token amount of owner. -pub async fn get_token_balance( - tendermint_addr: &str, - token: &Address, - owner: &Address, - height: Option, // Specify block height or None for latest -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_token_balance(&client, token, owner, height).await -} - -/// Check if the address exists on chain. Established address exists if it -/// has a stored validity predicate. Implicit and internal addresses -/// always return true. -pub async fn known_address( - tendermint_addr: &str, - address: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::known_address(&client, address).await -} - -/// Query the accunt substorage space of an address -pub async fn get_account_info( - tendermint_addr: &str, - owner: &Address, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_account_info(&client, owner).await -} - -/// Query if the public_key is revealed -pub async fn is_public_key_revealed( - tendermint_addr: &str, - owner: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::is_public_key_revealed(&client, owner).await -} - -/// Query an account substorage at a specific index -pub async fn get_public_key_at( - tendermint_addr: &str, - owner: &Address, - index: u8, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_public_key_at(&client, owner, index).await -} diff --git a/crates/light_sdk/src/reading/asynchronous/governance.rs b/crates/light_sdk/src/reading/asynchronous/governance.rs deleted file mode 100644 index e7c97fe45d8..00000000000 --- a/crates/light_sdk/src/reading/asynchronous/governance.rs +++ /dev/null @@ -1,43 +0,0 @@ -use namada_sdk::governance::parameters::GovernanceParameters; -use namada_sdk::governance::storage::proposal::StorageProposal; -use namada_sdk::governance::utils::Vote; - -use super::*; - -/// Query proposal by Id -pub async fn query_proposal_by_id( - tendermint_addr: &str, - proposal_id: u64, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_proposal_by_id(&client, proposal_id).await -} - -/// Get the givernance parameters -pub async fn query_governance_parameters( - tendermint_addr: &str, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - Ok(rpc::query_governance_parameters(&client).await) -} - -/// Get the givernance parameters -pub async fn query_proposal_votes( - tendermint_addr: &str, - proposal_id: u64, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_proposal_votes(&client, proposal_id).await -} diff --git a/crates/light_sdk/src/reading/asynchronous/mod.rs b/crates/light_sdk/src/reading/asynchronous/mod.rs deleted file mode 100644 index 0b407649557..00000000000 --- a/crates/light_sdk/src/reading/asynchronous/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::str::FromStr; - -use namada_sdk::address::Address; -use namada_sdk::error::{EncodingError, Error}; -use namada_sdk::io::StdIo; -use namada_sdk::queries::RPC; -use namada_sdk::rpc; -use namada_sdk::state::LastBlock; -use namada_sdk::storage::BlockResults; -use namada_sdk::token::{self, DenominatedAmount}; -use tendermint_config::net::Address as TendermintAddress; -use tendermint_rpc::HttpClient; - -pub mod account; -pub mod governance; -pub mod pgf; -pub mod pos; -pub mod tx; - -/// Query the address of the native token -pub async fn query_native_token( - tendermint_addr: &str, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_native_token(&client).await -} - -/// Query the last committed block, if any. -pub async fn query_block( - tendermint_addr: &str, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_block(&client).await -} - -/// Query the results of the last committed block -pub async fn query_results( - tendermint_addr: &str, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_results(&client).await -} - -/// Get a properly denominated amount of a token -pub async fn denominate_amount( - tendermint_addr: &str, - amount: u64, - token: &str, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let token = Address::decode(token) - .map_err(|e| Error::Encode(EncodingError::Decoding(e.to_string())))?; - Ok(rpc::denominate_amount( - &client, - &StdIo {}, - &token, - token::Amount::from(amount), - ) - .await) -} diff --git a/crates/light_sdk/src/reading/asynchronous/pgf.rs b/crates/light_sdk/src/reading/asynchronous/pgf.rs deleted file mode 100644 index 2123acb036c..00000000000 --- a/crates/light_sdk/src/reading/asynchronous/pgf.rs +++ /dev/null @@ -1,14 +0,0 @@ -use super::*; - -/// Check if the given address is a pgf steward. -pub async fn is_steward( - tendermint_addr: &str, - address: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - Ok(rpc::is_steward(&client, address).await) -} diff --git a/crates/light_sdk/src/reading/asynchronous/pos.rs b/crates/light_sdk/src/reading/asynchronous/pos.rs deleted file mode 100644 index 981f53c6e3d..00000000000 --- a/crates/light_sdk/src/reading/asynchronous/pos.rs +++ /dev/null @@ -1,319 +0,0 @@ -use std::collections::BTreeSet; - -use namada_sdk::chain::{BlockHeight, Epoch}; -use namada_sdk::collections::{HashMap, HashSet}; -use namada_sdk::key::common; -use namada_sdk::proof_of_stake::PosParams; -use namada_sdk::proof_of_stake::types::{ - BondsAndUnbondsDetails, CommissionPair, ValidatorMetaData, - ValidatorStateInfo, -}; -use namada_sdk::queries::vp::pos::EnrichedBondsAndUnbondsDetails; - -use super::*; - -/// Query the epoch of the last committed block -pub async fn query_epoch(tendermint_addr: &str) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_epoch(&client).await -} - -/// Query the epoch of the given block height, if it exists. -/// Will return none if the input block height is greater than -/// the latest committed block height. -pub async fn query_epoch_at_height( - tendermint_addr: &str, - height: BlockHeight, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_epoch_at_height(&client, height).await -} - -/// Check if the given address is a known validator. -pub async fn is_validator( - tendermint_addr: &str, - address: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::is_validator(&client, address).await -} - -/// Check if a given address is a known delegator -pub async fn is_delegator( - tendermint_addr: &str, - address: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::is_delegator(&client, address).await -} - -/// Check if a given address is a known delegator at the given epoch -pub async fn is_delegator_at( - tendermint_addr: &str, - address: &Address, - epoch: Epoch, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::is_delegator_at(&client, address, epoch).await -} - -/// Get the set of consensus keys registered in the network -pub async fn get_consensus_keys( - tendermint_addr: &str, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_consensus_keys(&client).await -} - -/// Get the PoS parameters -pub async fn get_pos_params(tendermint_addr: &str) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_pos_params(&client).await -} - -/// Get all validators in the given epoch -pub async fn get_all_validators( - tendermint_addr: &str, - epoch: Epoch, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_all_validators(&client, epoch).await -} - -/// Get the total staked tokens in the given epoch -pub async fn get_total_staked_tokens( - tendermint_addr: &str, - epoch: Epoch, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_total_staked_tokens(&client, epoch).await -} - -/// Get the given validator's stake at the given epoch -pub async fn get_validator_stake( - tendermint_addr: &str, - epoch: Epoch, - validator: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_validator_stake(&client, epoch, validator).await -} - -/// Query and return a validator's state -pub async fn get_validator_state( - tendermint_addr: &str, - validator: &Address, - epoch: Option, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_validator_state(&client, validator, epoch).await -} - -/// Get the delegator's delegation -pub async fn get_delegation_validators( - tendermint_addr: &str, - address: &Address, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let epoch = rpc::query_epoch(&client).await?; - rpc::get_delegation_validators(&client, address, epoch).await -} - -/// Get the delegator's delegation at some epoh -pub async fn get_delegations_of_delegator_at( - tendermint_addr: &str, - address: &Address, - epoch: Epoch, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_delegations_of_delegator_at(&client, address, epoch).await -} - -/// Query and return validator's commission rate and max commission rate -/// change per epoch -pub async fn query_commission_rate( - tendermint_addr: &str, - validator: &Address, - epoch: Option, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_commission_rate(&client, validator, epoch).await -} - -/// Query and return validator's metadata, including the commission rate and -/// max commission rate change -pub async fn query_metadata( - tendermint_addr: &str, - validator: &Address, - epoch: Option, -) -> Result<(Option, CommissionPair), Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_metadata(&client, validator, epoch).await -} - -/// Query and return the incoming redelegation epoch for a given pair of -/// source validator and delegator, if there is any. -pub async fn query_incoming_redelegations( - tendermint_addr: &str, - src_validator: &Address, - delegator: &Address, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_incoming_redelegations(&client, src_validator, delegator).await -} - -/// Query a validator's bonds for a given epoch -pub async fn query_bond( - tendermint_addr: &str, - source: &Address, - validator: &Address, - epoch: Option, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_bond(&client, source, validator, epoch).await -} - -/// Query withdrawable tokens in a validator account for a given epoch -pub async fn query_withdrawable_tokens( - tendermint_addr: &str, - bond_source: &Address, - validator: &Address, - epoch: Option, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_withdrawable_tokens(&client, bond_source, validator, epoch).await -} - -/// Query all unbonds for a validator, applying slashes -pub async fn query_unbond_with_slashing( - tendermint_addr: &str, - source: &Address, - validator: &Address, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::query_unbond_with_slashing(&client, source, validator).await -} - -/// Get the bond amount at the given epoch -pub async fn get_bond_amount_at( - tendermint_addr: &str, - delegator: &Address, - validator: &Address, - epoch: Epoch, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::get_bond_amount_at(&client, delegator, validator, epoch).await -} - -/// Get bonds and unbonds with all details (slashes and rewards, if any) -/// grouped by their bond IDs. -pub async fn bonds_and_unbonds( - tendermint_addr: &str, - source: &Option
, - validator: &Option
, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::bonds_and_unbonds(&client, source, validator).await -} - -/// Get bonds and unbonds with all details (slashes and rewards, if any) -/// grouped by their bond IDs, enriched with extra information calculated -/// from the data. -pub async fn enriched_bonds_and_unbonds( - tendermint_addr: &str, - current_epoch: Epoch, - source: &Option
, - validator: &Option
, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - rpc::enriched_bonds_and_unbonds(&client, current_epoch, source, validator) - .await -} diff --git a/crates/light_sdk/src/reading/asynchronous/tx.rs b/crates/light_sdk/src/reading/asynchronous/tx.rs deleted file mode 100644 index 148efa1096b..00000000000 --- a/crates/light_sdk/src/reading/asynchronous/tx.rs +++ /dev/null @@ -1,67 +0,0 @@ -use namada_sdk::rpc::{TxAppliedEvents, TxEventQuery, TxResponse}; -use namada_sdk::tx::data::DryRunResult; - -use super::*; - -/// Call the corresponding `tx_event_query` RPC method, to fetch -/// the current status of a transation. -pub async fn query_tx_events( - tendermint_addr: &str, - tx_hash: &str, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let tx_event_query = TxEventQuery::Applied(tx_hash); - rpc::query_tx_events(&client, tx_event_query) - .await - .map_err(|e| Error::Other(e.to_string())) -} - -/// Dry run a transaction -pub async fn dry_run_tx( - tendermint_addr: &str, - tx_bytes: Vec, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let (data, height, prove) = (Some(tx_bytes), None, false); - let result = RPC - .shell() - .dry_run_tx(&client, data, height, prove) - .await - .map_err(|err| { - Error::from(namada_sdk::error::QueryError::NoResponse( - err.to_string(), - )) - })? - .data; - Ok(result) -} - -/// Lookup the full response accompanying the specified transaction event -pub async fn query_tx_response( - tendermint_addr: &str, - tx_hash: &str, -) -> Result { - let events = query_tx_status(tendermint_addr, tx_hash).await?; - events.try_into().map_err(Error::Other) -} - -/// Query the status of a given transaction. -pub async fn query_tx_status( - tendermint_addr: &str, - tx_hash: &str, -) -> Result { - let maybe_event = query_tx_events(tendermint_addr, tx_hash).await?; - if let Some(e) = maybe_event { - Ok(e) - } else { - Err(Error::Tx(namada_sdk::error::TxSubmitError::AppliedTimeout)) - } -} diff --git a/crates/light_sdk/src/reading/blocking/account.rs b/crates/light_sdk/src/reading/blocking/account.rs deleted file mode 100644 index eb5bccc21d5..00000000000 --- a/crates/light_sdk/src/reading/blocking/account.rs +++ /dev/null @@ -1,78 +0,0 @@ -use namada_sdk::account::Account; -use namada_sdk::key::common; - -use super::*; - -/// Query token amount of owner. -pub fn get_token_balance( - tendermint_addr: &str, - token: &Address, - owner: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_token_balance(&client, token, owner)) -} - -/// Check if the address exists on chain. Established address exists if it -/// has a stored validity predicate. Implicit and internal addresses -/// always return true. -pub fn known_address( - tendermint_addr: &str, - address: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::known_address(&client, address)) -} - -/// Query the accunt substorage space of an address -pub fn get_account_info( - tendermint_addr: &str, - owner: &Address, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_account_info(&client, owner)) -} - -/// Query if the public_key is revealed -pub fn is_public_key_revealed( - tendermint_addr: &str, - owner: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::is_public_key_revealed(&client, owner)) -} - -/// Query an account substorage at a specific index -pub fn get_public_key_at( - tendermint_addr: &str, - owner: &Address, - index: u8, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_public_key_at(&client, owner, index)) -} diff --git a/crates/light_sdk/src/reading/blocking/governance.rs b/crates/light_sdk/src/reading/blocking/governance.rs deleted file mode 100644 index a1af6af1a15..00000000000 --- a/crates/light_sdk/src/reading/blocking/governance.rs +++ /dev/null @@ -1,46 +0,0 @@ -use namada_sdk::governance::parameters::GovernanceParameters; -use namada_sdk::governance::storage::proposal::StorageProposal; -use namada_sdk::governance::utils::Vote; - -use super::*; - -/// Query proposal by Id -pub fn query_proposal_by_id( - tendermint_addr: &str, - proposal_id: u64, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_proposal_by_id(&client, proposal_id)) -} - -/// Get the givernance parameters -pub fn query_governance_parameters( - tendermint_addr: &str, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - Ok(rt.block_on(rpc::query_governance_parameters(&client))) -} - -/// Get the givernance parameters -pub fn query_proposal_votes( - tendermint_addr: &str, - proposal_id: u64, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_proposal_votes(&client, proposal_id)) -} diff --git a/crates/light_sdk/src/reading/blocking/mod.rs b/crates/light_sdk/src/reading/blocking/mod.rs deleted file mode 100644 index a70b4633e4b..00000000000 --- a/crates/light_sdk/src/reading/blocking/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::str::FromStr; - -use namada_sdk::address::Address; -use namada_sdk::error::{EncodingError, Error}; -use namada_sdk::io::StdIo; -use namada_sdk::queries::RPC; -use namada_sdk::rpc; -use namada_sdk::state::LastBlock; -use namada_sdk::storage::BlockResults; -use namada_sdk::token::{self, DenominatedAmount}; -use tendermint_config::net::Address as TendermintAddress; -use tendermint_rpc::HttpClient; -use tokio::runtime::Runtime; - -pub mod account; -pub mod governance; -pub mod pgf; -pub mod pos; -pub mod tx; - -/// Query the address of the native token -pub fn query_native_token(tendermint_addr: &str) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_native_token(&client)) -} - -/// Query the last committed block, if any. -pub fn query_block(tendermint_addr: &str) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_block(&client)) -} - -/// Query the results of the last committed block -pub fn query_results( - tendermint_addr: &str, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_results(&client)) -} - -/// Get a properly denominated amount of a token -pub fn denominate_amount( - tendermint_addr: &str, - amount: u64, - token: &str, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let token = Address::decode(token) - .map_err(|e| Error::Encode(EncodingError::Decoding(e.to_string())))?; - let rt = Runtime::new().unwrap(); - Ok(rt.block_on(rpc::denominate_amount( - &client, - &StdIo {}, - &token, - token::Amount::from(amount), - ))) -} diff --git a/crates/light_sdk/src/reading/blocking/pgf.rs b/crates/light_sdk/src/reading/blocking/pgf.rs deleted file mode 100644 index e549f72fb1f..00000000000 --- a/crates/light_sdk/src/reading/blocking/pgf.rs +++ /dev/null @@ -1,15 +0,0 @@ -use super::*; - -/// Check if the given address is a pgf steward. -pub fn is_steward( - tendermint_addr: &str, - address: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - Ok(rt.block_on(rpc::is_steward(&client, address))) -} diff --git a/crates/light_sdk/src/reading/blocking/pos.rs b/crates/light_sdk/src/reading/blocking/pos.rs deleted file mode 100644 index a82df68ae8d..00000000000 --- a/crates/light_sdk/src/reading/blocking/pos.rs +++ /dev/null @@ -1,358 +0,0 @@ -use std::collections::BTreeSet; - -use namada_sdk::address::Address; -use namada_sdk::chain::{BlockHeight, Epoch}; -use namada_sdk::collections::{HashMap, HashSet}; -use namada_sdk::key::common; -use namada_sdk::proof_of_stake::PosParams; -use namada_sdk::proof_of_stake::types::{ - BondsAndUnbondsDetails, CommissionPair, ValidatorMetaData, ValidatorState, -}; -use namada_sdk::queries::vp::pos::EnrichedBondsAndUnbondsDetails; - -use super::*; - -/// Query the epoch of the last committed block -pub fn query_epoch(tendermint_addr: &str) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_epoch(&client)) -} - -/// Query the epoch of the given block height, if it exists. -/// Will return none if the input block height is greater than -/// the latest committed block height. -pub fn query_epoch_at_height( - tendermint_addr: &str, - height: BlockHeight, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_epoch_at_height(&client, height)) -} - -/// Check if the given address is a known validator. -pub fn is_validator( - tendermint_addr: &str, - address: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::is_validator(&client, address)) -} - -/// Check if a given address is a known delegator -pub fn is_delegator( - tendermint_addr: &str, - address: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::is_delegator(&client, address)) -} - -/// Check if a given address is a known delegator at the given epoch -pub fn is_delegator_at( - tendermint_addr: &str, - address: &Address, - epoch: Epoch, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::is_delegator_at(&client, address, epoch)) -} - -/// Get the set of consensus keys registered in the network -pub fn get_consensus_keys( - tendermint_addr: &str, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_consensus_keys(&client)) -} - -/// Get the PoS parameters -pub fn get_pos_params(tendermint_addr: &str) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_pos_params(&client)) -} - -/// Get all validators in the given epoch -pub fn get_all_validators( - tendermint_addr: &str, - epoch: Epoch, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_all_validators(&client, epoch)) -} - -/// Get the total staked tokens in the given epoch -pub fn get_total_staked_tokens( - tendermint_addr: &str, - epoch: Epoch, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_total_staked_tokens(&client, epoch)) -} - -/// Get the given validator's stake at the given epoch -pub fn get_validator_stake( - tendermint_addr: &str, - epoch: Epoch, - validator: &Address, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_validator_stake(&client, epoch, validator)) -} - -/// Query and return a validator's state -pub fn get_validator_state( - tendermint_addr: &str, - validator: &Address, - epoch: Option, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_validator_state(&client, validator, epoch)) -} - -/// Get the delegator's delegation -pub fn get_delegation_validators( - tendermint_addr: &str, - address: &Address, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - let epoch = rpc::query_epoch(&client).await?; - rt.block_on(rpc::get_delegation_validators(&client, address, epoch)) -} - -/// Get the delegator's delegation at some epoh -pub fn get_delegations_of_delegator_at( - tendermint_addr: &str, - address: &Address, - epoch: Epoch, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_delegations_of_delegator_at( - &client, address, epoch, - )) -} - -/// Query and return validator's commission rate and max commission rate -/// change per epoch -pub fn query_commission_rate( - tendermint_addr: &str, - validator: &Address, - epoch: Option, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_commission_rate(&client, validator, epoch)) -} - -/// Query and return validator's metadata, including the commission rate and -/// max commission rate change -pub fn query_metadata( - tendermint_addr: &str, - validator: &Address, - epoch: Option, -) -> Result<(Option, Option), Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_metadata(&client, validator, epoch)) -} - -/// Query and return the incoming redelegation epoch for a given pair of -/// source validator and delegator, if there is any. -pub fn query_incoming_redelegations( - tendermint_addr: &str, - src_validator: &Address, - delegator: &Address, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_incoming_redelegations( - &client, - src_validator, - delegator, - )) -} - -/// Query a validator's bonds for a given epoch -pub fn query_bond( - tendermint_addr: &str, - source: &Address, - validator: &Address, - epoch: Option, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_bond(&client, source, validator, epoch)) -} - -/// Query withdrawable tokens in a validator account for a given epoch -pub fn query_withdrawable_tokens( - tendermint_addr: &str, - bond_source: &Address, - validator: &Address, - epoch: Option, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_withdrawable_tokens( - &client, - bond_source, - validator, - epoch, - )) -} - -/// Query all unbonds for a validator, applying slashes -pub fn query_unbond_with_slashing( - tendermint_addr: &str, - source: &Address, - validator: &Address, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_unbond_with_slashing(&client, source, validator)) -} - -/// Get the bond amount at the given epoch -pub fn get_bond_amount_at( - tendermint_addr: &str, - delegator: &Address, - validator: &Address, - epoch: Epoch, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::get_bond_amount_at( - &client, delegator, validator, epoch, - )) -} - -/// Get bonds and unbonds with all details (slashes and rewards, if any) -/// grouped by their bond IDs. -pub fn bonds_and_unbonds( - tendermint_addr: &str, - source: &Option
, - validator: &Option
, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::bonds_and_unbonds(&client, source, validator)) -} - -/// Get bonds and unbonds with all details (slashes and rewards, if any) -/// grouped by their bond IDs, enriched with extra information calculated -/// from the data. -pub fn enriched_bonds_and_unbonds( - tendermint_addr: &str, - current_epoch: Epoch, - source: &Option
, - validator: &Option
, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::enriched_bonds_and_unbonds( - &client, - current_epoch, - source, - validator, - )) -} diff --git a/crates/light_sdk/src/reading/blocking/tx.rs b/crates/light_sdk/src/reading/blocking/tx.rs deleted file mode 100644 index 27aa61ef407..00000000000 --- a/crates/light_sdk/src/reading/blocking/tx.rs +++ /dev/null @@ -1,68 +0,0 @@ -use namada_sdk::events::Event; -use namada_sdk::rpc::{TxEventQuery, TxResponse}; -use namada_sdk::tx::data::TxResult; - -use super::*; - -/// Call the corresponding `tx_event_query` RPC method, to fetch -/// the current status of a transation. -pub fn query_tx_events( - tendermint_addr: &str, - tx_hash: &str, -) -> Result, Error> { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let tx_event_query = TxEventQuery::Applied(tx_hash); - - let rt = Runtime::new().unwrap(); - rt.block_on(rpc::query_tx_events(&client, tx_event_query)) - .map_err(|e| Error::Other(e.to_string())) -} - -/// Dry run a transaction -pub fn dry_run_tx( - tendermint_addr: &str, - tx_bytes: Vec, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - let (data, height, prove) = (Some(tx_bytes), None, false); - let rt = Runtime::new().unwrap(); - let result = rt - .block_on(RPC.shell().dry_run_tx(&client, data, height, prove)) - .map_err(|err| { - Error::from(namada_sdk::error::QueryError::NoResponse( - err.to_string(), - )) - })? - .data; - Ok(result) -} - -/// Lookup the full response accompanying the specified transaction event -pub fn query_tx_response( - tendermint_addr: &str, - tx_hash: &str, -) -> Result { - let event = query_tx_status(tendermint_addr, tx_hash)?; - event.try_into().map_err(Error::Other) -} - -/// Query the status of a given transaction. -pub fn query_tx_status( - tendermint_addr: &str, - tx_hash: &str, -) -> Result { - let maybe_event = query_tx_events(tendermint_addr, tx_hash)?; - if let Some(e) = maybe_event { - Ok(e) - } else { - Err(Error::Tx(namada_sdk::error::TxSubmitError::AppliedTimeout)) - } -} diff --git a/crates/light_sdk/src/reading/mod.rs b/crates/light_sdk/src/reading/mod.rs deleted file mode 100644 index 5a925ded686..00000000000 --- a/crates/light_sdk/src/reading/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(not(feature = "blocking"))] -pub mod asynchronous; -#[cfg(feature = "blocking")] -pub mod blocking; diff --git a/crates/light_sdk/src/transaction/account.rs b/crates/light_sdk/src/transaction/account.rs deleted file mode 100644 index 3b435210ce9..00000000000 --- a/crates/light_sdk/src/transaction/account.rs +++ /dev/null @@ -1,249 +0,0 @@ -use namada_sdk::address::Address; -use namada_sdk::hash::Hash; -use namada_sdk::key::common; -use namada_sdk::token::DenominatedAmount; -use namada_sdk::tx::data::GasLimit; -use namada_sdk::tx::{Authorization, Tx, TxError}; - -use super::{GlobalArgs, attach_fee, attach_fee_signature}; -use crate::transaction; - -const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; -const TX_REVEAL_PK_WASM: &str = "tx_reveal_pk.wasm"; -const TX_UPDATE_ACCOUNT_WASM: &str = "tx_update_account.wasm"; - -/// Transaction to initialize an established account -#[derive(Debug, Clone)] -pub struct InitAccount(Tx); - -impl InitAccount { - /// Build a raw InitAccount transaction from the given parameters - pub fn new( - public_keys: Vec, - vp_code_hash: Hash, - threshold: u8, - args: GlobalArgs, - ) -> Self { - let init_account = namada_sdk::account::InitAccount { - public_keys, - vp_code_hash, - threshold, - }; - - Self(transaction::build_tx( - args, - init_account, - TX_INIT_ACCOUNT_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to reveal a public key to the ledger to validate signatures of -/// an implicit account -pub struct RevealPk(Tx); - -impl RevealPk { - /// Build a raw Reveal Public Key transaction from the given parameters - pub fn new(public_key: common::PublicKey, args: GlobalArgs) -> Self { - Self(transaction::build_tx( - args, - public_key, - TX_REVEAL_PK_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to update the parameters of an established account -pub struct UpdateAccount(Tx); - -impl UpdateAccount { - /// Build a raw UpdateAccount transaction from the given parameters - pub fn new( - addr: Address, - vp_code_hash: Option, - public_keys: Vec, - threshold: Option, - args: GlobalArgs, - ) -> Self { - let update_account = namada_sdk::account::UpdateAccount { - addr, - vp_code_hash, - public_keys, - threshold, - }; - - Self(transaction::build_tx( - args, - update_account, - TX_UPDATE_ACCOUNT_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} diff --git a/crates/light_sdk/src/transaction/bridge.rs b/crates/light_sdk/src/transaction/bridge.rs deleted file mode 100644 index 23cf9ee0b51..00000000000 --- a/crates/light_sdk/src/transaction/bridge.rs +++ /dev/null @@ -1,90 +0,0 @@ -use namada_sdk::address::Address; -pub use namada_sdk::eth_bridge_pool::{GasFee, TransferToEthereum}; -use namada_sdk::hash::Hash; -use namada_sdk::key::common; -use namada_sdk::token::DenominatedAmount; -use namada_sdk::tx::data::GasLimit; -use namada_sdk::tx::{Authorization, Tx, TxError}; - -use super::{GlobalArgs, attach_fee, attach_fee_signature}; -use crate::transaction; - -const TX_BRIDGE_POOL_WASM: &str = "tx_bridge_pool.wasm"; - -/// A transfer over the Ethereum bridge -#[derive(Debug, Clone)] -pub struct BridgeTransfer(Tx); - -impl BridgeTransfer { - /// Build a raw BridgeTransfer transaction from the given parameters - pub fn new( - transfer: TransferToEthereum, - gas_fee: GasFee, - args: GlobalArgs, - ) -> Self { - let pending_transfer = - namada_sdk::eth_bridge_pool::PendingTransfer { transfer, gas_fee }; - - Self(transaction::build_tx( - args, - pending_transfer, - TX_BRIDGE_POOL_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} diff --git a/crates/light_sdk/src/transaction/governance.rs b/crates/light_sdk/src/transaction/governance.rs deleted file mode 100644 index 4414592a601..00000000000 --- a/crates/light_sdk/src/transaction/governance.rs +++ /dev/null @@ -1,176 +0,0 @@ -use namada_sdk::address::Address; -use namada_sdk::chain::Epoch; -use namada_sdk::governance::{ProposalType, ProposalVote}; -use namada_sdk::hash::Hash; -use namada_sdk::key::common; -use namada_sdk::token::DenominatedAmount; -use namada_sdk::tx::data::GasLimit; -use namada_sdk::tx::{Authorization, Tx, TxError}; - -use super::{GlobalArgs, attach_fee, attach_fee_signature}; -use crate::transaction; - -const TX_INIT_PROPOSAL_WASM: &str = "tx_init_proposal.wasm"; -const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm"; - -/// Transaction to initialize a governance proposal -#[derive(Debug, Clone)] -pub struct InitProposal(Tx); - -impl InitProposal { - /// Build a raw InitProposal transaction from the given parameters - #[allow(clippy::too_many_arguments)] - pub fn new( - content: Hash, - author: Address, - r#type: ProposalType, - voting_start_epoch: Epoch, - voting_end_epoch: Epoch, - activation_epoch: Epoch, - args: GlobalArgs, - ) -> Self { - let init_proposal = namada_sdk::governance::InitProposalData { - content, - author, - r#type, - voting_start_epoch, - voting_end_epoch, - activation_epoch, - }; - - Self(transaction::build_tx( - args, - init_proposal, - TX_INIT_PROPOSAL_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } -} - -/// Transaction to vote on a governance proposal -pub struct VoteProposal(Tx); - -impl VoteProposal { - /// Build a raw VoteProposal transaction from the given parameters - pub fn new( - id: u64, - vote: ProposalVote, - voter: Address, - args: GlobalArgs, - ) -> Self { - let vote_proposal = - namada_sdk::governance::VoteProposalData { id, vote, voter }; - - Self(transaction::build_tx( - args, - vote_proposal, - TX_VOTE_PROPOSAL.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} diff --git a/crates/light_sdk/src/transaction/ibc.rs b/crates/light_sdk/src/transaction/ibc.rs deleted file mode 100644 index be0e3309efe..00000000000 --- a/crates/light_sdk/src/transaction/ibc.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::str::FromStr; - -use namada_sdk::address::Address; -use namada_sdk::hash::Hash; -pub use namada_sdk::ibc::apps::transfer::types::msgs::transfer::MsgTransfer; -use namada_sdk::ibc::primitives::ToProto; -use namada_sdk::key::common; -use namada_sdk::time::DateTimeUtc; -use namada_sdk::token::DenominatedAmount; -use namada_sdk::tx::data::GasLimit; -use namada_sdk::tx::{Authorization, Tx, TxError}; - -use super::{GlobalArgs, attach_fee, attach_fee_signature}; -use crate::transaction; - -const TX_IBC_WASM: &str = "tx_ibc.wasm"; - -/// An IBC transfer -#[derive(Debug, Clone)] -pub struct IbcTransfer(Tx); - -impl IbcTransfer { - /// Build a raw IbcTransfer transaction from the given parameters - pub fn new( - packet_data: MsgTransfer, - GlobalArgs { - expiration, - code_hash, - chain_id, - }: GlobalArgs, - ) -> Self { - let mut tx = Tx::new(chain_id, expiration); - tx.header.timestamp = - DateTimeUtc::from_str("2000-01-01T00:00:00Z").unwrap(); - tx.add_code_from_hash(code_hash, Some(TX_IBC_WASM.to_string())); - - let mut data = vec![]; - prost::Message::encode(&packet_data.to_any(), &mut data).unwrap(); - tx.set_data(namada_sdk::tx::Data::new(data)); - - Self(tx) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} diff --git a/crates/light_sdk/src/transaction/mod.rs b/crates/light_sdk/src/transaction/mod.rs deleted file mode 100644 index ce3a4ca25b1..00000000000 --- a/crates/light_sdk/src/transaction/mod.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::collections::BTreeMap; -use std::str::FromStr; - -use borsh::BorshSerialize; -use namada_sdk::address::Address; -use namada_sdk::chain::ChainId; -use namada_sdk::hash::Hash; -use namada_sdk::key::common; -use namada_sdk::time::DateTimeUtc; -use namada_sdk::token::DenominatedAmount; -use namada_sdk::tx::data::{Fee, GasLimit}; -use namada_sdk::tx::{Authorization, Section, Signer, Tx}; - -pub mod account; -pub mod bridge; -pub mod governance; -pub mod ibc; -pub mod pgf; -pub mod pos; -pub mod transfer; - -/// Generic arguments required to construct a transaction -#[derive(Debug, Clone)] -pub struct GlobalArgs { - pub expiration: Option, - pub code_hash: Hash, - pub chain_id: ChainId, -} - -pub(in crate::transaction) fn build_tx( - GlobalArgs { - expiration, - code_hash, - chain_id, - }: GlobalArgs, - data: impl BorshSerialize, - code_tag: String, -) -> Tx { - let mut inner_tx = Tx::new(chain_id, expiration); - - inner_tx.header.timestamp = - DateTimeUtc::from_str("2000-01-01T00:00:00Z").unwrap(); - inner_tx.add_code_from_hash(code_hash, Some(code_tag)); - inner_tx.add_data(data); - - inner_tx -} - -pub(in crate::transaction) fn get_sign_bytes(tx: &Tx) -> Vec { - vec![tx.raw_header_hash()] -} - -pub(in crate::transaction) fn get_wrapper_sign_bytes(tx: &Tx) -> Hash { - let targets = tx.sechashes(); - // Commit to the given targets - let partial = Authorization { - targets, - signer: Signer::PubKeys(vec![]), - signatures: BTreeMap::new(), - }; - partial.get_raw_hash() -} - -pub(in crate::transaction) fn attach_raw_signatures( - mut tx: Tx, - signer: common::PublicKey, - signature: common::Signature, -) -> Tx { - tx.protocol_filter(); - tx.add_section(Section::Authorization(Authorization { - targets: vec![tx.raw_header_hash()], - signer: Signer::PubKeys(vec![signer]), - signatures: [(0, signature)].into_iter().collect(), - })); - tx -} - -pub(in crate::transaction) fn attach_fee( - mut tx: Tx, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, -) -> Tx { - tx.add_wrapper( - Fee { - amount_per_gas_unit: fee, - token, - }, - fee_payer, - gas_limit, - ); - tx -} - -pub(in crate::transaction) fn attach_fee_signature( - mut tx: Tx, - signer: common::PublicKey, - signature: common::Signature, -) -> Tx { - tx.protocol_filter(); - tx.add_section(Section::Authorization(Authorization { - targets: tx.sechashes(), - signer: Signer::PubKeys(vec![signer]), - signatures: [(0, signature)].into_iter().collect(), - })); - tx -} diff --git a/crates/light_sdk/src/transaction/pgf.rs b/crates/light_sdk/src/transaction/pgf.rs deleted file mode 100644 index efc66d85617..00000000000 --- a/crates/light_sdk/src/transaction/pgf.rs +++ /dev/null @@ -1,166 +0,0 @@ -use namada_sdk::address::Address; -use namada_sdk::collections::HashMap; -use namada_sdk::dec::Dec; -use namada_sdk::hash::Hash; -use namada_sdk::key::common; -use namada_sdk::token::DenominatedAmount; -use namada_sdk::tx::data::GasLimit; -use namada_sdk::tx::{Authorization, Tx, TxError}; - -use super::{GlobalArgs, attach_fee, attach_fee_signature}; -use crate::transaction; - -const TX_RESIGN_STEWARD: &str = "tx_resign_steward.wasm"; -const TX_UPDATE_STEWARD_COMMISSION: &str = "tx_update_steward_commission.wasm"; - -/// A transaction to resign from stewarding pgf -#[derive(Debug, Clone)] -pub struct ResignSteward(Tx); - -impl ResignSteward { - /// Build a raw ResignSteward transaction from the given parameters - pub fn new(steward: Address, args: GlobalArgs) -> Self { - Self(transaction::build_tx( - args, - steward, - TX_RESIGN_STEWARD.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to update a pgf steward's commission rate -pub struct UpdateStewardCommission(Tx); - -impl UpdateStewardCommission { - /// Build a raw UpdateStewardCommission transaction from the given - /// parameters - pub fn new( - steward: Address, - commission: HashMap, - args: GlobalArgs, - ) -> Self { - let update_commission = - namada_sdk::tx::data::pgf::UpdateStewardCommission { - steward, - commission, - }; - - Self(transaction::build_tx( - args, - update_commission, - TX_UPDATE_STEWARD_COMMISSION.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} diff --git a/crates/light_sdk/src/transaction/pos.rs b/crates/light_sdk/src/transaction/pos.rs deleted file mode 100644 index b9004f107ac..00000000000 --- a/crates/light_sdk/src/transaction/pos.rs +++ /dev/null @@ -1,858 +0,0 @@ -use namada_sdk::address::Address; -use namada_sdk::dec::Dec; -use namada_sdk::hash::Hash; -use namada_sdk::key::{common, secp256k1}; -use namada_sdk::token; -use namada_sdk::token::{Amount, DenominatedAmount}; -use namada_sdk::tx::data::GasLimit; -use namada_sdk::tx::data::pos::Redelegation; -use namada_sdk::tx::{Authorization, Tx, TxError}; - -use super::{GlobalArgs, attach_fee, attach_fee_signature}; -use crate::transaction; - -const TX_BOND_WASM: &str = "tx_bond.wasm"; -const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; -const TX_BECOME_VALIDATOR_WASM: &str = "tx_become_validator.wasm"; -const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; -const TX_DEACTIVATE_VALIDATOR_WASM: &str = "tx_deactivate_validator.wasm"; -const TX_REACTIVATE_VALIDATOR_WASM: &str = "tx_reactivate_validator.wasm"; -const TX_CLAIM_REWARDS_WASM: &str = "tx_claim_rewards.wasm"; -const TX_REDELEGATE_WASM: &str = "tx_redelegate.wasm"; -const TX_CHANGE_METADATA_WASM: &str = "tx_change_validator_metadata.wasm"; -const TX_CHANGE_CONSENSUS_KEY_WASM: &str = "tx_change_consensus_key.wasm"; -const TX_CHANGE_COMMISSION_WASM: &str = "tx_change_validator_commission.wasm"; -const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; - -/// A bond transaction -#[derive(Debug, Clone)] -pub struct Bond(Tx); - -impl Bond { - /// Build a raw Bond transaction from the given parameters - pub fn new( - validator: Address, - amount: token::Amount, - source: Option
, - args: GlobalArgs, - ) -> Self { - let unbond = namada_sdk::tx::data::pos::Bond { - validator, - amount, - source, - }; - - Self(transaction::build_tx( - args, - unbond, - TX_BOND_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// An unbond transaction -pub struct Unbond(Tx); - -impl Unbond { - /// Build a raw Unbond transaction from the given parameters - pub fn new( - validator: Address, - amount: token::Amount, - source: Option
, - args: GlobalArgs, - ) -> Self { - let unbond = namada_sdk::tx::data::pos::Unbond { - validator, - amount, - source, - }; - - Self(transaction::build_tx( - args, - unbond, - TX_UNBOND_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to initialize a new PoS validator -pub struct BecomeValidator(Tx); - -impl BecomeValidator { - /// Build a raw Init validator transaction from the given parameters - #[allow(clippy::too_many_arguments)] - pub fn new( - address: Address, - consensus_key: common::PublicKey, - eth_cold_key: secp256k1::PublicKey, - eth_hot_key: secp256k1::PublicKey, - protocol_key: common::PublicKey, - commission_rate: Dec, - max_commission_rate_change: Dec, - email: String, - description: Option, - website: Option, - discord_handle: Option, - avatar: Option, - name: Option, - args: GlobalArgs, - ) -> Self { - let update_account = namada_sdk::tx::data::pos::BecomeValidator { - address, - consensus_key, - eth_cold_key, - eth_hot_key, - protocol_key, - commission_rate, - max_commission_rate_change, - email, - description, - website, - discord_handle, - avatar, - name, - }; - - Self(transaction::build_tx( - args, - update_account, - TX_BECOME_VALIDATOR_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to unjail a PoS validator -pub struct UnjailValidator(Tx); - -impl UnjailValidator { - /// Build a raw Unjail validator transaction from the given parameters - pub fn new(address: Address, args: GlobalArgs) -> Self { - Self(transaction::build_tx( - args, - address, - TX_UNJAIL_VALIDATOR_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to deactivate a validator -pub struct DeactivateValidator(Tx); - -impl DeactivateValidator { - /// Build a raw DeactivateValidator transaction from the given parameters - pub fn new(address: Address, args: GlobalArgs) -> Self { - Self(transaction::build_tx( - args, - address, - TX_DEACTIVATE_VALIDATOR_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to reactivate a previously deactivated validator -pub struct ReactivateValidator(Tx); - -impl ReactivateValidator { - /// Build a raw ReactivateValidator transaction from the given parameters - pub fn new(address: Address, args: GlobalArgs) -> Self { - Self(transaction::build_tx( - args, - address, - TX_REACTIVATE_VALIDATOR_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to claim PoS rewards -pub struct ClaimRewards(Tx); - -impl ClaimRewards { - /// Build a raw ClaimRewards transaction from the given parameters - pub fn new( - validator: Address, - source: Option
, - args: GlobalArgs, - ) -> Self { - let init_proposal = - namada_sdk::tx::data::pos::Withdraw { validator, source }; - - Self(transaction::build_tx( - args, - init_proposal, - TX_CLAIM_REWARDS_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to change the validator's metadata -pub struct ChangeMetaData(Tx); - -impl ChangeMetaData { - /// Build a raw ChangeMetadata transaction from the given parameters - #[allow(clippy::too_many_arguments)] - pub fn new( - validator: Address, - email: Option, - description: Option, - website: Option, - discord_handle: Option, - avatar: Option, - name: Option, - commission_rate: Option, - args: GlobalArgs, - ) -> Self { - let init_proposal = namada_sdk::tx::data::pos::MetaDataChange { - validator, - email, - description, - website, - discord_handle, - avatar, - name, - commission_rate, - }; - - Self(transaction::build_tx( - args, - init_proposal, - TX_CHANGE_METADATA_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to modify the validator's consensus key -pub struct ChangeConsensusKey(Tx); - -impl ChangeConsensusKey { - /// Build a raw ChangeConsensusKey transaction from the given parameters - pub fn new( - validator: Address, - consensus_key: common::PublicKey, - args: GlobalArgs, - ) -> Self { - let init_proposal = namada_sdk::tx::data::pos::ConsensusKeyChange { - validator, - consensus_key, - }; - - Self(transaction::build_tx( - args, - init_proposal, - TX_CHANGE_CONSENSUS_KEY_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to modify the validator's commission rate -pub struct ChangeCommission(Tx); - -impl ChangeCommission { - /// Build a raw ChangeCommission transaction from the given parameters - pub fn new(validator: Address, new_rate: Dec, args: GlobalArgs) -> Self { - let init_proposal = namada_sdk::tx::data::pos::CommissionChange { - validator, - new_rate, - }; - - Self(transaction::build_tx( - args, - init_proposal, - TX_CHANGE_COMMISSION_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to withdraw previously unstaked funds -pub struct Withdraw(Tx); - -impl Withdraw { - /// Build a raw Withdraw transaction from the given parameters - pub fn new( - validator: Address, - source: Option
, - args: GlobalArgs, - ) -> Self { - let init_proposal = - namada_sdk::tx::data::pos::Withdraw { validator, source }; - - Self(transaction::build_tx( - args, - init_proposal, - TX_WITHDRAW_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} - -/// Transaction to redelegate -pub struct Redelegate(Tx); - -impl Redelegate { - /// Build a raw Redelegate transaction from the given parameters - pub fn new( - src_validator: Address, - dest_validator: Address, - owner: Address, - amount: Amount, - args: GlobalArgs, - ) -> Self { - let redelegation = Redelegation { - src_validator, - dest_validator, - owner, - amount, - }; - - Self(transaction::build_tx( - args, - redelegation, - TX_REDELEGATE_WASM.to_string(), - )) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} diff --git a/crates/light_sdk/src/transaction/transfer.rs b/crates/light_sdk/src/transaction/transfer.rs deleted file mode 100644 index 2e0e6833908..00000000000 --- a/crates/light_sdk/src/transaction/transfer.rs +++ /dev/null @@ -1,96 +0,0 @@ -use namada_sdk::address::Address; -use namada_sdk::hash::Hash; -use namada_sdk::key::common; -pub use namada_sdk::token::{ - DenominatedAmount, MaspTransaction, MaspTxId, Transfer, -}; -use namada_sdk::tx::data::GasLimit; -use namada_sdk::tx::{Authorization, TX_TRANSFER_WASM, Tx, TxError}; - -use super::{GlobalArgs, attach_fee, attach_fee_signature}; -use crate::transaction; - -/// A transfer transaction -#[derive(Debug, Clone)] -pub struct TransferBuilder(Tx); - -impl TransferBuilder { - /// Build a transparent transfer transaction from the given parameters - pub fn transfer(transfer: Transfer, args: GlobalArgs) -> Self { - Self(transaction::build_tx( - args, - transfer, - TX_TRANSFER_WASM.to_string(), - )) - } - - /// Build a shielded transfer transaction from the given parameters - pub fn shielded( - shielded_section_hash: MaspTxId, - transaction: MaspTransaction, - args: GlobalArgs, - ) -> Self { - let data = Transfer::masp(shielded_section_hash); - let mut tx = - transaction::build_tx(args, data, TX_TRANSFER_WASM.to_string()); - tx.add_masp_tx_section(transaction); - - Self(tx) - } - - /// Get the bytes to sign for the given transaction - pub fn get_sign_bytes(&self) -> Vec { - transaction::get_sign_bytes(&self.0) - } - - /// Attach the provided signatures to the tx - pub fn attach_signatures( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(transaction::attach_raw_signatures( - self.0, signer, signature, - )) - } - - /// Attach the fee data to the tx - pub fn attach_fee( - self, - fee: DenominatedAmount, - token: Address, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - Self(attach_fee(self.0, fee, token, fee_payer, gas_limit)) - } - - /// Get the bytes of the fee data to sign - pub fn get_fee_sig_bytes(&self) -> Hash { - transaction::get_wrapper_sign_bytes(&self.0) - } - - /// Attach a signature of the fee to the tx - pub fn attach_fee_signature( - self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - Self(attach_fee_signature(self.0, signer, signature)) - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - /// Gets the inner transaction without the domain wrapper - pub fn payload(self) -> Tx { - self.0 - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } -} diff --git a/crates/light_sdk/src/transaction/wrapper.rs b/crates/light_sdk/src/transaction/wrapper.rs deleted file mode 100644 index b8d9028e2e9..00000000000 --- a/crates/light_sdk/src/transaction/wrapper.rs +++ /dev/null @@ -1,56 +0,0 @@ -use namada_sdk::hash::Hash; -use namada_sdk::key::common; -use namada_sdk::chain::Epoch; -use namada_sdk::tx::data::{Fee, GasLimit}; -use namada_sdk::tx::{Section, Signature, Signer, Tx, TxError}; - -#[allow(missing_docs)] -pub struct Wrapper(Tx); - -impl Wrapper { - /// Takes a transaction and a signature and wraps them in a wrapper - /// transaction ready for submission - pub fn new( - mut tx: Tx, - fee: Fee, - fee_payer: common::PublicKey, - gas_limit: GasLimit, - ) -> Self { - tx.add_wrapper(fee, fee_payer, Epoch::default(), gas_limit); - - Self(tx) - } - - /// Returns the message to be signed for this transaction - pub fn get_sign_bytes(mut self) -> (Self, Vec) { - self.0.protocol_filter(); - let msg = self.0.sechashes(); - - (self, msg) - } - - /// Attach the given outer signature to the transaction - pub fn attach_signature( - mut self, - signer: common::PublicKey, - signature: common::Signature, - ) -> Self { - self.0.add_section(Section::Signature(Signature { - targets: self.0.sechashes(), - signer: Signer::PubKeys(vec![signer]), - signatures: [(0, signature)].into_iter().collect(), - })); - - self - } - - /// Validate this wrapper transaction - pub fn validate_tx(&self) -> Result, TxError> { - self.0.validate_tx() - } - - /// Generates the protobuf encoding of this transaction - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } -} diff --git a/crates/light_sdk/src/writing/asynchronous/mod.rs b/crates/light_sdk/src/writing/asynchronous/mod.rs deleted file mode 100644 index 23dda168e4c..00000000000 --- a/crates/light_sdk/src/writing/asynchronous/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::str::FromStr; - -use namada_sdk::error::{EncodingError, Error, TxSubmitError}; -use namada_sdk::io::Client; -use namada_sdk::tx::Tx; -use tendermint_config::net::Address as TendermintAddress; -use tendermint_rpc::HttpClient; -use tendermint_rpc::endpoint::broadcast::tx_sync::Response; -use tendermint_rpc::error::Error as RpcError; - -/// Broadcast a transaction to be included in the blockchain. This -/// -/// Checks that -/// 1. The tx has been successfully included into the mempool of a validator -/// 2. The tx has been included on the blockchain -/// -/// In the case of errors in any of those stages, an error message is returned -pub async fn broadcast_tx( - tendermint_addr: &str, - tx: Tx, -) -> Result { - let client = HttpClient::new( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .map_err(|e| Error::Other(e.to_string()))?; - - // NOTE: if we need an explicit client timeout, use - // `HttpClient::builder` to instantiate client - let response = client - .broadcast_tx_sync(tx.to_bytes()) - .await - .map_err(|e| Error::from(TxSubmitError::TxBroadcast(e)))?; - - if response.code == 0.into() { - Ok(response) - } else { - Err(Error::from(TxSubmitError::TxBroadcast(RpcError::server( - serde_json::to_string(&response).map_err(|err| { - Error::from(EncodingError::Serde(err.to_string())) - })?, - )))) - } -} diff --git a/crates/light_sdk/src/writing/blocking/mod.rs b/crates/light_sdk/src/writing/blocking/mod.rs deleted file mode 100644 index 39b9c072837..00000000000 --- a/crates/light_sdk/src/writing/blocking/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::str::FromStr; - -use namada_sdk::error::{EncodingError, Error, TxSubmitError}; -use namada_sdk::queries::Client; -use namada_sdk::tx::Tx; -use tendermint_config::net::Address as TendermintAddress; -use tendermint_rpc::HttpClient; -use tendermint_rpc::client::CompatMode; -use tendermint_rpc::endpoint::broadcast::tx_sync::Response; -use tendermint_rpc::error::Error as RpcError; -use tokio::runtime::Runtime; - -/// Broadcast a transaction to be included in the blockchain. This -/// -/// Checks that -/// 1. The tx has been successfully included into the mempool of a validator -/// 2. The tx has been included on the blockchain -/// -/// In the case of errors in any of those stages, an error message is returned -pub fn broadcast_tx(tendermint_addr: &str, tx: Tx) -> Result { - let client = HttpClient::builder( - TendermintAddress::from_str(tendermint_addr) - .map_err(|e| Error::Other(e.to_string()))?, - ) - .compat_mode(CompatMode::V0_37) - .timeout(std::time::Duration::from_secs(30)) - .build() - .map_err(|e| Error::Other(e.to_string()))?; - - let rt = Runtime::new().unwrap(); - - let wrapper_tx_hash = tx.header_hash().to_string(); - // We use this to determine when the inner tx makes it - // on-chain - let decrypted_tx_hash = tx.raw_header_hash().to_string(); - - let response = rt - .block_on(client.broadcast_tx_sync(tx.to_bytes())) - .map_err(|e| Error::from(TxSubmitError::TxBroadcast(e)))?; - - if response.code == 0.into() { - println!("Transaction added to mempool: {:?}", response); - // Print the transaction identifiers to enable the extraction of - // acceptance/application results later - { - println!("Wrapper transaction hash: {:?}", wrapper_tx_hash); - println!("Batch transaction hash: {:?}", decrypted_tx_hash); - } - Ok(response) - } else { - Err(Error::from(TxSubmitError::TxBroadcast(RpcError::server( - serde_json::to_string(&response).map_err(|err| { - Error::from(EncodingError::Serde(err.to_string())) - })?, - )))) - } -} diff --git a/crates/light_sdk/src/writing/mod.rs b/crates/light_sdk/src/writing/mod.rs deleted file mode 100644 index 5a925ded686..00000000000 --- a/crates/light_sdk/src/writing/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(not(feature = "blocking"))] -pub mod asynchronous; -#[cfg(feature = "blocking")] -pub mod blocking; diff --git a/crates/merkle_tree/src/eth_bridge_pool.rs b/crates/merkle_tree/src/eth_bridge_pool.rs deleted file mode 100644 index 1119a27411b..00000000000 --- a/crates/merkle_tree/src/eth_bridge_pool.rs +++ /dev/null @@ -1,985 +0,0 @@ -//! Ethereum bridge pool merkle tree - -use std::collections::{BTreeMap, BTreeSet}; - -use eyre::eyre; -use namada_core::borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use namada_core::chain::BlockHeight; -use namada_core::eth_abi::{Encode, Token}; -pub use namada_core::eth_bridge_pool::PendingTransfer; -use namada_core::hash::Hash; -use namada_core::keccak::keccak_hash; -use namada_core::storage; -use namada_core::storage::DbKeySeg; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; - -use crate::KeccakHash; - -#[derive(thiserror::Error, Debug)] -#[error(transparent)] -/// Generic error that may be returned by the validity predicate -pub struct Error(#[from] eyre::Error); - -/// Prefix to be used in Bridge pool tree root computations. -/// This value corresponds to leaf nodes. -const POOL_ROOT_PREFIX_LEAF: u8 = 0x00; - -/// Prefix to be used in Bridge pool tree root computations. -/// This value corresponds to non-leaf nodes. -const POOL_ROOT_PREFIX_NON_LEAF: u8 = 0xff; - -/// A simple Merkle tree for the Ethereum bridge pool -/// -/// Note that an empty tree has root [0u8; 20] by definition. -#[derive( - Debug, - Default, - Clone, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct BridgePoolTree { - /// Root of the tree - root: KeccakHash, - /// The underlying storage, containing hashes of [`PendingTransfer`]s. - leaves: BTreeMap, -} - -impl BridgePoolTree { - /// Create a new merkle tree for the Ethereum bridge pool - pub fn new( - root: KeccakHash, - store: BTreeMap, - ) -> Self { - Self { - root, - leaves: store, - } - } - - /// Parse the key to ensure it is of the correct type. - /// - /// If it is, it can be converted to a hash. - /// Checks if the hash is in the tree. - pub fn contains_key(&self, key: &storage::Key) -> Result { - Ok(self.leaves.contains_key(&Self::parse_key(key)?)) - } - - /// Get the height at which the key was inserted - /// - /// Returns the height if successful. Will - /// return an error if the key is malformed. - pub fn get(&self, key: &storage::Key) -> Result { - let hash = Self::parse_key(key)?; - self.leaves.get(&hash).cloned().ok_or_else(|| { - eyre!("Key not present in Bridge Pool merkle tree.").into() - }) - } - - /// Update the tree with a new value. - /// - /// Returns the new root if successful. Will - /// return an error if the key is malformed. - pub fn insert_key( - &mut self, - key: &storage::Key, - value: BlockHeight, - ) -> Result { - let hash = Self::parse_key(key)?; - _ = self.leaves.insert(hash, value); - self.root = self.compute_root(); - Ok(self.root().into()) - } - - /// Delete a key from storage and update the root - pub fn delete_key(&mut self, key: &storage::Key) -> Result<(), Error> { - let hash = Self::parse_key(key)?; - _ = self.leaves.remove(&hash); - self.root = self.compute_root(); - Ok(()) - } - - /// Compute the root of the merkle tree - fn compute_root(&self) -> KeccakHash { - let mut hashes: Vec = self.leaves.keys().cloned().collect(); - let mut prefix = POOL_ROOT_PREFIX_LEAF; - while hashes.len() > 1 { - let mut next_hashes = vec![]; - for pair in hashes.chunks(2) { - let left = pair[0].clone(); - let right = pair.get(1).cloned().unwrap_or_default(); - next_hashes.push(hash_pair(left, right, prefix)); - } - hashes = next_hashes; - prefix = POOL_ROOT_PREFIX_NON_LEAF; - } - - if hashes.is_empty() { - Default::default() - } else { - hashes.remove(0) - } - } - - /// Return the root as a [`struct@Hash`] type. - pub fn root(&self) -> KeccakHash { - self.root.clone() - } - - /// Recomputes the root and check if it matches the pre-computed root. - /// Used for checking if the underlying store is incorrect. - pub fn validate(&self) -> bool { - self.compute_root() == self.root - } - - /// Get a reference to the backing store - pub fn store(&self) -> &BTreeMap { - &self.leaves - } - - /// Create a batched membership proof for the provided keys - pub fn get_membership_proof( - &self, - mut values: Vec, - ) -> Result { - // sort the values according to their hash values - values.sort_by_key(|transfer| transfer.keccak256()); - - // get the leaf hashes - let leaves: BTreeSet = - values.iter().map(|v| v.keccak256()).collect(); - if !leaves.iter().all(|h| self.leaves.contains_key(h)) { - return Err(eyre!( - "Cannot generate proof for values that aren't in the tree" - ) - .into()); - } - let mut proof_hashes = vec![]; - let mut flags = vec![]; - let mut hashes: Vec<_> = self - .leaves - .keys() - .cloned() - .map(|hash| { - if leaves.contains(&hash) { - Node::OnPath(hash) - } else { - Node::OffPath(hash) - } - }) - .collect(); - - let mut prefix = POOL_ROOT_PREFIX_LEAF; - - while hashes.len() > 1 { - let mut next_hashes = vec![]; - - for pair in hashes.chunks(2) { - let left = pair[0].clone(); - let right = pair.get(1).cloned().unwrap_or_default(); - match (left, right) { - (Node::OnPath(left), Node::OnPath(right)) => { - flags.push(true); - next_hashes.push(Node::OnPath(hash_pair( - left.clone(), - right, - prefix, - ))); - } - (Node::OnPath(hash), Node::OffPath(sib)) => { - flags.push(false); - proof_hashes.push(sib.clone()); - next_hashes.push(Node::OnPath(hash_pair( - hash.clone(), - sib, - prefix, - ))); - } - (Node::OffPath(sib), Node::OnPath(hash)) => { - flags.push(false); - proof_hashes.push(sib.clone()); - next_hashes.push(Node::OnPath(hash_pair( - hash, - sib.clone(), - prefix, - ))); - } - (Node::OffPath(left), Node::OffPath(right)) => { - next_hashes.push(Node::OffPath(hash_pair( - left.clone(), - right, - prefix, - ))); - } - } - } - hashes = next_hashes; - prefix = POOL_ROOT_PREFIX_NON_LEAF; - } - // add the root to the proof - if flags.is_empty() && proof_hashes.is_empty() && leaves.is_empty() { - proof_hashes.push(self.root.clone()); - } - - Ok(BridgePoolProof { - proof: proof_hashes, - leaves: values, - flags, - }) - } - - /// Parse a db key to see if it is valid for the - /// bridge pool. - /// - /// It should have one string segment which should - /// parse into a [`struct@Hash`]. - pub fn parse_key(key: &storage::Key) -> Result { - if key.segments.len() == 1 { - match &key.segments[0] { - DbKeySeg::StringSeg(str) => { - str.as_str().try_into().map_err(|_| { - eyre!("Could not parse key segment as a hash").into() - }) - } - _ => Err(eyre!("Bridge pool keys should be strings.").into()), - } - } else { - Err(eyre!( - "Key for the bridge pool should have exactly one segment." - ) - .into()) - } - } -} - -/// Concatenate a byte prefix and two keccak hashes, -/// then compute the keccak hash of the resulting -/// byte array. -#[inline] -fn hash_pair(left: KeccakHash, right: KeccakHash, prefix: u8) -> KeccakHash { - let mut buf = [0u8; 32 + 32 + 1]; - buf[0] = prefix; - if left.0 < right.0 { - buf[1..33].copy_from_slice(&left.0); - buf[33..].copy_from_slice(&right.0); - } else { - buf[1..33].copy_from_slice(&right.0); - buf[33..].copy_from_slice(&left.0); - } - keccak_hash(buf) -} - -/// Keeps track if a node is on a path from the -/// root of the merkle tree to one of the leaves -/// being included in a multi-proof. -#[derive(Debug, Clone)] -enum Node { - /// Node is on a path from root to leaf in proof - OnPath(KeccakHash), - /// Node is not on a path from root to leaf in proof - OffPath(KeccakHash), -} - -impl Default for Node { - fn default() -> Self { - Self::OffPath(Default::default()) - } -} - -/// A multi-leaf membership proof -pub struct BridgePoolProof { - /// The hashes other than the provided leaves - pub proof: Vec, - /// The leaves; must be sorted - pub leaves: Vec, - /// Flags are used to indicate which consecutive - /// pairs of leaves in `leaves` are siblings. - pub flags: Vec, -} - -impl BridgePoolProof { - /// Verify a membership proof matches the provided root - pub fn verify(&self, root: KeccakHash) -> bool { - // Cannot overflow - #[allow(clippy::arithmetic_side_effects)] - let expected_len = self.flags.len() + 1; - #[allow(clippy::arithmetic_side_effects)] - let actual_len = self.proof.len() + self.leaves.len(); - - if actual_len != expected_len { - return false; - } - if self.flags.is_empty() { - return if let Some(leaf) = self.leaves.last() { - root == leaf.keccak256() - } else { - match self.proof.last() { - Some(proof_root) => &root == proof_root, - None => false, - } - }; - } - let total_hashes = self.flags.len(); - let leaf_len = self.leaves.len(); - - let mut hashes = vec![KeccakHash::default(); self.flags.len()]; - let mut hash_pos = 0usize; - let mut leaf_pos = 0usize; - let mut proof_pos = 0usize; - - // At most 2 additions per iter, cannot overflow usize - #[allow(clippy::arithmetic_side_effects)] - for i in 0..total_hashes { - let (left, prefix) = if leaf_pos < leaf_len { - let next = self.leaves[leaf_pos].keccak256(); - leaf_pos += 1; - (next, POOL_ROOT_PREFIX_LEAF) - } else { - let next = hashes[hash_pos].clone(); - hash_pos += 1; - (next, POOL_ROOT_PREFIX_NON_LEAF) - }; - let right = if self.flags[i] { - if leaf_pos < leaf_len { - let next = self.leaves[leaf_pos].keccak256(); - leaf_pos += 1; - next - } else { - let next = hashes[hash_pos].clone(); - hash_pos += 1; - next - } - } else { - let next = self.proof[proof_pos].clone(); - proof_pos += 1; - next - }; - hashes[i] = hash_pair(left, right, prefix); - } - - if let Some(computed) = hashes.last() { - *computed == root - } else { - false - } - } -} - -impl Encode<3> for BridgePoolProof { - fn tokenize(&self) -> [Token; 3] { - let BridgePoolProof { - proof, - leaves, - flags, - } = self; - let proof = Token::Array( - proof - .iter() - .map(|hash| Token::FixedBytes(hash.0.to_vec())) - .collect(), - ); - let transfers = Token::Array( - leaves - .iter() - .map(|t| Token::FixedArray(t.tokenize().to_vec())) - .collect(), - ); - let flags = - Token::Array(flags.iter().map(|flag| Token::Bool(*flag)).collect()); - [proof, transfers, flags] - } -} - -#[allow(clippy::cast_lossless)] -#[cfg(test)] -mod test_bridge_pool_tree { - - use assert_matches::assert_matches; - use itertools::Itertools; - use namada_core::address::Address; - use namada_core::address::testing::nam; - use namada_core::eth_bridge_pool::{ - GasFee, TransferToEthereum, TransferToEthereumKind, - }; - use namada_core::ethereum_events::EthAddress; - use namada_core::storage::Key; - use proptest::prelude::*; - - use super::*; - - /// An established user address for testing & development - fn bertha_address() -> Address { - Address::decode("tnam1qyctxtpnkhwaygye0sftkq28zedf774xc5a2m7st") - .expect("The token address decoding shouldn't fail") - } - - /// Test that if tree has a single leaf, its root is the hash - /// of that leaf - #[test] - fn test_update_single_key() { - let mut tree = BridgePoolTree::default(); - assert_eq!(tree.root().0, [0; 32]); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([1; 20]), - sender: bertha_address(), - recipient: EthAddress([2; 20]), - amount: 1.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let key = Key::from(&transfer); - let root = KeccakHash::from( - tree.insert_key(&key, BlockHeight(1)).expect("Test failed"), - ); - assert_eq!(root, transfer.keccak256()); - let height = tree.get(&key).expect("Test failed"); - assert_eq!(BlockHeight(1), height); - } - - #[test] - fn test_two_keys() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..2 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - let expected = hash_pair( - transfers[0].keccak256(), - transfers[1].keccak256(), - POOL_ROOT_PREFIX_LEAF, - ); - assert_eq!(tree.root(), expected); - } - - /// This is the first number of keys to use dummy leaves - #[test] - fn test_three_leaves() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..3 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - transfers.sort_by_key(|t| t.keccak256()); - let hashes: BTreeSet = - transfers.iter().map(|t| t.keccak256()).collect(); - assert_eq!( - hashes, - tree.leaves.keys().cloned().collect::>() - ); - - let left_hash = hash_pair( - transfers[0].keccak256(), - transfers[1].keccak256(), - POOL_ROOT_PREFIX_LEAF, - ); - let right_hash = hash_pair( - transfers[2].keccak256(), - Default::default(), - POOL_ROOT_PREFIX_LEAF, - ); - let expected = - hash_pair(left_hash, right_hash, POOL_ROOT_PREFIX_NON_LEAF); - assert_eq!(tree.root(), expected); - } - - /// Test removing all keys - #[test] - fn test_delete_all_keys() { - let mut tree = BridgePoolTree::default(); - - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([1; 20]), - sender: bertha_address(), - recipient: EthAddress([2; 20]), - amount: 1.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let key = Key::from(&transfer); - let root = KeccakHash::from( - tree.insert_key(&key, BlockHeight(1)).expect("Test failed"), - ); - assert_eq!(root, transfer.keccak256()); - tree.delete_key(&key).expect("Test failed"); - assert_eq!(tree.root().0, [0; 32]); - } - - /// Test deleting a key - #[test] - fn test_delete_key() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..3 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - transfers.sort_by_key(|t| t.keccak256()); - let deleted_key = Key::from(&transfers[1]); - tree.delete_key(&deleted_key).expect("Test failed"); - - let expected = hash_pair( - transfers[0].keccak256(), - transfers[2].keccak256(), - POOL_ROOT_PREFIX_LEAF, - ); - assert_eq!(tree.root(), expected); - assert_matches!(tree.get(&deleted_key), Err(_)); - } - - /// Test that parse key works correctly - #[test] - fn test_parse_key() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([1; 20]), - sender: bertha_address(), - recipient: EthAddress([2; 20]), - amount: 1u64.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let expected = transfer.keccak256(); - let key = Key::from(&transfer); - assert_eq!( - BridgePoolTree::parse_key(&key).expect("Test failed"), - expected - ); - } - - /// Test that parsing a key with multiple segments fails - #[test] - fn test_key_multiple_segments() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([1; 20]), - sender: bertha_address(), - recipient: EthAddress([2; 20]), - amount: 1u64.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let hash = transfer.keccak256().to_string(); - let key = Key { - segments: vec![ - DbKeySeg::AddressSeg(bertha_address()), - DbKeySeg::StringSeg(hash), - ], - }; - assert!(BridgePoolTree::parse_key(&key).is_err()); - } - - /// Test that parsing a key that is not a hash fails - #[test] - fn test_key_not_hash() { - let key = Key { - segments: vec![DbKeySeg::StringSeg("bloop".into())], - }; - assert!(BridgePoolTree::parse_key(&key).is_err()); - } - - /// Test that [`contains_key`] works correctly - #[test] - fn test_contains_key() { - let mut tree = BridgePoolTree::default(); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([1; 20]), - sender: bertha_address(), - recipient: EthAddress([2; 20]), - amount: 1.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - tree.insert_key(&Key::from(&transfer), BlockHeight(1)) - .expect("Test failed"); - assert!( - tree.contains_key(&Key::from(&transfer)) - .expect("Test failed") - ); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([1; 20]), - sender: bertha_address(), - recipient: EthAddress([0; 20]), - amount: 1u64.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - assert!( - !tree - .contains_key(&Key::from(&transfer)) - .expect("Test failed") - ); - } - - /// Test that the empty proof works. - #[test] - fn test_empty_proof() { - let tree = BridgePoolTree::default(); - let values = vec![]; - let proof = tree.get_membership_proof(values).expect("Test failed"); - assert!(proof.verify(Default::default())); - } - - /// Test that the proof works for proving the only leaf in the tree - #[test] - fn test_single_leaf() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - sender: bertha_address(), - recipient: EthAddress([0; 20]), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let mut tree = BridgePoolTree::default(); - let key = Key::from(&transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - let proof = tree - .get_membership_proof(vec![transfer]) - .expect("Test failed"); - assert!(proof.verify(tree.root())); - } - - /// Check proofs for membership of single transfer - /// in a tree with two leaves. - #[test] - fn test_one_leaf_of_two_proof() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..2 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - let proof = tree - .get_membership_proof(vec![transfers.remove(0)]) - .expect("Test failed"); - assert!(proof.verify(tree.root())); - } - - /// Test that a multiproof works for leaves who are siblings - #[test] - fn test_proof_two_out_of_three_leaves() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..3 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - transfers.sort_by_key(|t| t.keccak256()); - let values = vec![transfers[0].clone(), transfers[1].clone()]; - let proof = tree.get_membership_proof(values).expect("Test failed"); - assert!(proof.verify(tree.root())); - } - - /// Test that proving an empty subset of leaves always works - #[test] - fn test_proof_no_leaves() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..3 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - let values = vec![]; - let proof = tree.get_membership_proof(values).expect("Test failed"); - assert!(proof.verify(tree.root())) - } - - /// Test a proof for all the leaves - #[test] - fn test_proof_all_leaves() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..2 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - transfers.sort_by_key(|t| t.keccak256()); - let proof = tree.get_membership_proof(transfers).expect("Test failed"); - assert!(proof.verify(tree.root())); - } - - /// Test a proof for all the leaves when the number of leaves is odd - #[test] - fn test_proof_all_leaves_odd() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..3 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - transfers.sort_by_key(|t| t.keccak256()); - let proof = tree.get_membership_proof(transfers).expect("Test failed"); - assert!(proof.verify(tree.root())); - } - - /// Test proofs of large trees - #[test] - fn test_large_proof() { - let mut tree = BridgePoolTree::default(); - let mut transfers = vec![]; - for i in 0..5 { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([i; 20]), - sender: bertha_address(), - recipient: EthAddress([i + 1; 20]), - amount: (i as u64).into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - let key = Key::from(&transfer); - transfers.push(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - transfers.sort_by_key(|t| t.keccak256()); - let values: Vec<_> = transfers.iter().step_by(2).cloned().collect(); - let proof = tree.get_membership_proof(values).expect("Test failed"); - assert!(proof.verify(tree.root())); - } - - /// Create a random set of transfers. - fn random_transfers( - number: usize, - ) -> impl Strategy> { - prop::collection::vec(prop::array::uniform20(0u8..), 0..=number) - .prop_flat_map(|addrs| { - Just( - addrs - .into_iter() - .map(|addr| PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress(addr), - sender: bertha_address(), - recipient: EthAddress(addr), - amount: Default::default(), - }, - gas_fee: GasFee { - token: nam(), - amount: Default::default(), - payer: bertha_address(), - }, - }) - .dedup() - .collect::>(), - ) - }) - } - - prop_compose! { - /// Creates a random set of transfers and - /// then returns them along with a chosen subset. - fn arb_transfers_and_subset() - (transfers in random_transfers(50)) - ( - transfers in Just(transfers.clone()), - to_prove in proptest::sample::subsequence(transfers.clone(), 0..=transfers.len()), - ) - -> (Vec, Vec) { - (transfers, to_prove) - } - } - - proptest! { - /// Given a random tree and a subset of leaves, - /// verify that the constructed multi-proof correctly - /// verifies. - #[test] - fn test_verify_proof((transfers, mut to_prove) in arb_transfers_and_subset()) { - let mut tree = BridgePoolTree::default(); - for transfer in &transfers { - let key = Key::from(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - - to_prove.sort_by_key(|t| t.keccak256()); - let proof = tree.get_membership_proof(to_prove).expect("Test failed"); - assert!(proof.verify(tree.root())); - } - - /// Check that validate root passes when the tree is constructed correctly - #[test] - fn test_validate_root((transfers, _) in arb_transfers_and_subset()) { - let mut tree = BridgePoolTree::default(); - for transfer in &transfers { - let key = Key::from(transfer); - let _ = tree.insert_key(&key, BlockHeight(1)).expect("Test failed"); - } - assert!(tree.validate()); - } - } -} diff --git a/crates/merkle_tree/src/lib.rs b/crates/merkle_tree/src/lib.rs index 96832e7c500..4decb35bfe2 100644 --- a/crates/merkle_tree/src/lib.rs +++ b/crates/merkle_tree/src/lib.rs @@ -17,7 +17,6 @@ clippy::print_stderr )] -pub mod eth_bridge_pool; pub mod ics23_specs; use std::fmt; @@ -29,7 +28,6 @@ use arse_merkle_tree::error::Error as MtError; use arse_merkle_tree::{ Hash as SmtHash, Key as TreeKey, SparseMerkleTree as ArseMerkleTree, }; -use eth_bridge_pool::{BridgePoolProof, BridgePoolTree}; pub use ics23::CommitmentProof; use ics23::commitment_proof::Proof as Ics23Proof; use ics23::{ExistenceProof, NonExistenceProof}; @@ -38,9 +36,7 @@ use namada_core::address::{Address, InternalAddress}; use namada_core::borsh::{BorshDeserialize, BorshSerialize, BorshSerializeExt}; use namada_core::bytes::HEXLOWER; pub use namada_core::chain::{BlockHeight, Epoch}; -use namada_core::eth_bridge_pool::{PendingTransfer, is_pending_transfer_key}; pub use namada_core::hash::{Hash, StorageHasher}; -pub use namada_core::keccak::KeccakHash; pub use namada_core::storage::Key; use namada_core::storage::{ self, DbKeySeg, IBC_KEY_LIMIT, KeySeg, StringKey, TreeBytes, TreeKeyError, @@ -95,8 +91,6 @@ pub trait SubTreeWrite { pub enum MembershipProof { /// ICS23 compliant membership proof ICS23(CommitmentProof), - /// Bespoke membership proof for the Ethereum bridge pool - BridgePool(BridgePoolProof), } impl From for MembershipProof { @@ -105,12 +99,6 @@ impl From for MembershipProof { } } -impl From for MembershipProof { - fn from(proof: BridgePoolProof) -> Self { - Self::BridgePool(proof) - } -} - #[allow(missing_docs)] #[derive(Error, Debug)] pub enum Error { @@ -157,8 +145,6 @@ pub type StorageBytes<'a> = &'a [u8]; pub type SmtStore = DefaultStore; /// Arse-merkle-tree store pub type AmtStore = DefaultStore; -/// Bridge pool store -pub type BridgePoolStore = std::collections::BTreeMap; /// Sparse-merkle-tree pub type Smt = ArseMerkleTree; /// Arse-merkle-tree @@ -187,8 +173,6 @@ pub enum StoreType { Ibc, /// For PoS-related data PoS, - /// For the Ethereum bridge Pool transfers - BridgePool, /// For data not stored to diffs NoDiff, /// For the commit only data @@ -205,8 +189,6 @@ pub enum Store { Ibc(AmtStore), /// For PoS-related data PoS(SmtStore), - /// For the Ethereum bridge Pool transfers - BridgePool(BridgePoolStore), /// For data not stored to diffs NoDiff(SmtStore), /// For the commit only data @@ -221,7 +203,6 @@ impl Store { Self::Account(store) => StoreRef::Account(store), Self::Ibc(store) => StoreRef::Ibc(store), Self::PoS(store) => StoreRef::PoS(store), - Self::BridgePool(store) => StoreRef::BridgePool(store), Self::NoDiff(store) => StoreRef::NoDiff(store), Self::CommitData => StoreRef::CommitData, } @@ -238,8 +219,6 @@ pub enum StoreRef<'a> { Ibc(&'a AmtStore), /// For PoS-related data PoS(&'a SmtStore), - /// For the Ethereum bridge Pool transfers - BridgePool(&'a BridgePoolStore), /// For data not stored to diffs NoDiff(&'a SmtStore), /// For commit only data @@ -254,7 +233,6 @@ impl StoreRef<'_> { Self::Account(store) => Store::Account(store.to_owned()), Self::Ibc(store) => Store::Ibc(store.to_owned()), Self::PoS(store) => Store::PoS(store.to_owned()), - Self::BridgePool(store) => Store::BridgePool(store.to_owned()), Self::NoDiff(store) => Store::NoDiff(store.to_owned()), Self::CommitData => Store::CommitData, } @@ -267,7 +245,6 @@ impl StoreRef<'_> { Self::Account(store) => store.serialize_to_vec(), Self::Ibc(store) => store.serialize_to_vec(), Self::PoS(store) => store.serialize_to_vec(), - Self::BridgePool(store) => store.serialize_to_vec(), Self::NoDiff(store) => store.serialize_to_vec(), Self::CommitData => vec![], } @@ -277,12 +254,11 @@ impl StoreRef<'_> { impl StoreType { /// Get an iterator for the base tree and subtrees pub fn iter() -> std::slice::Iter<'static, Self> { - static SUB_TREE_TYPES: [StoreType; 7] = [ + static SUB_TREE_TYPES: [StoreType; 6] = [ StoreType::Base, StoreType::Account, StoreType::PoS, StoreType::Ibc, - StoreType::BridgePool, StoreType::NoDiff, StoreType::CommitData, ]; @@ -291,11 +267,10 @@ impl StoreType { /// Get an iterator for subtrees pub fn iter_subtrees() -> std::slice::Iter<'static, Self> { - static SUB_TREE_TYPES: [StoreType; 6] = [ + static SUB_TREE_TYPES: [StoreType; 5] = [ StoreType::Account, StoreType::PoS, StoreType::Ibc, - StoreType::BridgePool, StoreType::NoDiff, StoreType::CommitData, ]; @@ -304,8 +279,7 @@ impl StoreType { /// Get an iterator for the provable subtrees pub fn iter_provable() -> std::slice::Iter<'static, Self> { - static SUB_TREE_TYPES: [StoreType; 2] = - [StoreType::Ibc, StoreType::BridgePool]; + static SUB_TREE_TYPES: [StoreType; 1] = [StoreType::Ibc]; SUB_TREE_TYPES.iter() } @@ -338,15 +312,6 @@ impl StoreType { InternalAddress::Ibc => { Ok((StoreType::Ibc, key.sub_key()?)) } - InternalAddress::EthBridgePool => { - // the root of this sub-tree is kept in accounts - // storage along with a quorum of validator signatures - if is_pending_transfer_key(key) { - Ok((StoreType::BridgePool, key.sub_key()?)) - } else { - Ok((StoreType::Account, key.clone())) - } - } // use the same key for Parameters _ => Ok((StoreType::Account, key.clone())), } @@ -367,9 +332,6 @@ impl StoreType { pub fn provable_prefix(&self) -> Option { let addr = match self { Self::Ibc => Address::Internal(InternalAddress::Ibc), - Self::BridgePool => { - Address::Internal(InternalAddress::EthBridgePool) - } _ => return None, }; Some(addr.to_db_key().into()) @@ -385,7 +347,6 @@ impl StoreType { Self::Account => Ok(Store::Account(decode(bytes)?)), Self::Ibc => Ok(Store::Ibc(decode(bytes)?)), Self::PoS => Ok(Store::PoS(decode(bytes)?)), - Self::BridgePool => Ok(Store::BridgePool(decode(bytes)?)), Self::NoDiff => Ok(Store::NoDiff(decode(bytes)?)), Self::CommitData => Ok(Store::CommitData), } @@ -401,7 +362,6 @@ impl FromStr for StoreType { "account" => Ok(StoreType::Account), "ibc" => Ok(StoreType::Ibc), "pos" => Ok(StoreType::PoS), - "eth_bridge_pool" => Ok(StoreType::BridgePool), "no_diff" => Ok(StoreType::NoDiff), "commit_data" => Ok(StoreType::CommitData), _ => Err(Error::StoreType(s.to_string())), @@ -416,7 +376,6 @@ impl fmt::Display for StoreType { StoreType::Account => write!(f, "account"), StoreType::Ibc => write!(f, "ibc"), StoreType::PoS => write!(f, "pos"), - StoreType::BridgePool => write!(f, "eth_bridge_pool"), StoreType::NoDiff => write!(f, "no_diff"), StoreType::CommitData => write!(f, "commit_data"), } @@ -463,7 +422,6 @@ pub struct MerkleTree { account: Smt, ibc: Amt, pos: Smt, - bridge_pool: BridgePoolTree, no_diff: Smt, commit_data: CommitDataRoot, } @@ -484,8 +442,6 @@ impl MerkleTree { let account = Smt::new(stores.account.0.into(), stores.account.1); let ibc = Amt::new(stores.ibc.0.into(), stores.ibc.1); let pos = Smt::new(stores.pos.0.into(), stores.pos.1); - let bridge_pool = - BridgePoolTree::new(stores.bridge_pool.0, stores.bridge_pool.1); let no_diff = Smt::new(stores.no_diff.0.into(), stores.no_diff.1); let commit_data = stores.commit.into(); @@ -494,7 +450,6 @@ impl MerkleTree { account, ibc, pos, - bridge_pool, no_diff, commit_data, }; @@ -506,8 +461,6 @@ impl MerkleTree { let ibc_root = tree.base.get(&ibc_key.into())?; let pos_key = H::hash(StoreType::PoS.to_string()); let pos_root = tree.base.get(&pos_key.into())?; - let bp_key = H::hash(StoreType::BridgePool.to_string()); - let bp_root = tree.base.get(&bp_key.into())?; let no_diff_key = H::hash(StoreType::NoDiff.to_string()); let no_diff_root = tree.base.get(&no_diff_key.into())?; let commit_data_key = H::hash(StoreType::CommitData.to_string()); @@ -516,13 +469,11 @@ impl MerkleTree { && tree.account.root().is_zero() && tree.ibc.root().is_zero() && tree.pos.root().is_zero() - && tree.bridge_pool.root().is_zero() && tree.no_diff.root().is_zero() && tree.commit_data.0.is_zero() || (account_root == tree.account.root().into() && ibc_root == tree.ibc.root().into() && pos_root == tree.pos.root().into() - && bp_root == tree.bridge_pool.root().into() && no_diff_root == tree.no_diff.root().into() && commit_data_root == tree.commit_data.0) { @@ -540,8 +491,6 @@ impl MerkleTree { let account = Smt::new(stores.account.0.into(), stores.account.1); let ibc = Amt::new(stores.ibc.0.into(), stores.ibc.1); let pos = Smt::new(stores.pos.0.into(), stores.pos.1); - let bridge_pool = - BridgePoolTree::new(stores.bridge_pool.0, stores.bridge_pool.1); let no_diff = Smt::new(stores.no_diff.0.into(), stores.no_diff.1); let commit_data = stores.commit.into(); @@ -550,7 +499,6 @@ impl MerkleTree { account, ibc, pos, - bridge_pool, no_diff, commit_data, } @@ -562,7 +510,6 @@ impl MerkleTree { StoreType::Account => Box::new(&self.account), StoreType::Ibc => Box::new(&self.ibc), StoreType::PoS => Box::new(&self.pos), - StoreType::BridgePool => Box::new(&self.bridge_pool), StoreType::NoDiff => Box::new(&self.no_diff), StoreType::CommitData => Box::new(&self.commit_data), } @@ -577,7 +524,6 @@ impl MerkleTree { StoreType::Account => Box::new(&mut self.account), StoreType::Ibc => Box::new(&mut self.ibc), StoreType::PoS => Box::new(&mut self.pos), - StoreType::BridgePool => Box::new(&mut self.bridge_pool), StoreType::NoDiff => Box::new(&mut self.no_diff), StoreType::CommitData => Box::new(&mut self.commit_data), } @@ -649,7 +595,6 @@ impl MerkleTree { if self.account.validate() && self.ibc.validate() && self.pos.validate() - && self.bridge_pool.validate() && self.no_diff.validate() { let mut reconstructed = Smt::::default(); @@ -665,10 +610,6 @@ impl MerkleTree { H::hash(StoreType::Ibc.to_string()).into(), self.ibc.root().into(), )?; - reconstructed.update( - H::hash(StoreType::BridgePool.to_string()).into(), - self.bridge_pool.root().into(), - )?; reconstructed.update( H::hash(StoreType::NoDiff.to_string()).into(), self.no_diff.root().into(), @@ -699,10 +640,6 @@ impl MerkleTree { account: (self.account.root().into(), self.account.store()), ibc: (self.ibc.root().into(), self.ibc.store()), pos: (self.pos.root().into(), self.pos.store()), - bridge_pool: ( - self.bridge_pool.root().into(), - self.bridge_pool.store(), - ), no_diff: (self.no_diff.root().into(), self.no_diff.store()), commit: self.commit_data.0, } @@ -822,18 +759,6 @@ impl From<&H256> for MerkleRoot { } } -impl From for MerkleRoot { - fn from(root: KeccakHash) -> Self { - Self(root.0) - } -} - -impl From for KeccakHash { - fn from(root: MerkleRoot) -> Self { - Self(root.0) - } -} - impl From for Hash { fn from(root: MerkleRoot) -> Self { Self(root.0) @@ -853,7 +778,6 @@ pub struct MerkleTreeStoresRead { account: (Hash, SmtStore), ibc: (Hash, AmtStore), pos: (Hash, SmtStore), - bridge_pool: (KeccakHash, BridgePoolStore), no_diff: (Hash, SmtStore), commit: Hash, } @@ -866,7 +790,6 @@ impl MerkleTreeStoresRead { StoreType::Account => self.account.0 = root, StoreType::Ibc => self.ibc.0 = root, StoreType::PoS => self.pos.0 = root, - StoreType::BridgePool => self.bridge_pool.0 = root.into(), StoreType::NoDiff => self.no_diff.0 = root, StoreType::CommitData => self.commit = root, } @@ -879,7 +802,6 @@ impl MerkleTreeStoresRead { Store::Account(store) => self.account.1 = store, Store::Ibc(store) => self.ibc.1 = store, Store::PoS(store) => self.pos.1 = store, - Store::BridgePool(store) => self.bridge_pool.1 = store, Store::NoDiff(store) => self.no_diff.1 = store, Store::CommitData => (), } @@ -892,7 +814,6 @@ impl MerkleTreeStoresRead { StoreType::Account => StoreRef::Account(&self.account.1), StoreType::Ibc => StoreRef::Ibc(&self.ibc.1), StoreType::PoS => StoreRef::PoS(&self.pos.1), - StoreType::BridgePool => StoreRef::BridgePool(&self.bridge_pool.1), StoreType::NoDiff => StoreRef::NoDiff(&self.no_diff.1), StoreType::CommitData => StoreRef::CommitData, } @@ -905,7 +826,6 @@ impl MerkleTreeStoresRead { StoreType::Account => self.account.0, StoreType::Ibc => self.ibc.0, StoreType::PoS => self.pos.0, - StoreType::BridgePool => Hash(self.bridge_pool.0.0), StoreType::NoDiff => self.no_diff.0, StoreType::CommitData => Hash(self.commit.0), } @@ -918,7 +838,6 @@ pub struct MerkleTreeStoresWrite<'a> { account: (Hash, &'a SmtStore), ibc: (Hash, &'a AmtStore), pos: (Hash, &'a SmtStore), - bridge_pool: (Hash, &'a BridgePoolStore), no_diff: (Hash, &'a SmtStore), commit: Hash, } @@ -931,7 +850,6 @@ impl MerkleTreeStoresWrite<'_> { StoreType::Account => &self.account.0, StoreType::Ibc => &self.ibc.0, StoreType::PoS => &self.pos.0, - StoreType::BridgePool => &self.bridge_pool.0, StoreType::NoDiff => &self.no_diff.0, StoreType::CommitData => &self.commit, } @@ -944,7 +862,6 @@ impl MerkleTreeStoresWrite<'_> { StoreType::Account => StoreRef::Account(self.account.1), StoreType::Ibc => StoreRef::Ibc(self.ibc.1), StoreType::PoS => StoreRef::PoS(self.pos.1), - StoreType::BridgePool => StoreRef::BridgePool(self.bridge_pool.1), StoreType::NoDiff => StoreRef::NoDiff(self.no_diff.1), StoreType::CommitData => StoreRef::CommitData, } @@ -1144,61 +1061,6 @@ impl SubTreeWrite for &mut Amt { } } -impl SubTreeRead for &BridgePoolTree { - fn root(&self) -> MerkleRoot { - BridgePoolTree::root(self).into() - } - - fn validate(&self) -> bool { - BridgePoolTree::validate(self) - } - - fn subtree_has_key(&self, key: &Key) -> Result { - self.contains_key(key) - .map_err(|err| Error::MerkleTree(err.to_string())) - } - - fn subtree_get(&self, key: &Key) -> Result> { - match self.get(key) { - Ok(height) => Ok(height.serialize_to_vec()), - Err(err) => Err(Error::MerkleTree(err.to_string())), - } - } - - fn subtree_membership_proof( - &self, - _: &[Key], - values: Vec>, - ) -> Result { - let values = values - .iter() - .filter_map(|val| PendingTransfer::try_from_slice(val).ok()) - .collect(); - self.get_membership_proof(values) - .map(Into::into) - .map_err(|err| Error::MerkleTree(err.to_string())) - } -} - -impl SubTreeWrite for &mut BridgePoolTree { - fn subtree_update( - &mut self, - key: &Key, - value: StorageBytes<'_>, - ) -> Result { - let height = BlockHeight::try_from_slice(value) - .map_err(|err| Error::MerkleTree(err.to_string()))?; - self.insert_key(key, height) - .map_err(|err| Error::MerkleTree(err.to_string())) - } - - fn subtree_delete(&mut self, key: &Key) -> Result { - self.delete_key(key) - .map_err(|err| Error::MerkleTree(err.to_string()))?; - Ok(self.root().into()) - } -} - impl SubTreeRead for &CommitDataRoot { fn root(&self) -> MerkleRoot { self.0.into() @@ -1429,16 +1291,12 @@ mod test { tree.update(&pos_key, pos_val).unwrap(); let specs = ibc_proof_specs::(); - let proof = match tree + let MembershipProof::ICS23(proof) = tree .get_sub_tree_existence_proof( std::array::from_ref(&ibc_key), vec![&ibc_val], ) - .unwrap() - { - MembershipProof::ICS23(proof) => proof, - _ => panic!("Test failed"), - }; + .unwrap(); let proof = tree.get_sub_tree_proof(&ibc_key, proof).unwrap(); let (store_type, sub_key) = StoreType::sub_key(&ibc_key).unwrap(); let paths = [sub_key.to_string(), store_type.to_string()]; @@ -1492,16 +1350,12 @@ mod test { tree.update(&pos_key, pos_val.clone()).unwrap(); let specs = proof_specs::(); - let proof = match tree + let MembershipProof::ICS23(proof) = tree .get_sub_tree_existence_proof( std::array::from_ref(&pos_key), vec![&pos_val], ) - .unwrap() - { - MembershipProof::ICS23(proof) => proof, - _ => panic!("Test failed"), - }; + .unwrap(); let proof = tree.get_sub_tree_proof(&pos_key, proof).unwrap(); let (store_type, sub_key) = StoreType::sub_key(&pos_key).unwrap(); diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index a69b883bcd2..96df6656f98 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -32,7 +32,6 @@ benches = [ ] jemalloc = ["rocksdb/jemalloc"] migrations = ["namada_migrations", "namada_sdk/migrations", "linkme"] -namada-eth-bridge = ["namada_sdk/namada-eth-bridge"] [dependencies] namada_apps_lib.workspace = true @@ -43,7 +42,6 @@ namada_sdk = { workspace = true, default-features = true, features = [ ] } namada_test_utils = { workspace = true, optional = true } namada_vm.workspace = true -namada_vote_ext.workspace = true namada_vp.workspace = true arse-merkle-tree = { workspace = true, features = ["blake2b"] } diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index 2314eb0d4b9..0e32b88eadb 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -105,7 +105,7 @@ use namada_sdk::tx::{ Authorization, BatchedTx, BatchedTxRef, Code, Data, IndexedTx, Section, Tx, }; pub use namada_sdk::tx::{ - TX_BECOME_VALIDATOR_WASM, TX_BOND_WASM, TX_BRIDGE_POOL_WASM, + TX_BECOME_VALIDATOR_WASM, TX_BOND_WASM, TX_CHANGE_COMMISSION_WASM as TX_CHANGE_VALIDATOR_COMMISSION_WASM, TX_CHANGE_CONSENSUS_KEY_WASM, TX_CHANGE_METADATA_WASM as TX_CHANGE_VALIDATOR_METADATA_WASM, @@ -567,15 +567,12 @@ impl Default for BenchShell { .init(); }); - let (sender, _) = tokio::sync::mpsc::unbounded_channel(); let tempdir = tempfile::tempdir().unwrap(); let path = tempdir.path().canonicalize().unwrap(); let shell = Shell::new( config::Ledger::new(path, Default::default(), TendermintMode::Full), WASM_DIR.into(), - sender, - None, None, None, 50 * 1024 * 1024, // 50 kiB @@ -1056,7 +1053,7 @@ impl Client for BenchShell { // We can expect all the masp tranfers to have happened only in the last // block - let end_block_events = if height.value() + let finalize_block_events = if height.value() == shell.inner.state.in_mem().get_last_block_height().0 { let mut res = vec![]; @@ -1117,17 +1114,17 @@ impl Client for BenchShell { res.push(namada_sdk::tendermint::abci::Event::from(event)); } } - Some(res) + res } else { - None + Default::default() }; Ok(tendermint_rpc::endpoint::block_results::Response { height, txs_results: None, - finalize_block_events: vec![], + finalize_block_events, begin_block_events: None, - end_block_events, + end_block_events: None, validator_updates: vec![], consensus_param_updates: None, app_hash: namada_sdk::tendermint::hash::AppHash::default(), diff --git a/crates/node/src/broadcaster.rs b/crates/node/src/broadcaster.rs index a5d854737d4..e985f160dc8 100644 --- a/crates/node/src/broadcaster.rs +++ b/crates/node/src/broadcaster.rs @@ -27,7 +27,7 @@ impl Broadcaster { client: HttpClient::builder( format!("http://{}", url).as_str().try_into().unwrap(), ) - .compat_mode(CompatMode::V0_37) + .compat_mode(CompatMode::V0_38) .timeout(std::time::Duration::from_secs(30)) .build() .unwrap(), diff --git a/crates/node/src/ethereum_oracle/control.rs b/crates/node/src/ethereum_oracle/control.rs deleted file mode 100644 index 62a59692f50..00000000000 --- a/crates/node/src/ethereum_oracle/control.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! The oracle is controlled by sending commands over a channel. - -use namada_sdk::eth_bridge::oracle::config::Config; -use tokio::sync::mpsc; -use tokio::sync::mpsc::error::TrySendError; - -/// Used by an oracle to receive commands. -pub type Receiver = mpsc::Receiver; - -/// Used to send commands to an oracle. -#[derive(Debug)] -pub struct Sender { - last_command: Option, - inner_sender: mpsc::Sender, -} - -impl Sender { - /// Send a [`Command`] if the last one is not repeated. - pub fn try_send( - &mut self, - cmd: Command, - ) -> Result<(), TrySendError> { - // NOTE: this code may be buggy if we happen to need to - // send repeated commands - if self.last_command.as_ref() != Some(&cmd) { - self.last_command = Some(cmd.clone()); - self.inner_sender.try_send(cmd) - } else { - Ok(()) - } - } -} - -/// Returns two sides of a [`mpsc`] channel that can be used for controlling an -/// oracle. -pub fn channel() -> (Sender, Receiver) { - let (inner_sender, receiver) = mpsc::channel(5); - let sender = Sender { - last_command: None, - inner_sender, - }; - (sender, receiver) -} - -/// Commands used to configure and control an `Oracle`. -#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] -pub enum Command { - /// Update the config if it changes in storage. - /// Also used to send an initial configuration to the oracle for it to use. - /// The oracle will not do anything until this command has been sent. - UpdateConfig(Config), -} diff --git a/crates/node/src/ethereum_oracle/events.rs b/crates/node/src/ethereum_oracle/events.rs deleted file mode 100644 index 59a345fb3fd..00000000000 --- a/crates/node/src/ethereum_oracle/events.rs +++ /dev/null @@ -1,548 +0,0 @@ -pub mod eth_events { - #![allow(dead_code)] - use std::fmt::Debug; - use std::str::FromStr; - - use ethbridge_bridge_events::{ - BridgeEvents, TransferToChainFilter, TransferToErcFilter, - ValidatorSetUpdateFilter, - }; - use ethbridge_events::{DynEventCodec, Events as RawEvents}; - use namada_sdk::address::Address; - use namada_sdk::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, Uint, - }; - use namada_sdk::ethereum_structs; - use namada_sdk::hash::Hash; - use namada_sdk::keccak::KeccakHash; - use namada_sdk::token::Amount; - use num256::Uint256; - use thiserror::Error; - - #[derive(Error, Debug)] - pub enum Error { - #[error("Could not decode Ethereum event: {0}")] - Decode(String), - #[error("The given Ethereum contract is not in use: {0}")] - NotInUse(String), - } - - pub type Result = std::result::Result; - - #[derive(Clone, Debug, PartialEq)] - /// An event waiting for a certain number of confirmations - /// before being sent to the ledger - pub(in super::super) struct PendingEvent { - /// number of confirmations to consider this event finalized - confirmations: Uint256, - /// the block height from which this event originated - block_height: Uint256, - /// the event itself - pub event: EthereumEvent, - } - - impl PendingEvent { - /// Decodes bytes into an [`EthereumEvent`] based on the signature. - /// This is turned into a [`PendingEvent`] along with the block - /// height passed in here. - /// - /// If the event contains a confirmations field, - /// this is passed to the corresponding [`PendingEvent`] field, - /// otherwise a default is used. - pub fn decode( - event_codec: DynEventCodec, - block_height: Uint256, - log: ðabi::RawLog, - mut confirmations: Uint256, - ) -> Result { - let raw_event = event_codec - .decode(log) - .map_err(|e| Error::Decode(e.to_string()))?; - // NOTE: **DO NOT** do any partial pattern matches - // on the generated structs. destructuring will help - // us to find bugs, if the representation of Ethereum - // events changes between `ethbridge-rs` versions - let event = match raw_event { - RawEvents::Bridge(BridgeEvents::TransferToErcFilter( - TransferToErcFilter { - nonce, - transfers, - relayer_address, - }, - )) => EthereumEvent::TransfersToEthereum { - nonce: nonce.parse_uint256()?, - transfers: transfers.parse_transfer_to_eth_array()?, - relayer: relayer_address.parse_address()?, - }, - RawEvents::Bridge(BridgeEvents::TransferToChainFilter( - TransferToChainFilter { - nonce, - transfers, - confirmations: requested_confirmations, - }, - )) => { - confirmations = confirmations.max({ - let mut num_buf = [0; 32]; - requested_confirmations.to_little_endian(&mut num_buf); - Uint256::from_le_bytes(&num_buf) - }); - EthereumEvent::TransfersToNamada { - nonce: nonce.parse_uint256()?, - transfers: transfers - .parse_transfer_to_namada_array()?, - } - } - RawEvents::Bridge(BridgeEvents::ValidatorSetUpdateFilter( - ValidatorSetUpdateFilter { - validator_set_nonce, - bridge_validator_set_hash, - governance_validator_set_hash, - }, - )) => EthereumEvent::ValidatorSetUpdate { - nonce: validator_set_nonce.into(), - bridge_validator_hash: bridge_validator_set_hash - .parse_keccak()?, - governance_validator_hash: governance_validator_set_hash - .parse_keccak()?, - }, - }; - Ok(PendingEvent { - confirmations, - block_height, - event, - }) - } - - /// Check if the minimum number of confirmations has been - /// reached at the input block height. - pub fn is_confirmed(&self, height: &Uint256) -> bool { - use num_traits::CheckedSub; - - self.confirmations - <= height.checked_sub(&self.block_height).unwrap_or_default() - } - } - - #[allow(unused_macros)] - macro_rules! parse_method { - ($name:ident -> $type:ty) => { - fn $name(self) -> Result<$type> { - unimplemented!() - } - }; - } - - macro_rules! trait_parse_def { - ($($name:ident -> $type:ty;)*) => { - /// Trait to add parsing methods to foreign types. - trait Parse: Sized { - $( parse_method!($name -> $type); )* - } - } - } - - trait_parse_def! { - parse_address -> Address; - parse_address_array -> Vec
; - parse_amount -> Amount; - parse_amount_array -> Vec; - parse_bool -> bool; - parse_eth_address -> EthAddress; - parse_eth_address_array -> Vec; - parse_hash -> Hash; - parse_keccak -> KeccakHash; - parse_string -> String; - parse_string_array -> Vec; - parse_transfer_to_eth -> TransferToEthereum; - parse_transfer_to_eth_array -> Vec; - parse_transfer_to_namada -> TransferToNamada; - parse_transfer_to_namada_array -> Vec; - parse_u32 -> u32; - parse_uint256 -> Uint; - } - - impl Parse for ethabi::Address { - fn parse_eth_address(self) -> Result { - Ok(EthAddress(self.0)) - } - } - - impl Parse for String { - fn parse_address(self) -> Result
{ - Address::from_str(&self) - .map_err(|err| Error::Decode(format!("{:?}", err))) - } - - fn parse_string(self) -> Result { - Ok(self) - } - } - - impl Parse for ethabi::Uint { - fn parse_amount(self) -> Result { - let uint = { - use namada_sdk::uint::Uint as NamadaUint; - let mut num_buf = [0; 32]; - self.to_little_endian(&mut num_buf); - NamadaUint::from_little_endian(&num_buf) - }; - Amount::from_uint(uint, 0).map_err(|e| Error::Decode(e.to_string())) - } - - fn parse_u32(self) -> Result { - Ok(self.as_u32()) - } - - fn parse_uint256(self) -> Result { - Ok(self.into()) - } - } - - impl Parse for bool { - fn parse_bool(self) -> Result { - Ok(self) - } - } - - impl Parse for [u8; 32] { - fn parse_keccak(self) -> Result { - Ok(KeccakHash(self)) - } - - fn parse_hash(self) -> Result { - Ok(Hash(self)) - } - } - - impl Parse for Vec { - fn parse_amount_array(self) -> Result> { - self.into_iter().try_fold(Vec::new(), |mut acc, amount| { - acc.push(amount.parse_amount()?); - Ok(acc) - }) - } - } - - impl Parse for Vec { - fn parse_eth_address_array(self) -> Result> { - self.into_iter().try_fold(Vec::new(), |mut acc, addr| { - acc.push(addr.parse_eth_address()?); - Ok(acc) - }) - } - } - - impl Parse for Vec { - fn parse_transfer_to_namada_array( - self, - ) -> Result> { - self.into_iter().try_fold(Vec::new(), |mut acc, transf| { - acc.push(transf.parse_transfer_to_namada()?); - Ok(acc) - }) - } - } - - impl Parse for ethereum_structs::ChainTransfer { - fn parse_transfer_to_namada(self) -> Result { - let asset = self.from.parse_eth_address()?; - let amount = self.amount.parse_amount()?; - let receiver = self.to.parse_address()?; - Ok(TransferToNamada { - asset, - amount, - receiver, - }) - } - } - - impl Parse for Vec { - fn parse_transfer_to_eth_array( - self, - ) -> Result> { - self.into_iter().try_fold(Vec::new(), |mut acc, transf| { - acc.push(transf.parse_transfer_to_eth()?); - Ok(acc) - }) - } - } - - impl Parse for ethereum_structs::Erc20Transfer { - fn parse_transfer_to_eth(self) -> Result { - let asset = self.from.parse_eth_address()?; - let receiver = self.to.parse_eth_address()?; - let amount = self.amount.parse_amount()?; - let checksum = self.data_digest.parse_hash()?; - Ok(TransferToEthereum { - asset, - amount, - receiver, - checksum, - }) - } - } - - impl Parse for Vec { - fn parse_address_array(self) -> Result> { - self.into_iter().try_fold(Vec::new(), |mut acc, addr| { - acc.push(addr.parse_address()?); - Ok(acc) - }) - } - - fn parse_string_array(self) -> Result> { - Ok(self) - } - } - - #[cfg(test)] - mod test_events { - use assert_matches::assert_matches; - use ethabi::ethereum_types::{H160, U256}; - use ethbridge_events::{ - TRANSFER_TO_CHAIN_CODEC, TRANSFER_TO_ERC_CODEC, - VALIDATOR_SET_UPDATE_CODEC, - }; - use namada_sdk::eth_bridge::ethers::contract::EthEvent; - - use super::*; - use crate::ethereum_oracle::test_tools::event_log::GetLog; - - /// Test that for Ethereum events for which a custom number of - /// confirmations may be specified, if a value lower than the - /// protocol-specified minimum confirmations is attempted to be used, - /// then the protocol-specified minimum confirmations is used instead. - #[test] - fn test_min_confirmations_enforced() -> Result<()> { - let arbitrary_block_height: Uint256 = 123u64.into(); - let min_confirmations: Uint256 = 100u64.into(); - let lower_than_min_confirmations = 5u64; - - let (codec, event) = ( - TRANSFER_TO_CHAIN_CODEC, - TransferToChainFilter { - transfers: vec![], - nonce: 0.into(), - confirmations: lower_than_min_confirmations.into(), - }, - ); - let pending_event = PendingEvent::decode( - codec, - arbitrary_block_height, - &event.get_log(), - min_confirmations, - )?; - - assert_matches!( - pending_event, - PendingEvent { confirmations, .. } - if confirmations == min_confirmations - ); - - Ok(()) - } - - /// Test decoding a "Transfer to Namada" Ethereum event. - #[test] - fn test_transfer_to_namada_decode() { - let data = vec![ - 170, 156, 23, 249, 166, 216, 156, 37, 67, 204, 150, 161, 103, - 163, 161, 122, 243, 66, 109, 149, 141, 194, 27, 80, 238, 109, - 40, 128, 254, 233, 54, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 95, 189, 178, 49, 86, - 120, 175, 236, 179, 103, 240, 50, 217, 63, 100, 47, 100, 24, - 10, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 45, 116, 110, 97, 109, 49, 113, 57, 117, 104, 48, 54, - 100, 104, 50, 99, 114, 107, 53, 102, 122, 107, 56, 97, 99, 103, - 117, 57, 110, 99, 97, 113, 55, 107, 112, 99, 101, 112, 112, - 115, 115, 112, 117, 121, 97, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - - let raw: TransferToChainFilter = TRANSFER_TO_CHAIN_CODEC - .decode(ðabi::RawLog { - topics: vec![TransferToChainFilter::signature()], - data, - }) - .expect("Test failed") - .try_into() - .expect("Test failed"); - - assert_eq!( - raw.transfers, - vec![ethereum_structs::ChainTransfer { - amount: 100u64.into(), - from: ethabi::Address::from_str( - "0x5FbDB2315678afecb367f032d93F642f64180aa3" - ) - .unwrap(), - to: "tnam1q9uh06dh2crk5fzk8acgu9ncaq7kpceppsspuya2".into(), - }] - ) - } - - /// Test that for Ethereum events for which a custom number of - /// confirmations may be specified, the custom number is used if it is - /// at least the protocol-specified minimum confirmations. - #[test] - fn test_custom_confirmations_used() { - let arbitrary_block_height: Uint256 = 123u64.into(); - let min_confirmations: Uint256 = 100u64.into(); - let higher_than_min_confirmations = 200u64; - - let (codec, event) = ( - TRANSFER_TO_CHAIN_CODEC, - TransferToChainFilter { - transfers: vec![], - nonce: 0u64.into(), - confirmations: higher_than_min_confirmations.into(), - }, - ); - let pending_event = PendingEvent::decode( - codec, - arbitrary_block_height, - &event.get_log(), - min_confirmations, - ) - .unwrap(); - - assert_matches!( - pending_event, - PendingEvent { confirmations, .. } - if confirmations == higher_than_min_confirmations.into() - ); - } - - /// For each of the basic constituent types of Namada's - /// [`EthereumEvent`] enum variants, test that roundtrip - /// decoding from/to [`ethabi`] types works as expected. - #[test] - fn test_decoding_roundtrips() { - let erc = EthAddress([1; 20]); - let address = Address::from_str( - "tnam1q87teqzjytwa9xd9qk8u558xxnrwuzdjzs7zvhzr", - ) - .expect("Test failed"); - let amount = Amount::from(42u64); - let confs = 50u32; - let uint = Uint::from(42u64); - let boolean = true; - let string = String::from("test"); - let keccak = KeccakHash([2; 32]); - - let test_case = H160(erc.0); - assert_eq!( - test_case.parse_eth_address().expect("Test failed"), - erc - ); - - let test_case = address.to_string(); - assert_eq!( - test_case.parse_address().expect("Test failed"), - address - ); - - let test_case: U256 = amount.into(); - assert_eq!(test_case.parse_amount().expect("Test failed"), amount); - - let test_case = U256::from(confs); - assert_eq!(test_case.parse_u32().expect("Test failed"), confs); - - let test_case = U256(uint.0); - assert_eq!(test_case.parse_uint256().expect("Test failed"), uint); - - let test_case = boolean; - assert_eq!(test_case.parse_bool().expect("Test failed"), boolean); - - let test_case = string.clone(); - assert_eq!(test_case.parse_string().expect("Test failed"), string); - - let test_case = keccak.0; - assert_eq!(test_case.parse_keccak().expect("Test failed"), keccak); - } - - /// Test that serialization and deserialization of - /// complex composite types is a no-op - #[test] - fn test_complex_round_trips() { - let address: String = - "tnam1q87teqzjytwa9xd9qk8u558xxnrwuzdjzs7zvhzr".into(); - let nam_transfers = TransferToChainFilter { - transfers: vec![ - ethereum_structs::ChainTransfer { - amount: 0u64.into(), - from: H160([0; 20]), - to: address.clone(), - }; - 2 - ], - nonce: 0u64.into(), - confirmations: 0u64.into(), - }; - let eth_transfers = TransferToErcFilter { - transfers: vec![ - ethereum_structs::Erc20Transfer { - from: H160([1; 20]), - to: H160([2; 20]), - amount: 0u64.into(), - data_digest: [0; 32], - }; - 2 - ], - nonce: 0u64.into(), - relayer_address: address, - }; - let update = ValidatorSetUpdateFilter { - validator_set_nonce: 0u64.into(), - bridge_validator_set_hash: [1; 32], - governance_validator_set_hash: [2; 32], - }; - assert_eq!( - { - let decoded: TransferToChainFilter = - TRANSFER_TO_CHAIN_CODEC - .decode(&nam_transfers.clone().get_log()) - .expect("Test failed") - .try_into() - .expect("Test failed"); - decoded - }, - nam_transfers - ); - assert_eq!( - { - let decoded: TransferToErcFilter = TRANSFER_TO_ERC_CODEC - .decode(ð_transfers.clone().get_log()) - .expect("Test failed") - .try_into() - .expect("Test failed"); - decoded - }, - eth_transfers - ); - assert_eq!( - { - let decoded: ValidatorSetUpdateFilter = - VALIDATOR_SET_UPDATE_CODEC - .decode(&update.clone().get_log()) - .expect("Test failed") - .try_into() - .expect("Test failed"); - decoded - }, - update - ); - } - } -} - -pub use eth_events::*; diff --git a/crates/node/src/ethereum_oracle/mod.rs b/crates/node/src/ethereum_oracle/mod.rs deleted file mode 100644 index 94e70f02a70..00000000000 --- a/crates/node/src/ethereum_oracle/mod.rs +++ /dev/null @@ -1,1187 +0,0 @@ -pub mod control; -pub mod events; -pub mod test_tools; - -use std::ops::ControlFlow; - -use async_trait::async_trait; -use ethabi::Address; -use ethbridge_events::{EventKind, event_codecs}; -use itertools::Either; -use namada_sdk::control_flow::time::{Constant, Duration, Instant, Sleep}; -use namada_sdk::eth_bridge::ethers::providers::{Http, Middleware, Provider}; -use namada_sdk::eth_bridge::oracle::config::Config; -use namada_sdk::eth_bridge::{SyncStatus, eth_syncing_status_timeout, ethers}; -use namada_sdk::ethereum_events::EthereumEvent; -use namada_sdk::{ethereum_structs, hints}; -use num256::Uint256; -use thiserror::Error; -use tokio::sync::mpsc::Sender as BoundedSender; -use tokio::sync::mpsc::error::TryRecvError; -use tokio::task::LocalSet; - -use self::events::PendingEvent; -use super::abortable::AbortableSpawner; -use crate::oracle::control::Command; - -/// The default amount of time the oracle will wait between processing blocks -const DEFAULT_BACKOFF: Duration = Duration::from_millis(500); -const DEFAULT_CEILING: Duration = Duration::from_secs(30); - -#[derive(Error, Debug)] -pub enum Error { - #[error("Ethereum node has fallen out of sync")] - FallenBehind, - #[error( - "Couldn't check for events ({0} from {1}) with the RPC endpoint: {2}" - )] - CheckEvents(String, Address, String), - #[error("Could not send all bridge events ({0} from {1}) to the shell")] - Channel(String, Address), - #[error( - "Need more confirmations for oracle to continue processing blocks." - )] - MoreConfirmations, - #[error("The Ethereum oracle timed out")] - Timeout, -} - -/// Convert values to [`ethabi`] Ethereum event logs. -pub trait IntoEthAbiLog { - /// Convert an Ethereum event log to the corresponding - /// [`ethabi`] type. - fn into_ethabi_log(self) -> ethabi::RawLog; -} - -impl IntoEthAbiLog for ethers::types::Log { - #[inline] - fn into_ethabi_log(self) -> ethabi::RawLog { - self.into() - } -} - -impl IntoEthAbiLog for ethabi::RawLog { - #[inline] - fn into_ethabi_log(self) -> ethabi::RawLog { - self - } -} - -/// Client implementations that speak a subset of the -/// Geth JSONRPC protocol. -#[async_trait(?Send)] -pub trait RpcClient { - /// Ethereum event log. - type Log: IntoEthAbiLog; - - /// Instantiate a new client, pointing to the - /// given RPC url. - fn new_client(rpc_url: &str) -> Self - where - Self: Sized; - - /// Query a block for Ethereum events from a given ABI type - /// and contract address. - async fn check_events_in_block( - &self, - block: ethereum_structs::BlockHeight, - address: Address, - abi_signature: &str, - ) -> Result, Error>; - - /// Check if the fullnode we are connected to is syncing or is up - /// to date with the Ethereum (an return the block height). - /// - /// Note that the syncing call may return false inaccurately. In - /// that case, we must check if the block number is 0 or not. - async fn syncing( - &self, - last_processed_block: Option<ðereum_structs::BlockHeight>, - backoff: Duration, - deadline: Instant, - ) -> Result; - - /// Given its current state, check if this RPC client - /// may recover from the given [`enum@Error`]. - fn may_recover(&self, error: &Error) -> bool; -} - -#[async_trait(?Send)] -impl RpcClient for Provider { - type Log = ethers::types::Log; - - #[inline] - fn new_client(url: &str) -> Self - where - Self: Sized, - { - Provider::::try_from(url).expect("Invalid Ethereum RPC url") - } - - async fn check_events_in_block( - &self, - block: ethereum_structs::BlockHeight, - contract_address: Address, - abi_signature: &str, - ) -> Result, Error> { - let height = { - let n: Uint256 = block.into(); - let n: u64 = - n.0.try_into().expect("Ethereum block number overflow"); - n - }; - self.get_logs( - ðers::types::Filter::new() - .from_block(height) - .to_block(height) - .event(abi_signature) - .address(contract_address), - ) - .await - .map_err(|error| { - Error::CheckEvents( - abi_signature.into(), - contract_address, - error.to_string(), - ) - }) - } - - async fn syncing( - &self, - last_processed_block: Option<ðereum_structs::BlockHeight>, - backoff: Duration, - deadline: Instant, - ) -> Result { - match eth_syncing_status_timeout(self, backoff, deadline) - .await - .map_err(|_| Error::Timeout)? - { - s @ SyncStatus::Syncing => Ok(s), - SyncStatus::AtHeight(height) => match last_processed_block { - Some(last) if <&Uint256>::from(last) < &height => { - Ok(SyncStatus::AtHeight(height)) - } - None => Ok(SyncStatus::AtHeight(height)), - _ => Err(Error::FallenBehind), - }, - } - } - - #[inline(always)] - fn may_recover(&self, error: &Error) -> bool { - !matches!( - error, - Error::Timeout | Error::Channel(_, _) | Error::CheckEvents(_, _, _) - ) - } -} - -/// A client that can talk to geth and parse -/// and relay events relevant to Namada to the -/// ledger process -pub struct Oracle> { - /// The client that talks to the Ethereum fullnode - client: C, - /// A channel for sending processed and confirmed - /// events to the ledger process - sender: BoundedSender, - /// The most recently processed block is recorded here. - last_processed_block: last_processed_block::Sender, - /// How long the oracle should wait between checking blocks - backoff: Duration, - /// How long the oracle should allow the fullnode to be unresponsive - ceiling: Duration, - /// A channel for controlling and configuring the oracle. - control: control::Receiver, -} - -impl Oracle { - /// Construct a new [`Oracle`]. Note that it can not do anything until it - /// has been sent a configuration via the passed in `control` channel. - pub fn new( - client_or_url: Either, - sender: BoundedSender, - last_processed_block: last_processed_block::Sender, - backoff: Duration, - ceiling: Duration, - control: control::Receiver, - ) -> Self { - Self { - client: match client_or_url { - Either::Left(client) => client, - Either::Right(url) => C::new_client(url), - }, - sender, - backoff, - ceiling, - last_processed_block, - control, - } - } - - /// Send a series of [`EthereumEvent`]s to the Namada - /// ledger. Returns a boolean indicating that all sent - /// successfully. If false is returned, the receiver - /// has hung up. - /// - /// N.B. this will block if the internal channel buffer - /// is full. - async fn send(&self, events: Vec) -> bool { - if self.sender.is_closed() { - return false; - } - for event in events.into_iter() { - if self.sender.send(event).await.is_err() { - return false; - } - } - true - } - - /// Check if a new config has been sent from the Shell. - fn update_config(&mut self) -> Option { - match self.control.try_recv() { - Ok(Command::UpdateConfig(config)) => Some(config), - Err(TryRecvError::Disconnected) => panic!( - "The Ethereum oracle command channel has unexpectedly hung up." - ), - _ => None, - } - } - - /// If the bridge has been deactivated, block here until a new - /// config is passed that reactivates the bridge - async fn wait_on_reactivation(&mut self) -> Config { - loop { - if let Some(Command::UpdateConfig(c)) = self.control.recv().await { - if c.active { - return c; - } - } - } - } -} - -/// Block until an initial configuration is received via the command channel. -/// Returns the initial config once received, or `None` if the command channel -/// is closed. -async fn await_initial_configuration( - receiver: &mut control::Receiver, -) -> Option { - match receiver.recv().await { - Some(Command::UpdateConfig(config)) => Some(config), - _ => None, - } -} - -/// Set up an Oracle and run the process where the Oracle -/// processes and forwards Ethereum events to the ledger -pub fn run_oracle( - url: impl AsRef, - sender: BoundedSender, - control: control::Receiver, - last_processed_block: last_processed_block::Sender, - spawner: &mut AbortableSpawner, -) { - let url = url.as_ref().to_owned(); - spawner - .abortable("Ethereum Oracle", move |aborter| { - let rt = tokio::runtime::Handle::current(); - rt.block_on(async move { - LocalSet::new() - .run_until(async move { - tracing::info!( - ?url, - "Ethereum event oracle is starting" - ); - - let oracle = Oracle::::new( - Either::Right(&url), - sender, - last_processed_block, - DEFAULT_BACKOFF, - DEFAULT_CEILING, - control, - ); - run_oracle_aux(oracle).await; - - tracing::info!( - ?url, - "Ethereum event oracle is no longer running" - ); - }) - .await - }); - - drop(aborter); - Ok(()) - }) - .spawn_blocking(); -} - -/// Determine what action to take after attempting to -/// process events contained in an Ethereum block. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(crate) enum ProcessEventAction { - /// No events could be processed at this time, so we must keep - /// polling for new events. - ContinuePollingEvents, - /// Some error occurred while processing Ethereum events in - /// the current height. We must halt the oracle. - HaltOracle, - /// The current Ethereum block height has been processed. - /// We must advance to the next Ethereum height. - ProceedToNextBlock, -} - -impl ProcessEventAction { - /// Check whether the action commands a new block to be processed. - #[inline] - #[allow(dead_code)] - pub fn process_new_block(&self) -> bool { - matches!(self, Self::ProceedToNextBlock) - } -} - -impl ProcessEventAction { - /// Handles the requested oracle action, translating it to a format - /// understood by the set of [`Sleep`] abstractions. - fn handle(self) -> ControlFlow, ()> { - match self { - ProcessEventAction::ContinuePollingEvents => { - ControlFlow::Continue(()) - } - ProcessEventAction::HaltOracle => ControlFlow::Break(Err(())), - ProcessEventAction::ProceedToNextBlock => { - ControlFlow::Break(Ok(())) - } - } - } -} - -/// Tentatively process a batch of Ethereum events. -pub(crate) async fn try_process_eth_events( - oracle: &Oracle, - config: &Config, - next_block_to_process: ðereum_structs::BlockHeight, -) -> ProcessEventAction { - process_events_in_block(next_block_to_process, oracle, config) - .await - .map_or_else( - |error| { - if oracle.client.may_recover(&error) { - tracing::debug!( - %error, - block = ?next_block_to_process, - "Error while trying to process Ethereum block" - ); - ProcessEventAction::ContinuePollingEvents - } else { - tracing::error!( - reason = %error, - block = ?next_block_to_process, - "The Ethereum oracle has disconnected" - ); - ProcessEventAction::HaltOracle - } - }, - |()| ProcessEventAction::ProceedToNextBlock, - ) -} - -/// Given an oracle, watch for new Ethereum events, processing -/// them into Namada native types. -/// -/// It also checks that once the specified number of confirmations -/// is reached, an event is forwarded to the ledger process -async fn run_oracle_aux(mut oracle: Oracle) { - tracing::info!("Oracle is awaiting initial configuration"); - let mut config = - match await_initial_configuration(&mut oracle.control).await { - Some(config) => { - tracing::info!( - "Oracle received initial configuration - {:?}", - config - ); - config - } - None => { - tracing::debug!( - "Oracle control channel was closed before the oracle \ - could be configured" - ); - return; - } - }; - - let mut next_block_to_process = config.start_block.clone(); - - loop { - tracing::info!( - ?next_block_to_process, - "Checking Ethereum block for bridge events" - ); - let res = Sleep { strategy: Constant(oracle.backoff) }.run(|| async { - tokio::select! { - action = try_process_eth_events(&oracle, &config, &next_block_to_process) => { - action.handle() - }, - _ = oracle.sender.closed() => { - tracing::info!( - "Ethereum oracle can not send events to the ledger; the \ - receiver has hung up. Shutting down" - ); - ControlFlow::Break(Err(())) - } - } - }) - .await; - - if hints::unlikely(res.is_err()) { - break; - } - - oracle - .last_processed_block - .send_replace(Some(next_block_to_process.clone())); - // check if a new config has been sent. - if let Some(new_config) = oracle.update_config() { - config = new_config; - } - if !config.active { - config = oracle.wait_on_reactivation().await; - } - next_block_to_process = next_block_to_process.next(); - } -} - -/// Checks if the given block has any events relating to the bridge, and if so, -/// sends them to the oracle's `sender` channel -async fn process_events_in_block( - block_to_process: ðereum_structs::BlockHeight, - oracle: &Oracle, - config: &Config, -) -> Result<(), Error> { - let mut queue: Vec = vec![]; - let pending = &mut queue; - // update the latest block height - - let last_processed_block_ref = oracle.last_processed_block.borrow(); - let last_processed_block = last_processed_block_ref.as_ref(); - let backoff = oracle.backoff; - #[allow(clippy::arithmetic_side_effects)] - let deadline = Instant::now() + oracle.ceiling; - let latest_block = match oracle - .client - .syncing(last_processed_block, backoff, deadline) - .await? - { - SyncStatus::AtHeight(height) => height, - SyncStatus::Syncing => return Err(Error::FallenBehind), - } - .into(); - let minimum_latest_block = block_to_process.clone().unchecked_add( - ethereum_structs::BlockHeight::from(config.min_confirmations), - ); - if minimum_latest_block > latest_block { - tracing::debug!( - ?block_to_process, - ?latest_block, - ?minimum_latest_block, - "Waiting for enough Ethereum blocks to be synced" - ); - return Err(Error::MoreConfirmations); - } - tracing::debug!( - ?block_to_process, - ?latest_block, - "Got latest Ethereum block height" - ); - // check for events in Ethereum blocks that have reached the minimum number - // of confirmations - for codec in event_codecs() { - let sig = codec.event_signature(); - let addr: Address = match codec.kind() { - EventKind::Bridge => config.bridge_contract.into(), - }; - tracing::debug!( - ?block_to_process, - ?addr, - ?sig, - "Checking for bridge events" - ); - // fetch the events for matching the given signature - let mut events = { - let logs = oracle - .client - .check_events_in_block(block_to_process.clone(), addr, &sig) - .await?; - if !logs.is_empty() { - tracing::info!( - ?block_to_process, - ?addr, - ?sig, - n_events = logs.len(), - "Found bridge events in Ethereum block" - ) - } - logs.into_iter() - .map(IntoEthAbiLog::into_ethabi_log) - .filter_map(|log| { - match PendingEvent::decode( - codec, - block_to_process.clone().into(), - &log, - u64::from(config.min_confirmations).into(), - ) { - Ok(event) => Some(event), - Err(error) => { - tracing::error!( - ?error, - ?block_to_process, - ?addr, - ?sig, - "Couldn't decode event: {:#?}", - log - ); - None - } - } - }) - .collect() - }; - pending.append(&mut events); - if !pending.is_empty() { - tracing::info!( - ?block_to_process, - ?addr, - ?sig, - pending = pending.len(), - "There are Ethereum events pending" - ); - } - let confirmed = process_queue(&latest_block, pending); - if !confirmed.is_empty() { - tracing::info!( - ?block_to_process, - ?addr, - ?sig, - pending = pending.len(), - confirmed = confirmed.len(), - min_confirmations = ?config.min_confirmations, - "Some events that have reached the minimum number of \ - confirmations and will be sent onwards" - ); - } - if !oracle.send(confirmed).await { - return Err(Error::Channel(sig.into(), addr)); - } - } - Ok(()) -} - -/// Check which events in the queue have reached their -/// required number of confirmations and remove them -/// from the queue of pending events -fn process_queue( - latest_block: &Uint256, - pending: &mut Vec, -) -> Vec { - let mut pending_tmp: Vec = Vec::with_capacity(pending.len()); - std::mem::swap(&mut pending_tmp, pending); - let mut confirmed = vec![]; - for item in pending_tmp.into_iter() { - if item.is_confirmed(latest_block) { - confirmed.push(item.event); - } else { - pending.push(item); - } - } - confirmed -} - -pub mod last_processed_block { - //! Functionality to do with publishing which blocks we have processed. - use namada_sdk::ethereum_structs; - use tokio::sync::watch; - - pub type Sender = watch::Sender>; - pub type Receiver = watch::Receiver>; - - /// Construct a [`tokio::sync::watch`] channel to publish the most recently - /// processed block. Until the live oracle processes its first block, this - /// will be `None`. - pub fn channel() -> (Sender, Receiver) { - watch::channel(None) - } -} - -#[cfg(test)] -mod test_oracle { - use std::num::NonZeroU64; - - use ethbridge_bridge_events::{TransferToChainFilter, TransferToErcFilter}; - use namada_sdk::address::testing::gen_established_address; - use namada_sdk::eth_bridge::ethers::types::H160; - use namada_sdk::eth_bridge::structs::Erc20Transfer; - use namada_sdk::ethereum_events::{EthAddress, TransferToEthereum}; - use namada_sdk::hash::Hash; - use tokio::sync::oneshot::channel; - use tokio::time::timeout; - - use super::*; - use crate::ethereum_oracle::test_tools::event_log::GetLog; - use crate::ethereum_oracle::test_tools::mock_web3_client::{ - TestCmd, TestOracle, Web3Client, Web3Controller, event_signature, - }; - - /// The data returned from setting up a test - struct TestPackage { - oracle: TestOracle, - controller: Web3Controller, - eth_recv: tokio::sync::mpsc::Receiver, - control_sender: control::Sender, - blocks_processed_recv: tokio::sync::mpsc::UnboundedReceiver, - } - - /// Helper function that starts running the oracle in a new thread, and - /// initializes it with a simple default configuration that is appropriate - /// for tests. - async fn start_with_default_config( - oracle: TestOracle, - control_sender: &mut control::Sender, - config: Config, - ) -> tokio::task::JoinHandle<()> { - let handle = tokio::task::spawn_blocking(move || { - let rt = tokio::runtime::Handle::current(); - rt.block_on(async move { - LocalSet::new() - .run_until(async move { - run_oracle_aux(oracle).await; - }) - .await - }); - }); - control_sender - .try_send(control::Command::UpdateConfig(config)) - .unwrap(); - handle - } - - /// Set up an oracle with a mock web3 client that we can control - fn setup() -> TestPackage { - let (blocks_processed_recv, client) = Web3Client::setup(); - let (eth_sender, eth_receiver) = tokio::sync::mpsc::channel(1000); - let (last_processed_block_sender, _) = last_processed_block::channel(); - let (control_sender, control_receiver) = control::channel(); - let controller = client.controller(); - TestPackage { - oracle: TestOracle { - client, - sender: eth_sender, - last_processed_block: last_processed_block_sender, - // backoff should be short for tests so that they run faster - backoff: Duration::from_millis(5), - ceiling: DEFAULT_CEILING, - control: control_receiver, - }, - controller, - eth_recv: eth_receiver, - control_sender, - blocks_processed_recv, - } - } - - /// Test that if the fullnode stops, the oracle - /// shuts down, even if the web3 client is unresponsive - #[tokio::test] - async fn test_shutdown() { - let TestPackage { - oracle, - eth_recv, - controller, - mut control_sender, - .. - } = setup(); - let oracle = start_with_default_config( - oracle, - &mut control_sender, - Config::default(), - ) - .await; - controller.apply_cmd(TestCmd::Unresponsive); - drop(eth_recv); - oracle.await.expect("Test failed"); - } - - /// Test that if no logs are received from the web3 - /// client, no events are sent out - #[tokio::test] - async fn test_no_logs_no_op() { - let TestPackage { - oracle, - mut eth_recv, - controller, - blocks_processed_recv: _processed, - mut control_sender, - } = setup(); - let oracle = start_with_default_config( - oracle, - &mut control_sender, - Config::default(), - ) - .await; - controller.apply_cmd(TestCmd::NewHeight(Uint256::from(150u32))); - - let mut time = std::time::Duration::from_secs(1); - while time > std::time::Duration::from_millis(10) { - assert!(eth_recv.try_recv().is_err()); - time -= std::time::Duration::from_millis(10); - } - drop(eth_recv); - oracle.await.expect("Test failed"); - } - - /// Test that if a new block height doesn't increase, - /// no events are sent out even if there are - /// some in the logs. - #[tokio::test] - async fn test_cant_get_new_height() { - let TestPackage { - oracle, - mut eth_recv, - controller, - blocks_processed_recv: _processed, - mut control_sender, - } = setup(); - let min_confirmations = 100; - let config = Config { - min_confirmations: NonZeroU64::try_from(min_confirmations) - .expect("Test wasn't set up correctly"), - ..Config::default() - }; - let oracle = - start_with_default_config(oracle, &mut control_sender, config) - .await; - // Increase height above the configured minimum confirmations - controller.apply_cmd(TestCmd::NewHeight(min_confirmations.into())); - - let new_event = TransferToChainFilter { - nonce: 0.into(), - transfers: vec![], - confirmations: 100.into(), - } - .get_log(); - let (sender, _) = channel(); - controller.apply_cmd(TestCmd::NewEvent { - event_type: event_signature::(), - log: new_event, - height: 101, - seen: sender, - }); - // since height is not updating, we should not receive events - let mut time = std::time::Duration::from_secs(1); - while time > std::time::Duration::from_millis(10) { - assert!(eth_recv.try_recv().is_err()); - time -= std::time::Duration::from_millis(10); - } - drop(eth_recv); - oracle.await.expect("Test failed"); - } - - /// Test that the oracle waits until new logs - /// are received before sending them on. - #[tokio::test] - async fn test_wait_on_new_logs() { - let TestPackage { - oracle, - eth_recv, - controller, - blocks_processed_recv: _processed, - mut control_sender, - } = setup(); - let min_confirmations = 100; - let config = Config { - min_confirmations: NonZeroU64::try_from(min_confirmations) - .expect("Test wasn't set up correctly"), - ..Config::default() - }; - let oracle = - start_with_default_config(oracle, &mut control_sender, config) - .await; - // Increase height above the configured minimum confirmations - controller.apply_cmd(TestCmd::NewHeight(min_confirmations.into())); - - // set the oracle to be unresponsive - controller.apply_cmd(TestCmd::Unresponsive); - // send a new event to the oracle - let new_event = TransferToChainFilter { - nonce: 0.into(), - transfers: vec![], - confirmations: 100.into(), - } - .get_log(); - let (sender, mut seen) = channel(); - controller.apply_cmd(TestCmd::NewEvent { - event_type: event_signature::(), - log: new_event, - height: 150, - seen: sender, - }); - // set the height high enough to emit the event - controller.apply_cmd(TestCmd::NewHeight(Uint256::from(251u32))); - - // the event should not be emitted even though the height is large - // enough - let mut time = std::time::Duration::from_secs(1); - while time > std::time::Duration::from_millis(10) { - assert!(seen.try_recv().is_err()); - time -= std::time::Duration::from_millis(10); - } - // check that when web3 becomes responsive, oracle sends event - controller.apply_cmd(TestCmd::Normal); - seen.await.expect("Test failed"); - drop(eth_recv); - oracle.await.expect("Test failed"); - } - - /// Test that events are only sent when they - /// reach the required number of confirmations - #[tokio::test] - async fn test_finality_gadget() { - let TestPackage { - oracle, - mut eth_recv, - controller, - blocks_processed_recv: _processed, - mut control_sender, - } = setup(); - let min_confirmations = 100; - let config = Config { - min_confirmations: NonZeroU64::try_from(min_confirmations) - .expect("Test wasn't set up correctly"), - ..Config::default() - }; - let oracle = - start_with_default_config(oracle, &mut control_sender, config) - .await; - // Increase height above the configured minimum confirmations - controller.apply_cmd(TestCmd::NewHeight(min_confirmations.into())); - - // confirmed after 100 blocks - let first_event = TransferToChainFilter { - nonce: 0.into(), - transfers: vec![], - confirmations: 100.into(), - } - .get_log(); - - // confirmed after 125 blocks - let gas_payer = gen_established_address(); - let second_event = TransferToErcFilter { - transfers: vec![Erc20Transfer { - amount: 0.into(), - from: H160([0; 20]), - to: H160([1; 20]), - data_digest: [0; 32], - }], - relayer_address: gas_payer.to_string(), - nonce: 0.into(), - } - .get_log(); - - // send in the events to the logs - let (sender, seen_second) = channel(); - controller.apply_cmd(TestCmd::NewEvent { - event_type: event_signature::(), - log: second_event, - height: 125, - seen: sender, - }); - let (sender, _recv) = channel(); - controller.apply_cmd(TestCmd::NewEvent { - event_type: event_signature::(), - log: first_event, - height: 100, - seen: sender, - }); - - // increase block height so first event is confirmed but second is - // not. - controller.apply_cmd(TestCmd::NewHeight(Uint256::from(200u32))); - // check the correct event is received - let event = eth_recv.recv().await.expect("Test failed"); - if let EthereumEvent::TransfersToNamada { nonce, transfers } = event { - assert_eq!(nonce, 0.into()); - assert!(transfers.is_empty()); - } else { - panic!("Test failed, {:?}", event); - } - - // check no other events are received - let mut time = std::time::Duration::from_secs(1); - while time > std::time::Duration::from_millis(10) { - assert!(eth_recv.try_recv().is_err()); - time -= std::time::Duration::from_millis(10); - } - - // increase block height so second event is emitted - controller.apply_cmd(TestCmd::NewHeight(Uint256::from(225u32))); - // wait until event is emitted - seen_second.await.expect("Test failed"); - // increase block height so second event is confirmed - controller.apply_cmd(TestCmd::NewHeight(Uint256::from(250u32))); - // check correct event is received - let event = eth_recv.recv().await.expect("Test failed"); - if let EthereumEvent::TransfersToEthereum { mut transfers, .. } = event - { - assert_eq!(transfers.len(), 1); - let transfer = transfers.remove(0); - assert_eq!( - transfer, - TransferToEthereum { - amount: Default::default(), - asset: EthAddress([0; 20]), - receiver: EthAddress([1; 20]), - checksum: Hash::default(), - } - ); - } else { - panic!("Test failed"); - } - - drop(eth_recv); - oracle.await.expect("Test failed"); - } - - /// Test that Ethereum blocks are processed in sequence up to the latest - /// block that has reached the minimum number of confirmations - #[tokio::test] - async fn test_blocks_checked_sequence() { - let TestPackage { - oracle, - eth_recv, - controller, - mut blocks_processed_recv, - mut control_sender, - } = setup(); - let config = Config::default(); - let oracle = start_with_default_config( - oracle, - &mut control_sender, - config.clone(), - ) - .await; - - // set the height of the chain such that there are some blocks deep - // enough to be considered confirmed by the oracle - let confirmed_block_height = 9; // all blocks up to and including this block have enough confirmations - let synced_block_height = - u64::from(config.min_confirmations) + confirmed_block_height; - for height in 0..synced_block_height + 1 { - controller.apply_cmd(TestCmd::NewHeight(Uint256::from(height))); - } - // check that the oracle indeed processes the confirmed blocks - for height in 0u64..confirmed_block_height + 1 { - let block_processed = timeout( - std::time::Duration::from_secs(3), - blocks_processed_recv.recv(), - ) - .await - .expect("Timed out waiting for block to be checked") - .unwrap(); - assert_eq!(block_processed, Uint256::from(height)); - } - - // check that the oracle hasn't yet checked any further blocks - assert!( - timeout( - std::time::Duration::from_secs(1), - blocks_processed_recv.recv() - ) - .await - .is_err() - ); - - // increase the height of the chain by one, and check that the oracle - // processed the next confirmed block - let synced_block_height = synced_block_height + 1; - controller - .apply_cmd(TestCmd::NewHeight(Uint256::from(synced_block_height))); - - let block_processed = timeout( - std::time::Duration::from_secs(3), - blocks_processed_recv.recv(), - ) - .await - .expect("Timed out waiting for block to be checked") - .unwrap(); - assert_eq!(block_processed, Uint256::from(confirmed_block_height + 1)); - - drop(eth_recv); - oracle.await.expect("Test failed"); - } - - /// Test that Ethereum oracle can be deactivate and reactivated - /// via config updates. - /// NOTE: This test can flake due to async channel race - /// conditions. - #[tokio::test] - #[ignore] - async fn test_oracle_reactivation() { - let TestPackage { - oracle, - eth_recv, - controller, - mut blocks_processed_recv, - mut control_sender, - } = setup(); - let config = Config::default(); - let oracle = start_with_default_config( - oracle, - &mut control_sender, - config.clone(), - ) - .await; - - // set the height of the chain such that there are some blocks deep - // enough to be considered confirmed by the oracle - let confirmed_block_height = 9; // all blocks up to and including this block will have enough confirmations - let min_confirmations = u64::from(config.min_confirmations); - controller.apply_cmd(TestCmd::NewHeight(Uint256::from( - min_confirmations + confirmed_block_height - 5, - ))); - - // check that the oracle indeed processes the expected blocks - for height in 0u64..(confirmed_block_height - 4) { - let block_processed = timeout( - std::time::Duration::from_secs(3), - blocks_processed_recv.recv(), - ) - .await - .expect("Timed out waiting for block to be checked") - .unwrap(); - assert_eq!(block_processed, Uint256::from(height)); - } - - // Deactivate the bridge before all confirmed events are confirmed and - // processed There is a very fine needle to thread here. A block - // must be processed **after** this config is sent in order for - // the updated config to be received. However, this test can - // flake due to channel race conditions. - control_sender - .try_send(Command::UpdateConfig(Config { - active: false, - ..Default::default() - })) - .expect("Test failed"); - std::thread::sleep(Duration::from_secs(1)); - controller.apply_cmd(TestCmd::NewHeight(Uint256::from( - min_confirmations + confirmed_block_height - 4, - ))); - - let block_processed = timeout( - std::time::Duration::from_secs(3), - blocks_processed_recv.recv(), - ) - .await - .expect("Timed out waiting for block to be checked") - .unwrap(); - assert_eq!(block_processed, Uint256::from(confirmed_block_height - 4)); - - // check that the oracle hasn't yet checked any further blocks - let res = timeout( - std::time::Duration::from_secs(3), - blocks_processed_recv.recv(), - ) - .await; - assert!(res.is_err()); - - // reactivate the bridge and check that the oracle - // processed the rest of the confirmed blocks - control_sender - .try_send(Command::UpdateConfig(Default::default())) - .expect("Test failed"); - - controller.apply_cmd(TestCmd::NewHeight(Uint256::from( - min_confirmations + confirmed_block_height, - ))); - for height in (confirmed_block_height - 3)..=confirmed_block_height { - let block_processed = timeout( - std::time::Duration::from_secs(3), - blocks_processed_recv.recv(), - ) - .await - .expect("Timed out waiting for block to be checked") - .unwrap(); - assert_eq!(block_processed, Uint256::from(height)); - } - drop(eth_recv); - oracle.await.expect("Test failed"); - } - - /// Test that if the Ethereum RPC endpoint returns a latest block that is - /// more than one block later than the previous latest block we received, we - /// still check all the blocks in between - #[tokio::test] - async fn test_all_blocks_checked() { - let TestPackage { - oracle, - eth_recv, - controller, - mut blocks_processed_recv, - mut control_sender, - } = setup(); - let config = Config::default(); - let oracle = start_with_default_config( - oracle, - &mut control_sender, - config.clone(), - ) - .await; - - let confirmed_block_height = 9; // all blocks up to and including this block have enough confirmations - let synced_block_height = - u64::from(config.min_confirmations) + confirmed_block_height; - controller - .apply_cmd(TestCmd::NewHeight(Uint256::from(synced_block_height))); - - // check that the oracle has indeed processed the first `n` blocks, even - // though the first latest block that the oracle received was not 0 - for height in 0u64..confirmed_block_height + 1 { - let block_processed = timeout( - std::time::Duration::from_secs(3), - blocks_processed_recv.recv(), - ) - .await - .expect("Timed out waiting for block to be checked") - .unwrap(); - assert_eq!(block_processed, Uint256::from(height)); - } - - // the next time the oracle checks, the latest block will have increased - // by more than one - let difference = 10; - let synced_block_height = synced_block_height + difference; - controller - .apply_cmd(TestCmd::NewHeight(Uint256::from(synced_block_height))); - - // check that the oracle still checks the blocks in between - for height in (confirmed_block_height + 1) - ..(confirmed_block_height + difference + 1) - { - let block_processed = timeout( - std::time::Duration::from_secs(3), - blocks_processed_recv.recv(), - ) - .await - .expect("Timed out waiting for block to be checked") - .unwrap(); - assert_eq!(block_processed, Uint256::from(height)); - } - - drop(eth_recv); - oracle.await.expect("Test failed"); - } -} diff --git a/crates/node/src/ethereum_oracle/test_tools/events_endpoint.rs b/crates/node/src/ethereum_oracle/test_tools/events_endpoint.rs deleted file mode 100644 index 42aa22e059c..00000000000 --- a/crates/node/src/ethereum_oracle/test_tools/events_endpoint.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::net::SocketAddr; - -use namada_sdk::borsh::BorshDeserialize; -use namada_sdk::ethereum_events::EthereumEvent; -use tokio::sync::mpsc::Sender as BoundedSender; -use tokio::sync::oneshot::{Receiver, Sender}; -use warp::Filter; -use warp::reply::WithStatus; - -use crate::ethereum_oracle as oracle; - -/// The endpoint to which Borsh-serialized Ethereum events should be sent to, -/// via an HTTP POST request. -const EVENTS_POST_ENDPOINT: &str = "eth_events"; - -/// Starts a [`warp::Server`] that listens for Borsh-serialized Ethereum events -/// and then forwards them to `sender`. -/// -/// It shuts down if a signal is sent on the `abort_recv` channel. Accepts the -/// receive-half of an oracle control channel (`control_recv`) that will be kept -/// alive until shutdown. -pub async fn serve( - listen_addr: String, - sender: BoundedSender, - mut control_recv: oracle::control::Receiver, - abort_recv: Receiver>, -) { - let listen_addr: SocketAddr = listen_addr - .parse() - .expect("Failed to parse the events endpoint listen address"); - tracing::info!(?listen_addr, "Ethereum event endpoint is starting"); - let eth_events = warp::post() - .and(warp::path(EVENTS_POST_ENDPOINT)) - .and(warp::body::bytes()) - .then(move |bytes: bytes::Bytes| send(bytes, sender.clone())); - - let (_, future) = warp::serve(eth_events).bind_with_graceful_shutdown( - listen_addr, - async move { - tracing::info!( - ?listen_addr, - "Starting to listen for Borsh-serialized Ethereum events" - ); - let control_recv_discarder = tokio::spawn(async move { - while let Some(command) = control_recv.recv().await { - tracing::debug!( - ?command, - "Events endpoint received an oracle command which \ - will be ignored since we are not running a real \ - oracle" - ) - } - }); - match abort_recv.await { - Ok(abort_resp_send) => { - if abort_resp_send.send(()).is_err() { - tracing::warn!( - "Received signal to abort but failed to respond, \ - will abort now" - ) - } - } - Err(_) => tracing::warn!( - "Channel for receiving signal to abort was closed \ - abruptly, will abort now" - ), - }; - tracing::info!( - ?listen_addr, - "Stopping listening for Borsh-serialized Ethereum events" - ); - control_recv_discarder.abort(); - }, - ); - future.await -} - -/// Callback to send out events from the oracle -async fn send( - bytes: bytes::Bytes, - sender: BoundedSender, -) -> WithStatus<&'static str> { - tracing::info!(len = bytes.len(), "Received request"); - let event = match EthereumEvent::try_from_slice(&bytes) { - Ok(event) => event, - Err(error) => { - tracing::warn!(?error, "Couldn't handle request"); - return warp::reply::with_status( - "Bad request", - warp::http::StatusCode::BAD_REQUEST, - ); - } - }; - tracing::debug!("Serialized event - {:#?}", event); - match sender.send(event).await { - Ok(()) => warp::reply::with_status("OK", warp::http::StatusCode::OK), - Err(error) => { - tracing::warn!(?error, "Couldn't send event"); - warp::reply::with_status( - "Internal server error", - warp::http::StatusCode::INTERNAL_SERVER_ERROR, - ) - } - } -} diff --git a/crates/node/src/ethereum_oracle/test_tools/mod.rs b/crates/node/src/ethereum_oracle/test_tools/mod.rs deleted file mode 100644 index 34f0a704fe2..00000000000 --- a/crates/node/src/ethereum_oracle/test_tools/mod.rs +++ /dev/null @@ -1,249 +0,0 @@ -pub mod events_endpoint; - -#[cfg(test)] -pub mod event_log { - // praise be unto thee whom'st've read and understand this code - // p.s.: https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378 - - use ethbridge_bridge_events::{ - TransferToChainFilter, TransferToErcFilter, ValidatorSetUpdateFilter, - }; - use namada_sdk::eth_bridge::ethers::abi::AbiEncode; - use namada_sdk::eth_bridge::ethers::contract::EthEvent; - - /// Get an [`ethabi::RawLog`] from a given Ethereum event. - pub trait GetLog { - /// Return an [`ethabi::RawLog`]. - fn get_log(self) -> ethabi::RawLog; - } - - impl GetLog for TransferToChainFilter { - fn get_log(self) -> ethabi::RawLog { - ethabi::RawLog { - topics: vec![Self::signature()], - data: self.encode(), - } - } - } - - impl GetLog for TransferToErcFilter { - fn get_log(self) -> ethabi::RawLog { - ethabi::RawLog { - topics: vec![Self::signature(), { - let mut buf = [0; 32]; - self.nonce.to_big_endian(&mut buf); - ethabi::ethereum_types::H256(buf) - }], - data: (self.transfers, self.relayer_address).encode(), - } - } - } - - impl GetLog for ValidatorSetUpdateFilter { - fn get_log(self) -> ethabi::RawLog { - ethabi::RawLog { - topics: vec![Self::signature(), { - let mut buf = [0; 32]; - self.validator_set_nonce.to_big_endian(&mut buf); - ethabi::ethereum_types::H256(buf) - }], - data: ( - self.bridge_validator_set_hash, - self.governance_validator_set_hash, - ) - .encode(), - } - } - } -} - -#[cfg(any(test, feature = "testing"))] -pub mod mock_web3_client { - use std::borrow::Cow; - use std::fmt::Debug; - use std::marker::PhantomData; - use std::sync::{Arc, Mutex}; - - use async_trait::async_trait; - use ethabi::Address; - use ethbridge_events::EventCodec; - use namada_sdk::control_flow::time::{Duration, Instant}; - use namada_sdk::ethereum_structs::BlockHeight; - use num256::Uint256; - use tokio::sync::mpsc::{ - UnboundedReceiver, UnboundedSender, unbounded_channel, - }; - use tokio::sync::oneshot::Sender; - - use super::super::super::ethereum_oracle::{Error, Oracle, RpcClient}; - use crate::ethereum_oracle::SyncStatus; - - /// Mock oracle used during unit tests. - pub type TestOracle = Oracle; - - /// Commands we can send to the mock client - #[derive(Debug)] - pub enum TestCmd { - Normal, - Unresponsive, - NewHeight(Uint256), - NewEvent { - event_type: MockEventType, - log: ethabi::RawLog, - height: u32, - seen: Sender<()>, - }, - } - - /// The type of events supported - pub type MockEventType = Cow<'static, str>; - - /// A pointer to a mock Web3 client. The - /// reason is for interior mutability. - pub struct Web3Client(Arc>); - - /// Command sender for [`TestOracle`] instances. - pub struct Web3Controller(Arc>); - - impl Web3Controller { - /// Apply new oracle command. - pub fn apply_cmd(&self, cmd: TestCmd) { - let mut oracle = self.0.lock().unwrap(); - match cmd { - TestCmd::Normal => oracle.active = true, - TestCmd::Unresponsive => oracle.active = false, - TestCmd::NewHeight(height) => { - oracle.latest_block_height = height - } - TestCmd::NewEvent { - event_type: ty, - log, - height, - seen, - } => oracle.events.push((ty, log, height, seen)), - } - } - } - - impl Clone for Web3Controller { - #[inline] - fn clone(&self) -> Self { - Self(Arc::clone(&self.0)) - } - } - - /// A mock of a web3 api client connected to an ethereum fullnode. - /// It is not connected to a full node and is fully controllable - /// via a channel to allow us to mock different behavior for - /// testing purposes. - pub struct Web3ClientInner { - active: bool, - latest_block_height: Uint256, - events: Vec<(MockEventType, ethabi::RawLog, u32, Sender<()>)>, - blocks_processed: UnboundedSender, - last_block_processed: Option, - } - - #[async_trait(?Send)] - impl RpcClient for Web3Client { - type Log = ethabi::RawLog; - - #[cold] - fn new_client(_: &str) -> Self - where - Self: Sized, - { - panic!( - "Method is here for api completeness. It is not meant to be \ - used in tests." - ) - } - - async fn check_events_in_block( - &self, - block: BlockHeight, - addr: Address, - ty: &str, - ) -> Result, Error> { - let block_to_check: Uint256 = block.into(); - let mut client = self.0.lock().unwrap(); - if client.active { - let mut logs = vec![]; - let mut events = vec![]; - std::mem::swap(&mut client.events, &mut events); - for (event_ty, log, height, seen) in events.into_iter() { - if event_ty == ty && block_to_check >= Uint256::from(height) - { - seen.send(()).unwrap(); - logs.push(log); - } else { - client.events.push((event_ty, log, height, seen)); - } - } - if client.last_block_processed.as_ref() < Some(&block_to_check) - { - _ = client.blocks_processed.send(block_to_check); - client.last_block_processed = Some(block_to_check); - } - Ok(logs) - } else { - tracing::debug!( - "No events to be processed by the Test Ethereum oracle, \ - as it has been artificially set as unresponsive" - ); - Err(Error::CheckEvents( - ty.into(), - addr, - "Test oracle is not responding".into(), - )) - } - } - - async fn syncing( - &self, - _: Option<&BlockHeight>, - _: Duration, - _: Instant, - ) -> Result { - let height = self.0.lock().unwrap().latest_block_height; - Ok(SyncStatus::AtHeight(height)) - } - - #[inline(always)] - fn may_recover(&self, _: &Error) -> bool { - true - } - } - - impl Web3Client { - /// Return a new client and a separate sender - /// to send in admin commands - pub fn setup() -> (UnboundedReceiver, Self) { - let (block_processed_send, block_processed_recv) = - unbounded_channel(); - ( - block_processed_recv, - Self(Arc::new(Mutex::new(Web3ClientInner { - active: true, - latest_block_height: Default::default(), - events: vec![], - blocks_processed: block_processed_send, - last_block_processed: None, - }))), - ) - } - - /// Get a new [`Web3Controller`] for the current oracle. - pub fn controller(&self) -> Web3Controller { - Web3Controller(Arc::clone(&self.0)) - } - } - - /// Get the signature of the given Ethereum event. - pub fn event_signature() -> Cow<'static, str> - where - PhantomData: EventCodec, - { - PhantomData::.event_signature() - } -} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index bcfd575b9b5..a1136e9b61d 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -16,12 +16,9 @@ mod abortable; #[cfg(feature = "benches")] pub mod bench_utils; -mod broadcaster; mod dry_run_tx; -pub mod ethereum_oracle; pub mod protocol; pub mod shell; -pub mod shims; pub mod storage; pub mod tendermint_node; pub mod utils; @@ -43,32 +40,21 @@ pub use namada_apps_lib::{ tendermint, tendermint_config, tendermint_proto, tendermint_rpc, }; use namada_sdk::chain::BlockHeight; -use namada_sdk::eth_bridge::ethers::providers::{Http, Provider}; use namada_sdk::migrations::ScheduledMigration; -use namada_sdk::state::{DB, ProcessProposalCachedResult, StateRead}; +use namada_sdk::state::DB; use namada_sdk::storage::DbColFam; -use namada_sdk::tendermint::abci::request::CheckTxKind; -use namada_sdk::tendermint::abci::response::ProcessProposal; use namada_sdk::time::DateTimeUtc; use once_cell::unsync::Lazy; +use shell::abci; use sysinfo::{MemoryRefreshKind, RefreshKind, System}; -use tokio::sync::mpsc; use self::abortable::AbortableSpawner; -use self::ethereum_oracle::last_processed_block; -use self::shell::EthereumOracleChannels; -use self::shims::abcipp_shim::AbciService; -use crate::broadcaster::Broadcaster; -use crate::config::{TendermintMode, ethereum_bridge}; -use crate::ethereum_oracle as oracle; -use crate::shell::{Error, MempoolTxType, Shell}; -use crate::shims::abcipp_shim::AbcippShim; -use crate::shims::abcipp_shim_types::shim::{Request, Response}; -use crate::tendermint::abci::response; +use crate::config::TendermintMode; +use crate::shell::{Error, Shell}; use crate::tower_abci::{Server, split}; pub mod tower_abci { pub use tower_abci::BoxError; - pub use tower_abci::v037::*; + pub use tower_abci::v038::*; } /// Env. var to set a number of Tokio RT worker threads @@ -77,189 +63,6 @@ const ENV_VAR_TOKIO_THREADS: &str = "NAMADA_TOKIO_THREADS"; /// Env. var to set a number of Rayon global worker threads const ENV_VAR_RAYON_THREADS: &str = "NAMADA_RAYON_THREADS"; -// Until ABCI++ is ready, the shim provides the service implementation. -// We will add this part back in once the shim is no longer needed. -//``` -// impl Service for Shell { -// type Error = Error; -// type Future = -// Pin> + Send + -// 'static>>; type Response = Response; -// -// fn poll_ready( -// &mut self, -// _cx: &mut Context<'_>, -// ) -> Poll> { -// Poll::Ready(Ok(())) -// } -//``` -impl Shell { - fn call( - &mut self, - req: Request, - namada_version: &str, - ) -> Result { - match req { - Request::InitChain(init) => { - tracing::debug!("Request InitChain"); - self.init_chain( - init, - #[cfg(any( - test, - feature = "testing", - feature = "benches" - ))] - 1, - ) - .map(Response::InitChain) - } - Request::Info(_) => { - Ok(Response::Info(self.last_state(namada_version))) - } - Request::Query(query) => Ok(Response::Query(self.query(query))), - Request::PrepareProposal(block) => { - tracing::debug!("Request PrepareProposal"); - // TODO: use TM domain type in the handler - Ok(Response::PrepareProposal( - self.prepare_proposal(block.into()), - )) - } - Request::VerifyHeader(_req) => { - Ok(Response::VerifyHeader(self.verify_header(_req))) - } - Request::ProcessProposal(block) => { - tracing::debug!("Request ProcessProposal"); - // TODO: use TM domain type in the handler - // NOTE: make sure to put any checks inside process_proposal - // since that function is called in other places to rerun the - // checks if (when) needed. Every check living outside that - // function will not be correctly replicated in the other - // locations - let block_hash = block.hash.try_into(); - let (response, tx_results) = - self.process_proposal(block.into()); - // Cache the response in case of future calls from Namada. If - // hash conversion fails avoid caching - if let Ok(block_hash) = block_hash { - let result = if let ProcessProposal::Accept = response { - ProcessProposalCachedResult::Accepted( - tx_results - .into_iter() - .map(|res| res.into()) - .collect(), - ) - } else { - ProcessProposalCachedResult::Rejected - }; - - self.state - .in_mem_mut() - .block_proposals_cache - .put(block_hash, result); - } - Ok(Response::ProcessProposal(response)) - } - Request::RevertProposal(_req) => { - Ok(Response::RevertProposal(self.revert_proposal(_req))) - } - Request::FinalizeBlock(finalize) => { - tracing::debug!("Request FinalizeBlock"); - - self.try_recheck_process_proposal(&finalize)?; - self.finalize_block(finalize).map(Response::FinalizeBlock) - } - Request::Commit => { - tracing::debug!("Request Commit"); - Ok(self.commit()) - } - Request::Flush => Ok(Response::Flush), - Request::Echo(msg) => Ok(Response::Echo(response::Echo { - message: msg.message, - })), - Request::CheckTx(tx) => { - let mempool_tx_type = match tx.kind { - CheckTxKind::New => MempoolTxType::NewTransaction, - CheckTxKind::Recheck => MempoolTxType::RecheckTransaction, - }; - let r#type = mempool_tx_type; - Ok(Response::CheckTx(self.mempool_validate(&tx.tx, r#type))) - } - Request::ListSnapshots => { - Ok(Response::ListSnapshots(self.list_snapshots())) - } - Request::OfferSnapshot(req) => { - Ok(Response::OfferSnapshot(self.offer_snapshot(req))) - } - Request::LoadSnapshotChunk(req) => { - Ok(Response::LoadSnapshotChunk(self.load_snapshot_chunk(req))) - } - Request::ApplySnapshotChunk(req) => { - Ok(Response::ApplySnapshotChunk(self.apply_snapshot_chunk(req))) - } - } - } - - // Checks if a run of process proposal is required before finalize block - // (recheck) and, in case, performs it. Clears the cache before returning - fn try_recheck_process_proposal( - &mut self, - finalize_req: &shims::abcipp_shim_types::shim::request::FinalizeBlock, - ) -> Result<(), Error> { - let recheck_process_proposal = match self.mode { - shell::ShellMode::Validator { - ref local_config, .. - } => local_config - .as_ref() - .map(|cfg| cfg.recheck_process_proposal) - .unwrap_or_default(), - shell::ShellMode::Full { ref local_config } => local_config - .as_ref() - .map(|cfg| cfg.recheck_process_proposal) - .unwrap_or_default(), - shell::ShellMode::Seed => false, - }; - - if recheck_process_proposal { - let process_proposal_result = match self - .state - .in_mem_mut() - .block_proposals_cache - .get(&finalize_req.block_hash) - { - // We already have the result of process proposal for this block - // cached in memory - Some(res) => res.to_owned(), - None => { - let process_req = finalize_req - .clone() - .cast_to_process_proposal_req() - .map_err(|_| Error::InvalidBlockProposal)?; - // No need to cache the result since this is the last step - // before finalizing the block - if let ProcessProposal::Accept = - self.process_proposal(process_req.into()).0 - { - ProcessProposalCachedResult::Accepted(vec![]) - } else { - ProcessProposalCachedResult::Rejected - } - } - }; - - if let ProcessProposalCachedResult::Rejected = - process_proposal_result - { - return Err(Error::RejectedBlockProposal); - } - } - - // Clear the cache of proposed blocks' results - self.state.in_mem_mut().block_proposals_cache.clear(); - - Ok(()) - } -} - /// Determine if the ledger is migrating state. pub fn migrating_state() -> Option { const ENV_INITIAL_HEIGHT: &str = "NAMADA_INITIAL_HEIGHT"; @@ -458,10 +261,6 @@ pub fn rollback(config: config::Ledger) -> Result<(), shell::Error> { /// - A Tendermint node. /// - A shell which contains an ABCI server, for talking to the Tendermint /// node. -/// - A [`Broadcaster`], for the ledger to submit txs to Tendermint's mempool. -/// - An Ethereum full node. -/// - An oracle, to receive events from the Ethereum full node, and forward -/// them to the ledger. /// /// All must be alive for correct functioning. async fn run_aux( @@ -480,22 +279,13 @@ async fn run_aux( // Start Tendermint node start_tendermint(&mut spawner, &config, namada_version); - // Start oracle if necessary - let eth_oracle_channels = - match maybe_start_ethereum_oracle(&mut spawner, &config).await { - EthereumOracleTask::NotEnabled => None, - EthereumOracleTask::Enabled { channels } => Some(channels), - }; - tracing::info!("Loading MASP verifying keys."); let _ = namada_sdk::token::validation::preload_verifying_keys(); tracing::info!("Done loading MASP verifying keys."); - // Start ABCI server and broadcaster (the latter only if we are a validator - // node) - start_abci_broadcaster_shell( + // Start ABCI server + start_abci_shell( &mut spawner, - eth_oracle_channels, wasm_dir, setup_data, config, @@ -614,19 +404,16 @@ async fn run_aux_setup( } } -/// This function spawns an ABCI server and a [`Broadcaster`] into the -/// asynchronous runtime. Additionally, it executes a shell in -/// a new OS thread, to drive the ABCI server. -fn start_abci_broadcaster_shell( +/// This function spawns an ABCI server into the asynchronous runtime. +/// Additionally, it executes a shell in a new OS thread, to drive the ABCI +/// server. +fn start_abci_shell( spawner: &mut AbortableSpawner, - eth_oracle: Option, wasm_dir: PathBuf, setup_data: RunAuxSetup, config: config::Ledger, namada_version: &'static str, ) { - let rpc_address = - convert_tm_addr_to_socket_addr(&config.cometbft.rpc.laddr); let RunAuxSetup { vp_wasm_compilation_cache, tx_wasm_compilation_cache, @@ -634,35 +421,6 @@ fn start_abci_broadcaster_shell( scheduled_migration, } = setup_data; - // Channels for validators to send protocol txs to be broadcast to the - // broadcaster service - let (broadcaster_sender, broadcaster_receiver) = mpsc::unbounded_channel(); - let genesis_time = DateTimeUtc::try_from(config.genesis_time.clone()) - .expect("Should be able to parse genesis time"); - // Start broadcaster - if matches!(config.shell.tendermint_mode, TendermintMode::Validator) { - let (bc_abort_send, bc_abort_recv) = - tokio::sync::oneshot::channel::<()>(); - - spawner - .abortable("Broadcaster", move |aborter| async move { - // Construct a service for broadcasting protocol txs from - // the ledger - let mut broadcaster = - Broadcaster::new(rpc_address, broadcaster_receiver); - broadcaster.run(bc_abort_recv, genesis_time).await; - tracing::info!("Broadcaster is no longer running."); - - drop(aborter); - - Ok(()) - }) - .with_cleanup(async move { - let _ = bc_abort_send.send(()); - }) - .spawn(); - } - // Setup DB cache, it must outlive the DB instance that's in the shell let db_cache = rocksdb::Cache::new_lru_cache( usize::try_from(db_block_cache_size_bytes) @@ -674,16 +432,15 @@ fn start_abci_broadcaster_shell( let proxy_app_address = convert_tm_addr_to_socket_addr(&config.cometbft.proxy_app); - let (shell, abci_service, service_handle) = AbcippShim::new( + let (abci_service, shell_recv, service_handle) = + abci::Service::new(&config); + let shell = Shell::new( config, wasm_dir, - broadcaster_sender, - eth_oracle, - &db_cache, + Some(&db_cache), scheduled_migration, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - namada_version.to_string(), ); // Channel for signalling shut down to ABCI server @@ -720,7 +477,7 @@ fn start_abci_broadcaster_shell( tracing::info!("This node is not a validator"); } } - shell.run(); + abci::shell_loop(shell, shell_recv, namada_version); Ok(()) }) .with_cleanup(async { @@ -735,7 +492,7 @@ fn start_abci_broadcaster_shell( /// Runs the an asynchronous ABCI server with four sub-components for consensus, /// mempool, snapshot, and info. async fn run_abci( - abci_service: AbciService, + abci_service: abci::Service, service_handle: tokio::sync::broadcast::Sender<()>, proxy_app_address: SocketAddr, abort_recv: tokio::sync::oneshot::Receiver<()>, @@ -838,105 +595,6 @@ fn start_tendermint( .spawn(); } -/// Represents a [`tokio::task`] in which an Ethereum oracle may be running, and -/// if so, channels for communicating with it. -enum EthereumOracleTask { - NotEnabled, - Enabled { channels: EthereumOracleChannels }, -} - -/// Potentially starts an Ethereum event oracle. -async fn maybe_start_ethereum_oracle( - spawner: &mut AbortableSpawner, - config: &config::Ledger, -) -> EthereumOracleTask { - if !matches!(config.shell.tendermint_mode, TendermintMode::Validator) { - return EthereumOracleTask::NotEnabled; - } - - let ethereum_url = config.ethereum_bridge.oracle_rpc_endpoint.clone(); - - // Start the oracle for listening to Ethereum events - let (eth_sender, eth_receiver) = - mpsc::channel(config.ethereum_bridge.channel_buffer_size); - let (last_processed_block_sender, last_processed_block_receiver) = - last_processed_block::channel(); - let (control_sender, control_receiver) = oracle::control::channel(); - - match config.ethereum_bridge.mode { - ethereum_bridge::ledger::Mode::RemoteEndpoint => { - oracle::run_oracle::>( - ethereum_url, - eth_sender, - control_receiver, - last_processed_block_sender, - spawner, - ); - - EthereumOracleTask::Enabled { - channels: EthereumOracleChannels::new( - eth_receiver, - control_sender, - last_processed_block_receiver, - ), - } - } - ethereum_bridge::ledger::Mode::SelfHostedEndpoint => { - let (oracle_abort_send, oracle_abort_recv) = - tokio::sync::oneshot::channel::>( - ); - spawner - .abortable( - "Ethereum Events Endpoint", - move |aborter| async move { - oracle::test_tools::events_endpoint::serve( - ethereum_url, - eth_sender, - control_receiver, - oracle_abort_recv, - ) - .await; - tracing::info!( - "Ethereum events endpoint is no longer running." - ); - - drop(aborter); - - Ok(()) - }, - ) - .with_cleanup(async move { - let (oracle_abort_resp_send, oracle_abort_resp_recv) = - tokio::sync::oneshot::channel::<()>(); - - if let Ok(()) = - oracle_abort_send.send(oracle_abort_resp_send) - { - match oracle_abort_resp_recv.await { - Ok(()) => {} - Err(err) => { - tracing::error!( - "Failed to receive an abort response from \ - the Ethereum events endpoint task: {}", - err - ); - } - } - } - }) - .spawn(); - EthereumOracleTask::Enabled { - channels: EthereumOracleChannels::new( - eth_receiver, - control_sender, - last_processed_block_receiver, - ), - } - } - ethereum_bridge::ledger::Mode::Off => EthereumOracleTask::NotEnabled, - } -} - /// This function runs `Shell::init_chain` on the provided genesis files. /// This is to check that all the transactions included therein run /// successfully on chain initialization. @@ -948,17 +606,11 @@ pub fn test_genesis_files( use namada_sdk::hash::Sha256Hasher; use namada_sdk::state::mockdb::MockDB; - // Channels for validators to send protocol txs to be broadcast to the - // broadcaster service - let (broadcast_sender, _broadcaster_receiver) = mpsc::unbounded_channel(); - let chain_id = config.chain_id.to_string(); // start an instance of the ledger let mut shell = Shell::::new( config, wasm_dir, - broadcast_sender, - None, None, None, 50 * 1024 * 1024, diff --git a/crates/node/src/protocol.rs b/crates/node/src/protocol.rs index 7865ea4078f..b9881fd67f2 100644 --- a/crates/node/src/protocol.rs +++ b/crates/node/src/protocol.rs @@ -4,7 +4,6 @@ use std::collections::BTreeSet; use std::fmt::{Debug, Display}; use either::Either; -use eyre::{WrapErr, eyre}; use namada_sdk::address::{Address, InternalAddress}; use namada_sdk::booleans::BoolResultUnitExt; use namada_sdk::chain::BlockHeight; @@ -25,7 +24,6 @@ use namada_sdk::token::Amount; use namada_sdk::token::event::{TokenEvent, TokenOperation}; use namada_sdk::token::utils::is_masp_transfer; use namada_sdk::tx::action::{self, Read}; -use namada_sdk::tx::data::protocol::{ProtocolTx, ProtocolTxType}; use namada_sdk::tx::data::{ BatchedTxResult, TxResult, VpStatusFlags, VpsResult, WrapperTx, compute_inner_tx_hash, @@ -33,15 +31,14 @@ use namada_sdk::tx::data::{ use namada_sdk::tx::event::{MaspEvent, MaspEventKind, MaspTxRef}; use namada_sdk::tx::{BatchedTxRef, IndexedTx, Tx, TxCommitments}; use namada_sdk::validation::{ - EthBridgeNutVp, EthBridgePoolVp, EthBridgeVp, GovernanceVp, IbcVp, MaspVp, - MultitokenVp, NativeVpCtx, ParametersVp, PgfVp, PosVp, + GovernanceVp, IbcVp, MaspVp, MultitokenVp, NativeVpCtx, ParametersVp, + PgfVp, PosVp, }; -use namada_sdk::{governance, parameters, state, storage, token}; +use namada_sdk::{parameters, state, storage, token}; #[doc(inline)] pub use namada_vm::wasm::run::GasMeterKind; use namada_vm::wasm::{TxCache, VpCache}; use namada_vm::{self, WasmCacheAccess, wasm}; -use namada_vote_ext::EthereumTxData; use namada_vp::native_vp::NativeVp; use namada_vp::state::ReadConversionState; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; @@ -85,6 +82,8 @@ pub enum Error { NativeVpError(state::Error), #[error("Access to an internal address {0:?} is forbidden")] AccessForbidden(InternalAddress), + #[error("Triggered a deprecated ETH related VP")] + DeprecatedEthVp, } impl Error { @@ -167,8 +166,6 @@ impl From for DispatchError { /// Arguments for transactions' execution pub enum DispatchArgs<'a, CA: 'static + WasmCacheAccess + Sync> { - /// Protocol tx data - Protocol(&'a ProtocolTx), /// Raw tx data Raw { /// The tx index @@ -286,25 +283,6 @@ where }) } } - DispatchArgs::Protocol(protocol_tx) => { - // No bundles of protocol transactions, only take the first one - let cmt = tx.first_commitments().ok_or_else(|| { - Box::new(DispatchError::from(Error::MissingInnerTxs)) - })?; - let batched_tx_result = - apply_protocol_tx(protocol_tx.tx, tx.data(cmt), state) - .map_err(|e| Box::new(DispatchError::from(e)))?; - - Ok({ - let mut batch_results = TxResult::new(); - batch_results.insert_inner_tx_result( - None, - either::Right(cmt), - Ok(batched_tx_result), - ); - batch_results - }) - } DispatchArgs::Wrapper { wrapper, tx_bytes, @@ -1090,90 +1068,6 @@ where }) } -/// Apply a derived transaction to storage based on some protocol transaction. -/// The logic here must be completely deterministic and will be executed by all -/// full nodes every time a protocol transaction is included in a block. Storage -/// is updated natively rather than via the wasm environment, so gas does not -/// need to be metered and validity predicates are bypassed. A [`TxResult`] -/// containing changed keys and the like should be returned in the normal way. -pub(crate) fn apply_protocol_tx( - tx: ProtocolTxType, - data: Option>, - state: &mut WlState, -) -> Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - use namada_sdk::eth_bridge::protocol::transactions; - use namada_vote_ext::{ethereum_events, validator_set_update}; - - let Some(data) = data else { - return Err(Error::ProtocolTxError(eyre!( - "Protocol tx data must be present" - ))); - }; - let ethereum_tx_data = EthereumTxData::deserialize(&tx, &data) - .wrap_err_with(|| { - format!( - "Attempt made to apply an unsupported protocol transaction! - \ - {tx:?}", - ) - }) - .map_err(Error::ProtocolTxError)?; - - match ethereum_tx_data { - EthereumTxData::EthEventsVext( - namada_vote_ext::ethereum_events::SignedVext(ext), - ) => { - let ethereum_events::VextDigest { events, .. } = - ethereum_events::VextDigest::singleton(ext); - transactions::ethereum_events::apply_derived_tx::< - _, - _, - governance::Store<_>, - >(state, events) - .map_err(Error::ProtocolTxError) - } - EthereumTxData::BridgePoolVext(ext) => { - transactions::bridge_pool_roots::apply_derived_tx::< - _, - _, - governance::Store<_>, - >(state, ext.into()) - .map_err(Error::ProtocolTxError) - } - EthereumTxData::ValSetUpdateVext(ext) => { - // NOTE(feature = "abcipp"): with ABCI++, we can write the - // complete proof to storage in one go. the decided vote extension - // digest must already have >2/3 of the voting power behind it. - // with ABCI+, multiple vote extension protocol txs may be needed - // to reach a complete proof. - let signing_epoch = ext.data.signing_epoch; - transactions::validator_set_update::aggregate_votes::< - _, - _, - governance::Store<_>, - >( - state, - validator_set_update::VextDigest::singleton(ext), - signing_epoch, - ) - .map_err(Error::ProtocolTxError) - } - EthereumTxData::EthereumEvents(_) - | EthereumTxData::BridgePool(_) - | EthereumTxData::ValidatorSetUpdate(_) => { - // TODO(namada#198): implement this - tracing::warn!( - "Attempt made to apply an unimplemented protocol transaction, \ - no actions will be taken" - ); - Ok(BatchedTxResult::default()) - } - } -} - /// Execute a transaction code. Returns verifiers requested by the transaction. #[allow(clippy::too_many_arguments)] fn execute_tx( @@ -1406,35 +1300,7 @@ where &verifiers, ) .map_err(Error::NativeVpError), - InternalAddress::EthBridge => { - EthBridgeVp::validate_tx( - &ctx, - batched_tx, - &keys_changed, - &verifiers, - ) - .map_err(Error::NativeVpError) - } - InternalAddress::EthBridgePool => { - EthBridgePoolVp::validate_tx( - &ctx, - batched_tx, - &keys_changed, - &verifiers, - ) - .map_err(Error::NativeVpError) - } - InternalAddress::Nut(_) => { - EthBridgeNutVp::validate_tx( - &ctx, - batched_tx, - &keys_changed, - &verifiers, - ) - .map_err(Error::NativeVpError) - } - internal_addr @ (InternalAddress::IbcToken(_) - | InternalAddress::Erc20(_)) => { + internal_addr @ InternalAddress::IbcToken(_) => { // The address should be a part of a multitoken // key verifiers @@ -1460,6 +1326,13 @@ where (*internal_addr).clone(), ), ), + #[allow(deprecated)] + InternalAddress::EthBridge + | InternalAddress::EthBridgePool + | InternalAddress::Erc20 + | InternalAddress::Nut => { + Err(Error::DeprecatedEthVp) + } } } }; @@ -1539,166 +1412,23 @@ fn merge_vp_results( #[cfg(test)] mod tests { - use eyre::Result; use namada_sdk::account::pks_handle; - use namada_sdk::chain::BlockHeight; - use namada_sdk::collections::HashMap; - use namada_sdk::eth_bridge::protocol::transactions::votes::{ - EpochedVotingPower, Votes, - }; - use namada_sdk::eth_bridge::storage::eth_bridge_queries::EthBridgeQueries; - use namada_sdk::eth_bridge::storage::proof::EthereumProof; - use namada_sdk::eth_bridge::storage::{vote_tallies, vp}; - use namada_sdk::eth_bridge::test_utils; - use namada_sdk::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; - use namada_sdk::ethereum_events::{EthereumEvent, TransferToNamada}; - use namada_sdk::keccak::keccak_hash; + use namada_sdk::key; use namada_sdk::key::RefTo; use namada_sdk::testing::{ arb_tampered_inner_tx, arb_valid_signed_inner_tx, }; - use namada_sdk::tx::{SignableEthMessage, Signed}; - use namada_sdk::voting_power::FractionalVotingPower; - use namada_sdk::{address, key}; use namada_test_utils::TestWasms; - use namada_vote_ext::bridge_pool_roots::BridgePoolRootVext; - use namada_vote_ext::ethereum_events::EthereumEventsVext; use namada_vp::state::StorageWrite; use proptest::test_runner::{Config, TestRunner}; + use state::testing::TestState; use super::*; - fn apply_eth_tx( - tx: EthereumTxData, - state: &mut WlState, - ) -> Result - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - { - let (data, tx) = tx.serialize(); - let tx_result = apply_protocol_tx(tx, Some(data), state)?; - Ok(tx_result) - } - - #[test] - /// Tests that if the same [`ProtocolTxType::EthEventsVext`] is applied - /// twice within the same block, it doesn't result in voting power being - /// double counted. - fn test_apply_protocol_tx_duplicate_eth_events_vext() -> Result<()> { - let validator_a = address::testing::established_address_2(); - let validator_b = address::testing::established_address_3(); - let validator_a_stake = Amount::native_whole(100); - let validator_b_stake = Amount::native_whole(100); - let total_stake = validator_a_stake + validator_b_stake; - let (mut state, _) = test_utils::setup_storage_with_validators( - HashMap::from_iter(vec![ - (validator_a.clone(), validator_a_stake), - (validator_b, validator_b_stake), - ]), - ); - let event = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![TransferToNamada { - amount: Amount::from(100), - asset: DAI_ERC20_ETH_ADDRESS, - receiver: address::testing::established_address_4(), - }], - }; - let vext = EthereumEventsVext { - block_height: BlockHeight(100), - validator_addr: address::testing::established_address_2(), - ethereum_events: vec![event.clone()], - }; - let signing_key = key::testing::keypair_1(); - let signed = vext.sign(&signing_key); - let tx = EthereumTxData::EthEventsVext( - namada_vote_ext::ethereum_events::SignedVext(signed), - ); - - apply_eth_tx(tx.clone(), &mut state)?; - apply_eth_tx(tx, &mut state)?; - - let eth_msg_keys = vote_tallies::Keys::from(&event); - let seen_by: Votes = state.read(ð_msg_keys.seen_by())?.unwrap(); - assert_eq!(seen_by, Votes::from([(validator_a, BlockHeight(100))])); - - // the vote should have only be applied once - let voting_power: EpochedVotingPower = - state.read(ð_msg_keys.voting_power())?.unwrap(); - let expected = EpochedVotingPower::from([( - 0.into(), - FractionalVotingPower::HALF * total_stake, - )]); - assert_eq!(voting_power, expected); - - Ok(()) - } - - #[test] - /// Tests that if the same [`ProtocolTxType::BridgePoolVext`] is applied - /// twice within the same block, it doesn't result in voting power being - /// double counted. - fn test_apply_protocol_tx_duplicate_bp_roots_vext() -> Result<()> { - let validator_a = address::testing::established_address_2(); - let validator_b = address::testing::established_address_3(); - let validator_a_stake = Amount::native_whole(100); - let validator_b_stake = Amount::native_whole(100); - let total_stake = validator_a_stake + validator_b_stake; - let (mut state, keys) = test_utils::setup_storage_with_validators( - HashMap::from_iter(vec![ - (validator_a.clone(), validator_a_stake), - (validator_b, validator_b_stake), - ]), - ); - vp::bridge_pool::init_storage(&mut state); - - let root = state.ethbridge_queries().get_bridge_pool_root(); - let nonce = state.ethbridge_queries().get_bridge_pool_nonce(); - test_utils::commit_bridge_pool_root_at_height( - &mut state, - &root, - 100.into(), - ); - let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); - let signing_key = key::testing::keypair_1(); - let hot_key = - &keys[&address::testing::established_address_2()].eth_bridge; - let sig = Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig; - let vext = BridgePoolRootVext { - block_height: BlockHeight(100), - validator_addr: address::testing::established_address_2(), - sig, - } - .sign(&signing_key); - let tx = EthereumTxData::BridgePoolVext(vext); - apply_eth_tx(tx.clone(), &mut state)?; - apply_eth_tx(tx, &mut state)?; - - let bp_root_keys = vote_tallies::Keys::from(( - &vote_tallies::BridgePoolRoot(EthereumProof::new((root, nonce))), - 100.into(), - )); - let root_seen_by: Votes = state.read(&bp_root_keys.seen_by())?.unwrap(); - assert_eq!( - root_seen_by, - Votes::from([(validator_a, BlockHeight(100))]) - ); - // the vote should have only be applied once - let voting_power: EpochedVotingPower = - state.read(&bp_root_keys.voting_power())?.unwrap(); - let expected = EpochedVotingPower::from([( - 0.into(), - FractionalVotingPower::HALF * total_stake, - )]); - assert_eq!(voting_power, expected); - - Ok(()) - } - #[test] fn test_native_vp_out_of_gas() { - let (mut state, _validators) = test_utils::setup_default_storage(); + let mut state = TestState::default(); + parameters::init_test_storage(&mut state).unwrap(); // some random token address let token_address = Address::Established([0xff; 20].into()); @@ -1784,7 +1514,9 @@ mod tests { // the vps to detect a tx that has been tampered with #[test] fn test_tampered_inner_tx_rejected() { - let (mut state, _validators) = test_utils::setup_default_storage(); + let mut state = TestState::default(); + parameters::init_test_storage(&mut state).unwrap(); + let signing_key = key::testing::keypair_1(); let pk = signing_key.ref_to(); let addr = Address::from(&pk); diff --git a/crates/node/src/shell/abci.rs b/crates/node/src/shell/abci.rs new file mode 100644 index 00000000000..6a090e4e7ca --- /dev/null +++ b/crates/node/src/shell/abci.rs @@ -0,0 +1,551 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::future::FutureExt; +use namada_sdk::chain::BlockHeight; +use namada_sdk::hash::Hash; +use namada_sdk::state::ProcessProposalCachedResult; +use namada_sdk::time::{DateTimeUtc, Utc}; +use tokio::sync::broadcast; + +use super::ShellMode; +use crate::config::{Action, ActionAtHeight}; +use crate::shell::{Error, MempoolTxType, Shell, finalize_block}; +use crate::tendermint::abci::{Request, Response}; +pub use crate::tendermint::abci::{request, response}; +use crate::tower_abci::BoxError; +use crate::{config, tendermint}; + +/// Run the shell's blocking loop that receives messages from the receiver. +pub fn shell_loop( + mut shell: Shell, + mut shell_recv: tokio::sync::mpsc::UnboundedReceiver, + namada_version: &str, +) { + while let Some((req, resp_sender)) = shell_recv.blocking_recv() { + let resp = process_request(&mut shell, req, namada_version) + .map_err(|e| e.into()); + if resp_sender.send(resp).is_err() { + tracing::info!("ABCI response channel is closed") + } + } +} + +pub type TxBytes = prost::bytes::Bytes; + +/// A Tx and the result of calling Process Proposal on it +#[derive(Debug, Clone)] +pub struct ProcessedTx { + pub tx: TxBytes, + pub result: TxResult, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct TxResult { + pub code: u32, + pub info: String, +} + +impl From<(u32, String)> for TxResult { + fn from((code, info): (u32, String)) -> Self { + Self { code, info } + } +} + +impl From for (u32, String) { + fn from(TxResult { code, info }: TxResult) -> Self { + (code, info) + } +} + +fn process_request( + shell: &mut Shell, + req: Request, + namada_version: &str, +) -> Result { + match req { + Request::InitChain(init) => { + tracing::debug!("Request InitChain"); + shell + .init_chain( + init, + #[cfg(any( + test, + feature = "testing", + feature = "benches" + ))] + 1, + ) + .map(Response::InitChain) + } + Request::Info(_) => { + Ok(Response::Info(shell.last_state(namada_version))) + } + Request::Query(query) => Ok(Response::Query(shell.query(query))), + Request::PrepareProposal(block) => { + tracing::debug!("Request PrepareProposal"); + // TODO: use TM domain type in the handler + Ok(Response::PrepareProposal( + shell.prepare_proposal(block.into()), + )) + } + Request::ProcessProposal(block) => { + tracing::debug!("Request ProcessProposal"); + // TODO: use TM domain type in the handler + // NOTE: make sure to put any checks inside process_proposal + // since that function is called in other places to rerun the + // checks if (when) needed. Every check living outside that + // function will not be correctly replicated in the other + // locations + let block_hash = block.hash.try_into(); + let (response, tx_results) = shell.process_proposal(block.into()); + // Cache the response in case of future calls from Namada. If + // hash conversion fails avoid caching + if let Ok(block_hash) = block_hash { + let result = if let response::ProcessProposal::Accept = response + { + ProcessProposalCachedResult::Accepted( + tx_results.into_iter().map(|res| res.into()).collect(), + ) + } else { + ProcessProposalCachedResult::Rejected + }; + + shell + .state + .in_mem_mut() + .block_proposals_cache + .put(block_hash, result); + } + Ok(Response::ProcessProposal(response)) + } + Request::FinalizeBlock(request) => { + tracing::debug!("Request FinalizeBlock"); + + match shell.get_process_proposal_result(request.clone()) { + ProcessProposalCachedResult::Accepted(tx_results) => { + try_recheck_process_proposal(shell, &request)?; + + let request::FinalizeBlock { + txs, + decided_last_commit, + misbehavior, + hash, + height, + time, + next_validators_hash, + proposer_address, + } = request; + + let mut processed_txs = + Vec::with_capacity(tx_results.len()); + for (result, tx) in + tx_results.into_iter().zip(txs.into_iter()) + { + processed_txs.push(ProcessedTx { + tx, + result: result.into(), + }); + } + + #[allow(clippy::disallowed_methods)] + let hash = + Hash::try_from(hash.as_bytes()).unwrap_or_default(); + #[allow(clippy::disallowed_methods)] + let time = DateTimeUtc::try_from(time).unwrap(); + let next_validators_hash = + next_validators_hash.try_into().unwrap(); + let height = BlockHeight::from(height); + let request = finalize_block::Request { + txs: processed_txs, + decided_last_commit, + misbehavior, + hash, + height, + time, + next_validators_hash, + proposer_address, + }; + shell.finalize_block(request).map( + |finalize_block::Response { + events, + tx_results, + validator_updates, + app_hash, + }| { + Response::FinalizeBlock(response::FinalizeBlock { + events: events + .into_iter() + .map(tendermint::abci::Event::from) + .collect(), + tx_results, + validator_updates, + consensus_param_updates: None, + app_hash, + }) + }, + ) + } + ProcessProposalCachedResult::Rejected => { + Err(Error::RejectedBlockProposal) + } + } + } + Request::Commit => { + tracing::debug!("Request Commit"); + let response = shell.commit(); + let take_snapshot = shell.check_snapshot_required(); + shell.update_snapshot_task(take_snapshot); + Ok(Response::Commit(response)) + } + Request::Flush => Ok(Response::Flush), + Request::Echo(msg) => Ok(Response::Echo(response::Echo { + message: msg.message, + })), + Request::CheckTx(tx) => { + let mempool_tx_type = match tx.kind { + request::CheckTxKind::New => MempoolTxType::NewTransaction, + request::CheckTxKind::Recheck => { + MempoolTxType::RecheckTransaction + } + }; + let r#type = mempool_tx_type; + Ok(Response::CheckTx(shell.mempool_validate(&tx.tx, r#type))) + } + Request::ListSnapshots => { + Ok(Response::ListSnapshots(shell.list_snapshots())) + } + Request::OfferSnapshot(req) => { + Ok(Response::OfferSnapshot(shell.offer_snapshot(req))) + } + Request::LoadSnapshotChunk(req) => { + Ok(Response::LoadSnapshotChunk(shell.load_snapshot_chunk(req))) + } + Request::ApplySnapshotChunk(req) => Ok(Response::ApplySnapshotChunk( + shell.apply_snapshot_chunk(req), + )), + Request::ExtendVote(_req) => { + Ok(Response::ExtendVote(response::ExtendVote { + vote_extension: bytes::Bytes::new(), + })) + } + Request::VerifyVoteExtension(_verify_vote_extension) => { + Ok(Response::VerifyVoteExtension( + response::VerifyVoteExtension::Reject, + )) + } + } +} + +#[derive(Debug)] +pub struct Service { + /// A channel for forwarding requests to the shell + shell_send: tokio::sync::mpsc::UnboundedSender, + /// Indicates if the consensus connection is suspended. + suspended: bool, + /// This resolves the non-completing futures returned to tower-abci + /// during suspension. + shutdown: broadcast::Sender<()>, + /// An action to be taken at a specified block height. + action_at_height: Option, +} + +pub type ReqMsg = ( + Request, + tokio::sync::oneshot::Sender>, +); + +/// Indicates how [`Service`] should check whether or not it needs to take +/// action. +#[derive(Debug)] +enum CheckAction { + /// No check necessary. + NoAction, + /// Check a given block height. + Check(u64), + /// The action been taken. + AlreadySuspended, +} + +impl Service { + /// Create a shell with a ABCI service that passes messages to and from the + /// shell. + #[allow(clippy::too_many_arguments)] + pub fn new( + config: &config::Ledger, + ) -> ( + Self, + tokio::sync::mpsc::UnboundedReceiver, + broadcast::Sender<()>, + ) { + let (shell_send, shell_recv) = + tokio::sync::mpsc::unbounded_channel::(); + let (server_shutdown, _) = broadcast::channel::<()>(1); + let action_at_height = config.shell.action_at_height.clone(); + ( + Self { + shell_send, + shutdown: server_shutdown.clone(), + action_at_height, + suspended: false, + }, + shell_recv, + server_shutdown, + ) + } + + /// Check if we are at a block height with a scheduled action. + /// If so, perform the action. + fn maybe_take_action( + action_at_height: Option, + check: CheckAction, + mut shutdown_recv: broadcast::Receiver<()>, + ) -> (bool, Option<>::Future>) { + let hght = match check { + CheckAction::AlreadySuspended => BlockHeight(u64::MAX), + CheckAction::Check(hght) => BlockHeight(hght), + CheckAction::NoAction => BlockHeight::default(), + }; + match action_at_height { + Some(ActionAtHeight { + height, + action: Action::Suspend, + }) if height <= hght => { + if height == hght { + tracing::info!( + "Reached block height {}, suspending.", + height + ); + tracing::warn!( + "\x1b[93mThis feature is intended for debugging \ + purposes. Note that on shutdown a spurious panic \ + message will be produced.\x1b[0m" + ) + } + ( + true, + Some( + async move { + shutdown_recv.recv().await.unwrap(); + Err(BoxError::from( + "Not all tendermint responses were processed. \ + If the `--suspended` flag was passed, you \ + may ignore this error.", + )) + } + .boxed(), + ), + ) + } + Some(ActionAtHeight { + height, + action: Action::Halt, + }) if height == hght => { + tracing::info!( + "Reached block height {}, halting the chain.", + height + ); + ( + false, + Some( + async move { + Err(BoxError::from(format!( + "Reached block height {}, halting the chain.", + height + ))) + } + .boxed(), + ), + ) + } + _ => (false, None), + } + } + + /// If we are not taking special action for this request, forward it + /// normally. + fn forward_request( + &mut self, + req: Request, + ) -> >::Future { + let (resp_send, recv) = tokio::sync::oneshot::channel(); + let result = self.shell_send.send((req.clone(), resp_send)); + async move { + let genesis_time = if let Request::InitChain(ref init) = req { + Some( + DateTimeUtc::try_from(init.time) + .expect("Should be able to parse genesis time."), + ) + } else { + None + }; + if let Err(err) = result { + // The shell has shut-down + return Err(err.into()); + } + recv.await + .unwrap_or_else(|err| { + tracing::info!("ABCI response channel didn't respond"); + Err(err.into()) + }) + .inspect(|_| { + // emit a log line stating that we are sleeping until + // genesis. + #[allow(clippy::disallowed_methods)] + let now = Utc::now(); + if let Some(Ok(sleep_time)) = genesis_time + .map(|t| t.0.signed_duration_since(now).to_std()) + { + if !sleep_time.is_zero() { + tracing::info!( + "Waiting for ledger genesis time: {:?}, time \ + left: {:?}", + genesis_time.unwrap(), + sleep_time + ); + } + } + }) + } + .boxed() + } + + /// Given the type of request, determine if we need to check + /// to possibly take an action. + fn get_action(&self, req: &Request) -> Option { + match req { + Request::PrepareProposal(req) => { + Some(CheckAction::Check(req.height.into())) + } + Request::ProcessProposal(req) => { + Some(CheckAction::Check(req.height.into())) + } + Request::FinalizeBlock(req) => { + Some(CheckAction::Check(req.height.into())) + } + Request::InitChain(_) | Request::CheckTx(_) | Request::Commit => { + if self.suspended { + Some(CheckAction::AlreadySuspended) + } else { + Some(CheckAction::NoAction) + } + } + _ => None, + } + } +} + +/// The ABCI tower service implementation sends and receives messages to and +/// from the [`Service`] for requests from Tendermint. +impl tower::Service for Service { + type Error = BoxError; + type Future = Pin< + Box> + Send + 'static>, + >; + type Response = Response; + + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + // Nothing to check as the sender's channel is unbounded + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + let action = self.get_action(&req); + if let Some(action) = action { + let (suspended, fut) = Self::maybe_take_action( + self.action_at_height.clone(), + action, + self.shutdown.subscribe(), + ); + self.suspended = suspended; + fut.unwrap_or_else(|| self.forward_request(req)) + } else { + self.forward_request(req) + } + } +} + +// Checks if a run of process proposal is required before finalize block +// (recheck) and, in case, performs it. Clears the cache before returning +fn try_recheck_process_proposal( + shell: &mut Shell, + finalize_req: &tendermint::abci::request::FinalizeBlock, +) -> Result<(), Error> { + let recheck_process_proposal = match shell.mode { + ShellMode::Validator { + ref local_config, .. + } => local_config + .as_ref() + .map(|cfg| cfg.recheck_process_proposal) + .unwrap_or_default(), + ShellMode::Full { ref local_config } => local_config + .as_ref() + .map(|cfg| cfg.recheck_process_proposal) + .unwrap_or_default(), + ShellMode::Seed => false, + }; + + if recheck_process_proposal { + let process_proposal_result = match shell + .state + .in_mem_mut() + .block_proposals_cache + .get(&Hash::try_from(finalize_req.hash).unwrap()) + { + // We already have the result of process proposal for this block + // cached in memory + Some(res) => res.to_owned(), + None => { + let process_req = + finalize_block_to_process_proposal(finalize_req.clone()); + // No need to cache the result since this is the last step + // before finalizing the block + if let response::ProcessProposal::Accept = + shell.process_proposal(process_req.into()).0 + { + ProcessProposalCachedResult::Accepted(vec![]) + } else { + ProcessProposalCachedResult::Rejected + } + } + }; + + if let ProcessProposalCachedResult::Rejected = process_proposal_result { + return Err(Error::RejectedBlockProposal); + } + } + + // Clear the cache of proposed blocks' results + shell.state.in_mem_mut().block_proposals_cache.clear(); + + Ok(()) +} + +pub fn finalize_block_to_process_proposal( + req: request::FinalizeBlock, +) -> request::ProcessProposal { + let request::FinalizeBlock { + txs, + decided_last_commit, + misbehavior, + hash, + height, + time, + next_validators_hash, + proposer_address, + } = req; + request::ProcessProposal { + txs, + proposed_last_commit: Some(decided_last_commit), + misbehavior, + hash, + height, + time, + next_validators_hash, + proposer_address, + } +} diff --git a/crates/node/src/shell/block_alloc.rs b/crates/node/src/shell/block_alloc.rs index a20b05fb4a2..789f77bb6bc 100644 --- a/crates/node/src/shell/block_alloc.rs +++ b/crates/node/src/shell/block_alloc.rs @@ -13,16 +13,8 @@ //! //! # How space is allocated //! -//! In the current implementation, we allocate space for transactions -//! in the following order of preference: -//! -//! - First, we allot space for protocol txs. We allow them to take up at most -//! 1/2 of the total block space unless there is extra room due to a lack of -//! user txs. -//! - Next, we allot space for user submitted txs until the block is filled. -//! - If we cannot fill the block with normal txs, we try to fill it with -//! protocol txs that were not allocated in the initial phase. -//! +//! In the current implementation, we allot space for user submitted txs until +//! the block is filled. //! //! # How gas is allocated //! @@ -30,8 +22,6 @@ //! gas limit. We take this entire gas limit as the amount of gas requested by //! the tx. -pub mod states; - // TODO(namada#3250): what if a tx has a size greater than the threshold // for its bin? how do we handle this? if we keep it in the mempool // forever, it'll be a DoS vec, as we can make nodes run out of @@ -44,7 +34,6 @@ use std::marker::PhantomData; use namada_sdk::parameters; use namada_sdk::state::{self, WlState}; -use crate::shell::block_alloc::states::WithNormalTxs; #[allow(unused_imports)] use crate::tendermint_proto::abci::RequestPrepareProposal; @@ -106,27 +95,16 @@ impl Resource for BlockGas { /// Allotted resources for a batch of transactions in some proposed block. /// -/// We keep track of the current space utilized by: -/// -/// - normal transactions. -/// - Protocol transactions. +/// We keep track of the current space utilized by transactions. /// /// Gas usage of normal txs is also tracked. #[derive(Debug, Default)] -pub struct BlockAllocator { - /// The current state of the [`BlockAllocator`] state machine. - _state: PhantomData<*const State>, - /// The total space Tendermint has allotted to the - /// application for the current block height. - block: TxBin, - /// The current space utilized by protocol transactions. - protocol_txs: TxBin, +pub struct BlockAllocator { /// The current space and gas utilized by normal user transactions. normal_txs: NormalTxsBins, } -impl From<&WlState> - for BlockAllocator> +impl From<&WlState> for BlockAllocator where D: 'static + state::DB + for<'iter> state::DBIter<'iter>, H: 'static + state::StorageHasher, @@ -142,29 +120,7 @@ where } } -impl BlockAllocator> { - /// Construct a new [`BlockAllocator`], with an upper bound - /// on the max size of all txs in a block defined by CometBFT and an upper - /// bound on the max gas in a block. - #[inline] - pub fn init( - cometbft_max_block_space_in_bytes: u64, - max_block_gas: u64, - ) -> Self { - let max = cometbft_max_block_space_in_bytes; - Self { - _state: PhantomData, - block: TxBin::init(max), - protocol_txs: { - let allotted_space_in_bytes = threshold::ONE_HALF.over(max); - TxBin::init(allotted_space_in_bytes) - }, - normal_txs: NormalTxsBins::new(max_block_gas), - } - } -} - -impl BlockAllocator { +impl BlockAllocator { /// Construct a new [`BlockAllocator`], with an upper bound /// on the max size of all txs in a block defined by Tendermint and an upper /// bound on the max gas in a block. @@ -173,37 +129,21 @@ impl BlockAllocator { tendermint_max_block_space_in_bytes: u64, max_block_gas: u64, ) -> Self { - let max = tendermint_max_block_space_in_bytes; Self { - _state: PhantomData, - block: TxBin::init(max), - protocol_txs: TxBin::default(), normal_txs: NormalTxsBins { space: TxBin::init(tendermint_max_block_space_in_bytes), gas: TxBin::init(max_block_gas), }, } } -} -impl BlockAllocator { - /// Return the amount of space left to initialize in all - /// [`TxBin`] instances. - /// - /// This is calculated based on the difference between the Tendermint - /// block space for a given round and the sum of the allotted space - /// to each [`TxBin`] instance in a [`BlockAllocator`]. #[inline] - fn unoccupied_space_in_bytes(&self) -> u64 { - let total_bin_space = self - .protocol_txs - .occupied - .checked_add(self.normal_txs.space.occupied) - .expect("Shouldn't overflow"); - self.block - .allotted - .checked_sub(total_bin_space) - .expect("Shouldn't underflow") + pub fn try_alloc( + &mut self, + resource_required: BlockResources<'_>, + ) -> Result<(), AllocFailure> { + self.normal_txs.space.try_dump(resource_required.tx)?; + self.normal_txs.gas.try_dump(resource_required.gas) } } @@ -353,113 +293,17 @@ mod tests { use assert_matches::assert_matches; use proptest::prelude::*; - use super::states::{ - BuildingNormalTxBatch, BuildingProtocolTxBatch, NextState, TryAlloc, - }; use super::*; - use crate::shims::abcipp_shim_types::shim::TxBytes; - - /// Convenience alias for a block space allocator at a state with protocol - /// txs. - type BsaInitialProtocolTxs = - BlockAllocator>; - - /// Convenience alias for a block allocator at a state with protocol - /// txs. - type BsaNormalTxs = BlockAllocator; + use crate::shell::abci::TxBytes; /// Proptest generated txs. #[derive(Debug)] struct PropTx { tendermint_max_block_space_in_bytes: u64, max_block_gas: u64, - protocol_txs: Vec, normal_txs: Vec, } - /// Check that at most 1/2 of the block space is - /// reserved for each kind of tx type, in the - /// allocator's common path. Further check that - /// if not enough normal txs are present, the rest - /// is filled with protocol txs - #[test] - fn test_filling_up_with_protocol() { - const BLOCK_SIZE: u64 = 60; - const BLOCK_GAS: u64 = 1_000; - - // reserve block space for protocol txs - let mut alloc = BsaInitialProtocolTxs::init(BLOCK_SIZE, BLOCK_GAS); - - // allocate ~1/2 of the block space to wrapper txs - assert!(alloc.try_alloc(&[0; 29]).is_ok()); - - // reserve block space for normal txs - let mut alloc = alloc.next_state(); - - // the space we allotted to protocol txs was shrunk to - // the total space we actually used up - assert_eq!(alloc.protocol_txs.allotted, 29); - - // check that the allotted space for normal txs is correct - assert_eq!(alloc.normal_txs.space.allotted, BLOCK_SIZE - 29); - - // add about ~1/3 worth of normal txs - assert!(alloc.try_alloc(BlockResources::new(&[0; 17], 0)).is_ok()); - - // fill the rest of the block with protocol txs - let mut alloc = alloc.next_state(); - - // check that space was shrunk - assert_eq!(alloc.protocol_txs.allotted, BLOCK_SIZE - (29 + 17)); - - // add protocol txs to the block space allocator - assert!(alloc.try_alloc(&[0; 14]).is_ok()); - - // the block should be full at this point - assert_matches!( - alloc.try_alloc(&[0; 1]), - Err(AllocFailure::Rejected { .. }) - ); - } - - /// Test that if less than half of the block can be initially filled - /// with protocol txs, the rest if filled with normal txs. - #[test] - fn test_less_than_half_protocol() { - const BLOCK_SIZE: u64 = 60; - const BLOCK_GAS: u64 = 1_000; - - // reserve block space for protocol txs - let mut alloc = BsaInitialProtocolTxs::init(BLOCK_SIZE, BLOCK_GAS); - - // allocate ~1/3 of the block space to protocol txs - assert!(alloc.try_alloc(&[0; 18]).is_ok()); - - // reserve block space for normal txs - let mut alloc = alloc.next_state(); - - // the space we allotted to protocol txs was shrunk to - // the total space we actually used up - assert_eq!(alloc.protocol_txs.allotted, 18); - - // check that the allotted space for normal txs is correct - assert_eq!(alloc.normal_txs.space.allotted, BLOCK_SIZE - 18); - - // add about ~2/3 worth of normal txs - assert!(alloc.try_alloc(BlockResources::new(&[0; 42], 0)).is_ok()); - // the block should be full at this point - assert_matches!( - alloc.try_alloc(BlockResources::new(&[0; 1], 0)), - Err(AllocFailure::Rejected { .. }) - ); - - let mut alloc = alloc.next_state(); - assert_matches!( - alloc.try_alloc(&[0; 1]), - Err(AllocFailure::OverflowsBin { .. }) - ); - } - proptest! { /// Check if we reject a tx when its respective bin /// capacity has been reached on a [`BlockAllocator`]. @@ -468,13 +312,6 @@ mod tests { proptest_reject_tx_on_bin_cap_reached(max) } - /// Check if the initial bin capacity of the [`BlockAllocator`] - /// is correct. - #[test] - fn test_initial_bin_capacity(max in prop::num::u64::ANY) { - proptest_initial_bin_capacity(max) - } - /// Test that dumping txs whose total combined size /// is less than the bin cap does not fill up the bin. #[test] @@ -488,7 +325,7 @@ mod tests { tendermint_max_block_space_in_bytes: u64, ) { let mut bins = - BsaNormalTxs::init(tendermint_max_block_space_in_bytes, 1_000); + BlockAllocator::init(tendermint_max_block_space_in_bytes, 1_000); // fill the entire bin of protocol txs bins.normal_txs.space.occupied = bins.normal_txs.space.allotted; @@ -511,26 +348,11 @@ mod tests { ) } - /// Implementation of [`test_initial_bin_capacity`]. - fn proptest_initial_bin_capacity(tendermint_max_block_space_in_bytes: u64) { - let bins = BsaInitialProtocolTxs::init( - tendermint_max_block_space_in_bytes, - 1_000, - ); - let expected = tendermint_max_block_space_in_bytes; - assert_eq!( - bins.protocol_txs.allotted, - threshold::ONE_HALF.over(tendermint_max_block_space_in_bytes) - ); - assert_eq!(expected, bins.unoccupied_space_in_bytes()); - } - /// Implementation of [`test_tx_dump_doesnt_fill_up_bin`]. fn proptest_tx_dump_doesnt_fill_up_bin(args: PropTx) { let PropTx { tendermint_max_block_space_in_bytes, max_block_gas, - protocol_txs, normal_txs, } = args; @@ -540,27 +362,10 @@ mod tests { // iterate over the produced txs to make sure we can keep // dumping new txs without filling up the bins - let mut bins = BsaInitialProtocolTxs::init( + let mut bins = BlockAllocator::init( tendermint_max_block_space_in_bytes, max_block_gas, ); - let mut protocol_tx_iter = protocol_txs.iter(); - let mut allocated_txs = vec![]; - let mut new_size = 0; - for tx in protocol_tx_iter.by_ref() { - let bin = bins.protocol_txs; - if new_size + tx.len() as u64 >= bin.allotted { - break; - } else { - new_size += tx.len() as u64; - allocated_txs.push(tx); - } - } - for tx in allocated_txs { - assert!(bins.try_alloc(tx).is_ok()); - } - - let mut bins = bins.next_state(); let mut new_size = bins.normal_txs.space.allotted; let mut decrypted_txs = vec![]; for tx in normal_txs { @@ -575,23 +380,6 @@ mod tests { for tx in decrypted_txs { assert!(bins.try_alloc(BlockResources::new(&tx, 0)).is_ok()); } - - let mut bins = bins.next_state(); - let mut allocated_txs = vec![]; - let mut new_size = bins.protocol_txs.allotted; - for tx in protocol_tx_iter.by_ref() { - let bin = bins.protocol_txs; - if new_size + tx.len() as u64 >= bin.allotted { - break; - } else { - new_size += tx.len() as u64; - allocated_txs.push(tx); - } - } - - for tx in allocated_txs { - assert!(bins.try_alloc(tx).is_ok()); - } } prop_compose! { @@ -599,28 +387,26 @@ mod tests { fn arb_transactions() // create base strategies ( - (tendermint_max_block_space_in_bytes, max_block_gas, protocol_tx_max_bin_size, + (tendermint_max_block_space_in_bytes, max_block_gas, decrypted_tx_max_bin_size) in arb_max_bin_sizes(), ) // compose strategies ( tendermint_max_block_space_in_bytes in Just(tendermint_max_block_space_in_bytes), - max_block_gas in Just(max_block_gas), - protocol_txs in arb_tx_list(protocol_tx_max_bin_size), + max_block_gas in Just(max_block_gas), normal_txs in arb_tx_list(decrypted_tx_max_bin_size), ) -> PropTx { PropTx { tendermint_max_block_space_in_bytes, - max_block_gas, - protocol_txs: protocol_txs.into_iter().map(prost::bytes::Bytes::from).collect(), + max_block_gas, normal_txs: normal_txs.into_iter().map(prost::bytes::Bytes::from).collect(), } } } /// Return random bin sizes for a [`BlockAllocator`]. - fn arb_max_bin_sizes() -> impl Strategy { + fn arb_max_bin_sizes() -> impl Strategy { const MAX_BLOCK_SIZE_BYTES: u64 = 1000; (1..=MAX_BLOCK_SIZE_BYTES).prop_map( |tendermint_max_block_space_in_bytes| { @@ -630,9 +416,6 @@ mod tests { threshold::ONE_HALF .over(tendermint_max_block_space_in_bytes) as usize, - threshold::ONE_HALF - .over(tendermint_max_block_space_in_bytes) - as usize, ) }, ) diff --git a/crates/node/src/shell/block_alloc/states.rs b/crates/node/src/shell/block_alloc/states.rs deleted file mode 100644 index eb10d34a2f7..00000000000 --- a/crates/node/src/shell/block_alloc/states.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! All the states of the `BlockAllocator` state machine, -//! over the extent of a Tendermint consensus round -//! block proposal. -//! -//! # States -//! -//! The state machine moves through the following state DAG: -//! -//! 1. [`BuildingProtocolTxBatch`] - the initial state. In this state, we -//! populate a block with protocol txs. -//! 2. [`BuildingNormalTxBatch`] - the second state. In this state, we populate -//! a block with non-protocol txs. -//! 3. [`BuildingProtocolTxBatch`] - we return to this state to fill up any -//! remaining block space if possible. - -mod normal_txs; -mod protocol_txs; - -use super::AllocFailure; - -/// The leader of the current Tendermint round is building -/// a new batch of protocol txs. -/// -/// This happens twice, in the first stage, we fill up to 1/2 -/// of the block. At the end of allocating user txs, we fill -/// up any remaining space with un-allocated protocol txs. -/// -/// For more info, read the module docs of -/// [`crate::shell::block_alloc::states`]. -pub struct BuildingProtocolTxBatch { - /// One of [`WithNormalTxs`] and [`WithoutNormalTxs`]. - _mode: Mode, -} - -/// Allow block proposals to include user submitted txs. -/// -/// For more info, read the module docs of -/// [`crate::shell::block_alloc::states`]. -pub enum WithNormalTxs {} - -/// Allow block proposals to include wrapper txs. -/// -/// For more info, read the module docs of -/// [`crate::shell::block_alloc::states`]. -pub enum WithoutNormalTxs {} - -/// The leader of the current Tendermint round is building -/// a new batch of user submitted (non-protocol) transactions. -/// -/// For more info, read the module docs of -/// [`crate::shell::block_alloc::states`]. -pub struct BuildingNormalTxBatch {} - -/// Try to allocate a new transaction on a `BlockAllocator` state. -/// -/// For more info, read the module docs of -/// [`crate::shell::block_alloc::states`]. -pub trait TryAlloc { - type Resources<'tx>; - - /// Try to allocate resources for a new transaction. - fn try_alloc( - &mut self, - resource_required: Self::Resources<'_>, - ) -> Result<(), AllocFailure>; -} - -/// Represents a state transition in the `BlockAllocator` state machine. -/// -/// This trait should not be used directly. Instead, consider using -/// [`NextState`]. -/// -/// For more info, read the module docs of -/// [`crate::shell::block_alloc::states`]. -pub trait NextStateImpl { - /// The next state in the `BlockAllocator` state machine. - type Next; - - /// Transition to the next state in the `BlockAllocator`] state - /// machine. - fn next_state_impl(self) -> Self::Next; -} - -/// Convenience extension of [`NextStateImpl`], to transition to a new -/// state with a null transition function. -/// -/// For more info, read the module docs of -/// [`crate::shell::block_alloc::states`]. -pub trait NextState: NextStateImpl { - /// Transition to the next state in the `BlockAllocator` state, - /// using a null transiiton function. - #[inline] - fn next_state(self) -> Self::Next - where - Self: Sized, - { - self.next_state_impl() - } -} - -impl NextState for S where S: NextStateImpl {} diff --git a/crates/node/src/shell/block_alloc/states/normal_txs.rs b/crates/node/src/shell/block_alloc/states/normal_txs.rs deleted file mode 100644 index 1b84d08a163..00000000000 --- a/crates/node/src/shell/block_alloc/states/normal_txs.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::marker::PhantomData; - -use super::super::{AllocFailure, BlockAllocator, TxBin}; -use super::{ - BuildingNormalTxBatch, BuildingProtocolTxBatch, NextStateImpl, TryAlloc, - WithoutNormalTxs, -}; -use crate::shell::block_alloc::BlockResources; - -impl TryAlloc for BlockAllocator { - type Resources<'tx> = BlockResources<'tx>; - - #[inline] - fn try_alloc( - &mut self, - resource_required: Self::Resources<'_>, - ) -> Result<(), AllocFailure> { - self.normal_txs.space.try_dump(resource_required.tx)?; - self.normal_txs.gas.try_dump(resource_required.gas) - } -} - -impl NextStateImpl for BlockAllocator { - type Next = BlockAllocator>; - - #[inline] - fn next_state_impl(mut self) -> Self::Next { - let remaining_free_space = self.unoccupied_space_in_bytes(); - self.protocol_txs = TxBin::init(remaining_free_space); - // cast state - let Self { - block, - protocol_txs, - normal_txs, - .. - } = self; - - BlockAllocator { - _state: PhantomData, - block, - protocol_txs, - normal_txs, - } - } -} diff --git a/crates/node/src/shell/block_alloc/states/protocol_txs.rs b/crates/node/src/shell/block_alloc/states/protocol_txs.rs deleted file mode 100644 index 81aaba99426..00000000000 --- a/crates/node/src/shell/block_alloc/states/protocol_txs.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::marker::PhantomData; - -use super::super::{AllocFailure, BlockAllocator}; -use super::{ - BuildingNormalTxBatch, BuildingProtocolTxBatch, NextStateImpl, TryAlloc, - WithNormalTxs, -}; -use crate::shell::block_alloc::TxBin; - -impl TryAlloc for BlockAllocator> { - type Resources<'tx> = &'tx [u8]; - - #[inline] - fn try_alloc( - &mut self, - tx: Self::Resources<'_>, - ) -> Result<(), AllocFailure> { - self.protocol_txs.try_dump(tx) - } -} - -impl NextStateImpl for BlockAllocator> { - type Next = BlockAllocator; - - #[inline] - fn next_state_impl(mut self) -> Self::Next { - self.protocol_txs.shrink_to_fit(); - let remaining_free_space = self.unoccupied_space_in_bytes(); - self.normal_txs.space = TxBin::init(remaining_free_space); - // cast state - let BlockAllocator { - block, - protocol_txs, - normal_txs, - .. - } = self; - - BlockAllocator { - _state: PhantomData, - block, - protocol_txs, - normal_txs, - } - } -} diff --git a/crates/node/src/shell/finalize_block.rs b/crates/node/src/shell/finalize_block.rs index 2854d05daf0..39e098d28f2 100644 --- a/crates/node/src/shell/finalize_block.rs +++ b/crates/node/src/shell/finalize_block.rs @@ -19,20 +19,55 @@ use namada_sdk::state::{ EPOCH_SWITCH_BLOCKS_DELAY, Result, ResultExt, StorageWrite, }; use namada_sdk::storage::{BlockHeader, BlockResults, Epoch}; -use namada_sdk::tx::data::protocol::ProtocolTxType; use namada_sdk::tx::data::{VpStatusFlags, compute_inner_tx_hash}; use namada_sdk::tx::event::{Batch, Code}; use namada_sdk::tx::new_tx_event; use namada_sdk::{ibc, proof_of_stake}; -use namada_vote_ext::ethereum_events::MultiSignedEthEvent; -use namada_vote_ext::ethereum_tx_data_variants; use tendermint::abci::types::Misbehavior; use super::*; use crate::protocol::{DispatchArgs, DispatchError}; use crate::shell::stats::InternalStats; use crate::tendermint::abci::types::VoteInfo; -use crate::tendermint_proto; + +#[derive(Debug, Clone)] +pub struct Request { + /// List of transactions committed as part of the block. + pub txs: Vec, + /// Information about the last commit, obtained from the block that was + /// just decided. + /// + /// This includes the round, the list of validators, and which validators + /// signed the last block. + pub decided_last_commit: tendermint::abci::types::CommitInfo, + /// Evidence of validator misbehavior. + pub misbehavior: Vec, + /// Merkle root hash of the fields of the decided block. + pub hash: Hash, + /// The height of the finalized block. + pub height: BlockHeight, + /// Timestamp of the finalized block. + pub time: DateTimeUtc, + /// Merkle root of the next validator set. + pub next_validators_hash: Hash, + /// The address of the public key of the original proposer of the block. + pub proposer_address: tendermint::account::Id, +} + +#[derive(Clone, Debug)] +pub struct Response { + /// Set of block events emitted as part of executing the block + pub events: Vec, + /// The result of executing each transaction including the events + /// the particular transaction emitted. This should match the order + /// of the transactions delivered in the block itself + pub tx_results: Vec, + /// A list of updates to the validator set. + /// These will reflect the validator set at current height + 2. + pub validator_updates: Vec, + /// Merkle tree root hash + pub app_hash: AppHash, +} impl Shell where @@ -44,14 +79,54 @@ where /// etc. as necessary. /// /// Apply the transactions included in the block. - pub fn finalize_block( - &mut self, - req: shim::request::FinalizeBlock, - ) -> ShellResult { - let mut response = shim::response::FinalizeBlock::default(); + pub fn finalize_block(&mut self, req: Request) -> ShellResult { + let Request { + txs, + decided_last_commit, + misbehavior, + hash: _, + height: expected_height, + time, + next_validators_hash, + proposer_address, + } = req; + + // If this height has been previously finalized, we need to do it again. + if self.finalized_merkle_tree == Some(expected_height) { + // For that we have to reload merkle tree from DB. + let tree = self + .state + .restrict_writes_to_write_log() + .get_merkle_tree( + expected_height + .checked_sub(1) + .expect("There should be a previous height"), + None, + ) + .expect("Merkle tree should be restored"); + + tree.validate().unwrap(); + self.state.in_mem_mut().block.tree = tree; + } + + let mut tx_results: Vec = vec![]; + let mut validator_updates = vec![]; + let mut events: Vec = vec![]; // Begin the new block and check if a new epoch has begun - let (height, new_epoch) = self.update_state(req.header); + let header = BlockHeader { + hash: self.state.in_mem().merkle_root().into(), + time, + next_validators_hash, + }; + let (height, new_epoch) = self.update_state(header); + if expected_height != height { + #[cfg(not(test))] + return Err(Error::UnexpectedBlockHeight { + expected: expected_height, + got: height, + }); + } let masp_epoch_multiplier = parameters::read_masp_epoch_multiplier_parameter(&self.state) .expect("Must have parameters"); @@ -86,10 +161,10 @@ where self.state.in_mem().update_epoch_blocks_delay ); - let emit_events = &mut response.events; + let emit_events = &mut events; // Get the actual votes from cometBFT in the preferred format let votes = - pos_votes_from_abci(&self.state, &req.decided_last_commit.votes); + pos_votes_from_abci(&self.state, &decided_last_commit.votes); let validator_set_update_epoch = self.get_validator_set_update_epoch(current_epoch); let gas_scale = get_gas_scale(&self.state) @@ -115,7 +190,7 @@ where new_epoch, validator_set_update_epoch, votes, - req.byzantine_validators, + misbehavior, )?; // - IBC ibc::finalize_block(&mut self.state, emit_events, new_epoch)?; @@ -128,8 +203,7 @@ where let mut stats = InternalStats::default(); let native_block_proposer_address = { - let tm_raw_hash_string = - tm_raw_hash_to_string(req.proposer_address); + let tm_raw_hash_string = tm_raw_hash_to_string(proposer_address); find_validator_by_raw_hash(&self.state, tm_raw_hash_string) .unwrap() .expect( @@ -145,21 +219,22 @@ where // Execute wrapper and protocol transactions let successful_wrappers = self.retrieve_and_execute_transactions( &native_block_proposer_address, - &req.txs, + &txs, gas_scale, ExecutionArgs { - response: &mut response, + events: &mut events, changed_keys: &mut changed_keys, stats: &mut stats, height, }, + &mut tx_results, ); // Execute inner transactions self.execute_tx_batches( successful_wrappers, ExecutionArgs { - response: &mut response, + events: &mut events, changed_keys: &mut changed_keys, stats: &mut stats, height, @@ -196,10 +271,7 @@ where } if update_for_tendermint { - self.update_epoch(&mut response); - // send the latest oracle configs. These may have changed due to - // governance. - self.update_eth_oracle(&changed_keys); + validator_updates = self.update_epoch(); } write_last_block_proposer_address( @@ -207,10 +279,34 @@ where native_block_proposer_address, )?; - self.event_log_mut().emit_many(response.events.clone()); + self.event_log_mut().emit_many(events.clone()); tracing::debug!("End finalize_block {height} of epoch {current_epoch}"); - Ok(response) + debug_assert_eq!(txs.len(), tx_results.len()); + + self.state.pre_commit_block()?; + + if let Some(migration) = &self.scheduled_migration { + if height == migration.height { + let migration = migration + .load_and_validate() + .expect("The scheduled migration is not valid."); + migrations::commit(&mut self.state, migration); + } + } + + let merkle_root = self.state.in_mem().block.tree.root(); + let app_hash = AppHash::try_from(merkle_root.0.to_vec()) + .expect("expected a valid app hash"); + + self.finalized_merkle_tree = Some(height); + + Ok(Response { + events, + tx_results, + validator_updates, + app_hash, + }) } /// Sets the metadata necessary for a new block, including the height, @@ -246,17 +342,15 @@ where /// If a new epoch begins, we update the response to include /// changes to the validator sets and consensus parameters - fn update_epoch(&mut self, response: &mut shim::response::FinalizeBlock) { + fn update_epoch(&mut self) -> Vec { // Apply validator set update - response.validator_updates = self - .get_abci_validator_updates(false, |pk, power| { - let pub_key = tendermint_proto::crypto::PublicKey { - sum: Some(key_to_tendermint(&pk).unwrap()), - }; - let pub_key = Some(pub_key); - tendermint_proto::abci::ValidatorUpdate { pub_key, power } - }) - .expect("Must be able to update validator set"); + self.get_abci_validator_updates(false, |pk, power| { + let pub_key = tendermint::PublicKey::from(pk); + // TODO use u64 + let power = tendermint::vote::Power::try_from(power).unwrap(); + tendermint::validator::Update { pub_key, power } + }) + .expect("Must be able to update validator set") } /// Calculate the new inflation rate, mint the new tokens to the PoS @@ -337,7 +431,7 @@ where // batch execution fn evaluate_tx_result( &mut self, - response: &mut shim::response::FinalizeBlock, + events: &mut Vec, extended_dispatch_result: std::result::Result< namada_sdk::tx::data::TxResult, Box, @@ -370,7 +464,7 @@ where // Take the events from the batch result to // avoid emitting them again after the exection // of the entire batch - response.events.emit_many( + events.emit_many( std::mem::take(&mut batched_result.events) .into_iter() .map(|event| { @@ -391,7 +485,7 @@ where }); } _ => self.handle_inner_tx_results( - response, + events, tx_result, tx_data, &mut tx_logs, @@ -456,7 +550,7 @@ where .extend(Code(ResultCode::WasmRuntimeError)); self.handle_batch_error( - response, + events, &msg, tx_result, tx_data, @@ -466,7 +560,7 @@ where }, } - response.events.emit(tx_logs.tx_event); + events.emit(tx_logs.tx_event); None } @@ -474,7 +568,7 @@ where // the storage changes, update stats and event, manage replay protection. fn handle_inner_tx_results( &mut self, - response: &mut shim::response::FinalizeBlock, + events: &mut Vec, mut tx_result: namada_sdk::tx::data::TxResult, tx_data: TxData<'_>, tx_logs: &mut TxLogs<'_>, @@ -491,7 +585,7 @@ where .block .results .accept(tx_data.tx_index); - temp_log.commit(tx_logs, response); + temp_log.commit(tx_logs, events); // Atomic successful batches or non-atomic batches (even if the // inner txs failed) are marked as Ok @@ -518,7 +612,7 @@ where fn handle_batch_error( &mut self, - response: &mut shim::response::FinalizeBlock, + events: &mut Vec, msg: &Error, mut tx_result: namada_sdk::tx::data::TxResult, tx_data: TxData<'_>, @@ -550,7 +644,7 @@ where .block .results .accept(tx_data.tx_index); - temp_log.commit(tx_logs, response); + temp_log.commit(tx_logs, events); // Commit the successful inner transactions before the error. Drop // the current tx write log which might be still populated with data // to be discarded (this is the case when we propagate an error @@ -604,14 +698,15 @@ where fn retrieve_and_execute_transactions( &mut self, native_block_proposer_address: &Address, - processed_txs: &[shim::request::ProcessedTx], + processed_txs: &[abci::ProcessedTx], gas_scale: u64, ExecutionArgs { - response, + events, changed_keys, stats, height, }: ExecutionArgs<'_>, + tx_results: &mut Vec, ) -> Vec { let mut successful_wrappers = vec![]; @@ -631,6 +726,12 @@ where let result_code = ResultCode::from_u32(processed_tx.result.code) .expect("Result code conversion should not fail"); + let tx_hash = tx.header_hash(); + let result_info = serde_json::to_string(&serde_json::json!({ + "namada_tx_hash": tx_hash, + })) + .unwrap(); + let tx_header = tx.header(); // If [`process_proposal`] rejected a Tx, emit an event here and // move on to next tx @@ -640,9 +741,7 @@ where // signature, emit an event here and // move on to next tx ResultCode::InvalidSig => match tx.header().tx_type { - TxType::Wrapper(_) | TxType::Protocol(_) => { - new_tx_event(&tx, height.0) - } + TxType::Wrapper(_) => new_tx_event(&tx, height.0), _ => { tracing::error!( "Internal logic error: FinalizeBlock received \ @@ -655,7 +754,7 @@ where }, _ => new_tx_event(&tx, height.0), }; - response.events.emit( + events.emit( base_event .with(Code(result_code)) .with(Info(format!( @@ -665,6 +764,12 @@ where .with(GasUsed(0.into())), ); + tx_results.push(tendermint::abci::types::ExecTxResult { + code: result_code.into(), + info: result_info, + ..Default::default() + }); + continue; } @@ -679,7 +784,7 @@ where match wrapper.gas_limit.as_scaled_gas(gas_scale) { Ok(value) => value, Err(_) => { - response.events.emit( + events.emit( new_tx_event(&tx, height.0) .with(Code(ResultCode::InvalidTx)) .with(Info( @@ -689,6 +794,15 @@ where )) .with(GasUsed(0.into())), ); + + tx_results.push( + tendermint::abci::types::ExecTxResult { + code: result_code.into(), + info: result_info, + ..Default::default() + }, + ); + continue; } }; @@ -723,63 +837,18 @@ where ); continue; } - TxType::Protocol(protocol_tx) => { - match protocol_tx.tx { - ProtocolTxType::BridgePoolVext - | ProtocolTxType::BridgePool - | ProtocolTxType::ValSetUpdateVext - | ProtocolTxType::ValidatorSetUpdate => (), - - ProtocolTxType::EthEventsVext => { - let ext = - ethereum_tx_data_variants::EthEventsVext::try_from(&tx) - .unwrap(); - if self - .mode - .get_validator_address() - .map(|validator| { - validator == &ext.data.validator_addr - }) - .unwrap_or(false) - { - for event in ext.data.ethereum_events.iter() { - self.mode.dequeue_eth_event(event); - } - } - } - ProtocolTxType::EthereumEvents => { - let digest = - ethereum_tx_data_variants::EthereumEvents::try_from( - &tx, - ) - .unwrap(); - if let Some(address) = - self.mode.get_validator_address().cloned() - { - let this_signer = &( - address, - self.state.in_mem().get_last_block_height(), - ); - for MultiSignedEthEvent { event, signers } in - &digest.events - { - if signers.contains(this_signer) { - self.mode.dequeue_eth_event(event); - } - } - } - } - } - ( - DispatchArgs::Protocol(protocol_tx), - TxGasMeter::new(0, gas_scale), - ) + #[allow(deprecated)] + TxType::Protocol(_) => { + tracing::error!( + "Internal logic error: FinalizeBlock received a \ + TxType::Protocol transaction" + ); + continue; } }; let tx_event = new_tx_event(&tx, height.0); let is_atomic_batch = tx.header.atomic; let commitments_len = tx.commitments().len() as u64; - let tx_hash = tx.header_hash(); let tx_gas_meter = RefCell::new(tx_gas_meter); let dispatch_result = protocol::dispatch_tx( @@ -792,10 +861,24 @@ where let consumed_gas = tx_gas_meter.get_consumed_gas(); // save the gas cost - self.update_tx_gas(tx_hash, consumed_gas); + self.update_tx_gas(tx_hash, consumed_gas.clone()); + + #[allow(clippy::disallowed_methods)] + let gas_used = + i64::try_from(u64::from(consumed_gas)).unwrap_or_default(); + // The number of the `tx_results` has to match the number of txs in + // request, otherwise Comet crashes consensus with "failed to apply + // block; error expected tx results length to match size of + // transactions in block." + tx_results.push(tendermint::abci::types::ExecTxResult { + code: result_code.into(), + gas_used, + info: result_info, + ..Default::default() + }); if let Some(wrapper_cache) = self.evaluate_tx_result( - response, + events, dispatch_result, TxData { is_atomic_batch, @@ -824,7 +907,7 @@ where &mut self, successful_wrappers: Vec, ExecutionArgs { - response, + events, changed_keys, stats, height, @@ -869,7 +952,7 @@ where self.update_tx_gas(tx_hash, consumed_gas); self.evaluate_tx_result( - response, + events, dispatch_result, TxData { is_atomic_batch, @@ -891,7 +974,7 @@ where } struct ExecutionArgs<'finalize> { - response: &'finalize mut shim::response::FinalizeBlock, + events: &'finalize mut Vec, changed_keys: &'finalize mut BTreeSet, stats: &'finalize mut InternalStats, height: BlockHeight, @@ -957,16 +1040,12 @@ impl TempTxLogs { impl<'finalize> TempTxLogs { // Consumes the temporary logs and merges them to confirmed ones. Pushes ibc - // and eth events to the finalize block response - fn commit( - self, - logs: &mut TxLogs<'finalize>, - response: &mut shim::response::FinalizeBlock, - ) { + // events to the finalize block response + fn commit(self, logs: &mut TxLogs<'finalize>, events: &mut Vec) { logs.tx_event.merge(self.tx_event); logs.stats.merge(self.stats); logs.changed_keys.extend(self.changed_keys); - response.events.extend(self.response_events); + events.extend(self.response_events); } // Consumes the temporary logs and merges the statistics to confirmed ones. @@ -1260,40 +1339,57 @@ where ) } +// This is just to be used in testing. It is not a meaningful default. +#[cfg(any(test, feature = "testing"))] +impl Default for Request { + fn default() -> Self { + Request { + hash: Hash([0; 32]), + #[allow(clippy::disallowed_methods)] + time: DateTimeUtc::now(), + next_validators_hash: Default::default(), + misbehavior: Default::default(), + txs: Default::default(), + proposer_address: tendermint::account::Id::try_from( + HEXUPPER + .decode( + wallet::defaults::validator_keypair() + .to_public() + .tm_raw_hash() + .as_bytes(), + ) + .unwrap(), + ) + .unwrap(), + decided_last_commit: tendermint::abci::types::CommitInfo { + round: 0u8.into(), + votes: vec![], + }, + height: Default::default(), + } + } +} + /// We test the failure cases of [`finalize_block`]. The happy flows /// are covered by the e2e tests. #[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] #[cfg(test)] mod test_finalize_block { use std::collections::BTreeMap; - use std::num::NonZeroU64; use std::str::FromStr; use namada_apps_lib::wallet::defaults::albert_keypair; use namada_replay_protection as replay_protection; use namada_sdk::address; + use namada_sdk::borsh::BorshSerializeExt; use namada_sdk::collections::{HashMap, HashSet}; use namada_sdk::dec::{Dec, POS_DECIMAL_PRECISION}; - use namada_sdk::eth_bridge::MinimumConfirmations; - use namada_sdk::eth_bridge::storage::bridge_pool::{ - self, get_key_from_hash, get_nonce_key, get_signed_root_key, - }; - use namada_sdk::eth_bridge::storage::eth_bridge_queries::is_bridge_comptime_enabled; - use namada_sdk::eth_bridge::storage::vote_tallies::BridgePoolRoot; - use namada_sdk::eth_bridge::storage::{ - min_confirmations_key, wrapped_erc20s, - }; - use namada_sdk::ethereum_events::{EthAddress, Uint as ethUint}; - use namada_sdk::events::Event; use namada_sdk::events::extend::Log; - use namada_sdk::gas::{GasMeterKind, VpGasMeter}; - use namada_sdk::governance::storage::keys::get_proposal_execution_key; use namada_sdk::governance::storage::proposal::ProposalType; use namada_sdk::governance::{ InitProposalData, ProposalVote, VoteProposalData, }; use namada_sdk::hash::Hash; - use namada_sdk::keccak::KeccakHash; use namada_sdk::key::testing::common_sk_from_simple_seed; use namada_sdk::parameters::EpochDuration; use namada_sdk::proof_of_stake::storage::{ @@ -1326,20 +1422,15 @@ mod test_finalize_block { use namada_sdk::tx::event::types::APPLIED as APPLIED_TX; use namada_sdk::tx::{Authorization, Code, Data}; use namada_sdk::uint::Uint; - use namada_sdk::validation::ParametersVp; use namada_test_utils::TestWasms; use namada_test_utils::tx_data::TxWriteData; - use namada_vm::wasm::run::VpEvalWasm; - use namada_vote_ext::ethereum_events; use proof_of_stake::{PosParams, bond_tokens}; use test_log::test; use super::*; - use crate::oracle::control::Command; + use crate::shell::FinalizeBlockRequest; + use crate::shell::abci::ProcessedTx; use crate::shell::test_utils::*; - use crate::shims::abcipp_shim_types::shim::request::{ - FinalizeBlock, ProcessedTx, - }; use crate::tendermint::abci::types::Validator; const WRAPPER_GAS_LIMIT: u64 = 10_000_000; @@ -1470,7 +1561,7 @@ mod test_finalize_block { /// correct event is returned. #[test] fn test_process_proposal_rejected_wrapper_tx() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); let keypair = gen_keypair(); let mut processed_txs = vec![]; @@ -1496,7 +1587,7 @@ mod test_finalize_block { // check that the correct events were created for event in shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs.clone(), ..Default::default() }) @@ -1522,354 +1613,10 @@ mod test_finalize_block { } } - /// Test if a rejected protocol tx is applied and emits - /// the correct event - #[test] - fn test_rejected_protocol_tx() { - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - let (mut shell, _, _, _) = setup_at_height(LAST_HEIGHT); - let protocol_key = - shell.mode.get_protocol_key().expect("Test failed").clone(); - - let tx = EthereumTxData::EthereumEvents(ethereum_events::VextDigest { - signatures: Default::default(), - events: vec![], - }) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - - let req = FinalizeBlock { - txs: vec![ProcessedTx { - tx: tx.into(), - result: TxResult { - code: ResultCode::InvalidTx.into(), - info: Default::default(), - }, - }], - ..Default::default() - }; - let mut resp = shell.finalize_block(req).expect("Test failed"); - assert_eq!(resp.len(), 1); - let event = resp.remove(0); - assert_eq!(*event.kind(), APPLIED_TX); - let code = event.read_attribute::().expect("Test failed"); - assert_eq!(code, ResultCode::InvalidTx); - } - - /// Test that once a validator's vote for an Ethereum event lands - /// on-chain from a vote extension digest, it dequeues from the - /// list of events to vote on. - #[test] - fn test_eth_events_dequeued_digest() { - let (mut shell, _, oracle, _) = setup_at_height(3); - let protocol_key = - shell.mode.get_protocol_key().expect("Test failed").clone(); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - - // ---- the ledger receives a new Ethereum event - let event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - tokio_test::block_on(oracle.send(event.clone())).expect("Test failed"); - let [queued_event]: [EthereumEvent; 1] = - shell.new_ethereum_events().try_into().expect("Test failed"); - assert_eq!(queued_event, event); - - // ---- The protocol tx that includes this event on-chain - let ext = ethereum_events::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - ethereum_events: vec![event.clone()], - validator_addr: address.clone(), - } - .sign(&protocol_key); - - let processed_tx = { - let signed = MultiSignedEthEvent { - event, - signers: BTreeSet::from([( - address.clone(), - shell.state.in_mem().get_last_block_height(), - )]), - }; - - let digest = ethereum_events::VextDigest { - signatures: vec![( - (address, shell.state.in_mem().get_last_block_height()), - ext.sig, - )] - .into_iter() - .collect(), - events: vec![signed], - }; - ProcessedTx { - tx: EthereumTxData::EthereumEvents(digest) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes() - .into(), - result: TxResult { - code: ResultCode::Ok.into(), - info: "".into(), - }, - } - }; - - // ---- This protocol tx is accepted - let [result]: [Event; 1] = shell - .finalize_block(FinalizeBlock { - txs: vec![processed_tx], - ..Default::default() - }) - .expect("Test failed") - .try_into() - .expect("Test failed"); - assert_eq!(*result.kind(), APPLIED_TX); - let code = result.read_attribute::().expect("Test failed"); - assert_eq!(code, ResultCode::Ok); - - // --- The event is removed from the queue - assert!(shell.new_ethereum_events().is_empty()); - } - - /// Test that once a validator's vote for an Ethereum event lands - /// on-chain from a protocol tx, it dequeues from the - /// list of events to vote on. - #[test] - fn test_eth_events_dequeued_protocol_tx() { - let (mut shell, _, oracle, _) = setup_at_height(3); - let protocol_key = - shell.mode.get_protocol_key().expect("Test failed").clone(); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - - // ---- the ledger receives a new Ethereum event - let event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - tokio_test::block_on(oracle.send(event.clone())).expect("Test failed"); - let [queued_event]: [EthereumEvent; 1] = - shell.new_ethereum_events().try_into().expect("Test failed"); - assert_eq!(queued_event, event); - - // ---- The protocol tx that includes this event on-chain - let ext = ethereum_events::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - ethereum_events: vec![event], - validator_addr: address, - } - .sign(&protocol_key); - let processed_tx = ProcessedTx { - tx: EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes() - .into(), - result: TxResult { - code: ResultCode::Ok.into(), - info: "".into(), - }, - }; - - // ---- This protocol tx is accepted - let [result]: [Event; 1] = shell - .finalize_block(FinalizeBlock { - txs: vec![processed_tx], - ..Default::default() - }) - .expect("Test failed") - .try_into() - .expect("Test failed"); - assert_eq!(*result.kind(), APPLIED_TX); - let code = result.read_attribute::().expect("Test failed"); - assert_eq!(code, ResultCode::Ok); - - // --- The event is removed from the queue - assert!(shell.new_ethereum_events().is_empty()); - } - - /// Actions to perform in [`test_bp`]. - enum TestBpAction { - /// The tested unit correctly signed over the bridge pool root. - VerifySignedRoot, - /// The tested unit correctly incremented the bridge pool's nonce. - CheckNonceIncremented, - } - - /// Helper function for testing the relevant protocol tx - /// for signing bridge pool roots and nonces - fn test_bp(craft_tx: F) - where - F: FnOnce(&mut TestShell) -> (Tx, TestBpAction), - { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _, _, _) = setup_at_height(1u64); - namada_sdk::eth_bridge::test_utils::commit_bridge_pool_root_at_height( - &mut shell.state, - &KeccakHash([1; 32]), - 1.into(), - ); - let value = BlockHeight(2).serialize_to_vec(); - shell - .state - .in_mem_mut() - .block - .tree - .update(&get_key_from_hash(&KeccakHash([1; 32])), value) - .expect("Test failed"); - shell - .state - .db_write(&get_nonce_key(), Uint::from(1).serialize_to_vec()) - .expect("Test failed"); - let (tx, action) = craft_tx(&mut shell); - let processed_tx = ProcessedTx { - tx: tx.to_bytes().into(), - result: TxResult { - code: ResultCode::Ok.into(), - info: "".into(), - }, - }; - let req = FinalizeBlock { - txs: vec![processed_tx], - ..Default::default() - }; - let root = shell - .state - .read::<(BridgePoolRoot, BlockHeight)>(&get_signed_root_key()) - .expect("Reading signed Bridge pool root shouldn't fail."); - assert!(root.is_none()); - _ = shell.finalize_block(req).expect("Test failed"); - shell.state.commit_block().unwrap(); - match action { - TestBpAction::VerifySignedRoot => { - let (root, _) = shell - .state - .ethbridge_queries() - .get_signed_bridge_pool_root() - .expect("Test failed"); - assert_eq!(root.data.0, KeccakHash([1; 32])); - assert_eq!(root.data.1, ethUint::from(1)); - } - TestBpAction::CheckNonceIncremented => { - let nonce = - shell.state.ethbridge_queries().get_bridge_pool_nonce(); - assert_eq!(nonce, ethUint::from(2)); - } - } - } - - #[test] - /// Test that adding a new erc20 transfer to the bridge pool - /// increments the pool's nonce. - fn test_bp_nonce_is_incremented() { - test_bp(|shell: &mut TestShell| { - let asset = EthAddress([0xff; 20]); - let receiver = EthAddress([0xaa; 20]); - let bertha = namada_apps_lib::wallet::defaults::bertha_address(); - // add bertha's escrowed `asset` to the pool - { - let token = wrapped_erc20s::token(&asset); - let owner_key = token::storage_key::balance_key( - &token, - &bridge_pool::BRIDGE_POOL_ADDRESS, - ); - let supply_key = token::storage_key::minted_balance_key(&token); - let amt: Amount = 999_999_u64.into(); - shell.state.write(&owner_key, amt).expect("Test failed"); - shell.state.write(&supply_key, amt).expect("Test failed"); - } - // add bertha's gas fees the pool - { - let amt: Amount = 999_999_u64.into(); - let native_token = shell.state.in_mem().native_token.clone(); - update_balance( - &mut shell.state, - &native_token, - &bridge_pool::BRIDGE_POOL_ADDRESS, - |_| Ok(amt), - ) - .expect("Test failed"); - } - // write transfer to storage - let transfer = { - use namada_sdk::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, - TransferToEthereumKind, - }; - let pending = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - amount: 10u64.into(), - asset, - recipient: receiver, - sender: bertha.clone(), - }, - gas_fee: GasFee { - token: shell.state.in_mem().native_token.clone(), - amount: 10u64.into(), - payer: bertha.clone(), - }, - }; - let transfer = (&pending).into(); - shell - .state - .write(&bridge_pool::get_pending_key(&pending), pending) - .expect("Test failed"); - transfer - }; - let ethereum_event = EthereumEvent::TransfersToEthereum { - nonce: 1u64.into(), - transfers: vec![transfer], - relayer: bertha, - }; - let (protocol_key, _) = - namada_apps_lib::wallet::defaults::validator_keys(); - let validator_addr = - namada_apps_lib::wallet::defaults::validator_address(); - let ext = { - let ext = ethereum_events::Vext { - validator_addr, - block_height: shell.state.in_mem().get_last_block_height(), - ethereum_events: vec![ethereum_event], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()); - (tx, TestBpAction::CheckNonceIncremented) - }); - } - - #[test] - /// Test that the generated protocol tx passes Finalize Block - /// and effects the expected storage changes. - fn test_bp_roots_protocol_tx() { - test_bp(|shell: &mut TestShell| { - let vext = shell.extend_vote_with_bp_roots().expect("Test failed"); - let tx = EthereumTxData::BridgePoolVext(vext.into()).sign( - shell.mode.get_protocol_key().expect("Test failed"), - shell.chain_id.clone(), - ); - (tx, TestBpAction::VerifySignedRoot) - }); - } - /// Test the correct transition to a new masp epoch #[test] fn test_masp_epoch_progression() { - let (mut shell, _broadcaster, _, _eth_control) = setup(); + let mut shell = setup(); let masp_epoch_multiplier = namada_sdk::parameters::read_masp_epoch_multiplier_parameter( @@ -1901,7 +1648,7 @@ mod test_finalize_block { /// the DB. #[test] fn test_finalize_doesnt_commit_db() { - let (mut shell, _broadcaster, _, _eth_control) = setup(); + let mut shell = setup(); // Update epoch duration to make sure we go through couple epochs let epoch_duration = EpochDuration { @@ -2030,9 +1777,12 @@ mod test_finalize_block { txs.push(processed_tx); } - let req = FinalizeBlock { + let req = FinalizeBlockRequest { txs, - proposer_address: proposer_address.clone(), + proposer_address: tendermint::account::Id::try_from( + proposer_address.clone(), + ) + .unwrap(), decided_last_commit: tendermint::abci::types::CommitInfo { round: 0u8.into(), votes: votes.clone(), @@ -2044,9 +1794,9 @@ mod test_finalize_block { let _events = shell.finalize_block(req).unwrap(); - // the merkle tree root should not change after finalize_block + // the merkle tree root should change after finalize_block let root_post = shell.shell.state.in_mem().block.tree.root(); - assert_eq!(root_pre.0, root_post.0); + assert_ne!(root_pre.0, root_post.0); let new_state = store_block_state(&shell); // The new state must be unchanged itertools::assert_equal( @@ -2074,10 +1824,9 @@ mod test_finalize_block { // properly. At the end of the epoch, check that the validator rewards // products are appropriately updated. - let (mut shell, _recv, _, _) = setup_with_cfg(SetupCfg { + let mut shell = setup_with_cfg(SetupCfg { last_height: 0, num_validators: 4, - ..Default::default() }); let mut validator_set: BTreeSet = @@ -2360,10 +2109,9 @@ mod test_finalize_block { /// A unit test for PoS inflationary rewards claiming and querying #[test] fn test_claim_rewards() { - let (mut shell, _recv, _, _) = setup_with_cfg(SetupCfg { + let mut shell = setup_with_cfg(SetupCfg { last_height: 0, num_validators: 1, - ..Default::default() }); let mut validator_set: BTreeSet = @@ -2635,10 +2383,9 @@ mod test_finalize_block { /// A unit test for PoS inflationary rewards claiming #[test] fn test_claim_validator_commissions() { - let (mut shell, _recv, _, _) = setup_with_cfg(SetupCfg { + let mut shell = setup_with_cfg(SetupCfg { last_height: 0, num_validators: 1, - ..Default::default() }); let mut validator_set: BTreeSet = @@ -2802,10 +2549,9 @@ mod test_finalize_block { /// A unit test for changing consensus keys and communicating to CometBFT #[test] fn test_change_validator_consensus_key() { - let (mut shell, _recv, _, _) = setup_with_cfg(SetupCfg { + let mut shell = setup_with_cfg(SetupCfg { last_height: 0, num_validators: 3, - ..Default::default() }); let mut validators: BTreeSet = @@ -3131,7 +2877,7 @@ mod test_finalize_block { /// Test that replay protection keys are not added to the merkle tree #[test] fn test_replay_keys_not_merklized() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); let (wrapper_tx, processed_tx) = mk_wrapper_tx( &shell, @@ -3145,7 +2891,7 @@ mod test_finalize_block { let root_pre = shell.shell.state.in_mem().block.tree.root(); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -3154,9 +2900,9 @@ mod test_finalize_block { let code = event.read_attribute::().expect("Test failed"); assert_eq!(code, ResultCode::Ok); - // the merkle tree root should not change after finalize_block + // the merkle tree root should change after finalize_block let root_post = shell.shell.state.in_mem().block.tree.root(); - assert_eq!(root_pre.0, root_post.0); + assert_ne!(root_pre.0, root_post.0); // Check transaction's hash in storage assert!( @@ -3192,7 +2938,7 @@ mod test_finalize_block { /// Test that masp anchor keys are added to the merkle tree #[test] fn test_masp_anchors_merklized() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); let convert_key = namada_sdk::token::storage_key::masp_convert_anchor_key(); @@ -3214,7 +2960,7 @@ mod test_finalize_block { .protocol_write(&commitment_key, "random_data".serialize_to_vec()) .unwrap(); shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![], ..Default::default() }) @@ -3222,7 +2968,7 @@ mod test_finalize_block { // the merkle tree root should change after finalize_block let root_post = shell.shell.state.in_mem().block.tree.root(); - assert_eq!(root_pre.0, root_post.0); + assert_ne!(root_pre.0, root_post.0); // Check that the hashes are present in the merkle tree shell.state.commit_block().unwrap(); assert!( @@ -3251,7 +2997,7 @@ mod test_finalize_block { /// doesn't get reapplied #[test] fn test_duplicated_tx_same_block() { - let (mut shell, _broadcaster, _, _) = setup(); + let mut shell = setup(); let keypair = namada_apps_lib::wallet::defaults::albert_keypair(); let keypair_2 = namada_apps_lib::wallet::defaults::bertha_keypair(); @@ -3304,15 +3050,15 @@ mod test_finalize_block { let root_pre = shell.shell.state.in_mem().block.tree.root(); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs, ..Default::default() }) .expect("Test failed"); - // the merkle tree root should not change after finalize_block + // the merkle tree root should change after finalize_block let root_post = shell.shell.state.in_mem().block.tree.root(); - assert_eq!(root_pre.0, root_post.0); + assert_ne!(root_pre.0, root_post.0); assert_eq!(*event[0].kind(), APPLIED_TX); let code = event[0].read_attribute::().expect("Test failed"); @@ -3358,7 +3104,8 @@ mod test_finalize_block { // able to execute and pass #[test] fn test_duplicated_tx_same_block_with_failure() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); + let keypair = namada_apps_lib::wallet::defaults::albert_keypair(); let keypair_2 = namada_apps_lib::wallet::defaults::bertha_keypair(); @@ -3432,15 +3179,15 @@ mod test_finalize_block { let root_pre = shell.shell.state.in_mem().block.tree.root(); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs, ..Default::default() }) .expect("Test failed"); - // the merkle tree root should not change after finalize_block + // the merkle tree root should change after finalize_block let root_post = shell.shell.state.in_mem().block.tree.root(); - assert_eq!(root_pre.0, root_post.0); + assert_ne!(root_pre.0, root_post.0); assert_eq!(*event[0].kind(), APPLIED_TX); let code = event[0].read_attribute::().expect("Test failed"); @@ -3467,7 +3214,7 @@ mod test_finalize_block { /// hash written to storage. #[test] fn test_tx_hash_handling() { - let (mut shell, _broadcaster, _, _) = setup(); + let mut shell = setup(); let keypair = namada_apps_lib::wallet::defaults::bertha_keypair(); let mut out_of_gas_wrapper = { let mut wrapper_tx = @@ -3561,15 +3308,15 @@ mod test_finalize_block { let root_pre = shell.shell.state.in_mem().block.tree.root(); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs, ..Default::default() }) .expect("Test failed"); - // the merkle tree root should not change after finalize_block + // the merkle tree root should change after finalize_block let root_post = shell.shell.state.in_mem().block.tree.root(); - assert_eq!(root_pre.0, root_post.0); + assert_ne!(root_pre.0, root_post.0); assert_eq!(*event[0].kind(), APPLIED_TX); let code = event[0].read_attribute::().expect("Test failed"); @@ -3680,7 +3427,8 @@ mod test_finalize_block { /// even if the wrapper tx fails. The inner transaction hash must not be /// inserted fn test_commits_hash_if_wrapper_failure() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); + let keypair = gen_keypair(); let mut wrapper = @@ -3716,15 +3464,15 @@ mod test_finalize_block { let root_pre = shell.shell.state.in_mem().block.tree.root(); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs, ..Default::default() }) .expect("Test failed"); - // the merkle tree root should not change after finalize_block + // the merkle tree root should change after finalize_block let root_post = shell.shell.state.in_mem().block.tree.root(); - assert_eq!(root_pre.0, root_post.0); + assert_ne!(root_pre.0, root_post.0); assert_eq!(*event[0].kind(), APPLIED_TX); let code = event[0].read_attribute::().expect("Test failed"); @@ -3748,7 +3496,8 @@ mod test_finalize_block { // modifications are dropped #[test] fn test_fee_payment_if_invalid_inner_tx() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); + let keypair = namada_apps_lib::wallet::defaults::albert_keypair(); let mut wrapper = @@ -3793,7 +3542,7 @@ mod test_finalize_block { }; let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -3828,7 +3577,8 @@ mod test_finalize_block { // of the inner txs of the batch gets executed #[test] fn test_fee_payment_if_insufficient_balance() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); + let keypair = gen_keypair(); let native_token = shell.state.in_mem().native_token.clone(); @@ -3865,7 +3615,7 @@ mod test_finalize_block { assert!(fee_amount > initial_balance); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -3898,7 +3648,8 @@ mod test_finalize_block { // one is accepted #[test] fn test_fee_payment_whitelisted_token() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); + let btc = namada_sdk::address::testing::btc(); let btc_denom = read_denom(&shell.state, &btc).unwrap().unwrap(); let fee_amount: Amount = WRAPPER_GAS_LIMIT.into(); @@ -3953,7 +3704,7 @@ mod test_finalize_block { }; let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -3978,7 +3729,7 @@ mod test_finalize_block { // signer and credited to the block proposer #[test] fn test_fee_payment_to_block_proposer() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); let validator = shell.mode.get_validator_address().unwrap().to_owned(); let pos_params = read_pos_params(&shell.state).unwrap(); @@ -4037,9 +3788,12 @@ mod test_finalize_block { }; let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], - proposer_address, + proposer_address: tendermint::account::Id::try_from( + proposer_address, + ) + .unwrap(), ..Default::default() }) .expect("Test failed")[0]; @@ -4075,10 +3829,9 @@ mod test_finalize_block { #[test] fn test_ledger_slashing() -> namada_sdk::state::Result<()> { let num_validators = 7_u64; - let (mut shell, _recv, _, _) = setup_with_cfg(SetupCfg { + let mut shell = setup_with_cfg(SetupCfg { last_height: 0, num_validators, - ..Default::default() }); let mut params = read_pos_params(&shell.state).unwrap(); params.owned.unbonding_len = 4; @@ -4470,10 +4223,9 @@ mod test_finalize_block { ) -> namada_sdk::state::Result<()> { // Setup the network with pipeline_len = 2, unbonding_len = 4 // let num_validators = 8_u64; - let (mut shell, _recv, _, _) = setup_with_cfg(SetupCfg { + let mut shell = setup_with_cfg(SetupCfg { last_height: 0, num_validators, - ..Default::default() }); let mut params = read_pos_params(&shell.state).unwrap(); params.owned.unbonding_len = 4; @@ -5285,10 +5037,9 @@ mod test_finalize_block { #[test] fn test_jail_validator_for_inactivity() -> namada_sdk::state::Result<()> { let num_validators = 5_u64; - let (mut shell, _recv, _, _) = setup_with_cfg(SetupCfg { + let mut shell = setup_with_cfg(SetupCfg { last_height: 0, num_validators, - ..Default::default() }); let params = read_pos_params(&shell.state).unwrap(); @@ -5660,118 +5411,17 @@ mod test_finalize_block { ) } - /// Test that updating the ethereum bridge params via governance works. - #[tokio::test] - async fn test_eth_bridge_param_updates() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _broadcaster, _, mut control_receiver) = - setup_at_height(3u64); - let proposal_execution_key = get_proposal_execution_key(0); - shell - .state - .write(&proposal_execution_key, 0u64) - .expect("Test failed."); - let mut tx = Tx::new(shell.chain_id.clone(), None); - tx.add_code_from_hash(Hash::default(), None).add_data(0u64); - let new_min_confirmations = MinimumConfirmations::from(unsafe { - NonZeroU64::new_unchecked(42) - }); - shell - .state - .write(&min_confirmations_key(), new_min_confirmations) - .expect("Test failed"); - let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( - &TxGasMeter::new(u64::MAX, get_gas_scale(&shell.state).unwrap()), - )); - let keys_changed = BTreeSet::from([min_confirmations_key()]); - let verifiers = BTreeSet::default(); - let batched_tx = tx.batch_ref_first_tx().unwrap(); - let ctx = namada_vp::native_vp::Ctx::<_, _, VpEvalWasm<_, _, _>>::new( - shell.mode.get_validator_address().expect("Test failed"), - shell.state.read_only(), - batched_tx.tx, - batched_tx.cmt, - &TxIndex(0), - &gas_meter, - &keys_changed, - &verifiers, - shell.vp_wasm_cache.clone(), - GasMeterKind::MutGlobal, - ); - assert!( - ParametersVp::validate_tx( - &ctx, - &batched_tx, - &keys_changed, - &verifiers - ) - .is_ok() - ); - - // we advance forward to the next epoch - let mut req = FinalizeBlock::default(); - req.header.time = { - #[allow(clippy::disallowed_methods)] - namada_sdk::time::DateTimeUtc::now() - }; - let current_decision_height = shell.get_current_decision_height(); - if let Some(b) = shell.state.in_mem_mut().last_block.as_mut() { - b.height = current_decision_height + 11; - } - shell.finalize_block(req).expect("Test failed"); - shell.commit(); - - let consensus_set: Vec = - read_consensus_validator_set_addresses_with_stake( - &shell.state, - Epoch::default(), - ) - .unwrap() - .into_iter() - .collect(); - - let params = read_pos_params(&shell.state).unwrap(); - let val1 = consensus_set[0].clone(); - let pkh1 = get_pkh_from_address( - &shell.state, - ¶ms, - val1.address.clone(), - Epoch::default(), - ); - - let _ = control_receiver.recv().await.expect("Test failed"); - // Finalize block 2 - let votes = vec![VoteInfo { - validator: Validator { - address: pkh1, - power: (u128::try_from(val1.bonded_stake).expect("Test failed") - as u64) - .try_into() - .unwrap(), - }, - sig_info: tendermint::abci::types::BlockSignatureInfo::LegacySigned, - }]; - next_block_for_inflation(&mut shell, pkh1.to_vec(), votes, None); - let Command::UpdateConfig(cmd) = - control_receiver.recv().await.expect("Test failed"); - assert_eq!(u64::from(cmd.min_confirmations), 42); - } - // Test a successful tx batch containing three valid transactions #[test] fn test_successful_batch() { - let (mut shell, _broadcaster, _, _) = setup(); + let mut shell = setup(); let sk = wallet::defaults::bertha_keypair(); let (batch, processed_tx) = mk_tx_batch(&shell, &sk, false, false, false); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -5813,13 +5463,13 @@ mod test_finalize_block { // that the last transaction is never executed (batch short-circuit) #[test] fn test_failing_atomic_batch() { - let (mut shell, _broadcaster, _, _) = setup(); + let mut shell = setup(); let sk = wallet::defaults::bertha_keypair(); let (batch, processed_tx) = mk_tx_batch(&shell, &sk, true, true, false); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -5871,14 +5521,14 @@ mod test_finalize_block { // committed #[test] fn test_failing_non_atomic_batch() { - let (mut shell, _broadcaster, _, _) = setup(); + let mut shell = setup(); let sk = wallet::defaults::bertha_keypair(); let (batch, processed_tx) = mk_tx_batch(&shell, &sk, false, true, false); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -5949,13 +5599,14 @@ mod test_finalize_block { // successful txs. Verify that no changes are committed #[test] fn test_gas_error_atomic_batch() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); + let sk = wallet::defaults::bertha_keypair(); let (batch, processed_tx) = mk_tx_batch(&shell, &sk, true, false, true); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -6006,14 +5657,15 @@ mod test_finalize_block { // successful txs. Verify that changes from the first tx are committed #[test] fn test_gas_error_non_atomic_batch() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); + let sk = wallet::defaults::bertha_keypair(); let (batch, processed_tx) = mk_tx_batch(&shell, &sk, false, false, true); let event = &shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: vec![processed_tx], ..Default::default() }) @@ -6070,7 +5722,7 @@ mod test_finalize_block { #[test] fn test_multiple_events_from_batch_tx_all_valid() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); let sk = wallet::defaults::bertha_keypair(); @@ -6115,7 +5767,7 @@ mod test_finalize_block { }]; let mut events = shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs, ..Default::default() }) @@ -6152,7 +5804,7 @@ mod test_finalize_block { #[test] fn test_multiple_identical_events_from_batch_tx_all_valid() { const EVENT_MSG: &str = "bing"; - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); let sk = wallet::defaults::bertha_keypair(); @@ -6197,7 +5849,7 @@ mod test_finalize_block { }]; let mut events = shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs, ..Default::default() }) @@ -6230,7 +5882,7 @@ mod test_finalize_block { #[test] fn test_multiple_events_from_batch_tx_one_valid_other_invalid() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); let sk = wallet::defaults::bertha_keypair(); @@ -6274,7 +5926,7 @@ mod test_finalize_block { }]; let mut events = shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs, ..Default::default() }) @@ -6301,7 +5953,7 @@ mod test_finalize_block { #[test] fn test_multiple_events_from_atomic_batch_tx_one_valid_other_invalid() { - let (mut shell, _, _, _) = setup(); + let mut shell = setup(); let sk = wallet::defaults::bertha_keypair(); @@ -6345,7 +5997,7 @@ mod test_finalize_block { }]; let mut events = shell - .finalize_block(FinalizeBlock { + .finalize_block(FinalizeBlockRequest { txs: processed_txs, ..Default::default() }) diff --git a/crates/node/src/shell/init_chain.rs b/crates/node/src/shell/init_chain.rs index 7402c1364a3..98c828dcef5 100644 --- a/crates/node/src/shell/init_chain.rs +++ b/crates/node/src/shell/init_chain.rs @@ -7,15 +7,14 @@ use masp_primitives::sapling::Node; use masp_proofs::bls12_381; use namada_sdk::account::protocol_pk_key; use namada_sdk::collections::HashMap; -use namada_sdk::eth_bridge::EthBridgeStatus; use namada_sdk::hash::Hash as CodeHash; +use namada_sdk::ibc; use namada_sdk::parameters::Parameters; use namada_sdk::proof_of_stake::{self, BecomeValidator, PosParams}; use namada_sdk::state::StorageWrite; use namada_sdk::time::{TimeZone, Utc}; use namada_sdk::token::storage_key::masp_token_map_key; use namada_sdk::token::{credit_tokens, write_denom}; -use namada_sdk::{eth_bridge, ibc}; use namada_vm::validate_untrusted_wasm; use super::*; @@ -81,7 +80,6 @@ where /// 2. Setting up the validity predicates for both users and tokens /// 3. Validators /// 4. The PoS system - /// 5. The Ethereum bridge parameters /// /// INVARIANT: This method must not commit the state changes to DB. pub fn init_chain( @@ -226,20 +224,6 @@ where let gov_params = genesis.get_gov_params(); gov_params.init_storage(&mut self.state).unwrap(); - // configure the Ethereum bridge if the configuration is set. - if let Some(config) = genesis.get_eth_bridge_params() { - tracing::debug!("Initializing Ethereum bridge storage."); - config.init_storage(&mut self.state); - self.update_eth_oracle(&Default::default()); - } else { - self.state - .write( - ð_bridge::storage::active_key(), - EthBridgeStatus::Disabled, - ) - .unwrap(); - } - // Initialize IBC parameters let ibc_params = genesis.get_ibc_params(); ibc_params.init_storage(&mut self.state).unwrap(); @@ -989,7 +973,7 @@ mod test { /// DB. #[test] fn test_init_chain_doesnt_commit_db() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); // Collect all storage key-vals into a sorted map let store_block_state = |shell: &TestShell| -> BTreeMap<_, _> { @@ -1022,7 +1006,7 @@ mod test { /// * cannot be read from disk. #[test] fn test_dry_run_lookup_vp() { - let (mut shell, _x, _y, _z) = TestShell::new_at_height(0); + let mut shell = TestShell::new_at_height(0); shell.wasm_dir = PathBuf::new(); let mut genesis = genesis::make_dev_genesis(1, &shell.base_dir); let mut initializer = InitChainValidation::new(&mut shell, true); @@ -1061,7 +1045,7 @@ mod test { /// * no vp_implicit wasm is stored #[test] fn test_dry_run_store_wasms() { - let (mut shell, _x, _y, _z) = TestShell::new_at_height(0); + let mut shell = TestShell::new_at_height(0); let test_dir = tempfile::tempdir().unwrap(); shell.wasm_dir = test_dir.path().into(); @@ -1129,7 +1113,7 @@ mod test { /// corresponding config is encountered. #[test] fn test_dry_run_init_token_balance() { - let (mut shell, _x, _y, _z) = TestShell::new_at_height(0); + let mut shell = TestShell::new_at_height(0); shell.wasm_dir = PathBuf::new(); let mut genesis = genesis::make_dev_genesis(1, &shell.base_dir); let mut initializer = InitChainValidation::new(&mut shell, true); @@ -1154,7 +1138,7 @@ mod test { /// * bonding to a non-validator #[test] fn test_dry_run_genesis_bonds() { - let (mut shell, _x, _y, _z) = TestShell::new_at_height(0); + let mut shell = TestShell::new_at_height(0); shell.wasm_dir = PathBuf::new(); let mut genesis = genesis::make_dev_genesis(1, &shell.base_dir); let mut initializer = InitChainValidation::new(&mut shell, true); @@ -1227,7 +1211,7 @@ mod test { #[test] fn test_dry_run_native_token_masp_params() { - let (mut shell, _x, _y, _z) = TestShell::new_at_height(0); + let mut shell = TestShell::new_at_height(0); shell.wasm_dir = PathBuf::new(); let mut genesis = genesis::make_dev_genesis(1, &shell.base_dir); let mut initializer = InitChainValidation::new(&mut shell, true); diff --git a/crates/node/src/shell/mod.rs b/crates/node/src/shell/mod.rs index a640d9e1b95..a6793ccd406 100644 --- a/crates/node/src/shell/mod.rs +++ b/crates/node/src/shell/mod.rs @@ -10,11 +10,13 @@ mod finalize_block; mod init_chain; pub use init_chain::InitChainValidation; use namada_apps_lib::config::NodeLocalConfig; +use namada_sdk::state; use namada_sdk::state::StateRead; use namada_vm::wasm::run::check_tx_allowed; pub mod prepare_proposal; use namada_sdk::ibc; -use namada_sdk::state::State; +use namada_sdk::state::{DbError, ProcessProposalCachedResult, State}; +pub mod abci; pub mod process_proposal; pub(super) mod queries; mod snapshots; @@ -22,7 +24,6 @@ mod stats; #[cfg(any(test, feature = "testing"))] #[allow(dead_code)] pub mod testing; -mod vote_extensions; use std::cell::RefCell; use std::collections::BTreeSet; @@ -31,16 +32,15 @@ use std::path::{Path, PathBuf}; #[allow(unused_imports)] use std::rc::Rc; -use namada_apps_lib::wallet::{self, ValidatorData, ValidatorKeys}; +use abci::TxResult; +pub use finalize_block::{ + Request as FinalizeBlockRequest, Response as FinalizeBlockResponse, +}; +use namada_apps_lib::wallet::{self, ValidatorData}; use namada_sdk::address::Address; -use namada_sdk::borsh::{BorshDeserialize, BorshSerializeExt}; +use namada_sdk::borsh::BorshDeserialize; use namada_sdk::chain::{BlockHeight, ChainId}; use namada_sdk::collections::HashMap; -use namada_sdk::eth_bridge::protocol::validation::bridge_pool_roots::validate_bp_roots_vext; -use namada_sdk::eth_bridge::protocol::validation::ethereum_events::validate_eth_events_vext; -use namada_sdk::eth_bridge::protocol::validation::validator_set_update::validate_valset_upd_vext; -use namada_sdk::eth_bridge::{EthBridgeQueries, EthereumOracleConfig}; -use namada_sdk::ethereum_events::EthereumEvent; use namada_sdk::events::log::EventLog; use namada_sdk::gas::{Gas, GasMetering, TxGasMeter}; use namada_sdk::hash::Hash; @@ -51,7 +51,6 @@ use namada_sdk::proof_of_stake::storage::read_pos_params; use namada_sdk::proof_of_stake::types::{ ConsensusValidator, ValidatorSetUpdate, }; -use namada_sdk::state::tx_queue::ExpiredTx; use namada_sdk::state::{ DB, DBIter, EPOCH_SWITCH_BLOCKS_DELAY, FullAccessState, Sha256Hasher, StorageHasher, StorageRead, TempWlState, WlState, @@ -62,43 +61,21 @@ use namada_sdk::time::DateTimeUtc; pub use namada_sdk::tx::data::ResultCode; use namada_sdk::tx::data::{TxType, WrapperTx}; use namada_sdk::tx::{Section, Tx}; -use namada_sdk::{ - eth_bridge, governance, hints, migrations, parameters, proof_of_stake, - token, -}; +use namada_sdk::{governance, migrations, parameters, proof_of_stake, token}; use namada_vm::wasm::{TxCache, VpCache}; use namada_vm::{WasmCacheAccess, WasmCacheRwAccess}; -use namada_vote_ext::EthereumTxData; use thiserror::Error; -use tokio::sync::mpsc::{Receiver, UnboundedSender}; -use super::ethereum_oracle::{self as oracle, last_processed_block}; use crate::config::{self, TendermintMode, ValidatorLocalConfig, genesis}; use crate::protocol::ShellParams; -use crate::shims::abcipp_shim_types::shim; -use crate::shims::abcipp_shim_types::shim::TakeSnapshot; -use crate::shims::abcipp_shim_types::shim::response::TxResult; +use crate::storage::DbSnapshot; use crate::tendermint::abci::{request, response}; use crate::tendermint::{self, validator}; -use crate::tendermint_proto::crypto::public_key; use crate::{protocol, storage, tendermint_node}; /// A cap on a number of tx sections pub const MAX_TX_SECTIONS_LEN: usize = 10_000; -fn key_to_tendermint( - pk: &common::PublicKey, -) -> std::result::Result { - match pk { - common::PublicKey::Ed25519(_) => ed25519::PublicKey::try_from_pk(pk) - .map(|pk| public_key::Sum::Ed25519(pk.serialize_to_vec())), - common::PublicKey::Secp256k1(_) => { - secp256k1::PublicKey::try_from_pk(pk) - .map(|pk| public_key::Sum::Secp256k1(pk.serialize_to_vec())) - } - } -} - #[derive(Error, Debug)] pub enum Error { #[error("Error removing the DB data: {0}")] @@ -111,8 +88,6 @@ pub enum Error { TxApply(protocol::Error), #[error("{0}")] Tendermint(tendermint_node::Error), - #[error("{0}")] - Ethereum(super::ethereum_oracle::Error), #[error("Server error: {0}")] TowerServer(String), #[error("{0}")] @@ -136,6 +111,11 @@ pub enum Error { RejectedBlockProposal, #[error("Received an invalid block proposal")] InvalidBlockProposal, + #[error("Unexpected block height, expected: {expected}, got: {got}")] + UnexpectedBlockHeight { + expected: BlockHeight, + got: BlockHeight, + }, } impl From for TxResult { @@ -188,8 +168,6 @@ pub fn rollback(config: config::Ledger) -> ShellResult<()> { pub(super) enum ShellMode { Validator { data: ValidatorData, - broadcast_sender: UnboundedSender>, - eth_oracle: Option, validator_local_config: Option, local_config: Option, }, @@ -199,145 +177,15 @@ pub(super) enum ShellMode { Seed, } -/// A channel for pulling events from the Ethereum oracle -/// and queueing them up for inclusion in vote extensions -#[derive(Debug)] -pub(super) struct EthereumReceiver { - channel: Receiver, - queue: BTreeSet, -} - -impl EthereumReceiver { - /// Create a new [`EthereumReceiver`] from a channel connected - /// to an Ethereum oracle - pub fn new(channel: Receiver) -> Self { - Self { - channel, - queue: BTreeSet::new(), - } - } - - /// Pull Ethereum events from the oracle and queue them to - /// be voted on. - /// - /// Since vote extensions require ordering of Ethereum - /// events, we do that here. We also de-duplicate events. - /// Events may be filtered out of the queue with a provided - /// predicate. - pub fn fill_queue(&mut self, mut keep_event: F) - where - F: FnMut(&EthereumEvent) -> bool, - { - let mut new_events: usize = 0; - let mut filtered_events: usize = 0; - while let Ok(eth_event) = self.channel.try_recv() { - if keep_event(ð_event) && self.queue.insert(eth_event) { - new_events = - new_events.checked_add(1).expect("Cannot overflow"); - } else { - filtered_events = - filtered_events.checked_add(1).expect("Cannot overflow"); - } - } - if new_events - .checked_add(filtered_events) - .expect("Cannot overflow") - > 0 - { - tracing::info!( - new_events, - filtered_events, - "received Ethereum events" - ); - } - } - - /// Get a copy of the queue - pub fn get_events(&self) -> Vec { - self.queue.iter().cloned().collect() - } - - /// Remove the given [`EthereumEvent`] from the queue, if present. - /// - /// **INVARIANT:** This method preserves the sorting and de-duplication - /// of events in the queue. - pub fn remove_event(&mut self, event: &EthereumEvent) { - self.queue.remove(event); - } -} - impl ShellMode { /// Get the validator address if ledger is in validator mode + #[cfg(test)] pub fn get_validator_address(&self) -> Option<&Address> { match &self { ShellMode::Validator { data, .. } => Some(&data.address), _ => None, } } - - /// Remove an Ethereum event from the internal queue - pub fn dequeue_eth_event(&mut self, event: &EthereumEvent) { - if let ShellMode::Validator { - eth_oracle: - Some(EthereumOracleChannels { - ethereum_receiver, .. - }), - .. - } = self - { - ethereum_receiver.remove_event(event); - } - } - - /// Get the protocol keypair for this validator. - pub fn get_protocol_key(&self) -> Option<&common::SecretKey> { - match self { - ShellMode::Validator { - data: - ValidatorData { - keys: - ValidatorKeys { - protocol_keypair, .. - }, - .. - }, - .. - } => Some(protocol_keypair), - _ => None, - } - } - - /// Get the Ethereum bridge keypair for this validator. - #[cfg_attr(not(test), allow(dead_code))] - pub fn get_eth_bridge_keypair(&self) -> Option<&common::SecretKey> { - match self { - ShellMode::Validator { - data: - ValidatorData { - keys: - ValidatorKeys { - eth_bridge_keypair, .. - }, - .. - }, - .. - } => Some(eth_bridge_keypair), - _ => None, - } - } - - /// If this node is a validator, broadcast a tx - /// to the mempool using the broadcaster subprocess - pub fn broadcast(&self, data: Vec) { - if let Self::Validator { - broadcast_sender, .. - } = self - { - broadcast_sender - .send(data) - .expect("The broadcaster should be running for a validator"); - } - } } #[derive(Clone, Debug, Default)] @@ -391,9 +239,12 @@ where /// When set, indicates after how many blocks a new snapshot /// will be taken (counting from the first block) pub blocks_between_snapshots: Option, + snapshot_task: Option>>, + snapshots_to_keep: u64, /// Data for a node downloading and apply snapshots as part of /// the fast sync protocol. pub syncing: Option, + pub finalized_merkle_tree: Option, } /// Storage key filter to store the diffs into the storage. Return `false` for @@ -409,28 +260,6 @@ pub fn is_key_diff_storable(key: &namada_sdk::storage::Key) -> bool { || proof_of_stake::storage_key::is_delegation_targets_key(key)) } -/// Channels for communicating with an Ethereum oracle. -#[derive(Debug)] -pub struct EthereumOracleChannels { - ethereum_receiver: EthereumReceiver, - control_sender: oracle::control::Sender, - last_processed_block_receiver: last_processed_block::Receiver, -} - -impl EthereumOracleChannels { - pub fn new( - events_receiver: Receiver, - control_sender: oracle::control::Sender, - last_processed_block_receiver: last_processed_block::Receiver, - ) -> Self { - Self { - ethereum_receiver: EthereumReceiver::new(events_receiver), - control_sender, - last_processed_block_receiver, - } - } -} - impl Shell { /// Restore the database with data fetched from the State Sync protocol. pub fn restore_database_from_state_sync(&mut self) { @@ -485,8 +314,6 @@ where pub fn new( config: config::Ledger, wasm_dir: PathBuf, - broadcast_sender: UnboundedSender>, - eth_oracle: Option, db_cache: Option<&D::Cache>, scheduled_migration: Option, vp_wasm_compilation_cache: u64, @@ -588,8 +415,6 @@ where .take_validator_data() .map(|data| ShellMode::Validator { data, - broadcast_sender, - eth_oracle, validator_local_config, local_config, }) @@ -605,13 +430,11 @@ where ShellMode::Validator { data: ValidatorData { address: wallet::defaults::validator_address(), - keys: ValidatorKeys { + keys: wallet::ValidatorKeys { protocol_keypair, eth_bridge_keypair, }, }, - broadcast_sender, - eth_oracle, validator_local_config: None, local_config: None, } @@ -659,7 +482,10 @@ where } } - let mut shell = Self { + let snapshots_to_keep = + config.shell.snapshots_to_keep.map(|n| n.get()).unwrap_or(1); + + Self { chain_id, state, base_dir, @@ -682,10 +508,11 @@ where event_log: EventLog::default(), scheduled_migration, blocks_between_snapshots: config.shell.blocks_between_snapshots, + snapshot_task: None, + snapshots_to_keep, syncing: None, - }; - shell.update_eth_oracle(&Default::default()); - shell + finalized_merkle_tree: None, + } } /// Return a reference to the [`EventLog`]. @@ -803,51 +630,36 @@ where /// Commit a block. Persist the application state and return the Merkle root /// hash. - pub fn commit(&mut self) -> shim::Response { - self.bump_last_processed_eth_block(); + pub fn commit(&mut self) -> response::Commit { + let merkle_root_pre = self.state.in_mem().block.tree.root(); + let height_to_commit = self.state.in_mem().block.height; - let migration = match self.scheduled_migration.as_ref() { - Some(migration) if height_to_commit == migration.height => Some( - self.scheduled_migration - .take() - .unwrap() - .load_and_validate() - .expect("The scheduled migration is not valid."), - ), - _ => None, - }; + if let Some(migration) = self.scheduled_migration.as_ref() { + if height_to_commit == migration.height { + // Remove migration applied in FinalizeBlock + self.scheduled_migration.take().unwrap(); + } + } self.state .commit_block() .expect("Encountered a storage error while committing a block"); - if let Some(migration) = migration { - migrations::commit(&mut self.state, migration); - self.state - .update_last_block_merkle_tree() - .expect("Must update merkle tree after migration"); - } - let merkle_root = self.state.in_mem().merkle_root(); + assert_eq!(merkle_root_pre, merkle_root); tracing::info!( "Committed block hash: {merkle_root}, height: {height_to_commit}", ); - self.broadcast_queued_txs(); - let take_snapshot = self.check_snapshot_required(); - - shim::Response::Commit( - response::Commit { - // NB: by passing 0, we forbid CometBFT from deleting - // data pertaining to past blocks - retain_height: tendermint::block::Height::from(0_u32), - // NB: current application hash - data: merkle_root.0.to_vec().into(), - }, - take_snapshot, - ) + response::Commit { + // NB: by passing 0, we forbid CometBFT from deleting + // data pertaining to past blocks + retain_height: tendermint::block::Height::from(0_u32), + // NB: current application hash + data: merkle_root.0.to_vec().into(), + } } /// Check if we have reached a block height at which we should take a @@ -869,203 +681,6 @@ where } } - /// Updates the Ethereum oracle's last processed block. - #[inline] - fn bump_last_processed_eth_block(&mut self) { - if let ShellMode::Validator { - eth_oracle: Some(eth_oracle), - .. - } = &self.mode - { - // update the oracle's last processed eth block - let last_processed_block = eth_oracle - .last_processed_block_receiver - .borrow() - .as_ref() - .cloned(); - if let Some(eth_height) = last_processed_block { - tracing::info!( - "Ethereum oracle's most recently processed Ethereum block \ - is {}", - eth_height - ); - self.state.in_mem_mut().ethereum_height = Some(eth_height); - } - } - } - - /// Empties all the ledger's queues of transactions to be broadcasted - /// via CometBFT's P2P network. - #[inline] - fn broadcast_queued_txs(&mut self) { - if let ShellMode::Validator { .. } = &self.mode { - self.broadcast_protocol_txs(); - self.broadcast_expired_txs(); - } - } - - /// Broadcast any pending protocol transactions. - fn broadcast_protocol_txs(&mut self) { - use crate::shell::vote_extensions::iter_protocol_txs; - - let ext = self.craft_extension(); - - let protocol_key = self - .mode - .get_protocol_key() - .expect("Validators should have protocol keys"); - - let protocol_txs = iter_protocol_txs(ext).map(|protocol_tx| { - protocol_tx - .sign(protocol_key, self.chain_id.clone()) - .to_bytes() - }); - - for tx in protocol_txs { - self.mode.broadcast(tx); - } - } - - /// Broadcast any expired transactions. - fn broadcast_expired_txs(&mut self) { - let eth_events = { - let mut events: Vec<_> = self - .state - .in_mem_mut() - .expired_txs_queue - .drain() - .map(|expired_tx| match expired_tx { - ExpiredTx::EthereumEvent(event) => event, - }) - .collect(); - events.sort(); - events - }; - if hints::likely(eth_events.is_empty()) { - // more often than not, there won't by any expired - // Ethereum events to retransmit - return; - } - if let Some(vote_extension) = self.sign_ethereum_events(eth_events) { - let protocol_key = self - .mode - .get_protocol_key() - .expect("Validators should have protocol keys"); - - let signed_tx = EthereumTxData::EthEventsVext( - namada_vote_ext::ethereum_events::SignedVext(vote_extension), - ) - .sign(protocol_key, self.chain_id.clone()) - .to_bytes(); - - self.mode.broadcast(signed_tx); - } - } - - /// If a handle to an Ethereum oracle was provided to the [`Shell`], attempt - /// to send it an updated configuration, using a configuration - /// based on Ethereum bridge parameters in blockchain storage. - /// - /// This method must be safe to call even before ABCI `InitChain` has been - /// called (i.e. when storage is empty), as we may want to do this check - /// every time the shell starts up (including the first time ever at which - /// time storage will be empty). - /// - /// This method is also called during `FinalizeBlock` to update the oracle - /// if relevant storage changes have occurred. This includes deactivating - /// and reactivating the bridge. - fn update_eth_oracle(&mut self, changed_keys: &BTreeSet) { - if let ShellMode::Validator { - eth_oracle: Some(EthereumOracleChannels { control_sender, .. }), - .. - } = &mut self.mode - { - // We *always* expect a value describing the status of the Ethereum - // bridge to be present under [`eth_bridge::storage::active_key`], - // once a chain has been initialized. We need to explicitly check if - // this key is present here because we may be starting up the shell - // for the first time ever, in which case the chain hasn't been - // initialized yet. - let has_key = self - .state - .has_key(ð_bridge::storage::active_key()) - .expect( - "We should always be able to check whether a key exists \ - in storage or not", - ); - if !has_key { - tracing::debug!( - "Not starting oracle yet as storage has not been \ - initialized" - ); - return; - } - let Some(config) = EthereumOracleConfig::read(&self.state) else { - tracing::debug!( - "Not starting oracle as the Ethereum bridge config \ - couldn't be found in storage" - ); - return; - }; - let active = if !self.state.ethbridge_queries().is_bridge_active() { - if !changed_keys.contains(ð_bridge::storage::active_key()) { - tracing::debug!( - "Not starting oracle as the Ethereum bridge is \ - disabled" - ); - return; - } else { - tracing::debug!( - "Disabling oracle as the bridge has been disabled" - ); - false - } - } else { - true - }; - - let start_block = self - .state - .in_mem() - .ethereum_height - .clone() - .unwrap_or(config.eth_start_height); - tracing::info!( - ?start_block, - "Found Ethereum height from which the Ethereum oracle should \ - be updated" - ); - let config = eth_bridge::oracle::config::Config { - min_confirmations: config.min_confirmations.into(), - bridge_contract: config.contracts.bridge.address, - start_block, - active, - }; - tracing::info!( - ?config, - "Updating the Ethereum oracle using values from block storage" - ); - if let Err(error) = control_sender - .try_send(oracle::control::Command::UpdateConfig(config)) - { - match error { - tokio::sync::mpsc::error::TrySendError::Full(_) => { - panic!( - "The Ethereum oracle communication channel is \ - full!" - ) - } - tokio::sync::mpsc::error::TrySendError::Closed(_) => { - panic!( - "The Ethereum oracle can no longer be \ - communicated with" - ) - } - } - } - } - } - /// Validate a transaction request. On success, the transaction will /// included in the mempool and propagated to peers, otherwise it will be /// rejected. @@ -1074,9 +689,6 @@ where tx_bytes: &[u8], r#_type: MempoolTxType, ) -> response::CheckTx { - use namada_sdk::tx::data::protocol::ProtocolTxType; - use namada_vote_ext::ethereum_tx_data_variants; - let mut response = response::CheckTx::default(); const VALID_MSG: &str = "Mempool validation passed"; @@ -1154,115 +766,7 @@ where } }; - // try to parse a vote extension protocol tx from - // the provided tx data - macro_rules! try_vote_extension { - ($kind:expr, $rsp:expr, $result:expr $(,)?) => { - match $result { - Ok(ext) => ext, - Err(err) => { - $rsp.code = ResultCode::InvalidVoteExtension.into(); - $rsp.log = format!( - "{INVALID_MSG}: Invalid {} vote extension: {err}", - $kind, - ); - return $rsp; - } - } - }; - } - match tx_type.tx_type { - TxType::Protocol(protocol_tx) => match protocol_tx.tx { - ProtocolTxType::EthEventsVext => { - let ext = try_vote_extension!( - "Ethereum events", - response, - ethereum_tx_data_variants::EthEventsVext::try_from(&tx), - ); - if let Err(err) = - validate_eth_events_vext::<_, _, governance::Store<_>>( - &self.state, - &ext.0, - self.state.in_mem().get_last_block_height(), - ) - { - response.code = ResultCode::InvalidVoteExtension.into(); - response.log = format!( - "{INVALID_MSG}: Invalid Ethereum events vote \ - extension: {err}", - ); - } else { - response.log = String::from(VALID_MSG); - } - } - ProtocolTxType::BridgePoolVext => { - let ext = try_vote_extension!( - "Bridge pool roots", - response, - ethereum_tx_data_variants::BridgePoolVext::try_from( - &tx - ), - ); - if let Err(err) = - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &self.state, - &ext.0, - self.state.in_mem().get_last_block_height(), - ) - { - response.code = ResultCode::InvalidVoteExtension.into(); - response.log = format!( - "{INVALID_MSG}: Invalid Bridge pool roots vote \ - extension: {err}", - ); - } else { - response.log = String::from(VALID_MSG); - } - } - ProtocolTxType::ValSetUpdateVext => { - let ext = try_vote_extension!( - "validator set update", - response, - ethereum_tx_data_variants::ValSetUpdateVext::try_from( - &tx - ), - ); - if let Err(err) = - validate_valset_upd_vext::<_, _, governance::Store<_>>( - &self.state, - &ext, - // n.b. only accept validator set updates - // issued at the last committed epoch - // (signing off on the validators of the - // next epoch). at the second height - // within an epoch, the new epoch is - // committed to storage, so `last_epoch` - // reflects the current value of the - // epoch. - self.state.in_mem().last_epoch, - ) - { - response.code = ResultCode::InvalidVoteExtension.into(); - response.log = format!( - "{INVALID_MSG}: Invalid validator set update vote \ - extension: {err}", - ); - } else { - response.log = String::from(VALID_MSG); - // validator set update votes should be decided - // as soon as possible - response.priority = i64::MAX; - } - } - _ => { - response.code = ResultCode::InvalidTx.into(); - response.log = format!( - "{INVALID_MSG}: The given protocol tx cannot be added \ - to the mempool" - ); - } - }, TxType::Wrapper(wrapper) => { // Get the gas scale first let gas_scale = match get_gas_scale(&self.state) { @@ -1392,6 +896,11 @@ where the mempool" ); } + #[allow(deprecated)] + TxType::Protocol(_) => { + response.code = ResultCode::DeprecatedProtocolTx.into(); + response.info = "Protocol txs are deprecated".into(); + } } if response.code == ResultCode::Ok.into() { @@ -1456,6 +965,157 @@ where pub fn is_deciding_offset_within_epoch(&self, height_offset: u64) -> bool { self.state.is_deciding_offset_within_epoch(height_offset) } + + // Retrieve the cached result of process proposal for the given block or + // compute it if missing + fn get_process_proposal_result( + &mut self, + request: request::FinalizeBlock, + ) -> ProcessProposalCachedResult { + match namada_sdk::hash::Hash::try_from(request.hash) { + Ok(block_hash) => { + match self + .state + .in_mem_mut() + .block_proposals_cache + .get(&block_hash) + { + // We already have the result of process proposal for + // this block cached in memory + Some(res) => res.to_owned(), + None => { + // Need to run process proposal to extract the data we + // need for finalize block (tx results) + let process_req = + abci::finalize_block_to_process_proposal(request); + + let (process_resp, res) = + self.process_proposal(process_req.into()); + let result = if let response::ProcessProposal::Accept = + process_resp + { + ProcessProposalCachedResult::Accepted( + res.into_iter().map(|res| res.into()).collect(), + ) + } else { + ProcessProposalCachedResult::Rejected + }; + + // Cache the result + self.state + .in_mem_mut() + .block_proposals_cache + .put(block_hash.to_owned(), result.clone()); + + result + } + } + } + Err(_) => { + // Need to run process proposal to extract the data we need for + // finalize block (tx results) + let process_req = + abci::finalize_block_to_process_proposal(request); + + // Do not cache the result in this case since we + // don't have the hash of the block + let (process_resp, res) = + self.process_proposal(process_req.into()); + if let response::ProcessProposal::Accept = process_resp { + ProcessProposalCachedResult::Accepted( + res.into_iter().map(|res| res.into()).collect(), + ) + } else { + ProcessProposalCachedResult::Rejected + } + } + } + } + + fn update_snapshot_task(&mut self, take_snapshot: TakeSnapshot) { + let snapshot_taken = + self.snapshot_task.as_ref().map(|t| t.is_finished()); + match snapshot_taken { + Some(true) => { + let task = self.snapshot_task.take().unwrap(); + match task.join() { + Ok(Err(e)) => tracing::error!( + "Failed to create snapshot with error: {:?}", + e + ), + Err(e) => tracing::error!( + "Failed to join thread creating snapshot: {:?}", + e + ), + _ => {} + } + } + Some(false) => { + // if a snapshot task is still running, + // we don't start a new one. This is not + // expected to happen if snapshots are spaced + // far enough apart. + tracing::warn!( + "Previous snapshot task was still running when a new \ + snapshot was scheduled" + ); + return; + } + _ => {} + } + + let TakeSnapshot::Yes(db_path, height) = take_snapshot else { + return; + }; + // Ensure that the DB is flushed before making a checkpoint + state::DB::flush(self.state.db(), true).unwrap(); + let base_dir = self.base_dir.clone(); + + let (snap_send, snap_recv) = tokio::sync::oneshot::channel(); + + let snapshots_to_keep = self.snapshots_to_keep; + let snapshot_task = std::thread::spawn(move || { + let db = crate::storage::open(db_path, true, None) + .expect("Could not open DB"); + let snapshot = db.checkpoint(base_dir.clone(), height)?; + // signal to main thread that the snapshot has finished + snap_send.send(()).unwrap(); + DbSnapshot::cleanup(height, &base_dir, snapshots_to_keep) + .map_err(|e| DbError::DBError(e.to_string()))?; + snapshot + .package() + .map_err(|e| DbError::DBError(e.to_string())) + }); + + // it's important that the thread is + // blocked until the snapshot is created so that no writes + // happen to the db while snapshotting. We want the db frozen + // at this specific point in time. + if snap_recv.blocking_recv().is_err() { + tracing::error!("Failed to start snapshot task.") + } else { + self.snapshot_task.replace(snapshot_task); + } + } +} + +#[derive(Debug, Clone)] +/// Indicate whether a state snapshot should be created +/// at a certain point in time +pub enum TakeSnapshot { + No, + Yes(PathBuf, BlockHeight), +} + +impl> From> + for TakeSnapshot +{ + fn from(value: Option<(T, BlockHeight)>) -> Self { + match value { + None => TakeSnapshot::No, + Some(p) => TakeSnapshot::Yes(p.0.as_ref().to_path_buf(), p.1), + } + } } /// Checks that neither the wrapper nor the inner transaction batch have already @@ -1570,26 +1230,19 @@ pub mod test_utils { use std::ops::{Deref, DerefMut}; use data_encoding::HEXUPPER; - use namada_sdk::ethereum_events::Uint; use namada_sdk::events::Event; - use namada_sdk::hash::Hash; - use namada_sdk::keccak::KeccakHash; use namada_sdk::key::*; use namada_sdk::proof_of_stake::parameters::PosParams; use namada_sdk::proof_of_stake::storage::validator_consensus_key_handle; + use namada_sdk::state::LastBlock; use namada_sdk::state::mockdb::MockDB; - use namada_sdk::state::{LastBlock, StorageWrite}; - use namada_sdk::storage::{BlockHeader, Epoch}; + use namada_sdk::storage::Epoch; use namada_sdk::tendermint::abci::types::VoteInfo; + use namada_sdk::time::Duration; use tempfile::tempdir; - use tokio::sync::mpsc::{Sender, UnboundedReceiver}; use super::*; - use crate::config::ethereum_bridge::ledger::ORACLE_CHANNEL_BUFFER_SIZE; - use crate::shims::abcipp_shim_types; - use crate::shims::abcipp_shim_types::shim::request::{ - FinalizeBlock, ProcessedTx, - }; + use crate::shell::abci::ProcessedTx; use crate::tendermint::abci::types::Misbehavior; use crate::tendermint_proto::abci::{ RequestPrepareProposal, RequestProcessProposal, @@ -1666,22 +1319,6 @@ pub mod test_utils { } } - /// Get the default bridge pool vext bytes to be signed. - pub fn get_bp_bytes_to_sign() -> KeccakHash { - use namada_sdk::keccak::{Hasher, Keccak}; - - let root = [0; 32]; - let nonce = Uint::from(0).to_bytes(); - - let mut output = [0u8; 32]; - let mut hasher = Keccak::v256(); - hasher.update(&root); - hasher.update(&nonce); - hasher.finalize(&mut output); - - KeccakHash(output) - } - /// A wrapper around the shell that implements /// Drop so as to clean up the files that it /// generates. Also allows illegal state @@ -1712,32 +1349,8 @@ pub mod test_utils { } impl TestShell { - /// Returns a new shell with - /// - A broadcast receiver, which will receive any protocol txs sent - /// by the shell. - /// - A sender that can send Ethereum events into the ledger, mocking - /// the Ethereum fullnode process - /// - A receiver for control commands sent by the shell to the - /// Ethereum oracle - pub fn new_at_height>( - height: H, - ) -> ( - Self, - UnboundedReceiver>, - Sender, - Receiver, - ) { - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - let (eth_sender, eth_receiver) = - tokio::sync::mpsc::channel(ORACLE_CHANNEL_BUFFER_SIZE); - let (_, last_processed_block_receiver) = - last_processed_block::channel(); - let (control_sender, control_receiver) = oracle::control::channel(); - let eth_oracle = EthereumOracleChannels::new( - eth_receiver, - control_sender, - last_processed_block_receiver, - ); + /// Returns a new shell + pub fn new_at_height>(height: H) -> Self { let base_dir = tempdir().unwrap().as_ref().canonicalize().unwrap(); let vp_wasm_compilation_cache = 50 * 1024 * 1024; // 50 kiB let tx_wasm_compilation_cache = 50 * 1024 * 1024; // 50 kiB @@ -1748,27 +1361,20 @@ pub mod test_utils { TendermintMode::Validator, ), top_level_directory().join("wasm"), - sender, - Some(eth_oracle), None, None, vp_wasm_compilation_cache, tx_wasm_compilation_cache, ); shell.state.in_mem_mut().block.height = height.into(); - (Self { shell }, receiver, eth_sender, control_receiver) + Self { shell } } /// Same as [`TestShell::new_at_height`], but returns a shell at block /// height 0. #[inline] #[allow(dead_code)] - pub fn new() -> ( - Self, - UnboundedReceiver>, - Sender, - Receiver, - ) { + pub fn new() -> Self { Self::new_at_height(BlockHeight(1)) } @@ -1834,19 +1440,16 @@ pub mod test_utils { /// the events created for each transaction pub fn finalize_block( &mut self, - req: FinalizeBlock, + req: finalize_block::Request, ) -> ShellResult> { - match self.shell.finalize_block(req) { - Ok(resp) => Ok(resp.events), - Err(err) => Err(err), - } + self.shell.finalize_block(req).map(|resp| resp.events) } /// Forward a PrepareProposal request pub fn prepare_proposal( &self, mut req: RequestPrepareProposal, - ) -> abcipp_shim_types::shim::response::PrepareProposal { + ) -> response::PrepareProposal { req.proposer_address = HEXUPPER .decode( wallet::defaults::validator_keypair() @@ -1864,26 +1467,29 @@ pub mod test_utils { self.state.in_mem_mut().next_epoch_min_start_height = self.state.in_mem().get_last_block_height() + num_blocks; self.state.in_mem_mut().next_epoch_min_start_time = { - #[allow(clippy::disallowed_methods)] - DateTimeUtc::now() + ({ + #[allow(clippy::disallowed_methods)] + DateTimeUtc::now() + }) - Duration::seconds(1) }; } /// Simultaneously call the `FinalizeBlock` and /// `Commit` handlers. - pub fn finalize_and_commit(&mut self, req: Option) { - let mut req = req.unwrap_or_default(); - req.header.time = { - #[allow(clippy::disallowed_methods)] - DateTimeUtc::now() - }; - + pub fn finalize_and_commit( + &mut self, + req: Option, + ) { + let req = req.unwrap_or_default(); self.finalize_block(req).expect("Test failed"); self.commit(); } /// Immediately change to the next epoch. - pub fn start_new_epoch(&mut self, req: Option) -> Epoch { + pub fn start_new_epoch( + &mut self, + req: Option, + ) -> Epoch { self.start_new_epoch_in(1); let next_epoch_min_start_height = @@ -1911,8 +1517,6 @@ pub mod test_utils { /// The number of validators to configure // in `InitChain`. pub num_validators: u64, - /// Whether to enable the Ethereum oracle or not. - pub enable_ethereum_oracle: bool, } impl Default for SetupCfg { @@ -1920,7 +1524,6 @@ pub mod test_utils { Self { last_height: H::default(), num_validators: 1, - enable_ethereum_oracle: true, } } } @@ -1932,22 +1535,9 @@ pub mod test_utils { SetupCfg { last_height, num_validators, - enable_ethereum_oracle, }: SetupCfg, - ) -> ( - TestShell, - UnboundedReceiver>, - Sender, - Receiver, - ) { - let (mut test, receiver, eth_sender, control_receiver) = - TestShell::new_at_height(last_height); - if !enable_ethereum_oracle { - if let ShellMode::Validator { eth_oracle, .. } = &mut test.mode { - // drop the eth oracle event receiver - _ = eth_oracle.take(); - } - } + ) -> TestShell { + let mut test = TestShell::new_at_height(last_height); let req = request::InitChain { time: Timestamp { seconds: 0, @@ -1983,20 +1573,13 @@ pub mod test_utils { }; test.init_chain(req, num_validators); test.state.commit_block().expect("Test failed"); - (test, receiver, eth_sender, control_receiver) + test } /// Same as [`setup_at_height`], but returns a shell at the given block /// height, with a single validator. #[inline] - pub fn setup_at_height>( - last_height: H, - ) -> ( - TestShell, - UnboundedReceiver>, - Sender, - Receiver, - ) { + pub fn setup_at_height>(last_height: H) -> TestShell { let last_height = last_height.into(); setup_with_cfg(SetupCfg { last_height, @@ -2007,56 +1590,10 @@ pub mod test_utils { /// Same as [`setup_with_cfg`], but returns a shell at block height 0, /// with a single validator. #[inline] - pub fn setup() -> ( - TestShell, - UnboundedReceiver>, - Sender, - Receiver, - ) { + pub fn setup() -> TestShell { setup_with_cfg(SetupCfg::::default()) } - /// This is just to be used in testing. It is not - /// a meaningful default. - impl Default for FinalizeBlock { - fn default() -> Self { - FinalizeBlock { - header: BlockHeader { - hash: Hash([0; 32]), - #[allow(clippy::disallowed_methods)] - time: DateTimeUtc::now(), - next_validators_hash: Hash([0; 32]), - }, - block_hash: Hash([0; 32]), - byzantine_validators: vec![], - txs: vec![], - proposer_address: HEXUPPER - .decode( - wallet::defaults::validator_keypair() - .to_public() - .tm_raw_hash() - .as_bytes(), - ) - .unwrap(), - height: 0u8.into(), - decided_last_commit: tendermint::abci::types::CommitInfo { - round: 0u8.into(), - votes: vec![], - }, - } - } - } - - /// Set the Ethereum bridge to be inactive - pub fn deactivate_bridge(shell: &mut TestShell) { - use eth_bridge::storage::active_key; - use eth_bridge::storage::eth_bridge_queries::EthBridgeStatus; - shell - .state - .write(&active_key(), EthBridgeStatus::Disabled) - .expect("Test failed"); - } - pub fn get_pkh_from_address( storage: &S, params: &PosParams, @@ -2081,14 +1618,14 @@ pub mod test_utils { votes: Vec, byzantine_validators: Option>, ) { - // Let the header time be always ahead of the next epoch min start time - let header = BlockHeader { + let mut req = finalize_block::Request { + // Let the header time be always ahead of the next epoch min start + // time time: shell.state.in_mem().next_epoch_min_start_time.next_second(), - ..Default::default() - }; - let mut req = FinalizeBlock { - header, - proposer_address, + proposer_address: tendermint::account::Id::try_from( + proposer_address, + ) + .unwrap(), decided_last_commit: tendermint::abci::types::CommitInfo { round: 0u8.into(), votes, @@ -2096,7 +1633,7 @@ pub mod test_utils { ..Default::default() }; if let Some(byz_vals) = byzantine_validators { - req.byzantine_validators = byz_vals; + req.misbehavior = byz_vals; } shell.finalize_block(req).unwrap(); shell.commit(); @@ -2108,17 +1645,11 @@ mod shell_tests { use std::collections::BTreeMap; use std::fs::File; - use eth_bridge::storage::eth_bridge_queries::is_bridge_comptime_enabled; use namada_apps_lib::state::StorageWrite; use namada_sdk::address; - use namada_sdk::chain::Epoch; use namada_sdk::token::read_denom; use namada_sdk::tx::data::Fee; - use namada_sdk::tx::data::protocol::{ProtocolTx, ProtocolTxType}; - use namada_sdk::tx::{Code, Data, Signed}; - use namada_vote_ext::{ - bridge_pool_roots, ethereum_events, ethereum_tx_data_variants, - }; + use namada_sdk::tx::{Code, Data}; use tempfile::tempdir; use {namada_replay_protection as replay_protection, wallet}; @@ -2129,321 +1660,10 @@ mod shell_tests { const GAS_LIMIT: u64 = 100_000; - /// Check that the shell broadcasts validator set updates, - /// even when the Ethereum oracle is not running (e.g. - /// because the bridge is disabled). - #[tokio::test] - async fn test_broadcast_valset_upd_inspite_oracle_off() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - - // this height should result in a validator set - // update being broadcasted - let (mut shell, mut broadcaster_rx, _, _) = - test_utils::setup_with_cfg(test_utils::SetupCfg { - last_height: 1, - enable_ethereum_oracle: false, - ..Default::default() - }); - - // broadcast validator set update - shell.broadcast_protocol_txs(); - - // check data inside tx - it should be a validator set update - // signed at epoch 0 - let signed_valset_upd = loop { - // attempt to receive validator set update - let serialized_tx = tokio::time::timeout( - std::time::Duration::from_secs(1), - async { broadcaster_rx.recv().await.unwrap() }, - ) - .await - .unwrap(); - let tx = Tx::try_from_bytes(&serialized_tx[..]).unwrap(); - - match ethereum_tx_data_variants::ValSetUpdateVext::try_from(&tx) { - Ok(signed_valset_upd) => break signed_valset_upd, - Err(_) => continue, - } - }; - - assert_eq!(signed_valset_upd.data.signing_epoch, Epoch(0)); - } - - /// Check that broadcasting expired Ethereum events works - /// as expected. - #[test] - fn test_commit_broadcasts_expired_eth_events() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - - let (mut shell, mut broadcaster_rx, _, _) = - test_utils::setup_at_height(5); - - // push expired events to queue - let ethereum_event_0 = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let ethereum_event_1 = EthereumEvent::TransfersToNamada { - nonce: 1u64.into(), - transfers: vec![], - }; - shell - .state - .in_mem_mut() - .expired_txs_queue - .push(ExpiredTx::EthereumEvent(ethereum_event_0.clone())); - shell - .state - .in_mem_mut() - .expired_txs_queue - .push(ExpiredTx::EthereumEvent(ethereum_event_1.clone())); - - // broadcast them - shell.broadcast_expired_txs(); - - // attempt to receive vote extension tx aggregating - // all expired events - let serialized_tx = broadcaster_rx.blocking_recv().unwrap(); - let tx = Tx::try_from_bytes(&serialized_tx[..]).unwrap(); - - // check data inside tx - let vote_extension = - ethereum_tx_data_variants::EthEventsVext::try_from(&tx).unwrap(); - assert_eq!( - vote_extension.data.ethereum_events, - vec![ethereum_event_0, ethereum_event_1] - ); - } - - /// Test that Ethereum events with outdated nonces are - /// not validated by `CheckTx`. - #[test] - fn test_outdated_nonce_mempool_validate() { - use namada_sdk::storage::InnerEthEventsQueue; - - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - - let (mut shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - shell - .state - .in_mem_mut() - .eth_events_queue - // sent transfers to namada nonce to 5 - .transfers_to_namada = InnerEthEventsQueue::new_at(5.into()); - - let (protocol_key, _) = wallet::defaults::validator_keys(); - - // only bad events - { - let ethereum_event = EthereumEvent::TransfersToNamada { - nonce: 3u64.into(), - transfers: vec![], - }; - let ext = { - let ext = ethereum_events::Vext { - validator_addr: wallet::defaults::validator_address(), - block_height: LAST_HEIGHT, - ethereum_events: vec![ethereum_event], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let rsp = shell.mempool_validate(&tx, Default::default()); - assert!( - rsp.code != ResultCode::Ok.into(), - "Validation should have failed" - ); - } - - // at least one good event - { - let e1 = EthereumEvent::TransfersToNamada { - nonce: 3u64.into(), - transfers: vec![], - }; - let e2 = EthereumEvent::TransfersToNamada { - nonce: 5u64.into(), - transfers: vec![], - }; - let ext = { - let ext = ethereum_events::Vext { - validator_addr: wallet::defaults::validator_address(), - block_height: LAST_HEIGHT, - ethereum_events: vec![e1, e2], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let rsp = shell.mempool_validate(&tx, Default::default()); - assert!( - rsp.code == ResultCode::Ok.into(), - "Validation should have passed" - ); - } - } - - /// Test that we do not include protocol txs in the mempool, - /// voting on ethereum events or signing bridge pool roots - /// and nonces if the bridge is inactive. - #[test] - fn test_mempool_filter_protocol_txs_bridge_inactive() { - let (mut shell, _, _, _) = test_utils::setup_at_height(3); - test_utils::deactivate_bridge(&mut shell); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - let protocol_key = shell.mode.get_protocol_key().expect("Test failed"); - let ethereum_event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let eth_vext = EthereumTxData::EthEventsVext( - ethereum_events::Vext { - validator_addr: address.clone(), - block_height: shell.state.in_mem().get_last_block_height(), - ethereum_events: vec![ethereum_event], - } - .sign(protocol_key) - .into(), - ) - .sign(protocol_key, shell.chain_id.clone()) - .to_bytes(); - - let to_sign = test_utils::get_bp_bytes_to_sign(); - let hot_key = shell.mode.get_eth_bridge_keypair().expect("Test failed"); - let sig = Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig; - let bp_vext = EthereumTxData::BridgePoolVext( - bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address, - sig, - } - .sign(protocol_key), - ) - .sign(protocol_key, shell.chain_id.clone()) - .to_bytes(); - let txs_to_validate = [ - (eth_vext, "Incorrectly validated eth events vext"), - (bp_vext, "Incorrectly validated bp roots vext"), - ]; - for (tx_bytes, err_msg) in txs_to_validate { - let rsp = shell.mempool_validate(&tx_bytes, Default::default()); - assert!( - rsp.code == ResultCode::InvalidVoteExtension.into(), - "{err_msg}" - ); - } - } - - /// Test if Ethereum events validation behaves as expected, - /// considering honest validators. - #[test] - fn test_mempool_eth_events_vext_normal_op() { - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - - let (shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - - let (protocol_key, _) = wallet::defaults::validator_keys(); - let validator_addr = wallet::defaults::validator_address(); - - let ethereum_event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let ext = { - let ext = ethereum_events::Vext { - validator_addr, - block_height: LAST_HEIGHT, - ethereum_events: vec![ethereum_event], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let rsp = shell.mempool_validate(&tx, Default::default()); - assert_eq!(rsp.code, 0.into()); - } - - /// Test if Ethereum events validation fails, if the underlying - /// protocol transaction type is different from the vote extension - /// contained in the transaction's data field. - #[test] - fn test_mempool_eth_events_vext_data_mismatch() { - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - - let (shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - - let (protocol_key, _) = wallet::defaults::validator_keys(); - let validator_addr = wallet::defaults::validator_address(); - - let ethereum_event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let ext = { - let ext = ethereum_events::Vext { - validator_addr, - block_height: LAST_HEIGHT, - ethereum_events: vec![ethereum_event], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = { - let mut tx = - Tx::from_type(TxType::Protocol(Box::new(ProtocolTx { - pk: protocol_key.ref_to(), - tx: ProtocolTxType::BridgePoolVext, - }))); - // invalid tx type, it doesn't match the - // tx type declared in the header - tx.set_data(Data::new(ext.serialize_to_vec())); - tx.sign_wrapper(protocol_key); - tx - } - .to_bytes(); - let rsp = shell.mempool_validate(&tx, Default::default()); - assert_eq!(rsp.code, ResultCode::InvalidVoteExtension.into()); - } - /// Mempool validation must reject unsigned wrappers #[test] fn test_missing_signature() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = super::test_utils::gen_keypair(); @@ -2480,7 +1700,7 @@ mod shell_tests { /// Mempool validation must reject wrappers with an invalid signature #[test] fn test_invalid_signature() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = super::test_utils::gen_keypair(); @@ -2525,7 +1745,7 @@ mod shell_tests { /// Mempool validation must reject non-wrapper txs #[test] fn test_wrong_tx_type() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let mut tx = Tx::new(shell.chain_id.clone(), None); tx.add_code("wasm_code".as_bytes().to_owned(), None); @@ -2546,7 +1766,7 @@ mod shell_tests { /// transactions #[test] fn test_replay_attack() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( @@ -2656,7 +1876,7 @@ mod shell_tests { /// Check that a transaction with a wrong chain id gets discarded #[test] fn test_wrong_chain_id() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = super::test_utils::gen_keypair(); @@ -2684,7 +1904,7 @@ mod shell_tests { /// Check that an expired transaction gets rejected #[test] fn test_expired_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = super::test_utils::gen_keypair(); @@ -2704,7 +1924,7 @@ mod shell_tests { /// Check that a tx requiring more gas than the block limit gets rejected #[test] fn test_exceeding_max_block_gas_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let block_gas_limit = parameters::get_max_block_gas(&shell.state).unwrap(); @@ -2734,7 +1954,7 @@ mod shell_tests { // Check that a tx requiring more gas than its limit gets rejected #[test] fn test_exceeding_gas_limit_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = super::test_utils::gen_keypair(); let mut wrapper = @@ -2762,7 +1982,7 @@ mod shell_tests { // rejected #[test] fn test_fee_non_whitelisted_token() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let apfel_denom = read_denom(&shell.state, &address::testing::apfel()) .expect("unable to read denomination from storage") .expect("unable to find denomination of apfels"); @@ -2795,7 +2015,7 @@ mod shell_tests { // is accepted #[test] fn test_fee_whitelisted_non_native_token() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let apfel_denom = read_denom(&shell.state, &address::testing::apfel()) .expect("unable to read denomination from storage") .expect("unable to find denomination of apfels"); @@ -2854,7 +2074,7 @@ mod shell_tests { // is rejected #[test] fn test_fee_wrong_minimum_amount() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( @@ -2880,7 +2100,7 @@ mod shell_tests { // Check that a wrapper transactions whose fees cannot be paid is rejected #[test] fn test_insufficient_balance_for_fee() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( @@ -2908,7 +2128,7 @@ mod shell_tests { // Check that a fee overflow in the wrapper transaction is rejected #[test] fn test_wrapper_fee_overflow() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( @@ -2936,7 +2156,7 @@ mod shell_tests { /// Test max tx bytes parameter in CheckTx #[test] fn test_max_tx_bytes_check_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let max_tx_bytes: u32 = { let key = parameters::storage::get_max_tx_bytes_key(); @@ -2987,7 +2207,7 @@ mod shell_tests { /// Test max tx sections limit in CheckTx #[test] fn test_max_tx_sections_check_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let new_tx = |num_of_sections: usize| { let keypair = super::test_utils::gen_keypair(); @@ -3036,8 +2256,6 @@ mod shell_tests { /// from a snapshot if it is not syncing #[test] fn test_restore_database_from_snapshot() { - let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel(); - let base_dir = tempdir().unwrap().as_ref().canonicalize().unwrap(); let vp_wasm_compilation_cache = 50 * 1024 * 1024; // 50 kiB let tx_wasm_compilation_cache = 50 * 1024 * 1024; // 50 kiB @@ -3049,8 +2267,6 @@ mod shell_tests { let mut shell = Shell::::new( config.clone(), top_level_directory().join("wasm"), - sender, - None, None, None, vp_wasm_compilation_cache, @@ -3071,7 +2287,7 @@ mod shell_tests { .expect("Test failed"); shell.state.commit_block().expect("Test failed"); let new_root = shell.state.in_mem().merkle_root(); - assert_ne!(new_root, original_root); + assert_eq!(new_root, original_root); shell.restore_database_from_state_sync(); assert_eq!(shell.state.in_mem().merkle_root(), new_root,); diff --git a/crates/node/src/shell/prepare_proposal.rs b/crates/node/src/shell/prepare_proposal.rs index 5461021deb6..7632ef92c36 100644 --- a/crates/node/src/shell/prepare_proposal.rs +++ b/crates/node/src/shell/prepare_proposal.rs @@ -15,15 +15,11 @@ use namada_vm::WasmCacheAccess; use namada_vm::wasm::{TxCache, VpCache}; use super::super::*; -use super::block_alloc::states::{ - BuildingNormalTxBatch, BuildingProtocolTxBatch, NextState, TryAlloc, - WithNormalTxs, WithoutNormalTxs, -}; use super::block_alloc::{AllocFailure, BlockAllocator, BlockResources}; use crate::config::ValidatorLocalConfig; use crate::protocol::{self, ShellParams}; use crate::shell::ShellMode; -use crate::shims::abcipp_shim_types::shim::{TxBytes, response}; +use crate::shell::abci::{TxBytes, response}; use crate::tendermint_proto::abci::RequestPrepareProposal; use crate::tendermint_proto::google::protobuf::Timestamp; @@ -43,7 +39,7 @@ where /// affect the ability of a tx to pay its wrapper fees. pub fn prepare_proposal( &self, - mut req: RequestPrepareProposal, + req: RequestPrepareProposal, ) -> response::PrepareProposal { let txs = if let ShellMode::Validator { ref validator_local_config, @@ -51,10 +47,8 @@ where } = self.mode { // start counting allotted space for txs - let alloc = self.get_protocol_txs_allocator(); - // add initial protocol txs - let (alloc, mut txs) = - self.build_protocol_tx_with_normal_txs(alloc, &mut req.txs); + let alloc: BlockAllocator = self.state.read_only().into(); + let mut txs = vec![]; // add wrapper txs let tm_raw_hash_string = @@ -66,7 +60,7 @@ where "Unable to find native validator address of block \ proposer from tendermint raw hash", ); - let (mut normal_txs, alloc) = self.build_normal_txs( + let mut normal_txs = self.build_normal_txs( alloc, &req.txs, req.time, @@ -74,9 +68,6 @@ where validator_local_config.as_ref(), ); txs.append(&mut normal_txs); - let mut remaining_txs = - self.build_protocol_tx_without_normal_txs(alloc, &mut req.txs); - txs.append(&mut remaining_txs); txs } else { vec![] @@ -91,28 +82,16 @@ where response::PrepareProposal { txs } } - /// Get the first state of the block allocator. This is for protocol - /// transactions. - #[inline] - fn get_protocol_txs_allocator( - &self, - ) -> BlockAllocator> { - self.state.read_only().into() - } - /// Builds a batch of wrapper transactions, retrieved from /// CometBFT's mempool. fn build_normal_txs( &self, - mut alloc: BlockAllocator, + mut alloc: BlockAllocator, txs: &[TxBytes], block_time: Option, block_proposer: &Address, proposer_local_config: Option<&ValidatorLocalConfig>, - ) -> ( - Vec, - BlockAllocator>, - ) { + ) -> Vec { let block_time = block_time.and_then(|block_time| { // If error in conversion, default to last block datetime, it's // valid because of mempool check @@ -126,7 +105,7 @@ where let mut vp_wasm_cache = self.vp_wasm_cache.clone(); let mut tx_wasm_cache = self.tx_wasm_cache.clone(); - let txs = txs + txs .iter() .enumerate() .filter_map(|(tx_index, tx_bytes)| { @@ -182,93 +161,7 @@ where ) }) .map(|(tx, _)| tx) - .collect(); - let alloc = alloc.next_state(); - - (txs, alloc) - } - - /// Allocate an initial set of protocol txs and advance to the - /// next allocation state. - fn build_protocol_tx_with_normal_txs( - &self, - alloc: BlockAllocator>, - txs: &mut Vec, - ) -> (BlockAllocator, Vec) { - let (alloc, txs) = self.build_protocol_txs(alloc, txs); - (alloc.next_state(), txs) - } - - /// Allocate protocol txs into any remaining space. After this, no - /// more allocation will take place. - fn build_protocol_tx_without_normal_txs( - &self, - alloc: BlockAllocator>, - txs: &mut Vec, - ) -> Vec { - let (_, txs) = self.build_protocol_txs(alloc, txs); - txs - } - - /// Builds a batch of protocol transactions. - fn build_protocol_txs( - &self, - mut alloc: BlockAllocator>, - txs: &mut Vec, - ) -> (BlockAllocator>, Vec) { - if self.state.in_mem().last_block.is_none() { - // genesis should not contain vote extensions. - // - // this is because we have not decided any block through - // consensus yet (hence height 0), which in turn means we - // have not committed any vote extensions to a block either. - return (alloc, vec![]); - } - - let mut deserialized_iter = self.deserialize_vote_extensions(txs); - - let taken = deserialized_iter.by_ref().take_while(|tx_bytes| - alloc.try_alloc(&tx_bytes[..]) - .map_or_else( - |status| match status { - AllocFailure::Rejected { bin_resource_left} => { - // TODO(namada#3250): maybe we should find a way to include - // validator set updates all the time. for instance, - // we could have recursive bins -> bin space within - // a bin is partitioned into yet more bins. so, we - // could have, say, 2/3 of the bin space available - // for eth events, and 1/3 available for valset - // upds. to be determined, as we implement CheckTx - // changes (issue #367) - tracing::debug!( - ?tx_bytes, - bin_resource_left, - proposal_height = - ?self.get_current_decision_height(), - "Dropping protocol tx from the current proposal", - ); - false - } - AllocFailure::OverflowsBin { bin_resource} => { - // TODO(namada#3250): handle tx whose size is greater - // than bin size - tracing::warn!( - ?tx_bytes, - bin_resource, - proposal_height = - ?self.get_current_decision_height(), - "Dropping large protocol tx from the current proposal", - ); - true - } - }, - |()| true, - ) - ) - .collect(); - // avoid dropping the txs that couldn't be included in the block - deserialized_iter.keep_rest(); - (alloc, taken) + .collect() } } @@ -420,49 +313,19 @@ where // TODO(namada#3249): write tests for validator set update vote extensions in // prepare proposals mod test_prepare_proposal { - use std::collections::{BTreeMap, BTreeSet}; + use std::collections::BTreeMap; use namada_apps_lib::wallet; use namada_replay_protection as replay_protection; - use namada_sdk::ethereum_events::EthereumEvent; use namada_sdk::key::RefTo; - use namada_sdk::proof_of_stake::Epoch; - use namada_sdk::proof_of_stake::storage::{ - consensus_validator_set_handle, - read_consensus_validator_set_addresses_with_stake, read_pos_params, - }; - use namada_sdk::proof_of_stake::types::WeightedValidator; - use namada_sdk::state::collections::lazy_map::{NestedSubKey, SubKey}; - use namada_sdk::storage::{ - BlockHeight, InnerEthEventsQueue, StorageRead, StorageWrite, - }; + use namada_sdk::storage::StorageWrite; use namada_sdk::token::read_denom; use namada_sdk::tx::data::{Fee, TxType}; - use namada_sdk::tx::{Code, Data, Signed}; - use namada_sdk::{address, governance, token}; - use namada_vote_ext::{ethereum_events, ethereum_tx_data_variants}; + use namada_sdk::tx::{Code, Data}; + use namada_sdk::{address, token}; use super::*; - use crate::shell::EthereumTxData; - use crate::shell::test_utils::{ - self, TestShell, gen_keypair, get_pkh_from_address, - }; - use crate::shims::abcipp_shim_types::shim::request::FinalizeBlock; - - /// Check if we are filtering out an invalid vote extension `vext` - fn check_eth_events_filtering( - shell: &TestShell, - vext: Signed, - ) { - let tx = EthereumTxData::EthEventsVext(vext.into()) - .sign( - shell.mode.get_protocol_key().expect("Test failed"), - shell.chain_id.clone(), - ) - .to_bytes(); - let rsp = shell.mempool_validate(&tx, Default::default()); - assert!(rsp.code != 0.into(), "{}", rsp.log); - } + use crate::shell::test_utils::{self, gen_keypair}; const GAS_LIMIT: u64 = 50_000; @@ -471,7 +334,7 @@ mod test_prepare_proposal { /// proposed block. #[test] fn test_prepare_proposal_rejects_non_wrapper_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let mut tx = Tx::from_type(TxType::Raw); tx.push_default_inner_tx(); tx.header.chain_id = shell.chain_id.clone(); @@ -487,7 +350,7 @@ mod test_prepare_proposal { /// we simply exclude it from the proposal #[test] fn test_error_in_processing_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = gen_keypair(); // an unsigned wrapper will cause an error in processing let mut wrapper = @@ -513,261 +376,11 @@ mod test_prepare_proposal { assert!(shell.prepare_proposal(req).txs.is_empty()); } - /// Test if we are filtering out Ethereum events with bad - /// signatures in a prepare proposal. - #[test] - fn test_prepare_proposal_filter_out_bad_vext_signatures() { - const LAST_HEIGHT: BlockHeight = BlockHeight(2); - - let (shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - - let signed_vote_extension = { - let (protocol_key, _) = wallet::defaults::validator_keys(); - let validator_addr = wallet::defaults::validator_address(); - - // generate a valid signature - let mut ext = ethereum_events::Vext { - validator_addr, - block_height: LAST_HEIGHT, - ethereum_events: vec![], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - - // modify this signature such that it becomes invalid - ext.sig = test_utils::invalidate_signature(ext.sig); - ext - }; - - check_eth_events_filtering(&shell, signed_vote_extension); - } - - /// Test if we are filtering out Ethereum events seen at - /// unexpected block heights. - /// - /// In case of ABCI++, we should only accept vote extensions - /// from `last_height`, whereas with ABCI+, vote extensions - /// before `last_height` are accepted. In either case, vote - /// extensions after `last_height` aren't accepted. - #[test] - fn test_prepare_proposal_filter_out_bad_vext_bheights() { - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - - fn check_invalid(shell: &TestShell, height: BlockHeight) { - let (protocol_key, _) = wallet::defaults::validator_keys(); - let validator_addr = wallet::defaults::validator_address(); - - let signed_vote_extension = { - let ext = ethereum_events::Vext { - validator_addr, - block_height: height, - ethereum_events: vec![], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - - check_eth_events_filtering(shell, signed_vote_extension); - } - - let (shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - assert_eq!(shell.state.in_mem().get_last_block_height(), LAST_HEIGHT); - - check_invalid(&shell, LAST_HEIGHT + 2); - check_invalid(&shell, LAST_HEIGHT + 1); - check_invalid(&shell, 0.into()); - } - - /// Test if we are filtering out Ethereum events seen by - /// non-validator nodes. - #[test] - fn test_prepare_proposal_filter_out_bad_vext_validators() { - const LAST_HEIGHT: BlockHeight = BlockHeight(2); - - let (shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - - let (validator_addr, protocol_key) = { - let bertha_key = wallet::defaults::bertha_keypair(); - let bertha_addr = wallet::defaults::bertha_address(); - (bertha_addr, bertha_key) - }; - - let signed_vote_extension = { - let ext = ethereum_events::Vext { - validator_addr, - block_height: LAST_HEIGHT, - ethereum_events: vec![], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - - check_eth_events_filtering(&shell, signed_vote_extension); - } - - /// Test if Ethereum events validation and inclusion in a block - /// behaves as expected, considering <= 2/3 voting power. - #[test] - fn test_prepare_proposal_vext_insufficient_voting_power() { - use namada_sdk::tendermint::abci::types::{Validator, VoteInfo}; - - const FIRST_HEIGHT: BlockHeight = BlockHeight(1); - const LAST_HEIGHT: BlockHeight = BlockHeight(FIRST_HEIGHT.0 + 11); - - let (mut shell, _recv, _, _oracle_control_recv) = - test_utils::setup_with_cfg(test_utils::SetupCfg { - last_height: FIRST_HEIGHT, - num_validators: 2, - ..Default::default() - }); - - let params = - read_pos_params::<_, governance::Store<_>>(&shell.state).unwrap(); - - // artificially change the voting power of the default validator to - // one, change the block height, and commit a dummy block, - // to move to a new epoch - let events_epoch = shell - .state - .get_epoch_at_height(FIRST_HEIGHT) - .unwrap() - .expect("Test failed"); - let validators_handle = - consensus_validator_set_handle().at(&events_epoch); - let consensus_in_mem = validators_handle - .iter(&shell.state) - .expect("Test failed") - .map(|val| { - let ( - NestedSubKey::Data { - key: stake, - nested_sub_key: SubKey::Data(position), - }, - address, - ) = val.expect("Test failed"); - (stake, position, address) - }) - .collect::>(); - - let mut consensus_set: BTreeSet = - read_consensus_validator_set_addresses_with_stake( - &shell.state, - Epoch::default(), - ) - .unwrap() - .into_iter() - .collect(); - let val1 = consensus_set.pop_first().unwrap(); - let val2 = consensus_set.pop_first().unwrap(); - let pkh1 = get_pkh_from_address( - &shell.state, - ¶ms, - val1.address.clone(), - Epoch::default(), - ); - let pkh2 = get_pkh_from_address( - &shell.state, - ¶ms, - val2.address.clone(), - Epoch::default(), - ); - - for (val_stake, val_position, address) in consensus_in_mem.into_iter() { - if address == wallet::defaults::validator_address() { - validators_handle - .at(&val_stake) - .remove(&mut shell.state, &val_position) - .expect("Test failed"); - validators_handle - .at(&1.into()) - .insert(&mut shell.state, val_position, address) - .expect("Test failed"); - } - } - // Insert some stake for the second validator to prevent total stake - // from going to 0 - - let votes = vec![ - VoteInfo { - validator: Validator { - address: pkh1, - power: (u128::try_from(val1.bonded_stake).expect("Test failed") as u64).try_into().unwrap(), - }, - sig_info: crate::tendermint::abci::types::BlockSignatureInfo::LegacySigned, - }, - VoteInfo { - validator: Validator { - address: pkh2, - power: (u128::try_from(val2.bonded_stake).expect("Test failed") as u64).try_into().unwrap(), - }, - sig_info: crate::tendermint::abci::types::BlockSignatureInfo::LegacySigned, - }, - ]; - let req = FinalizeBlock { - proposer_address: pkh1.to_vec(), - decided_last_commit: crate::tendermint::abci::types::CommitInfo { - round: 0u8.into(), - votes, - }, - ..Default::default() - }; - shell.start_new_epoch(Some(req)); - assert_eq!( - shell - .state - .get_epoch_at_height(shell.get_current_decision_height()) - .unwrap(), - Some(Epoch(1)) - ); - - // test prepare proposal - let (protocol_key, _) = wallet::defaults::validator_keys(); - let validator_addr = wallet::defaults::validator_address(); - - let ethereum_event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let signed_eth_ev_vote_extension = { - let ext = ethereum_events::Vext { - validator_addr, - block_height: LAST_HEIGHT, - ethereum_events: vec![ethereum_event], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - - let vote = EthereumTxData::EthEventsVext( - signed_eth_ev_vote_extension.clone().into(), - ) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let mut rsp = shell.prepare_proposal(RequestPrepareProposal { - txs: vec![vote.into()], - ..Default::default() - }); - assert_eq!(rsp.txs.len(), 1); - - let tx_bytes = rsp.txs.remove(0); - let got = Tx::try_from_bytes(&tx_bytes[..]).unwrap(); - let eth_tx_data = (&got).try_into().expect("Test failed"); - let rsp_ext = match eth_tx_data { - EthereumTxData::EthEventsVext(ext) => ext, - _ => panic!("Test failed"), - }; - - assert_eq!(signed_eth_ev_vote_extension, rsp_ext.0); - } - /// Test that if the wrapper tx hash is known (replay attack), the /// transaction is not included in the block #[test] fn test_wrapper_tx_hash() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); let mut wrapper = @@ -805,7 +418,7 @@ mod test_prepare_proposal { /// one gets accepted #[test] fn test_wrapper_tx_hash_same_block() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); let mut wrapper = @@ -834,7 +447,7 @@ mod test_prepare_proposal { /// transaction is not included in the block #[test] fn test_inner_tx_hash() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); let mut wrapper = @@ -874,7 +487,7 @@ mod test_prepare_proposal { /// both get accepted #[test] fn test_inner_tx_hash_same_block() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); let keypair_2 = namada_apps_lib::wallet::defaults::albert_keypair(); @@ -916,7 +529,7 @@ mod test_prepare_proposal { /// Test that expired wrapper transactions are not included in the block #[test] fn test_expired_wrapper_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = gen_keypair(); let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( @@ -955,7 +568,7 @@ mod test_prepare_proposal { /// in the block #[test] fn test_exceeding_max_block_gas_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let block_gas_limit = namada_sdk::parameters::get_max_block_gas(&shell.state).unwrap(); @@ -990,7 +603,7 @@ mod test_prepare_proposal { /// included #[test] fn test_exceeding_available_block_gas_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let block_gas_limit = namada_sdk::parameters::get_max_block_gas(&shell.state).unwrap(); @@ -1032,7 +645,7 @@ mod test_prepare_proposal { // the block #[test] fn test_exceeding_gas_limit_wrapper() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = gen_keypair(); let wrapper = WrapperTx::new( @@ -1065,7 +678,7 @@ mod test_prepare_proposal { // payment is not included in the block #[test] fn test_fee_non_accepted_token() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); // Update local validator configuration for gas tokens if let ShellMode::Validator { validator_local_config, @@ -1118,7 +731,7 @@ mod test_prepare_proposal { // included in the block #[test] fn test_fee_non_whitelisted_token() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let apfel_denom = read_denom(&shell.state, &address::testing::apfel()) .expect("unable to read denomination from storage") @@ -1158,7 +771,7 @@ mod test_prepare_proposal { // is included in the block #[test] fn test_fee_whitelisted_non_native_token() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let apfel_denom = read_denom(&shell.state, &address::testing::apfel()) .expect("unable to read denomination from storage") @@ -1223,7 +836,7 @@ mod test_prepare_proposal { // by the validator is not included in the block #[test] fn test_fee_wrong_minimum_accepted_amount() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); // Update local validator configuration for gas tokens if let ShellMode::Validator { validator_local_config, @@ -1268,7 +881,7 @@ mod test_prepare_proposal { // is not included in the block #[test] fn test_fee_wrong_minimum_amount() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let wrapper = WrapperTx::new( Fee { @@ -1299,7 +912,7 @@ mod test_prepare_proposal { // Check that a wrapper transactions whose fees cannot be paid is rejected #[test] fn test_insufficient_balance_for_fee() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let wrapper = WrapperTx::new( Fee { @@ -1332,7 +945,7 @@ mod test_prepare_proposal { // Check that a fee overflow in the wrapper transaction is rejected #[test] fn test_wrapper_fee_overflow() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let wrapper = WrapperTx::new( Fee { @@ -1362,123 +975,12 @@ mod test_prepare_proposal { assert!(result.txs.is_empty()); } - /// Test that Ethereum events with outdated nonces are - /// not proposed during `PrepareProposal`. - #[test] - fn test_outdated_nonce_proposal() { - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - - let (mut shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - shell - .state - .in_mem_mut() - .eth_events_queue - // sent transfers to namada nonce to 5 - .transfers_to_namada = InnerEthEventsQueue::new_at(5.into()); - - let (protocol_key, _) = wallet::defaults::validator_keys(); - let validator_addr = wallet::defaults::validator_address(); - - // test an extension containing solely events with - // bad nonces - { - let ethereum_event = EthereumEvent::TransfersToNamada { - // outdated nonce (3 < 5) - nonce: 3u64.into(), - transfers: vec![], - }; - let ext = { - let ext = ethereum_events::Vext { - validator_addr: validator_addr.clone(), - block_height: LAST_HEIGHT, - ethereum_events: vec![ethereum_event], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let req = RequestPrepareProposal { - txs: vec![tx.into()], - ..Default::default() - }; - let proposed_txs = - shell.prepare_proposal(req).txs.into_iter().map(|tx_bytes| { - Tx::try_from_bytes(tx_bytes.as_ref()).expect("Test failed") - }); - // since no events with valid nonces are contained in the vote - // extension, we drop it from the proposal - for tx in proposed_txs { - if ethereum_tx_data_variants::EthEventsVext::try_from(&tx) - .is_ok() - { - panic!( - "No ethereum events should have been found in the \ - proposal" - ); - } - } - } - - // test an extension containing at least one event - // with a good nonce - { - let event1 = EthereumEvent::TransfersToNamada { - // outdated nonce (3 < 5) - nonce: 3u64.into(), - transfers: vec![], - }; - let event2 = EthereumEvent::TransfersToNamada { - // outdated nonce (10 >= 5) - nonce: 10u64.into(), - transfers: vec![], - }; - let ext = { - let ext = ethereum_events::Vext { - validator_addr, - block_height: LAST_HEIGHT, - ethereum_events: vec![event1, event2.clone()], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let req = RequestPrepareProposal { - txs: vec![tx.into()], - ..Default::default() - }; - let proposed_txs = - shell.prepare_proposal(req).txs.into_iter().map(|tx_bytes| { - Tx::try_from_bytes(tx_bytes.as_ref()).expect("Test failed") - }); - // find the event with the good nonce - let mut ext = 'ext: { - for tx in proposed_txs { - if let Ok(ext) = - ethereum_tx_data_variants::EthEventsVext::try_from(&tx) - { - break 'ext ext; - } - } - panic!("No ethereum events found in proposal"); - }; - assert_eq!(ext.data.ethereum_events.len(), 2); - let found_event = ext.0.data.ethereum_events.remove(1); - assert_eq!(found_event, event2); - } - } - /// Test that if a validator's local config minimum /// gas price is lower than the consensus value, the /// validator defaults to the latter. #[test] fn test_default_validator_min_gas_price() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let temp_state = shell.state.with_temp_write_log(); let validator_min_gas_price = Amount::zero(); diff --git a/crates/node/src/shell/process_proposal.rs b/crates/node/src/shell/process_proposal.rs index 915b8daf9e0..f23c26c4f55 100644 --- a/crates/node/src/shell/process_proposal.rs +++ b/crates/node/src/shell/process_proposal.rs @@ -4,14 +4,12 @@ use data_encoding::HEXUPPER; use namada_sdk::parameters; use namada_sdk::proof_of_stake::storage::find_validator_by_raw_hash; -use namada_sdk::tx::data::protocol::ProtocolTxType; -use namada_vote_ext::ethereum_tx_data_variants; use super::block_alloc::{BlockGas, BlockSpace}; use super::*; +use crate::shell::abci::TxBytes; +use crate::shell::abci::response::ProcessProposal; use crate::shell::block_alloc::{AllocFailure, TxBin}; -use crate::shims::abcipp_shim_types::shim::TxBytes; -use crate::shims::abcipp_shim_types::shim::response::ProcessProposal; use crate::tendermint_proto::abci::RequestProcessProposal; /// Validation metadata, to keep track of used resources or @@ -45,14 +43,6 @@ where D: DB + for<'iter> DBIter<'iter> + Sync + 'static, H: StorageHasher + Sync + 'static, { - /// INVARIANT: This method must be stateless. - pub fn verify_header( - &self, - _req: shim::request::VerifyHeader, - ) -> shim::response::VerifyHeader { - Default::default() - } - /// Check all the txs in a block. Some txs may be incorrect, /// but we only reject the entire block if the order of the /// included txs violates the order decided upon in the previous @@ -275,141 +265,6 @@ where supported" .into(), }, - TxType::Protocol(protocol_tx) => { - // Tx chain id - if tx_chain_id != self.chain_id { - return TxResult { - code: ResultCode::InvalidChainId.into(), - info: format!( - "Tx carries a wrong chain id: expected {}, found \ - {}", - self.chain_id, tx_chain_id - ), - }; - } - - // Tx expiration - if let Some(exp) = tx_expiration { - if block_time > exp { - return TxResult { - code: ResultCode::ExpiredTx.into(), - info: format!( - "Tx expired at {:#?}, block time: {:#?}", - exp, block_time - ), - }; - } - } - - match protocol_tx.tx { - ProtocolTxType::EthEventsVext => { - ethereum_tx_data_variants::EthEventsVext::try_from(&tx) - .map_err(|err| err.to_string()) - .and_then(|ext| { - validate_eth_events_vext::< - _, - _, - governance::Store<_>, - >( - &self.state, - &ext.0, - self.state.in_mem().get_last_block_height(), - ) - .map(|_| TxResult { - code: ResultCode::Ok.into(), - info: "Process Proposal accepted this \ - transaction" - .into(), - }) - .map_err(|err| err.to_string()) - }) - .unwrap_or_else(|err| TxResult { - code: ResultCode::InvalidVoteExtension.into(), - info: format!( - "Process proposal rejected this proposal \ - because one of the included Ethereum \ - events vote extensions was invalid: {err}" - ), - }) - } - ProtocolTxType::BridgePoolVext => { - ethereum_tx_data_variants::BridgePoolVext::try_from(&tx) - .map_err(|err| err.to_string()) - .and_then(|ext| { - validate_bp_roots_vext::< - _, - _, - governance::Store<_>, - >( - &self.state, - &ext.0, - self.state.in_mem().get_last_block_height(), - ) - .map(|_| TxResult { - code: ResultCode::Ok.into(), - info: "Process Proposal accepted this \ - transaction" - .into(), - }) - .map_err(|err| err.to_string()) - }) - .unwrap_or_else(|err| TxResult { - code: ResultCode::InvalidVoteExtension.into(), - info: format!( - "Process proposal rejected this proposal \ - because one of the included Bridge pool \ - root's vote extensions was invalid: {err}" - ), - }) - } - ProtocolTxType::ValSetUpdateVext => { - ethereum_tx_data_variants::ValSetUpdateVext::try_from( - &tx, - ) - .map_err(|err| err.to_string()) - .and_then(|ext| { - validate_valset_upd_vext::<_, _, governance::Store<_>>( - &self.state, - &ext, - // n.b. only accept validator set updates - // issued at - // the current epoch (signing off on the - // validators - // of the next epoch) - self.state.in_mem().get_current_epoch().0, - ) - .map(|_| TxResult { - code: ResultCode::Ok.into(), - info: "Process Proposal accepted this \ - transaction" - .into(), - }) - .map_err(|err| err.to_string()) - }) - .unwrap_or_else(|err| { - TxResult { - code: ResultCode::InvalidVoteExtension.into(), - info: format!( - "Process proposal rejected this proposal \ - because one of the included validator \ - set update vote extensions was invalid: \ - {err}" - ), - } - }) - } - ProtocolTxType::EthereumEvents - | ProtocolTxType::BridgePool - | ProtocolTxType::ValidatorSetUpdate => TxResult { - code: ResultCode::InvalidVoteExtension.into(), - info: "Process proposal rejected this proposal \ - because one of the included vote extensions \ - was invalid: ABCI++ code paths are unreachable \ - in Namada" - .to_string(), - }, - } - } TxType::Wrapper(wrapper) => { // Validate wrapper first // Account for the tx's resources @@ -553,15 +408,13 @@ where info: "Process proposal accepted this transaction".into(), } } + #[allow(deprecated)] + TxType::Protocol(_) => TxResult { + code: ResultCode::DeprecatedProtocolTx.into(), + info: "Protocol txs are deprecated".into(), + }, } } - - pub fn revert_proposal( - &mut self, - _req: shim::request::RevertProposal, - ) -> shim::response::RevertProposal { - Default::default() - } } fn process_proposal_fee_check( @@ -601,311 +454,25 @@ mod test_process_proposal { use namada_apps_lib::wallet; use namada_replay_protection as replay_protection; use namada_sdk::address; - use namada_sdk::eth_bridge::storage::eth_bridge_queries::{ - EthBridgeQueries, is_bridge_comptime_enabled, - }; use namada_sdk::key::*; use namada_sdk::state::StorageWrite; use namada_sdk::testing::{arb_tampered_wrapper_tx, arb_valid_signed_tx}; use namada_sdk::token::{Amount, DenominatedAmount, read_denom}; use namada_sdk::tx::data::Fee; - use namada_sdk::tx::{Code, Data, Signed}; - use namada_vote_ext::{ - bridge_pool_roots, ethereum_events, validator_set_update, - }; + use namada_sdk::tx::{Code, Data}; use proptest::prop_assert; use proptest::test_runner::{Config, TestCaseError, TestRunner}; use super::*; - use crate::shell::test_utils::{ - ProcessProposal, TestError, TestShell, deactivate_bridge, gen_keypair, - get_bp_bytes_to_sign, - }; - use crate::shims::abcipp_shim_types::shim::request::ProcessedTx; + use crate::shell::test_utils::{ProcessProposal, TestError, gen_keypair}; const GAS_LIMIT: u64 = 100_000; - /// Check that we reject a validator set update protocol tx - /// if the bridge is not active. - #[test] - fn check_rejected_valset_upd_bridge_inactive() { - if is_bridge_comptime_enabled() { - // NOTE: validator set updates are always signed - // when the bridge is enabled at compile time - return; - } - - let (shell, _, _, _) = test_utils::setup_at_height(3); - let ext = { - let eth_hot_key = - shell.mode.get_eth_bridge_keypair().expect("Test failed"); - let signing_epoch = shell.state.in_mem().get_current_epoch().0; - let next_epoch = signing_epoch.next(); - let voting_powers = shell - .state - .ethbridge_queries() - .get_consensus_eth_addresses::>(next_epoch) - .map(|(eth_addr_book, _, voting_power)| { - (eth_addr_book, voting_power) - }) - .collect(); - let validator_addr = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - let ext = validator_set_update::Vext { - voting_powers, - validator_addr, - signing_epoch, - }; - ext.sign(eth_hot_key) - }; - let request = { - let protocol_key = - shell.mode.get_protocol_key().expect("Test failed"); - let tx = EthereumTxData::ValSetUpdateVext(ext) - .sign(protocol_key, shell.chain_id.clone()) - .to_bytes(); - ProcessProposal { txs: vec![tx] } - }; - - let response = if let Err(TestError::RejectProposal(resp)) = - shell.process_proposal(request) - { - if let [resp] = resp.as_slice() { - resp.clone() - } else { - panic!("Test failed") - } - } else { - panic!("Test failed") - }; - assert_eq!( - response.result.code, - u32::from(ResultCode::InvalidVoteExtension) - ); - } - - /// Check that we reject an eth events protocol tx - /// if the bridge is not active. - #[test] - fn check_rejected_eth_events_bridge_inactive() { - let (mut shell, _, _, _) = test_utils::setup_at_height(3); - let protocol_key = shell.mode.get_protocol_key().expect("Test failed"); - let addr = shell.mode.get_validator_address().expect("Test failed"); - let event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let ext = ethereum_events::Vext { - validator_addr: addr.clone(), - block_height: shell.state.in_mem().get_last_block_height(), - ethereum_events: vec![event], - } - .sign(protocol_key); - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(protocol_key, shell.chain_id.clone()) - .to_bytes(); - let request = ProcessProposal { txs: vec![tx] }; - - if is_bridge_comptime_enabled() { - let [resp]: [ProcessedTx; 1] = shell - .process_proposal(request.clone()) - .expect("Test failed") - .try_into() - .expect("Test failed"); - assert_eq!(resp.result.code, u32::from(ResultCode::Ok)); - deactivate_bridge(&mut shell); - } - let response = if let Err(TestError::RejectProposal(resp)) = - shell.process_proposal(request) - { - if let [resp] = resp.as_slice() { - resp.clone() - } else { - panic!("Test failed") - } - } else { - panic!("Test failed") - }; - assert_eq!( - response.result.code, - u32::from(ResultCode::InvalidVoteExtension) - ); - } - - /// Check that we reject an bp roots protocol tx - /// if the bridge is not active. - #[test] - fn check_rejected_bp_roots_bridge_inactive() { - let (mut shell, _a, _b, _c) = test_utils::setup_at_height(1); - shell.state.in_mem_mut().block.height = - shell.state.in_mem().get_last_block_height(); - shell.commit(); - let protocol_key = shell.mode.get_protocol_key().expect("Test failed"); - let addr = shell.mode.get_validator_address().expect("Test failed"); - let to_sign = get_bp_bytes_to_sign(); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let vote_ext = bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: addr.clone(), - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - let tx = EthereumTxData::BridgePoolVext(vote_ext) - .sign(protocol_key, shell.chain_id.clone()) - .to_bytes(); - let request = ProcessProposal { txs: vec![tx] }; - - if is_bridge_comptime_enabled() { - let [resp]: [ProcessedTx; 1] = shell - .process_proposal(request.clone()) - .expect("Test failed") - .try_into() - .expect("Test failed"); - - assert_eq!(resp.result.code, u32::from(ResultCode::Ok)); - deactivate_bridge(&mut shell); - } - let response = if let Err(TestError::RejectProposal(resp)) = - shell.process_proposal(request) - { - if let [resp] = resp.as_slice() { - resp.clone() - } else { - panic!("Test failed") - } - } else { - panic!("Test failed") - }; - assert_eq!( - response.result.code, - u32::from(ResultCode::InvalidVoteExtension) - ); - } - - fn check_rejected_eth_events( - shell: &mut TestShell, - vote_extension: ethereum_events::SignedVext, - protocol_key: common::SecretKey, - ) { - let tx = EthereumTxData::EthEventsVext(vote_extension) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let request = ProcessProposal { txs: vec![tx] }; - let response = if let Err(TestError::RejectProposal(resp)) = - shell.process_proposal(request) - { - if let [resp] = resp.as_slice() { - resp.clone() - } else { - panic!("Test failed") - } - } else { - panic!("Test failed") - }; - assert_eq!( - response.result.code, - u32::from(ResultCode::InvalidVoteExtension) - ); - } - - /// Test that if a proposal contains Ethereum events with - /// invalid validator signatures, we reject it. - #[test] - fn test_drop_vext_with_invalid_sigs() { - const LAST_HEIGHT: BlockHeight = BlockHeight(2); - let (mut shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - let (protocol_key, _) = wallet::defaults::validator_keys(); - let addr = wallet::defaults::validator_address(); - let event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let ext = { - // generate a valid signature - #[allow(clippy::redundant_clone)] - let mut ext = ethereum_events::Vext { - validator_addr: addr.clone(), - block_height: LAST_HEIGHT, - ethereum_events: vec![event.clone()], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - - // modify this signature such that it becomes invalid - ext.sig = test_utils::invalidate_signature(ext.sig); - ext - }; - check_rejected_eth_events(&mut shell, ext.into(), protocol_key); - } - - /// Test that if a proposal contains Ethereum events with - /// invalid block heights, we reject it. - #[test] - fn test_drop_vext_with_invalid_bheights() { - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - const INVALID_HEIGHT: BlockHeight = BlockHeight(LAST_HEIGHT.0 + 1); - let (mut shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - let (protocol_key, _) = wallet::defaults::validator_keys(); - let addr = wallet::defaults::validator_address(); - let event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let ext = { - #[allow(clippy::redundant_clone)] - let ext = ethereum_events::Vext { - validator_addr: addr.clone(), - block_height: INVALID_HEIGHT, - ethereum_events: vec![event.clone()], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - check_rejected_eth_events(&mut shell, ext.into(), protocol_key); - } - - /// Test that if a proposal contains Ethereum events with - /// invalid validators, we reject it. - #[test] - fn test_drop_vext_with_invalid_validators() { - const LAST_HEIGHT: BlockHeight = BlockHeight(2); - let (mut shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - let (addr, protocol_key) = { - let bertha_key = wallet::defaults::bertha_keypair(); - let bertha_addr = wallet::defaults::bertha_address(); - (bertha_addr, bertha_key) - }; - let event = EthereumEvent::TransfersToNamada { - nonce: 0u64.into(), - transfers: vec![], - }; - let ext = { - #[allow(clippy::redundant_clone)] - let ext = ethereum_events::Vext { - validator_addr: addr.clone(), - block_height: LAST_HEIGHT, - ethereum_events: vec![event.clone()], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - check_rejected_eth_events(&mut shell, ext.into(), protocol_key); - } - /// Test that if a wrapper tx is not signed, the block is rejected /// by [`process_proposal`]. #[test] fn test_unsigned_wrapper_rejected() { - let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let shell = test_utils::setup_at_height(3u64); let keypair = gen_keypair(); let public_key = keypair.ref_to(); let mut outer_tx = @@ -956,7 +523,7 @@ mod test_process_proposal { /// rejected #[test] fn test_wrapper_bad_signature() { - let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let shell = test_utils::setup_at_height(3u64); let mut runner = TestRunner::new(Config::default()); // Test that the strategy produces valid txs first @@ -1004,7 +571,7 @@ mod test_process_proposal { /// payment #[test] fn test_wrapper_unknown_address() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let mut shell = test_utils::setup_at_height(3u64); let keypair = gen_keypair(); let address = Address::from(&keypair.ref_to()); let balance_key = token::storage_key::balance_key( @@ -1048,7 +615,7 @@ mod test_process_proposal { /// balance to pay the fee, [`process_proposal`] rejects the entire block #[test] fn test_wrapper_insufficient_balance_address() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let mut shell = test_utils::setup_at_height(3u64); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); // reduce address balance to match the 100 token min fee let balance_key = token::storage_key::balance_key( @@ -1106,7 +673,7 @@ mod test_process_proposal { /// Process Proposal should reject a block containing a RawTx, but not panic #[test] fn test_raw_tx_rejected() { - let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let shell = test_utils::setup_at_height(3u64); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); @@ -1144,7 +711,7 @@ mod test_process_proposal { /// block is rejected #[test] fn test_wrapper_tx_hash() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); @@ -1200,7 +767,7 @@ mod test_process_proposal { /// Test that a block containing two identical wrapper txs is rejected #[test] fn test_wrapper_tx_hash_same_block() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); @@ -1256,7 +823,7 @@ mod test_process_proposal { /// block is rejected #[test] fn test_inner_tx_hash() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); @@ -1312,7 +879,7 @@ mod test_process_proposal { /// accepted #[test] fn test_inner_tx_hash_same_block() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); let keypair_2 = namada_apps_lib::wallet::defaults::albert_keypair(); @@ -1352,11 +919,11 @@ mod test_process_proposal { } } - /// Test that a wrapper or protocol transaction with a mismatching chain id - /// causes the entire block to be rejected + /// Test that a wrapper transaction with a mismatching chain id causes the + /// entire block to be rejected #[test] fn test_wrong_chain_id() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); let mut wrapper = @@ -1376,19 +943,9 @@ mod test_process_proposal { wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); wrapper.sign_wrapper(keypair); - let protocol_key = shell.mode.get_protocol_key().expect("Test failed"); - let protocol_tx = EthereumTxData::EthEventsVext({ - let bertha_key = wallet::defaults::bertha_keypair(); - let bertha_addr = wallet::defaults::bertha_address(); - ethereum_events::Vext::empty(1234_u64.into(), bertha_addr) - .sign(&bertha_key) - .into() - }) - .sign(protocol_key, wrong_chain_id.clone()); - // Run validation let request = ProcessProposal { - txs: vec![wrapper.to_bytes(), protocol_tx.to_bytes()], + txs: vec![wrapper.to_bytes()], }; match shell.process_proposal(request) { Ok(_) => panic!("Test failed"), @@ -1414,7 +971,7 @@ mod test_process_proposal { /// Test that an expired wrapper transaction causes a block rejection #[test] fn test_expired_wrapper() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); let mut wrapper = @@ -1451,7 +1008,7 @@ mod test_process_proposal { /// rejection #[test] fn test_exceeding_max_block_gas_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let block_gas_limit = parameters::get_max_block_gas(&shell.state).unwrap(); @@ -1490,7 +1047,7 @@ mod test_process_proposal { /// causes a block rejection #[test] fn test_exceeding_available_block_gas_tx() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let block_gas_limit = parameters::get_max_block_gas(&shell.state).unwrap(); @@ -1536,7 +1093,7 @@ mod test_process_proposal { // rejection #[test] fn test_exceeding_gas_limit_wrapper() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let keypair = super::test_utils::gen_keypair(); let mut wrapper = @@ -1572,7 +1129,7 @@ mod test_process_proposal { // a block rejection #[test] fn test_fee_non_whitelisted_token() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let apfel_denom = read_denom(&shell.state, &address::testing::apfel()) .expect("unable to read denomination from storage") @@ -1615,7 +1172,7 @@ mod test_process_proposal { // is accepted #[test] fn test_fee_whitelisted_non_native_token() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let mut shell = test_utils::setup(); let apfel_denom = read_denom(&shell.state, &address::testing::apfel()) .expect("unable to read denomination from storage") @@ -1675,7 +1232,7 @@ mod test_process_proposal { // causes a block rejection #[test] fn test_fee_wrong_minimum_amount() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( @@ -1711,7 +1268,7 @@ mod test_process_proposal { // block rejection #[test] fn test_insufficient_balance_for_fee() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( @@ -1749,7 +1306,7 @@ mod test_process_proposal { // rejection #[test] fn test_wrapper_fee_overflow() { - let (shell, _recv, _, _) = test_utils::setup(); + let shell = test_utils::setup(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( @@ -1787,7 +1344,7 @@ mod test_process_proposal { #[test] fn test_max_tx_bytes_process_proposal() { use parameters::storage::get_max_tx_bytes_key; - let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let shell = test_utils::setup_at_height(3u64); let max_tx_bytes: u32 = { let key = get_max_tx_bytes_key(); @@ -1845,89 +1402,11 @@ mod test_process_proposal { } } - /// Test that Ethereum events with outdated nonces are - /// not validated by `ProcessProposal`. - #[test] - fn test_outdated_nonce_process_proposal() { - use namada_sdk::storage::InnerEthEventsQueue; - - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - - let (mut shell, _recv, _, _) = test_utils::setup_at_height(LAST_HEIGHT); - shell - .state - .in_mem_mut() - .eth_events_queue - // sent transfers to namada nonce to 5 - .transfers_to_namada = InnerEthEventsQueue::new_at(5.into()); - - let (protocol_key, _) = wallet::defaults::validator_keys(); - - // only bad events - { - let ethereum_event = EthereumEvent::TransfersToNamada { - // outdated nonce (3 < 5) - nonce: 3u64.into(), - transfers: vec![], - }; - let ext = { - let ext = ethereum_events::Vext { - validator_addr: wallet::defaults::validator_address(), - block_height: LAST_HEIGHT, - ethereum_events: vec![ethereum_event], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let req = ProcessProposal { txs: vec![tx] }; - let rsp = shell.process_proposal(req); - assert!(rsp.is_err()); - } - - // at least one good event - { - let e1 = EthereumEvent::TransfersToNamada { - nonce: 3u64.into(), - transfers: vec![], - }; - let e2 = EthereumEvent::TransfersToNamada { - nonce: 5u64.into(), - transfers: vec![], - }; - let ext = { - let ext = ethereum_events::Vext { - validator_addr: wallet::defaults::validator_address(), - block_height: LAST_HEIGHT, - ethereum_events: vec![e1, e2], - } - .sign(&protocol_key); - assert!(ext.verify(&protocol_key.ref_to()).is_ok()); - ext - }; - let tx = EthereumTxData::EthEventsVext(ext.into()) - .sign(&protocol_key, shell.chain_id.clone()) - .to_bytes(); - let req = ProcessProposal { txs: vec![tx] }; - let rsp = shell.process_proposal(req); - assert!(rsp.is_ok()); - } - } - /// Process Proposal should reject a block containing a tx with a number of /// sections exceeding the limit #[test] fn test_max_sections_exceeded_tx_rejected() { - let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let shell = test_utils::setup_at_height(3u64); let keypair = namada_apps_lib::wallet::defaults::daewon_keypair(); diff --git a/crates/node/src/shell/queries.rs b/crates/node/src/shell/queries.rs index d0c6a3740a9..d9c6f1935e2 100644 --- a/crates/node/src/shell/queries.rs +++ b/crates/node/src/shell/queries.rs @@ -73,158 +73,3 @@ where .expect("Token balance read in the protocol must not fail") } } - -// NOTE: we are testing `namada_sdk::queries_ext`, -// which is not possible from `namada` since we do not have -// access to the `Shell` there -#[allow(clippy::cast_possible_truncation)] -#[cfg(test)] -mod test_queries { - use namada_sdk::chain::Epoch; - use namada_sdk::eth_bridge::SendValsetUpd; - use namada_sdk::eth_bridge::storage::eth_bridge_queries::is_bridge_comptime_enabled; - use namada_sdk::proof_of_stake::storage::read_consensus_validator_set_addresses_with_stake; - use namada_sdk::proof_of_stake::types::WeightedValidator; - use namada_sdk::tendermint::abci::types::VoteInfo; - - use super::*; - use crate::shell::test_utils::get_pkh_from_address; - use crate::shims::abcipp_shim_types::shim::request::FinalizeBlock; - - macro_rules! test_must_send_valset_upd { - (epoch_assertions: $epoch_assertions:expr $(,)?) => { - /// Test if [`EthBridgeQueries::must_send_valset_upd`] behaves as - /// expected. - #[test] - fn test_must_send_valset_upd() { - const EPOCH_NUM_BLOCKS: u64 = - 10 - EPOCH_SWITCH_BLOCKS_DELAY as u64; - - let (mut shell, _recv, _, _oracle_control_recv) = - test_utils::setup_at_height(0u64); - - let epoch_assertions = $epoch_assertions; - - let mut prev_epoch = None; - - // test `SendValsetUpd::Now` and `SendValsetUpd::AtPrevHeight` - for (curr_epoch, curr_block_height, can_send) in - epoch_assertions - { - shell.state - .in_mem_mut() - .begin_block(curr_block_height.into()) - .unwrap(); - - if prev_epoch != Some(curr_epoch) { - prev_epoch = Some(curr_epoch); - shell.start_new_epoch_in(EPOCH_NUM_BLOCKS); - } - if let Some(b) = - shell.state.in_mem_mut().last_block.as_mut() - { - b.height = BlockHeight(curr_block_height - 1); - } - assert_eq!( - curr_block_height, - shell - .get_current_decision_height() - .0 - ); - assert_eq!( - shell - .state - .get_epoch_at_height(curr_block_height.into()) - .unwrap(), - Some(Epoch(curr_epoch)) - ); - assert_eq!( - shell - .state - .ethbridge_queries() - .must_send_valset_upd(SendValsetUpd::Now), - can_send, - ); - let params = read_pos_params::<_, governance::Store<_>>(&shell.state).unwrap(); - let consensus_set: Vec = - read_consensus_validator_set_addresses_with_stake( - &shell.state, - Epoch::default(), - ) - .unwrap() - .into_iter() - .collect(); - - let val1 = consensus_set[0].clone(); - let pkh1 = get_pkh_from_address( - &shell.state, - ¶ms, - val1.address.clone(), - Epoch::default(), - ); - let votes = vec![VoteInfo { - validator: namada_sdk::tendermint::abci::types::Validator { - address: pkh1.clone().into(), - power: (u128::try_from(val1.bonded_stake).expect("Test failed") as u64).try_into().unwrap(), - }, - sig_info: namada_sdk::tendermint::abci::types::BlockSignatureInfo::LegacySigned, - }]; - let req = FinalizeBlock { - proposer_address: pkh1.to_vec(), - decided_last_commit: namada_sdk::tendermint::abci::types::CommitInfo{ - round: 0u8.into(), - votes - }, - ..Default::default() - }; - shell.finalize_and_commit(Some(req)); - } - } - }; - } - - const fn send_valset(value: bool) -> bool { - if !is_bridge_comptime_enabled() { - false - } else { - value - } - } - - test_must_send_valset_upd! { - epoch_assertions: [ - // (current epoch, current block height, must send valset upd) - // NOTE: can send valset upd on every 2nd block of an epoch - (0, 1, send_valset(false)), - (0, 2, send_valset(true)), - (0, 3, send_valset(false)), - (0, 4, send_valset(false)), - (0, 5, send_valset(false)), - (0, 6, send_valset(false)), - (0, 7, send_valset(false)), - (0, 8, send_valset(false)), - (0, 9, send_valset(false)), - // we will change epoch here - (0, 10, send_valset(false)), - (1, 11, send_valset(true)), - (1, 12, send_valset(false)), - (1, 13, send_valset(false)), - (1, 14, send_valset(false)), - (1, 15, send_valset(false)), - (1, 16, send_valset(false)), - (1, 17, send_valset(false)), - (1, 18, send_valset(false)), - (1, 19, send_valset(false)), - // we will change epoch here - (1, 20, send_valset(false)), - (2, 21, send_valset(true)), - (2, 22, send_valset(false)), - (2, 23, send_valset(false)), - (2, 24, send_valset(false)), - (2, 25, send_valset(false)), - (2, 26, send_valset(false)), - (2, 27, send_valset(false)), - (2, 28, send_valset(false)), - ], - } -} diff --git a/crates/node/src/shell/testing/client.rs b/crates/node/src/shell/testing/client.rs index 03834746a97..e343bd59ec6 100644 --- a/crates/node/src/shell/testing/client.rs +++ b/crates/node/src/shell/testing/client.rs @@ -2,9 +2,7 @@ use clap::Command as App; use eyre::Report; use namada_apps_lib::cli::api::{CliApi, CliClient}; use namada_apps_lib::cli::args::Global; -use namada_apps_lib::cli::{ - Cmd, Context, NamadaClient, NamadaRelayer, args, cmds, -}; +use namada_apps_lib::cli::{Cmd, Context, NamadaClient, args, cmds}; use namada_sdk::error::Error as SdkError; use namada_sdk::io::Io; @@ -67,32 +65,6 @@ pub fn run( .expect("Could not parse wallet command"); rt.block_on(CliApi::handle_wallet_command(cmd, ctx, &TestingIo)) } - Bin::Relayer => { - args.insert(0, "relayer"); - let app = App::new("test"); - let app = cmds::NamadaRelayer::add_sub(args::Global::def(app)); - let matches = app.get_matches_from(args.clone()); - let cmd = match cmds::NamadaRelayer::parse(&matches) - .expect("Could not parse relayer command") - { - cmds::NamadaRelayer::EthBridgePool( - cmds::EthBridgePool::WithContext(sub_cmd), - ) => NamadaRelayer::EthBridgePoolWithCtx(Box::new(( - sub_cmd, ctx, - ))), - cmds::NamadaRelayer::EthBridgePool( - cmds::EthBridgePool::WithoutContext(sub_cmd), - ) => NamadaRelayer::EthBridgePoolWithoutCtx(sub_cmd), - cmds::NamadaRelayer::ValidatorSet(sub_cmd) => { - NamadaRelayer::ValidatorSet(sub_cmd) - } - }; - rt.block_on(CliApi::handle_relayer_command( - Some(node.clone()), - cmd, - TestingIo, - )) - } } } diff --git a/crates/node/src/shell/testing/node.rs b/crates/node/src/shell/testing/node.rs index c4cfd3976c8..4dfc51fc534 100644 --- a/crates/node/src/shell/testing/node.rs +++ b/crates/node/src/shell/testing/node.rs @@ -8,14 +8,10 @@ use std::task::Poll; use color_eyre::eyre::{Report, Result}; use data_encoding::HEXUPPER; -use itertools::Either; use lazy_static::lazy_static; use namada_sdk::address::Address; -use namada_sdk::chain::{BlockHeader, BlockHeight, Epoch}; +use namada_sdk::chain::{BlockHeight, Epoch}; use namada_sdk::collections::HashMap; -use namada_sdk::control_flow::time::Duration; -use namada_sdk::eth_bridge::oracle::config::Config as OracleConfig; -use namada_sdk::ethereum_events::EthereumEvent; use namada_sdk::events::Event; use namada_sdk::events::extend::Height as HeightAttr; use namada_sdk::events::log::dumb_queries; @@ -39,23 +35,14 @@ use namada_sdk::tendermint_proto::google::protobuf::Timestamp; use namada_sdk::time::DateTimeUtc; use namada_sdk::tx::data::ResultCode; use namada_sdk::tx::event::{Batch as BatchAttr, Code as CodeAttr}; -use namada_sdk::{borsh, ethereum_structs, governance}; +use namada_sdk::{borsh, governance}; use regex::Regex; use tokio::sync::mpsc; -use crate::ethereum_oracle::test_tools::mock_web3_client::{ - TestOracle, Web3Client, Web3Controller, -}; -use crate::ethereum_oracle::{ - control, last_processed_block, try_process_eth_events, -}; +use crate::shell::abci::{ProcessedTx, TxResult}; use crate::shell::testing::utils::TestDir; use crate::shell::token::MaspEpoch; -use crate::shell::{EthereumOracleChannels, Shell}; -use crate::shims::abcipp_shim_types::shim::request::{ - FinalizeBlock, ProcessedTx, -}; -use crate::shims::abcipp_shim_types::shim::response::TxResult; +use crate::shell::{Shell, finalize_block}; use crate::tendermint_proto::abci::{ RequestPrepareProposal, RequestProcessProposal, }; @@ -64,42 +51,11 @@ use crate::tendermint_rpc::endpoint::block; use crate::tendermint_rpc::error::Error as RpcError; use crate::{dry_run_tx, storage, tendermint, tendermint_rpc}; -/// Mock Ethereum oracle used for testing purposes. -struct MockEthOracle { - /// The inner oracle. - oracle: TestOracle, - /// The inner oracle's configuration. - config: OracleConfig, - /// The inner oracle's next block to process. - next_block_to_process: tokio::sync::RwLock, -} - -impl MockEthOracle { - /// Updates the state of the Ethereum oracle. - /// - /// This includes sending any confirmed Ethereum events to - /// the shell and updating the height of the next Ethereum - /// block to process. Upon a successfully processed block, - /// this functions returns `true`. - async fn drive(&self) -> bool { - try_process_eth_events( - &self.oracle, - &self.config, - &*self.next_block_to_process.read().await, - ) - .await - .process_new_block() - } -} - /// Services mocking the operation of the ledger's various async tasks. pub struct MockServices { /// Receives transactions that are supposed to be broadcasted /// to the network. tx_receiver: tokio::sync::Mutex>>, - /// Mock Ethereum oracle, that processes blocks from Ethereum - /// in order to find events emitted by a transaction to vote on. - ethereum_oracle: MockEthOracle, } /// Actions to be performed by the mock node, as a result @@ -107,8 +63,6 @@ pub struct MockServices { pub enum MockServiceAction { /// The ledger should broadcast new transactions. BroadcastTxs(Vec>), - /// Progress to the next Ethereum block to process. - IncrementEthHeight, } impl MockServices { @@ -116,14 +70,6 @@ impl MockServices { async fn drive(&self) -> Vec { let mut actions = vec![]; - // process new eth events - // NOTE: this may result in a deadlock, if the events - // sent to the shell exceed the capacity of the oracle's - // events channel! - if self.ethereum_oracle.drive().await { - actions.push(MockServiceAction::IncrementEthHeight); - } - // receive txs from the broadcaster let txs = { let mut txs = vec![]; @@ -150,90 +96,26 @@ impl MockServices { /// Controller of various mock node services. pub struct MockServicesController { - /// Ethereum oracle controller. - pub eth_oracle: Web3Controller, - /// Handler to the Ethereum oracle sender channel. - /// - /// Bypasses the Ethereum oracle service and sends - /// events directly to the [`Shell`]. - pub eth_events: mpsc::Sender, /// Transaction broadcaster handle. pub tx_broadcaster: mpsc::UnboundedSender>, } -/// Service handlers to be passed to a [`Shell`], when building -/// a mock node. -pub struct MockServiceShellHandlers { - /// Transaction broadcaster handle. - pub tx_broadcaster: mpsc::UnboundedSender>, - /// Ethereum oracle channel handlers. - pub eth_oracle_channels: Option, -} - /// Mock services data returned by [`mock_services`]. pub struct MockServicesPackage { - /// Whether to automatically drive mock services or not. - pub auto_drive_services: bool, /// Mock services stored by the [`MockNode`]. pub services: MockServices, - /// Handlers to mock services stored by the [`Shell`]. - pub shell_handlers: MockServiceShellHandlers, /// Handler to the mock services controller. pub controller: MockServicesController, } -/// Mock services config. -pub struct MockServicesCfg { - /// Whether to automatically drive mock services or not. - pub auto_drive_services: bool, - /// Whether to enable the Ethereum oracle or not. - pub enable_eth_oracle: bool, -} - /// Instantiate mock services for a node. -pub fn mock_services(cfg: MockServicesCfg) -> MockServicesPackage { - let (_, eth_client) = Web3Client::setup(); - let (eth_sender, eth_receiver) = mpsc::channel(1000); - let (last_processed_block_sender, last_processed_block_receiver) = - last_processed_block::channel(); - let (control_sender, control_receiver) = control::channel(); - let eth_oracle_controller = eth_client.controller(); - let oracle = TestOracle::new( - Either::Left(eth_client), - eth_sender.clone(), - last_processed_block_sender, - Duration::from_millis(5), - Duration::from_secs(30), - control_receiver, - ); - let eth_oracle_channels = EthereumOracleChannels::new( - eth_receiver, - control_sender, - last_processed_block_receiver, - ); +pub fn mock_services() -> MockServicesPackage { let (tx_broadcaster, tx_receiver) = mpsc::unbounded_channel(); - let ethereum_oracle = MockEthOracle { - oracle, - config: Default::default(), - next_block_to_process: tokio::sync::RwLock::new(Default::default()), - }; MockServicesPackage { - auto_drive_services: cfg.auto_drive_services, services: MockServices { - ethereum_oracle, tx_receiver: tokio::sync::Mutex::new(tx_receiver), }, - shell_handlers: MockServiceShellHandlers { - tx_broadcaster: tx_broadcaster.clone(), - eth_oracle_channels: cfg - .enable_eth_oracle - .then_some(eth_oracle_channels), - }, - controller: MockServicesController { - eth_oracle: eth_oracle_controller, - eth_events: eth_sender, - tx_broadcaster, - }, + controller: MockServicesController { tx_broadcaster }, } } @@ -255,7 +137,6 @@ pub struct InnerMockNode { pub tx_results: Mutex>>, pub blocks: Mutex>, pub services: MockServices, - pub auto_drive_services: bool, } #[derive(Clone)] @@ -312,15 +193,6 @@ impl MockNode { MockServiceAction::BroadcastTxs(txs) => { self.submit_txs(txs); } - MockServiceAction::IncrementEthHeight => { - let mut height = self - .services - .ethereum_oracle - .next_block_to_process - .write() - .await; - *height = height.next(); - } } } @@ -330,12 +202,6 @@ impl MockNode { } } - async fn drive_mock_services_bg(&self) { - if self.auto_drive_services { - self.drive_mock_services().await; - } - } - pub fn genesis_dir(&self) -> PathBuf { self.test_dir .path() @@ -516,18 +382,18 @@ impl MockNode { .collect() }; // build finalize block abci request - let req = FinalizeBlock { - header: BlockHeader { - hash: Hash([0; 32]), - #[allow(clippy::disallowed_methods)] - time: header_time.unwrap_or_else(DateTimeUtc::now), - next_validators_hash: Hash([0; 32]), - }, - block_hash: Hash([0; 32]), - byzantine_validators: vec![], + let req = finalize_block::Request { + hash: Hash([0; 32]), + #[allow(clippy::disallowed_methods)] + time: header_time.unwrap_or_else(DateTimeUtc::now), + next_validators_hash: Hash([0; 32]), + misbehavior: vec![], txs: txs.clone(), - proposer_address, - height: height.try_into().unwrap(), + proposer_address: tendermint::account::Id::try_from( + proposer_address, + ) + .unwrap(), + height, decided_last_commit: tendermint::abci::types::CommitInfo { round: 0u8.into(), votes, @@ -605,6 +471,79 @@ impl MockNode { ); } + /// Call the `FinalizeBlock` handler. + pub fn finalize_block(&self, header_time: Option) { + let (proposer_address, votes) = self.prepare_request(); + + let height = self.last_block_height().next_height(); + let mut locked = self.shell.lock().unwrap(); + + // check if we have protocol txs to be included + // in the finalize block request + let txs: Vec = { + let req = RequestPrepareProposal { + proposer_address: proposer_address.clone().into(), + ..Default::default() + }; + let txs = locked.prepare_proposal(req).txs; + + txs.into_iter() + .map(|tx| ProcessedTx { + tx, + result: TxResult { + code: 0, + info: String::new(), + }, + }) + .collect() + }; + // build finalize block abci request + let req = finalize_block::Request { + hash: Hash([0; 32]), + #[allow(clippy::disallowed_methods)] + time: header_time.unwrap_or_else(DateTimeUtc::now), + next_validators_hash: Hash([0; 32]), + misbehavior: vec![], + txs: txs.clone(), + proposer_address: tendermint::account::Id::try_from( + proposer_address, + ) + .unwrap(), + height, + decided_last_commit: tendermint::abci::types::CommitInfo { + round: 0u8.into(), + votes, + }, + }; + + let resp = locked.finalize_block(req).expect("Test failed"); + let mut result_codes = resp + .events + .iter() + .filter_map(|e| { + e.read_attribute_opt::() + .unwrap() + .map(|result_code| { + if result_code == ResultCode::Ok { + NodeResults::Ok + } else { + NodeResults::Failed(result_code) + } + }) + }) + .collect::>(); + let mut tx_results = resp + .events + .into_iter() + .filter_map(|e| e.read_attribute_opt::>().unwrap()) + .collect::>(); + self.tx_result_codes + .lock() + .unwrap() + .append(&mut result_codes); + self.tx_results.lock().unwrap().append(&mut tx_results); + } + /// Send a tx through Process Proposal and Finalize Block /// and register the results. pub fn submit_txs(&self, txs: Vec>) { @@ -642,6 +581,24 @@ impl MockNode { } // process proposal succeeded, now run finalize block + let processed_txs: Vec = { + let req = RequestPrepareProposal { + proposer_address: proposer_address.clone().into(), + txs: txs.clone().into_iter().map(|tx| tx.into()).collect(), + ..Default::default() + }; + let txs = locked.prepare_proposal(req).txs; + + txs.into_iter() + .map(|tx| ProcessedTx { + tx, + result: TxResult { + code: 0, + info: String::new(), + }, + }) + .collect() + }; let time = { #[allow(clippy::disallowed_methods)] @@ -651,26 +608,17 @@ impl MockNode { let dur = namada_sdk::time::Duration::minutes(10); time - dur }; - let req = FinalizeBlock { - header: BlockHeader { - hash: Hash([0; 32]), - #[allow(clippy::disallowed_methods)] - time, - next_validators_hash: Hash([0; 32]), - }, - block_hash: Hash([0; 32]), - byzantine_validators: vec![], - txs: txs - .clone() - .into_iter() - .zip(tx_results) - .map(|(tx, result)| ProcessedTx { - tx: tx.into(), - result, - }) - .collect(), - proposer_address, - height: height.try_into().unwrap(), + let req = finalize_block::Request { + hash: Hash([0; 32]), + time, + next_validators_hash: Hash([0; 32]), + misbehavior: vec![], + txs: processed_txs.clone(), + proposer_address: tendermint::account::Id::try_from( + proposer_address, + ) + .unwrap(), + height, decided_last_commit: tendermint::abci::types::CommitInfo { round: 0u8.into(), votes, @@ -809,7 +757,6 @@ impl Client for MockNode { height: Option, prove: bool, ) -> std::result::Result { - self.drive_mock_services_bg().await; let rpc = RPC; let data = data.unwrap_or_default(); let latest_height = { @@ -870,7 +817,6 @@ impl Client for MockNode { /// `/abci_info`: get information about the ABCI application. async fn abci_info(&self) -> Result { - self.drive_mock_services_bg().await; let locked = self.shell.lock().unwrap(); Ok(Info { data: "Namada".to_string(), @@ -895,7 +841,6 @@ impl Client for MockNode { tx: impl Into>, ) -> Result { - self.drive_mock_services_bg().await; let mut resp = tendermint_rpc::endpoint::broadcast::tx_sync::Response { codespace: Default::default(), code: Default::default(), @@ -926,7 +871,6 @@ impl Client for MockNode { _order: namada_sdk::tendermint_rpc::Order, ) -> Result { - self.drive_mock_services_bg().await; let matcher = parse_tm_query(query); let borrowed = self.shell.lock().unwrap(); @@ -959,7 +903,6 @@ impl Client for MockNode { where H: TryInto + Send, { - self.drive_mock_services_bg().await; let height = height.try_into().map_err(|_| { RpcError::parse("Failed to cast block height".to_string()) })?; @@ -987,13 +930,12 @@ impl Client for MockNode { namada_sdk::tendermint::abci::Event::from(event.clone()) }) .collect(); - let has_events = !events.is_empty(); Ok(tendermint_rpc::endpoint::block_results::Response { height, txs_results: None, - finalize_block_events: vec![], + finalize_block_events: events, begin_block_events: None, - end_block_events: has_events.then_some(events), + end_block_events: None, validator_updates: vec![], consensus_param_updates: None, app_hash: namada_sdk::tendermint::hash::AppHash::default(), @@ -1050,7 +992,6 @@ impl Client for MockNode { /// Returns empty result (200 OK) on success, no response in case of an /// error. async fn health(&self) -> Result<(), RpcError> { - self.drive_mock_services_bg().await; Ok(()) } } diff --git a/crates/node/src/shell/testing/utils.rs b/crates/node/src/shell/testing/utils.rs index fcbe77bb1d0..d792b6f1afe 100644 --- a/crates/node/src/shell/testing/utils.rs +++ b/crates/node/src/shell/testing/utils.rs @@ -16,12 +16,11 @@ pub enum Bin { Node, Client, Wallet, - Relayer, } /// A temporary directory for testing #[derive(Debug)] -pub struct TestDir(PathBuf); +pub struct TestDir(pub PathBuf); impl TestDir { /// Creat a new temp directory. This will have to be manually diff --git a/crates/node/src/shell/vote_extensions.rs b/crates/node/src/shell/vote_extensions.rs deleted file mode 100644 index 229b5526b7a..00000000000 --- a/crates/node/src/shell/vote_extensions.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! Extend Tendermint votes with Ethereum bridge logic. - -pub mod bridge_pool_vext; -pub mod eth_events; -pub mod val_set_update; - -use drain_filter_polyfill::DrainFilter; -use namada_sdk::eth_bridge::protocol::transactions::bridge_pool_roots::sign_bridge_pool_root; -use namada_sdk::eth_bridge::protocol::transactions::ethereum_events::sign_ethereum_events; -use namada_sdk::eth_bridge::protocol::transactions::validator_set_update::sign_validator_set_update; -pub use namada_sdk::eth_bridge::protocol::validation::VoteExtensionError; -use namada_sdk::tx::Signed; -use namada_vote_ext::{ - VoteExtension, bridge_pool_roots, ethereum_events, validator_set_update, -}; - -use super::*; -use crate::shims::abcipp_shim_types::shim::TxBytes; - -/// Message to be passed to `.expect()` calls in this module. -const VALIDATOR_EXPECT_MSG: &str = "Only validators receive this method call."; - -impl Shell -where - D: DB + for<'iter> DBIter<'iter> + Sync + 'static, - H: StorageHasher + Sync + 'static, -{ - /// Creates the data to be added to a vote extension. - /// - /// INVARIANT: This method must be stateless. - #[inline] - pub fn craft_extension(&mut self) -> VoteExtension { - VoteExtension { - ethereum_events: self.extend_vote_with_ethereum_events(), - bridge_pool_root: self - .extend_vote_with_bp_roots() - .map(bridge_pool_roots::SignedVext), - validator_set_update: self.extend_vote_with_valset_update(), - } - } - - /// Extend PreCommit votes with [`ethereum_events::Vext`] instances. - #[inline] - pub fn extend_vote_with_ethereum_events( - &mut self, - ) -> Option> { - let events = self.new_ethereum_events(); - self.sign_ethereum_events(events) - } - - /// Sign the given Ethereum events, and return the associated - /// vote extension protocol transaction. - pub fn sign_ethereum_events( - &self, - ethereum_events: Vec, - ) -> Option> { - let validator_addr = self - .mode - .get_validator_address() - .expect(VALIDATOR_EXPECT_MSG); - let protocol_key = match &self.mode { - ShellMode::Validator { data, .. } => &data.keys.protocol_keypair, - _ => unreachable!("{VALIDATOR_EXPECT_MSG}"), - }; - sign_ethereum_events( - &self.state, - validator_addr, - protocol_key, - ethereum_events, - ) - .map(|ethereum_events::SignedVext(ext)| ext) - } - - /// Extend PreCommit votes with [`bridge_pool_roots::Vext`] instances. - pub fn extend_vote_with_bp_roots( - &self, - ) -> Option> { - let validator_addr = self - .mode - .get_validator_address() - .expect(VALIDATOR_EXPECT_MSG); - let eth_hot_key = self - .mode - .get_eth_bridge_keypair() - .expect(VALIDATOR_EXPECT_MSG); - let protocol_key = match &self.mode { - ShellMode::Validator { data, .. } => &data.keys.protocol_keypair, - _ => unreachable!("{VALIDATOR_EXPECT_MSG}"), - }; - sign_bridge_pool_root( - &self.state, - validator_addr, - eth_hot_key, - protocol_key, - ) - .map(|bridge_pool_roots::SignedVext(ext)| ext) - } - - /// Extend PreCommit votes with [`validator_set_update::Vext`] - /// instances. - pub fn extend_vote_with_valset_update( - &self, - ) -> Option { - let validator_addr = self - .mode - .get_validator_address() - .expect(VALIDATOR_EXPECT_MSG); - let eth_hot_key = self - .mode - .get_eth_bridge_keypair() - .expect("{VALIDATOR_EXPECT_MSG}"); - sign_validator_set_update::<_, _, governance::Store<_>>( - &self.state, - validator_addr, - eth_hot_key, - ) - } - - /// Given a slice of [`TxBytes`], return an iterator over the - /// ones we could deserialize to vote extension protocol txs. - pub fn deserialize_vote_extensions<'shell>( - &'shell self, - txs: &'shell mut Vec, - ) -> DrainFilter<'shell, TxBytes, impl FnMut(&mut TxBytes) -> bool + 'shell> - { - drain_filter_polyfill::VecExt::drain_filter(txs, move |tx_bytes| { - let tx = match Tx::try_from_bytes(tx_bytes.as_ref()) { - Ok(tx) => tx, - Err(err) => { - tracing::warn!( - ?err, - "Failed to deserialize tx in \ - deserialize_vote_extensions" - ); - return false; - } - }; - match (&tx).try_into().ok() { - Some(EthereumTxData::BridgePoolVext(_)) => true, - Some(EthereumTxData::EthEventsVext(ext)) => { - // NB: only propose events with at least - // one valid nonce - ext.data.ethereum_events.iter().any(|event| { - self.state - .ethbridge_queries() - .validate_eth_event_nonce(event) - }) - } - Some(EthereumTxData::ValSetUpdateVext(ext)) => { - // only include non-stale validator set updates - // in block proposals. it might be sitting long - // enough in the mempool for it to no longer be - // relevant to propose (e.g. a proof was constructed - // before this validator set update got a chance - // to be decided). unfortunately, we won't be able - // to remove it from the mempool this way, but it - // will eventually be evicted, getting replaced - // by newer txs. - let is_seen = self - .state - .ethbridge_queries() - .valset_upd_seen(ext.data.signing_epoch.next()); - !is_seen - } - _ => false, - } - }) - } -} - -/// Yields an iterator over the protocol transactions -/// in a [`VoteExtension`]. -pub fn iter_protocol_txs( - ext: VoteExtension, -) -> impl Iterator { - let VoteExtension { - ethereum_events, - bridge_pool_root, - validator_set_update, - } = ext; - [ - ethereum_events.map(|e| { - EthereumTxData::EthEventsVext(ethereum_events::SignedVext(e)) - }), - bridge_pool_root.map(EthereumTxData::BridgePoolVext), - validator_set_update.map(EthereumTxData::ValSetUpdateVext), - ] - .into_iter() - .flatten() -} diff --git a/crates/node/src/shell/vote_extensions/bridge_pool_vext.rs b/crates/node/src/shell/vote_extensions/bridge_pool_vext.rs deleted file mode 100644 index 3e8de3ac038..00000000000 --- a/crates/node/src/shell/vote_extensions/bridge_pool_vext.rs +++ /dev/null @@ -1,608 +0,0 @@ -//! Extend Tendermint votes with signatures of the Ethereum -//! bridge pool root and nonce seen by a quorum of validators. -use itertools::Itertools; - -use super::*; - -impl Shell -where - D: DB + for<'iter> DBIter<'iter> + Sync + 'static, - H: StorageHasher + Sync + 'static, -{ - /// Takes an iterator over Bridge pool root vote extension instances, - /// and returns another iterator. The latter yields - /// valid Bridge pool root vote extensions, or the reason why these - /// are invalid, in the form of a `VoteExtensionError`. - #[inline] - pub fn validate_bp_roots_vext_list<'iter>( - &'iter self, - vote_extensions: impl IntoIterator> - + 'iter, - ) -> impl Iterator< - Item = std::result::Result< - Signed, - VoteExtensionError, - >, - > + 'iter { - vote_extensions.into_iter().map(|vote_extension| { - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &self.state, - &vote_extension, - self.state.in_mem().get_last_block_height(), - )?; - Ok(vote_extension) - }) - } - - /// Takes a list of signed Bridge pool root vote extensions, - /// and filters out invalid instances. This also de-duplicates - /// the iterator to be unique per validator address. - #[inline] - pub fn filter_invalid_bp_roots_vexts<'iter>( - &'iter self, - vote_extensions: impl IntoIterator> - + 'iter, - ) -> impl Iterator> + 'iter { - self.validate_bp_roots_vext_list(vote_extensions) - .filter_map(|ext| ext.ok()) - .dedup_by(|ext_1, ext_2| { - ext_1.data.validator_addr == ext_2.data.validator_addr - }) - } -} - -#[allow(clippy::cast_possible_truncation)] -#[cfg(test)] -mod test_bp_vote_extensions { - use namada_apps_lib::wallet::defaults::{bertha_address, bertha_keypair}; - use namada_sdk::chain::BlockHeight; - use namada_sdk::eth_bridge::protocol::validation::bridge_pool_roots::validate_bp_roots_vext; - use namada_sdk::eth_bridge::storage::bridge_pool::get_key_from_hash; - use namada_sdk::eth_bridge::storage::eth_bridge_queries::{ - EthBridgeQueries, is_bridge_comptime_enabled, - }; - use namada_sdk::ethereum_events::Uint; - use namada_sdk::keccak::{KeccakHash, keccak_hash}; - use namada_sdk::key::*; - use namada_sdk::proof_of_stake::storage::{ - consensus_validator_set_handle, - read_consensus_validator_set_addresses_with_stake, read_pos_params, - }; - use namada_sdk::proof_of_stake::types::{ - Position as ValidatorPosition, WeightedValidator, - }; - use namada_sdk::proof_of_stake::{ - BecomeValidator, Epoch, become_validator, - }; - use namada_sdk::state::StorageWrite; - use namada_sdk::tendermint::abci::types::VoteInfo; - use namada_sdk::tx::Signed; - use namada_sdk::{governance, token}; - use namada_vote_ext::bridge_pool_roots; - - use crate::shell::test_utils::*; - use crate::shims::abcipp_shim_types::shim::request::FinalizeBlock; - - /// Make Bertha a validator. - fn add_validator(shell: &mut TestShell) { - // We make a change so that there Bertha is - // a validator in the next epoch - let validators_handle = consensus_validator_set_handle(); - validators_handle - .at(&1.into()) - .at(&token::Amount::native_whole(100)) - .insert(&mut shell.state, ValidatorPosition(1), bertha_address()) - .expect("Test failed"); - - // change pipeline length to 1 - let mut params = - read_pos_params::<_, governance::Store<_>>(&shell.state).unwrap(); - params.owned.pipeline_len = 1; - - let consensus_key = gen_keypair(); - let protocol_key = bertha_keypair(); - let hot_key = gen_secp256k1_keypair(); - let cold_key = gen_secp256k1_keypair(); - - become_validator::<_, governance::Store<_>>( - &mut shell.state, - BecomeValidator { - params: ¶ms, - address: &bertha_address(), - consensus_key: &consensus_key.ref_to(), - protocol_key: &protocol_key.ref_to(), - eth_hot_key: &hot_key.ref_to(), - eth_cold_key: &cold_key.ref_to(), - current_epoch: 0.into(), - commission_rate: Default::default(), - max_commission_rate_change: Default::default(), - metadata: Default::default(), - offset_opt: None, - }, - ) - .expect("Test failed"); - - // we advance forward to the next epoch - let consensus_set: Vec = - read_consensus_validator_set_addresses_with_stake( - &shell.state, - Epoch::default(), - ) - .unwrap() - .into_iter() - .collect(); - - let val1 = consensus_set[0].clone(); - let pkh1 = get_pkh_from_address( - &shell.state, - ¶ms, - val1.address.clone(), - Epoch::default(), - ); - let votes = vec![VoteInfo { - validator: crate::tendermint::abci::types::Validator { - address: pkh1, - power: (u128::try_from(val1.bonded_stake).expect("Test failed") - as u64) - .try_into() - .unwrap(), - }, - sig_info: - crate::tendermint::abci::types::BlockSignatureInfo::LegacySigned, - }]; - let req = FinalizeBlock { - proposer_address: pkh1.to_vec(), - decided_last_commit: crate::tendermint::abci::types::CommitInfo { - round: 0u8.into(), - votes, - }, - ..Default::default() - }; - assert_eq!(shell.start_new_epoch(Some(req)).0, 1); - - // Check that Bertha's vote extensions pass validation. - let to_sign = get_bp_bytes_to_sign(); - let sig = Signed::<_, SignableEthMessage>::new(&hot_key, to_sign).sig; - let vote_ext = bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: bertha_address(), - sig, - } - .sign(&bertha_keypair()); - shell.state.in_mem_mut().block.height = - shell.state.in_mem().get_last_block_height(); - shell.commit(); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &vote_ext.0, - shell.state.in_mem().get_last_block_height() - ) - .is_ok() - ); - } - - /// Test that the function crafting the bridge pool root - /// vext creates the expected payload. Check that this - /// payload passes validation. - #[test] - fn test_happy_flow() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _broadcaster, _, _oracle_control_recv) = - setup_at_height(1u64); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - shell.state.in_mem_mut().block.height = - shell.state.in_mem().get_last_block_height(); - shell.commit(); - let to_sign = get_bp_bytes_to_sign(); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let vote_ext = bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address, - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - assert_eq!( - vote_ext.0, - shell.extend_vote_with_bp_roots().expect("Test failed") - ); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &vote_ext.0, - shell.state.in_mem().get_last_block_height(), - ) - .is_ok() - ) - } - - /// Test that we de-duplicate the bridge pool vexts - /// in a block proposal by validator address. - #[test] - fn test_vexts_are_de_duped() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _broadcaster, _, _oracle_control_recv) = - setup_at_height(1u64); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - shell.state.in_mem_mut().block.height = - shell.state.in_mem().get_last_block_height(); - shell.commit(); - let to_sign = get_bp_bytes_to_sign(); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let vote_ext = bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address, - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - let valid = shell - .filter_invalid_bp_roots_vexts(vec![ - vote_ext.0.clone(), - vote_ext.0.clone(), - ]) - .collect::>(); - assert_eq!(valid, vec![vote_ext.0]); - } - - /// Test that Bridge pool roots signed by a non-validator are rejected - /// even if the vext is signed by a validator - #[test] - fn test_bp_roots_must_be_signed_by_validator() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _broadcaster, _, _oracle_control_recv) = - setup_at_height(1u64); - let signing_key = gen_keypair(); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - shell.state.in_mem_mut().block.height = - shell.state.in_mem().get_last_block_height(); - shell.commit(); - let to_sign = get_bp_bytes_to_sign(); - let sig = - Signed::<_, SignableEthMessage>::new(&signing_key, to_sign).sig; - let bp_root = bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address, - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &bp_root.0, - shell.get_current_decision_height(), - ) - .is_err() - ) - } - - /// Test that Bridge pool root vext and inner signature - /// are from the same validator. - #[test] - fn test_bp_root_sigs_from_same_validator() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _broadcaster, _, _oracle_control_recv) = - setup_at_height(3u64); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - add_validator(&mut shell); - let to_sign = get_bp_bytes_to_sign(); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let bp_root = bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address, - sig, - } - .sign(&bertha_keypair()); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &bp_root.0, - shell.state.in_mem().get_last_block_height() - ) - .is_err() - ) - } - - fn reject_incorrect_block_number(height: BlockHeight, shell: &TestShell) { - let address = shell.mode.get_validator_address().unwrap().clone(); - let to_sign = get_bp_bytes_to_sign(); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let bp_root = bridge_pool_roots::Vext { - block_height: height, - validator_addr: address, - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &bp_root.0, - shell.state.in_mem().get_last_block_height() - ) - .is_err() - ) - } - - /// Test that an [`bridge_pool_roots::Vext`] that labels its included - /// block height as greater than the latest block height is rejected. - #[test] - fn test_block_height_too_high() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _, _, _) = setup_at_height(3u64); - reject_incorrect_block_number( - shell.state.in_mem().get_last_block_height() + 1, - &shell, - ); - } - - /// Test if we reject Bridge pool roots vote extensions - /// issued at genesis. - #[test] - fn test_reject_genesis_vexts() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _, _, _) = setup(); - reject_incorrect_block_number(0.into(), &shell); - } - - /// Test that a bridge pool root vext is rejected - /// if the nonce is incorrect. - #[test] - fn test_incorrect_nonce() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _, _, _) = setup(); - let address = shell.mode.get_validator_address().unwrap().clone(); - let to_sign = get_bp_bytes_to_sign(); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let bp_root = bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address, - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &bp_root.0, - shell.state.in_mem().get_last_block_height() - ) - .is_err() - ) - } - - /// Test that a bridge pool root vext is rejected - /// if the root is incorrect. - #[test] - fn test_incorrect_root() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _, _, _) = setup(); - let address = shell.mode.get_validator_address().unwrap().clone(); - let to_sign = get_bp_bytes_to_sign(); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let bp_root = bridge_pool_roots::Vext { - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address, - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &bp_root.0, - shell.state.in_mem().get_last_block_height() - ) - .is_err() - ) - } - - /// Test that we can verify vext from several block heights - /// prior. - #[test] - fn test_vext_for_old_height() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _recv, _, _oracle_control_recv) = setup_at_height(1u64); - let address = shell.mode.get_validator_address().unwrap().clone(); - shell.state.in_mem_mut().block.height = 2.into(); - let key = get_key_from_hash(&KeccakHash([1; 32])); - let height = shell.state.in_mem().block.height; - shell.state.write(&key, height).expect("Test failed"); - shell.commit(); - assert_eq!( - shell - .state - .ethbridge_queries() - .get_bridge_pool_root_at_height(2.into()) - .unwrap(), - KeccakHash([1; 32]) - ); - shell.state.in_mem_mut().block.height = 3.into(); - shell.state.delete(&key).expect("Test failed"); - let key = get_key_from_hash(&KeccakHash([2; 32])); - let height = shell.state.in_mem().block.height; - shell.state.write(&key, height).expect("Test failed"); - shell.commit(); - assert_eq!( - shell - .state - .ethbridge_queries() - .get_bridge_pool_root_at_height(3.into()) - .unwrap(), - KeccakHash([2; 32]) - ); - let to_sign = keccak_hash([[1; 32], Uint::from(0).to_bytes()].concat()); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let bp_root = bridge_pool_roots::Vext { - block_height: 2.into(), - validator_addr: address.clone(), - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &bp_root.0, - shell.get_current_decision_height() - ) - .is_ok() - ); - let to_sign = keccak_hash([[2; 32], Uint::from(0).to_bytes()].concat()); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let bp_root = bridge_pool_roots::Vext { - block_height: 3.into(), - validator_addr: address, - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &bp_root.0, - shell.get_current_decision_height() - ) - .is_ok() - ); - } - - /// Test that if the wrong block height is given for the provided root, - /// we reject. - #[test] - fn test_wrong_height_for_root() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _recv, _, _oracle_control_recv) = setup_at_height(1u64); - let address = shell.mode.get_validator_address().unwrap().clone(); - shell.state.in_mem_mut().block.height = 2.into(); - let key = get_key_from_hash(&KeccakHash([1; 32])); - let height = shell.state.in_mem().block.height; - shell.state.write(&key, height).expect("Test failed"); - shell.commit(); - assert_eq!( - shell - .state - .ethbridge_queries() - .get_bridge_pool_root_at_height(2.into()) - .unwrap(), - KeccakHash([1; 32]) - ); - shell.state.in_mem_mut().block.height = 3.into(); - shell.state.delete(&key).expect("Test failed"); - let key = get_key_from_hash(&KeccakHash([2; 32])); - let height = shell.state.in_mem().block.height; - shell.state.write(&key, height).expect("Test failed"); - shell.commit(); - assert_eq!( - shell - .state - .ethbridge_queries() - .get_bridge_pool_root_at_height(3.into()) - .unwrap(), - KeccakHash([2; 32]) - ); - let to_sign = keccak_hash([[1; 32], Uint::from(0).to_bytes()].concat()); - let sig = Signed::<_, SignableEthMessage>::new( - shell.mode.get_eth_bridge_keypair().expect("Test failed"), - to_sign, - ) - .sig; - let bp_root = bridge_pool_roots::Vext { - block_height: 3.into(), - validator_addr: address, - sig, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - assert!( - validate_bp_roots_vext::<_, _, governance::Store<_>>( - &shell.state, - &bp_root.0, - shell.get_current_decision_height() - ) - .is_err() - ); - } -} diff --git a/crates/node/src/shell/vote_extensions/eth_events.rs b/crates/node/src/shell/vote_extensions/eth_events.rs deleted file mode 100644 index 626d875bf10..00000000000 --- a/crates/node/src/shell/vote_extensions/eth_events.rs +++ /dev/null @@ -1,598 +0,0 @@ -//! Extend Tendermint votes with Ethereum events seen by a quorum of validators. - -use std::collections::BTreeMap; - -use namada_sdk::collections::HashMap; -use namada_vote_ext::ethereum_events::MultiSignedEthEvent; - -use super::*; - -impl Shell -where - D: DB + for<'iter> DBIter<'iter> + Sync + 'static, - H: StorageHasher + Sync + 'static, -{ - /// Checks the channel from the Ethereum oracle monitoring - /// the fullnode and retrieves all seen Ethereum events. - pub fn new_ethereum_events(&mut self) -> Vec { - let queries = self.state.ethbridge_queries(); - match &mut self.mode { - ShellMode::Validator { - eth_oracle: - Some(EthereumOracleChannels { - ethereum_receiver, .. - }), - .. - } => { - ethereum_receiver.fill_queue(|event| { - queries.validate_eth_event_nonce(event) - }); - ethereum_receiver.get_events() - } - _ => vec![], - } - } - - /// Takes an iterator over Ethereum events vote extension instances, - /// and returns another iterator. The latter yields - /// valid Ethereum events vote extensions, or the reason why these - /// are invalid, in the form of a `VoteExtensionError`. - #[inline] - pub fn validate_eth_events_vext_list<'iter>( - &'iter self, - vote_extensions: impl IntoIterator> - + 'iter, - ) -> impl Iterator< - Item = std::result::Result< - Signed, - VoteExtensionError, - >, - > + 'iter { - vote_extensions.into_iter().map(|vote_extension| { - validate_eth_events_vext::<_, _, governance::Store<_>>( - &self.state, - &vote_extension, - self.state.in_mem().get_last_block_height(), - )?; - Ok(vote_extension) - }) - } - - /// Takes a list of signed Ethereum events vote extensions, - /// and filters out invalid instances. - #[inline] - pub fn filter_invalid_eth_events_vexts<'iter>( - &'iter self, - vote_extensions: impl IntoIterator> - + 'iter, - ) -> impl Iterator> + 'iter { - self.validate_eth_events_vext_list(vote_extensions) - .filter_map(|ext| ext.ok()) - } - - /// Compresses a set of signed Ethereum events into a single - /// [`ethereum_events::VextDigest`], whilst filtering invalid - /// [`Signed`] instances in the process. - /// - /// When vote extensions are being used, this performs a check - /// that at least 2/3 of the validators by voting power have - /// included ethereum events in their vote extension. - pub fn compress_ethereum_events( - &self, - vote_extensions: Vec>, - ) -> Option { - #[allow(clippy::question_mark)] - if self.state.in_mem().last_block.is_none() { - return None; - } - - let mut event_observers = BTreeMap::new(); - let mut signatures = HashMap::new(); - - for vote_extension in - self.filter_invalid_eth_events_vexts(vote_extensions) - { - let validator_addr = vote_extension.data.validator_addr; - let block_height = vote_extension.data.block_height; - - // register all ethereum events seen by `validator_addr` - for ev in vote_extension.data.ethereum_events { - let signers = - event_observers.entry(ev).or_insert_with(BTreeSet::new); - signers.insert((validator_addr.clone(), block_height)); - } - - // register the signature of `validator_addr` - let addr = validator_addr.clone(); - let sig = vote_extension.sig; - - let key = (addr, block_height); - tracing::debug!( - ?key, - ?sig, - ?validator_addr, - "Inserting signature into ethereum_events::VextDigest" - ); - if let Some(existing_sig) = signatures.insert(key, sig.clone()) { - tracing::warn!( - ?sig, - ?existing_sig, - ?validator_addr, - "Overwrote old signature from validator while \ - constructing ethereum_events::VextDigest - maybe private \ - key of validator is being used by multiple nodes?" - ); - } - } - - let events: Vec = event_observers - .into_iter() - .map(|(event, signers)| MultiSignedEthEvent { event, signers }) - .collect(); - - Some(ethereum_events::VextDigest { events, signatures }) - } -} - -#[allow(clippy::cast_possible_truncation)] -#[cfg(test)] -mod test_vote_extensions { - use namada_sdk::address::testing::gen_established_address; - use namada_sdk::eth_bridge::EthBridgeQueries; - use namada_sdk::eth_bridge::storage::bridge_pool; - use namada_sdk::eth_bridge::storage::eth_bridge_queries::is_bridge_comptime_enabled; - use namada_sdk::eth_bridge::test_utils::GovStore; - use namada_sdk::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, Uint, - }; - use namada_sdk::governance; - use namada_sdk::hash::Hash; - use namada_sdk::key::*; - use namada_sdk::proof_of_stake::queries::get_consensus_validator_from_protocol_pk; - use namada_sdk::proof_of_stake::storage::{ - consensus_validator_set_handle, - read_consensus_validator_set_addresses_with_stake, read_pos_params, - }; - use namada_sdk::proof_of_stake::types::WeightedValidator; - use namada_sdk::state::collections::lazy_map::{NestedSubKey, SubKey}; - use namada_sdk::storage::{Epoch, InnerEthEventsQueue, StorageWrite}; - use namada_sdk::tendermint::abci::types::VoteInfo; - use namada_vote_ext::ethereum_events; - - use super::validate_eth_events_vext; - use crate::shell::test_utils::*; - use crate::shims::abcipp_shim_types::shim::request::FinalizeBlock; - - /// Test validating Ethereum events. - #[test] - fn test_eth_event_validate() { - let (mut shell, _, _, _) = setup(); - let nonce: Uint = 10u64.into(); - - // write bp nonce to storage - shell - .state - .write(&bridge_pool::get_nonce_key(), nonce) - .expect("Test failed"); - - // write nam nonce to the eth events queue - shell - .state - .in_mem_mut() - .eth_events_queue - .transfers_to_namada = InnerEthEventsQueue::new_at(nonce); - - // eth transfers with the same nonce as the bp nonce in storage are - // valid - shell - .state - .ethbridge_queries() - .validate_eth_event_nonce(&EthereumEvent::TransfersToEthereum { - nonce, - transfers: vec![], - relayer: gen_established_address(), - }) - .then_some(()) - .ok_or(()) - .expect("Test failed"); - - // eth transfers with different nonces are invalid - shell - .state - .ethbridge_queries() - .validate_eth_event_nonce(&EthereumEvent::TransfersToEthereum { - nonce: nonce + 1, - transfers: vec![], - relayer: gen_established_address(), - }) - .then_some(()) - .ok_or(()) - .expect_err("Test failed"); - shell - .state - .ethbridge_queries() - .validate_eth_event_nonce(&EthereumEvent::TransfersToEthereum { - nonce: nonce - 1, - transfers: vec![], - relayer: gen_established_address(), - }) - .then_some(()) - .ok_or(()) - .expect_err("Test failed"); - - // nam transfers with nonces >= the nonce in storage are valid - shell - .state - .ethbridge_queries() - .validate_eth_event_nonce(&EthereumEvent::TransfersToNamada { - nonce, - transfers: vec![], - }) - .then_some(()) - .ok_or(()) - .expect("Test failed"); - shell - .state - .ethbridge_queries() - .validate_eth_event_nonce(&EthereumEvent::TransfersToNamada { - nonce: nonce + 5, - transfers: vec![], - }) - .then_some(()) - .ok_or(()) - .expect("Test failed"); - - // nam transfers with lower nonces are invalid - shell - .state - .ethbridge_queries() - .validate_eth_event_nonce(&EthereumEvent::TransfersToNamada { - nonce: nonce - 1, - transfers: vec![], - }) - .then_some(()) - .ok_or(()) - .expect_err("Test failed"); - shell - .state - .ethbridge_queries() - .validate_eth_event_nonce(&EthereumEvent::TransfersToNamada { - nonce: nonce - 2, - transfers: vec![], - }) - .then_some(()) - .ok_or(()) - .expect_err("Test failed"); - } - - /// Test that we successfully receive ethereum events - /// from the channel to fullnode process - /// - /// We further check that ledger side buffering is done if multiple - /// events are in the channel and that queueing and de-duplicating is - /// done - #[test] - fn test_get_eth_events() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _, oracle, _) = setup(); - let event_1 = EthereumEvent::TransfersToEthereum { - nonce: 0.into(), - transfers: vec![TransferToEthereum { - amount: 100.into(), - asset: EthAddress([1; 20]), - receiver: EthAddress([2; 20]), - checksum: Hash::default(), - }], - relayer: gen_established_address(), - }; - let event_2 = EthereumEvent::TransfersToEthereum { - nonce: 1.into(), - transfers: vec![TransferToEthereum { - amount: 100.into(), - asset: EthAddress([1; 20]), - receiver: EthAddress([2; 20]), - checksum: Hash::default(), - }], - relayer: gen_established_address(), - }; - let event_3 = EthereumEvent::TransfersToNamada { - nonce: 0.into(), - transfers: vec![], - }; - let event_4 = EthereumEvent::TransfersToNamada { - nonce: 1.into(), - transfers: vec![], - }; - - // send valid events - tokio_test::block_on(oracle.send(event_1.clone())) - .expect("Test failed"); - tokio_test::block_on(oracle.send(event_3.clone())) - .expect("Test failed"); - - let got_events: [EthereumEvent; 2] = - shell.new_ethereum_events().try_into().expect("Test failed"); - let expected_events: Vec<_> = std::collections::BTreeSet::from([ - event_1.clone(), - event_3.clone(), - ]) - .into_iter() - .collect(); - assert_eq!(expected_events, got_events); - - // we cannot get two transfer to ethereum events within - // the same block height on ethereum. this is because we - // require a confirmation eth event on namada to increment - // the bridge pool nonce. this event should get ignored - tokio_test::block_on(oracle.send(event_2)).expect("Test failed"); - - // check that we queue and de-duplicate events - tokio_test::block_on(oracle.send(event_3.clone())) - .expect("Test failed"); - tokio_test::block_on(oracle.send(event_4.clone())) - .expect("Test failed"); - - let got_events: [EthereumEvent; 3] = - shell.new_ethereum_events().try_into().expect("Test failed"); - let expected_events: Vec<_> = - std::collections::BTreeSet::from([event_1, event_3, event_4]) - .into_iter() - .collect(); - assert_eq!(expected_events, got_events); - } - - /// Test that Ethereum events signed by a non-validator are rejected - #[test] - fn test_eth_events_must_be_signed_by_validator() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _, _, _) = setup_at_height(3u64); - let signing_key = gen_keypair(); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - #[allow(clippy::redundant_clone)] - let ethereum_events = ethereum_events::Vext { - ethereum_events: vec![EthereumEvent::TransfersToEthereum { - nonce: 0.into(), - transfers: vec![TransferToEthereum { - amount: 100.into(), - asset: EthAddress([1; 20]), - receiver: EthAddress([2; 20]), - checksum: Hash::default(), - }], - relayer: gen_established_address(), - }], - block_height: shell.get_current_decision_height(), - validator_addr: address.clone(), - } - .sign(&signing_key); - assert!( - validate_eth_events_vext::<_, _, governance::Store<_>>( - &shell.state, - ðereum_events, - shell.get_current_decision_height(), - ) - .is_err() - ) - } - - /// Test that validation of Ethereum events cast during the - /// previous block are accepted for the current block. This - /// should pass even if the epoch changed resulting in a - /// change to the validator set. - #[test] - fn test_validate_eth_events_vexts() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _recv, _, _oracle_control_recv) = setup_at_height(3u64); - let signing_key = - shell.mode.get_protocol_key().expect("Test failed").clone(); - let address = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - let signed_height = shell.get_current_decision_height(); - let vote_ext = ethereum_events::Vext { - ethereum_events: vec![EthereumEvent::TransfersToEthereum { - nonce: 0.into(), - transfers: vec![TransferToEthereum { - amount: 100.into(), - asset: EthAddress([1; 20]), - receiver: EthAddress([2; 20]), - checksum: Hash::default(), - }], - relayer: gen_established_address(), - }], - block_height: signed_height, - validator_addr: address, - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - - assert_eq!(shell.state.in_mem().get_current_epoch().0.0, 0); - // remove all validators of the next epoch - let validators_handle = consensus_validator_set_handle().at(&1.into()); - let consensus_in_mem = validators_handle - .iter(&shell.state) - .expect("Test failed") - .map(|val| { - let ( - NestedSubKey::Data { - key: stake, - nested_sub_key: SubKey::Data(position), - }, - .., - ) = val.expect("Test failed"); - (stake, position) - }) - .collect::>(); - for (val_stake, val_position) in consensus_in_mem.into_iter() { - validators_handle - .at(&val_stake) - .remove(&mut shell.state, &val_position) - .expect("Test failed"); - } - // we advance forward to the next epoch - let consensus_set: Vec = - read_consensus_validator_set_addresses_with_stake( - &shell.state, - Epoch::default(), - ) - .unwrap() - .into_iter() - .collect(); - - let params = - read_pos_params::<_, governance::Store<_>>(&shell.state).unwrap(); - let val1 = consensus_set[0].clone(); - let pkh1 = get_pkh_from_address( - &shell.state, - ¶ms, - val1.address.clone(), - Epoch::default(), - ); - let votes = vec![VoteInfo { - validator: crate::tendermint::abci::types::Validator { - address: pkh1, - power: (u128::try_from(val1.bonded_stake).expect("Test failed") - as u64) - .try_into() - .unwrap(), - }, - sig_info: - crate::tendermint::abci::types::BlockSignatureInfo::LegacySigned, - }]; - let req = FinalizeBlock { - proposer_address: pkh1.to_vec(), - decided_last_commit: crate::tendermint::abci::types::CommitInfo { - round: 0u8.into(), - votes, - }, - ..Default::default() - }; - assert_eq!(shell.start_new_epoch(Some(req)).0, 1); - assert!( - get_consensus_validator_from_protocol_pk::<_, GovStore<_>>( - &shell.state, - &signing_key.ref_to(), - None - ) - .unwrap() - .is_none() - ); - let prev_epoch = - Epoch(shell.state.in_mem().get_current_epoch().0.0 - 1); - assert!( - get_consensus_validator_from_protocol_pk::<_, GovStore<_>>( - &shell.state, - &signing_key.ref_to(), - Some(prev_epoch) - ) - .unwrap() - .is_some() - ); - - assert!( - validate_eth_events_vext::<_, _, governance::Store<_>>( - &shell.state, - &vote_ext, - signed_height - ) - .is_ok() - ); - } - - /// Test for ABCI++ that an [`ethereum_events::Vext`] that incorrectly - /// labels what block it was included on in a vote extension is - /// rejected. For ABCI+, test that it is rejected if the block height is - /// greater than latest block height. - #[test] - fn reject_incorrect_block_number() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _, _, _) = setup_at_height(3u64); - let address = shell.mode.get_validator_address().unwrap().clone(); - #[allow(clippy::redundant_clone)] - let mut ethereum_events = ethereum_events::Vext { - ethereum_events: vec![EthereumEvent::TransfersToEthereum { - nonce: 0.into(), - transfers: vec![TransferToEthereum { - amount: 100.into(), - asset: EthAddress([1; 20]), - receiver: EthAddress([2; 20]), - checksum: Hash::default(), - }], - relayer: gen_established_address(), - }], - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address.clone(), - }; - - ethereum_events.block_height = - shell.state.in_mem().get_last_block_height() + 1; - let signed_vext = ethereum_events - .sign(shell.mode.get_protocol_key().expect("Test failed")); - assert!( - validate_eth_events_vext::<_, _, governance::Store<_>>( - &shell.state, - &signed_vext, - shell.state.in_mem().get_last_block_height() - ) - .is_err() - ) - } - - /// Test if we reject Ethereum events vote extensions - /// issued at genesis - #[test] - fn test_reject_genesis_vexts() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _, _, _) = setup(); - let address = shell.mode.get_validator_address().unwrap().clone(); - #[allow(clippy::redundant_clone)] - let vote_ext = ethereum_events::Vext { - ethereum_events: vec![EthereumEvent::TransfersToEthereum { - nonce: 0.into(), - transfers: vec![TransferToEthereum { - amount: 100.into(), - asset: EthAddress([1; 20]), - receiver: EthAddress([2; 20]), - checksum: Hash::default(), - }], - relayer: gen_established_address(), - }], - block_height: shell.state.in_mem().get_last_block_height(), - validator_addr: address.clone(), - } - .sign(shell.mode.get_protocol_key().expect("Test failed")); - - assert!( - validate_eth_events_vext::<_, _, governance::Store<_>>( - &shell.state, - &vote_ext, - shell.state.in_mem().get_last_block_height() - ) - .is_err() - ) - } -} diff --git a/crates/node/src/shell/vote_extensions/val_set_update.rs b/crates/node/src/shell/vote_extensions/val_set_update.rs deleted file mode 100644 index e6eb001f7fc..00000000000 --- a/crates/node/src/shell/vote_extensions/val_set_update.rs +++ /dev/null @@ -1,418 +0,0 @@ -//! Extend Tendermint votes with validator set updates, to be relayed to -//! Namada's Ethereum bridge smart contracts. - -use namada_sdk::collections::HashMap; - -use super::*; - -impl Shell -where - D: DB + for<'iter> DBIter<'iter> + Sync + 'static, - H: StorageHasher + Sync + 'static, -{ - /// Takes an iterator over validator set update vote extension instances, - /// and returns another iterator. The latter yields - /// valid validator set update vote extensions, or the reason why these - /// are invalid, in the form of a `VoteExtensionError`. - #[inline] - pub fn validate_valset_upd_vext_list( - &self, - vote_extensions: impl IntoIterator - + 'static, - ) -> impl Iterator< - Item = std::result::Result< - validator_set_update::SignedVext, - VoteExtensionError, - >, - > + '_ { - vote_extensions.into_iter().map(|vote_extension| { - validate_valset_upd_vext::<_, _, governance::Store<_>>( - &self.state, - &vote_extension, - self.state.in_mem().get_current_epoch().0, - )?; - Ok(vote_extension) - }) - } - - /// Takes a list of signed validator set update vote extensions, - /// and filters out invalid instances. - #[inline] - pub fn filter_invalid_valset_upd_vexts( - &self, - vote_extensions: impl IntoIterator - + 'static, - ) -> impl Iterator + '_ { - self.validate_valset_upd_vext_list(vote_extensions) - .filter_map(|ext| ext.ok()) - } - - /// Compresses a set of signed validator set update vote extensions into a - /// single [`validator_set_update::VextDigest`], whilst filtering - /// invalid [`validator_set_update::SignedVext`] instances in the - /// process. - pub fn compress_valset_updates( - &self, - vote_extensions: Vec, - ) -> Option { - #[allow(clippy::question_mark)] - if self.state.in_mem().last_block.is_none() { - return None; - } - - let mut voting_powers = None; - let mut signatures = HashMap::new(); - - for validator_set_update::SignedVext(mut vote_extension) in - self.filter_invalid_valset_upd_vexts(vote_extensions) - { - if voting_powers.is_none() { - voting_powers = Some(std::mem::take( - &mut vote_extension.data.voting_powers, - )); - } - - let validator_addr = vote_extension.data.validator_addr; - let signing_epoch = vote_extension.data.signing_epoch; - - // register the signature of `validator_addr` - let addr = validator_addr.clone(); - let sig = vote_extension.sig.clone(); - - tracing::debug!( - ?sig, - ?signing_epoch, - %validator_addr, - "Inserting signature into validator_set_update::VextDigest" - ); - if let Some(existing_sig) = signatures.insert(addr, sig) { - tracing::warn!( - sig = ?vote_extension.sig, - ?existing_sig, - ?validator_addr, - ?signing_epoch, - "Overwrote old signature from validator while \ - constructing validator_set_update::VextDigest - maybe \ - private key of validator is being used by multiple nodes?" - ); - } - } - - let voting_powers = voting_powers.unwrap_or_default(); - - Some(validator_set_update::VextDigest { - signatures, - voting_powers, - }) - } -} - -#[allow(clippy::cast_possible_truncation)] -#[cfg(test)] -mod test_vote_extensions { - use namada_apps_lib::wallet; - use namada_sdk::eth_bridge::EthBridgeQueries; - use namada_sdk::eth_bridge::storage::eth_bridge_queries::is_bridge_comptime_enabled; - use namada_sdk::eth_bridge::test_utils::GovStore; - use namada_sdk::governance; - use namada_sdk::key::RefTo; - use namada_sdk::proof_of_stake::Epoch; - use namada_sdk::proof_of_stake::queries::get_consensus_validator_from_protocol_pk; - use namada_sdk::proof_of_stake::storage::{ - consensus_validator_set_handle, - read_consensus_validator_set_addresses_with_stake, read_pos_params, - }; - use namada_sdk::proof_of_stake::types::WeightedValidator; - use namada_sdk::state::collections::lazy_map::{NestedSubKey, SubKey}; - use namada_sdk::tendermint::abci::types::VoteInfo; - use namada_vote_ext::validator_set_update; - - use super::validate_valset_upd_vext; - use crate::shell::test_utils::{self, get_pkh_from_address}; - use crate::shims::abcipp_shim_types::shim::request::FinalizeBlock; - - /// Test if a [`validator_set_update::Vext`] that incorrectly labels what - /// epoch it was included on in a vote extension is rejected - #[test] - fn test_reject_incorrect_epoch() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _recv, _, _) = test_utils::setup(); - let validator_addr = - shell.mode.get_validator_address().unwrap().clone(); - - let eth_bridge_key = - shell.mode.get_eth_bridge_keypair().expect("Test failed"); - - let signing_epoch = shell.state.in_mem().get_current_epoch().0; - let next_epoch = signing_epoch.next(); - - let voting_powers = { - shell - .state - .ethbridge_queries() - .get_consensus_eth_addresses::>(next_epoch) - .map(|(eth_addr_book, _, voting_power)| { - (eth_addr_book, voting_power) - }) - .collect() - }; - #[allow(clippy::redundant_clone)] - let validator_set_update = validator_set_update::Vext { - voting_powers, - validator_addr: validator_addr.clone(), - // invalid epoch - signing_epoch: next_epoch, - } - .sign(eth_bridge_key); - assert!( - validate_valset_upd_vext::<_, _, governance::Store<_>>( - &shell.state, - &validator_set_update, - signing_epoch, - ) - .is_err() - ) - } - - /// Test that validator set update vote extensions signed by - /// a non-validator are rejected - #[test] - fn test_valset_upd_must_be_signed_by_validator() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _recv, _, _) = test_utils::setup(); - let (eth_bridge_key, _protocol_key, validator_addr) = { - let bertha_key = wallet::defaults::bertha_keypair(); - let bertha_addr = wallet::defaults::bertha_address(); - (test_utils::gen_secp256k1_keypair(), bertha_key, bertha_addr) - }; - let signing_epoch = shell.state.in_mem().get_current_epoch().0; - let voting_powers = { - let next_epoch = signing_epoch.next(); - shell - .state - .ethbridge_queries() - .get_consensus_eth_addresses::>(next_epoch) - .map(|(eth_addr_book, _, voting_power)| { - (eth_addr_book, voting_power) - }) - .collect() - }; - #[allow(clippy::redundant_clone)] - let validator_set_update = validator_set_update::Vext { - voting_powers, - signing_epoch, - validator_addr: validator_addr.clone(), - } - .sign(ð_bridge_key); - assert!( - validate_valset_upd_vext::<_, _, governance::Store<_>>( - &shell.state, - &validator_set_update, - signing_epoch, - ) - .is_err() - ); - } - - /// Test the validation of a validator set update emitted for - /// some epoch `E`. The test should pass even if the epoch - /// changed to some epoch `E': E' > E`, resulting in a - /// change to the validator set. - #[test] - fn test_validate_valset_upd_vexts() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (mut shell, _recv, _, _oracle_control_recv) = test_utils::setup(); - - // validators from the current epoch sign over validator - // set of the next epoch - let signing_epoch = shell.state.in_mem().get_current_epoch().0; - assert_eq!(signing_epoch.0, 0); - - // remove all validators of the next epoch - let validators_handle = consensus_validator_set_handle().at(&1.into()); - let consensus_in_mem = validators_handle - .iter(&shell.state) - .expect("Test failed") - .map(|val| { - let ( - NestedSubKey::Data { - key: stake, - nested_sub_key: SubKey::Data(position), - }, - .., - ) = val.expect("Test failed"); - (stake, position) - }) - .collect::>(); - for (val_stake, val_position) in consensus_in_mem.into_iter() { - validators_handle - .at(&val_stake) - .remove(&mut shell.state, &val_position) - .expect("Test failed"); - } - - // sign validator set update - let protocol_key = - shell.mode.get_protocol_key().expect("Test failed").clone(); - let eth_bridge_key = shell - .mode - .get_eth_bridge_keypair() - .expect("Test failed") - .clone(); - let validator_addr = shell - .mode - .get_validator_address() - .expect("Test failed") - .clone(); - let voting_powers = { - let next_epoch = signing_epoch.next(); - shell - .state - .ethbridge_queries() - .get_consensus_eth_addresses::>(next_epoch) - .map(|(eth_addr_book, _, voting_power)| { - (eth_addr_book, voting_power) - }) - .collect() - }; - let vote_ext = validator_set_update::Vext { - voting_powers, - signing_epoch, - validator_addr, - } - .sign(ð_bridge_key); - assert!(vote_ext.data.voting_powers.is_empty()); - - // we advance forward to the next epoch - let params = - read_pos_params::<_, governance::Store<_>>(&shell.state).unwrap(); - let mut consensus_set: Vec = - read_consensus_validator_set_addresses_with_stake( - &shell.state, - 0.into(), - ) - .unwrap() - .into_iter() - .collect(); - assert_eq!(consensus_set.len(), 1); - let val1 = consensus_set.remove(0); - let pkh1 = get_pkh_from_address( - &shell.state, - ¶ms, - val1.address, - Epoch::default(), - ); - let votes = vec![VoteInfo { - validator: crate::tendermint::abci::types::Validator { - address: pkh1, - power: (u128::try_from(val1.bonded_stake).expect("Test failed") - as u64) - .try_into() - .unwrap(), - }, - sig_info: - crate::tendermint::abci::types::BlockSignatureInfo::LegacySigned, - }]; - let req = FinalizeBlock { - proposer_address: pkh1.to_vec(), - decided_last_commit: crate::tendermint::abci::types::CommitInfo { - round: 0u8.into(), - votes, - }, - ..Default::default() - }; - assert_eq!(shell.start_new_epoch(Some(req)).0, 1); - assert!( - get_consensus_validator_from_protocol_pk::<_, GovStore<_>>( - &shell.state, - &protocol_key.ref_to(), - None - ) - .unwrap() - .is_none() - ); - let prev_epoch = shell.state.in_mem().get_current_epoch().0 - 1; - assert!( - get_consensus_validator_from_protocol_pk::<_, GovStore<_>>( - &shell.state, - &protocol_key.ref_to(), - Some(prev_epoch) - ) - .unwrap() - .is_some() - ); - - // check validation of the vext passes - assert!( - validate_valset_upd_vext::<_, _, governance::Store<_>>( - &shell.state, - &vote_ext, - signing_epoch - ) - .is_ok() - ); - } - - /// Test if a [`validator_set_update::Vext`] with an incorrect signature - /// is rejected - #[test] - fn test_reject_bad_signatures() { - if !is_bridge_comptime_enabled() { - // NOTE: this test doesn't work if the ethereum bridge - // is disabled at compile time. - return; - } - let (shell, _recv, _, _) = test_utils::setup(); - let validator_addr = - shell.mode.get_validator_address().unwrap().clone(); - - let eth_bridge_key = - shell.mode.get_eth_bridge_keypair().expect("Test failed"); - - let signing_epoch = shell.state.in_mem().get_current_epoch().0; - #[allow(clippy::redundant_clone)] - let validator_set_update = { - let voting_powers = { - let next_epoch = signing_epoch.next(); - shell - .state - .ethbridge_queries() - .get_consensus_eth_addresses::>( - next_epoch, - ) - .map(|(eth_addr_book, _, voting_power)| { - (eth_addr_book, voting_power) - }) - .collect() - }; - let mut ext = validator_set_update::Vext { - voting_powers, - signing_epoch, - validator_addr: validator_addr.clone(), - } - .sign(eth_bridge_key); - ext.0.sig = test_utils::invalidate_signature(ext.0.sig); - Some(ext) - }; - assert!( - validate_valset_upd_vext::<_, _, governance::Store<_>>( - &shell.state, - &validator_set_update.unwrap(), - signing_epoch, - ) - .is_err() - ); - } -} diff --git a/crates/node/src/shims/abcipp_shim.rs b/crates/node/src/shims/abcipp_shim.rs deleted file mode 100644 index 96fcc1f3543..00000000000 --- a/crates/node/src/shims/abcipp_shim.rs +++ /dev/null @@ -1,541 +0,0 @@ -use std::future::Future; -use std::path::PathBuf; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use futures::future::FutureExt; -use namada_apps_lib::state::DbError; -use namada_sdk::chain::BlockHeight; -use namada_sdk::hash::Hash; -use namada_sdk::migrations::ScheduledMigration; -use namada_sdk::state::ProcessProposalCachedResult; -use namada_sdk::tendermint::abci::response::ProcessProposal; -use namada_sdk::time::{DateTimeUtc, Utc}; -use namada_sdk::tx::data::hash_tx; -use tokio::sync::broadcast; -use tokio::sync::mpsc::UnboundedSender; -use tower::Service; - -use super::abcipp_shim_types::shim::request::{ - CheckProcessProposal, FinalizeBlock, ProcessedTx, -}; -use super::abcipp_shim_types::shim::{ - Error, Request, Response, TakeSnapshot, TxBytes, -}; -use crate::config; -use crate::config::{Action, ActionAtHeight}; -use crate::shell::{EthereumOracleChannels, Shell}; -use crate::storage::DbSnapshot; -use crate::tendermint::abci::{Request as Req, Response as Resp, request}; -use crate::tower_abci::BoxError; - -/// The shim wraps the shell, which implements ABCI++. -/// The shim makes a crude translation between the ABCI interface currently used -/// by tendermint and the shell's interface. -#[derive(Debug)] -pub struct AbcippShim { - service: Shell, - begin_block_request: Option, - delivered_txs: Vec, - shell_recv: std::sync::mpsc::Receiver<( - Req, - tokio::sync::oneshot::Sender>, - )>, - snapshot_task: Option>>, - snapshots_to_keep: u64, - namada_version: String, -} - -impl AbcippShim { - /// Create a shell with a ABCI service that passes messages to and from the - /// shell. - #[allow(clippy::too_many_arguments)] - pub fn new( - config: config::Ledger, - wasm_dir: PathBuf, - broadcast_sender: UnboundedSender>, - eth_oracle: Option, - db_cache: &rocksdb::Cache, - scheduled_migration: Option, - vp_wasm_compilation_cache: u64, - tx_wasm_compilation_cache: u64, - namada_version: String, - ) -> (Self, AbciService, broadcast::Sender<()>) { - // We can use an unbounded channel here, because tower-abci limits the - // the number of requests that can come in - - let (shell_send, shell_recv) = std::sync::mpsc::channel(); - let (server_shutdown, _) = broadcast::channel::<()>(1); - let action_at_height = config.shell.action_at_height.clone(); - let snapshots_to_keep = - config.shell.snapshots_to_keep.map(|n| n.get()).unwrap_or(1); - ( - Self { - service: Shell::new( - config, - wasm_dir, - broadcast_sender, - eth_oracle, - Some(db_cache), - scheduled_migration, - vp_wasm_compilation_cache, - tx_wasm_compilation_cache, - ), - begin_block_request: None, - delivered_txs: vec![], - shell_recv, - snapshot_task: None, - snapshots_to_keep, - namada_version, - }, - AbciService { - shell_send, - shutdown: server_shutdown.clone(), - action_at_height, - suspended: false, - }, - server_shutdown, - ) - } - - /// Get the hash of the txs in the block - pub fn get_hash(&self) -> Hash { - let bytes: Vec = - self.delivered_txs.iter().flat_map(Clone::clone).collect(); - hash_tx(bytes.as_slice()) - } - - /// Run the shell's blocking loop that receives messages from the - /// [`AbciService`]. - pub fn run(mut self) { - while let Ok((req, resp_sender)) = self.shell_recv.recv() { - let resp = match req { - Req::ProcessProposal(proposal) => self - .service - .call( - Request::ProcessProposal(proposal), - &self.namada_version, - ) - .map_err(Error::from) - .and_then(|resp| resp.try_into()), - Req::BeginBlock(block) => { - // we save this data to be forwarded to finalize later - self.begin_block_request = Some(block); - Ok(Resp::BeginBlock(Default::default())) - } - Req::DeliverTx(tx) => { - self.delivered_txs.push(tx.tx); - Ok(Resp::DeliverTx(Default::default())) - } - Req::EndBlock(_) => { - let begin_block_request = - self.begin_block_request.take().unwrap(); - - match self.get_process_proposal_result( - begin_block_request.clone(), - ) { - ProcessProposalCachedResult::Accepted(tx_results) => { - let mut txs = - Vec::with_capacity(self.delivered_txs.len()); - let delivered = - std::mem::take(&mut self.delivered_txs); - for (result, tx) in tx_results - .into_iter() - .zip(delivered.into_iter()) - { - txs.push(ProcessedTx { - tx, - result: result.into(), - }); - } - let mut end_block_request: FinalizeBlock = - begin_block_request.into(); - end_block_request.txs = txs; - self.service - .call(Request::FinalizeBlock(end_block_request), &self.namada_version) - .map_err(Error::from) - .and_then(|res| match res { - Response::FinalizeBlock(resp) => { - Ok(Resp::EndBlock(crate::tendermint_proto::abci::ResponseEndBlock::from(resp).try_into().unwrap())) - } - _ => Err(Error::ConvertResp(res)), - }) - } - ProcessProposalCachedResult::Rejected => { - Err(Error::Shell( - crate::shell::Error::RejectedBlockProposal, - )) - } - } - } - Req::Commit => match self - .service - .call(Request::Commit, &self.namada_version) - { - Ok(Response::Commit(res, take_snapshot)) => { - self.update_snapshot_task(take_snapshot); - Ok(Resp::Commit(res)) - } - Ok(resp) => Err(Error::ConvertResp(resp)), - Err(e) => Err(Error::Shell(e)), - }, - _ => match Request::try_from(req.clone()) { - Ok(request) => self - .service - .call(request, &self.namada_version) - .map(Resp::try_from) - .map_err(Error::Shell) - .and_then(|inner| inner), - Err(err) => Err(err), - }, - }; - - let resp = resp.map_err(|e| e.into()); - if resp_sender.send(resp).is_err() { - tracing::info!("ABCI response channel is closed") - } - } - } - - fn update_snapshot_task(&mut self, take_snapshot: TakeSnapshot) { - let snapshot_taken = - self.snapshot_task.as_ref().map(|t| t.is_finished()); - match snapshot_taken { - Some(true) => { - let task = self.snapshot_task.take().unwrap(); - match task.join() { - Ok(Err(e)) => tracing::error!( - "Failed to create snapshot with error: {:?}", - e - ), - Err(e) => tracing::error!( - "Failed to join thread creating snapshot: {:?}", - e - ), - _ => {} - } - } - Some(false) => { - // if a snapshot task is still running, - // we don't start a new one. This is not - // expected to happen if snapshots are spaced - // far enough apart. - tracing::warn!( - "Previous snapshot task was still running when a new \ - snapshot was scheduled" - ); - return; - } - _ => {} - } - - let TakeSnapshot::Yes(db_path, height) = take_snapshot else { - return; - }; - // Ensure that the DB is flushed before making a checkpoint - namada_sdk::state::DB::flush(self.service.state.db(), true).unwrap(); - let base_dir = self.service.base_dir.clone(); - - let (snap_send, snap_recv) = tokio::sync::oneshot::channel(); - - let snapshots_to_keep = self.snapshots_to_keep; - let snapshot_task = std::thread::spawn(move || { - let db = crate::storage::open(db_path, true, None) - .expect("Could not open DB"); - let snapshot = db.checkpoint(base_dir.clone(), height)?; - // signal to main thread that the snapshot has finished - snap_send.send(()).unwrap(); - DbSnapshot::cleanup(height, &base_dir, snapshots_to_keep) - .map_err(|e| DbError::DBError(e.to_string()))?; - snapshot - .package() - .map_err(|e| DbError::DBError(e.to_string())) - }); - - // it's important that the thread is - // blocked until the snapshot is created so that no writes - // happen to the db while snapshotting. We want the db frozen - // at this specific point in time. - if snap_recv.blocking_recv().is_err() { - tracing::error!("Failed to start snapshot task.") - } else { - self.snapshot_task.replace(snapshot_task); - } - } - - // Retrieve the cached result of process proposal for the given block or - // compute it if missing - fn get_process_proposal_result( - &mut self, - begin_block_request: request::BeginBlock, - ) -> ProcessProposalCachedResult { - match namada_sdk::hash::Hash::try_from(begin_block_request.hash) { - Ok(block_hash) => { - match self - .service - .state - .in_mem_mut() - .block_proposals_cache - .get(&block_hash) - { - // We already have the result of process proposal for - // this block cached in memory - Some(res) => res.to_owned(), - None => { - // Need to run process proposal to extract the data we - // need for finalize block (tx results) - let process_req = - CheckProcessProposal::from(begin_block_request) - .cast_to_tendermint_req( - self.delivered_txs.clone(), - ); - - let (process_resp, res) = - self.service.process_proposal(process_req.into()); - let result = if let ProcessProposal::Accept = - process_resp - { - ProcessProposalCachedResult::Accepted( - res.into_iter().map(|res| res.into()).collect(), - ) - } else { - ProcessProposalCachedResult::Rejected - }; - - // Cache the result - self.service - .state - .in_mem_mut() - .block_proposals_cache - .put(block_hash.to_owned(), result.clone()); - - result - } - } - } - Err(_) => { - // Need to run process proposal to extract the data we need for - // finalize block (tx results) - let process_req = - CheckProcessProposal::from(begin_block_request) - .cast_to_tendermint_req(self.delivered_txs.clone()); - - // Do not cache the result in this case since we - // don't have the hash of the block - let (process_resp, res) = - self.service.process_proposal(process_req.into()); - if let ProcessProposal::Accept = process_resp { - ProcessProposalCachedResult::Accepted( - res.into_iter().map(|res| res.into()).collect(), - ) - } else { - ProcessProposalCachedResult::Rejected - } - } - } - } -} - -/// Indicates how [`AbciService`] should -/// check whether or not it needs to take -/// action. -#[derive(Debug)] -enum CheckAction { - /// No check necessary. - NoAction, - /// Check a given block height. - Check(i64), - /// The action been taken. - AlreadySuspended, -} - -#[derive(Debug)] -pub struct AbciService { - /// A channel for forwarding requests to the shell - shell_send: std::sync::mpsc::Sender<( - Req, - tokio::sync::oneshot::Sender>, - )>, - /// Indicates if the consensus connection is suspended. - suspended: bool, - /// This resolves the non-completing futures returned to tower-abci - /// during suspension. - shutdown: broadcast::Sender<()>, - /// An action to be taken at a specified block height. - action_at_height: Option, -} - -impl AbciService { - /// Check if we are at a block height with a scheduled action. - /// If so, perform the action. - fn maybe_take_action( - action_at_height: Option, - check: CheckAction, - mut shutdown_recv: broadcast::Receiver<()>, - ) -> (bool, Option<>::Future>) { - let hght = match check { - CheckAction::AlreadySuspended => BlockHeight::from(u64::MAX), - CheckAction::Check(hght) => BlockHeight::from( - u64::try_from(hght).expect("Height cannot be negative"), - ), - CheckAction::NoAction => BlockHeight::default(), - }; - match action_at_height { - Some(ActionAtHeight { - height, - action: Action::Suspend, - }) if height <= hght => { - if height == hght { - tracing::info!( - "Reached block height {}, suspending.", - height - ); - tracing::warn!( - "\x1b[93mThis feature is intended for debugging \ - purposes. Note that on shutdown a spurious panic \ - message will be produced.\x1b[0m" - ) - } - ( - true, - Some( - async move { - shutdown_recv.recv().await.unwrap(); - Err(BoxError::from( - "Not all tendermint responses were processed. \ - If the `--suspended` flag was passed, you \ - may ignore this error.", - )) - } - .boxed(), - ), - ) - } - Some(ActionAtHeight { - height, - action: Action::Halt, - }) if height == hght => { - tracing::info!( - "Reached block height {}, halting the chain.", - height - ); - ( - false, - Some( - async move { - Err(BoxError::from(format!( - "Reached block height {}, halting the chain.", - height - ))) - } - .boxed(), - ), - ) - } - _ => (false, None), - } - } - - /// If we are not taking special action for this request, - /// forward it normally. - fn forward_request(&mut self, req: Req) -> >::Future { - let (resp_send, recv) = tokio::sync::oneshot::channel(); - let result = self.shell_send.send((req.clone(), resp_send)); - async move { - let genesis_time = if let Req::InitChain(init) = req { - Some( - DateTimeUtc::try_from(init.time) - .expect("Should be able to parse genesis time."), - ) - } else { - None - }; - if let Err(err) = result { - // The shell has shut-down - return Err(err.into()); - } - recv.await - .unwrap_or_else(|err| { - tracing::info!("ABCI response channel didn't respond"); - Err(err.into()) - }) - .inspect(|_| { - // emit a log line stating that we are sleeping until - // genesis. - #[allow(clippy::disallowed_methods)] - let now = Utc::now(); - if let Some(Ok(sleep_time)) = genesis_time - .map(|t| t.0.signed_duration_since(now).to_std()) - { - if !sleep_time.is_zero() { - tracing::info!( - "Waiting for ledger genesis time: {:?}, time \ - left: {:?}", - genesis_time.unwrap(), - sleep_time - ); - } - } - }) - } - .boxed() - } - - /// Given the type of request, determine if we need to check - /// to possibly take an action. - fn get_action(&self, req: &Req) -> Option { - match req { - Req::PrepareProposal(req) => { - Some(CheckAction::Check(req.height.into())) - } - Req::ProcessProposal(req) => { - Some(CheckAction::Check(req.height.into())) - } - Req::EndBlock(req) => Some(CheckAction::Check(req.height)), - Req::BeginBlock(_) - | Req::DeliverTx(_) - | Req::InitChain(_) - | Req::CheckTx(_) - | Req::Commit => { - if self.suspended { - Some(CheckAction::AlreadySuspended) - } else { - Some(CheckAction::NoAction) - } - } - _ => None, - } - } -} - -/// The ABCI tower service implementation sends and receives messages to and -/// from the [`AbcippShim`] for requests from Tendermint. -impl Service for AbciService { - type Error = BoxError; - type Future = - Pin> + Send + 'static>>; - type Response = Resp; - - fn poll_ready( - &mut self, - _cx: &mut Context<'_>, - ) -> Poll> { - // Nothing to check as the sender's channel is unbounded - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Req) -> Self::Future { - let action = self.get_action(&req); - if let Some(action) = action { - let (suspended, fut) = Self::maybe_take_action( - self.action_at_height.clone(), - action, - self.shutdown.subscribe(), - ); - self.suspended = suspended; - fut.unwrap_or_else(|| self.forward_request(req)) - } else { - self.forward_request(req) - } - } -} diff --git a/crates/node/src/shims/abcipp_shim_types.rs b/crates/node/src/shims/abcipp_shim_types.rs deleted file mode 100644 index b5d9690f63e..00000000000 --- a/crates/node/src/shims/abcipp_shim_types.rs +++ /dev/null @@ -1,380 +0,0 @@ -use crate::tendermint::abci::{Request, Response}; - -pub mod shim { - use std::fmt::Debug; - use std::path::PathBuf; - - use namada_sdk::state::BlockHeight; - use thiserror::Error; - - use super::{Request as Req, Response as Resp}; - use crate::shell; - use crate::tendermint::abci::{ - request as tm_request, response as tm_response, - }; - - pub type TxBytes = prost::bytes::Bytes; - - #[derive(Error, Debug)] - #[allow(clippy::large_enum_variant)] - pub enum Error { - #[error("Error converting Request from ABCI to ABCI++: {0:?}")] - ConvertReq(Req), - #[error("Error converting Response from ABCI++ to ABCI: {0:?}")] - ConvertResp(Response), - #[error("{0:?}")] - Shell(shell::Error), - } - - /// Errors from the shell need to be propagated upward - impl From for Error { - fn from(err: shell::Error) -> Self { - Self::Shell(err) - } - } - - #[derive(Debug, Clone)] - /// Indicate whether a state snapshot should be created - /// at a certain point in time - pub enum TakeSnapshot { - No, - Yes(PathBuf, BlockHeight), - } - - impl> From> - for TakeSnapshot - { - fn from(value: Option<(T, BlockHeight)>) -> Self { - match value { - None => TakeSnapshot::No, - Some(p) => TakeSnapshot::Yes(p.0.as_ref().to_path_buf(), p.1), - } - } - } - - #[allow(clippy::large_enum_variant)] - /// Our custom request types. It is the duty of the shim to change - /// the request types coming from tower-abci to these before forwarding - /// it to the shell - /// - /// Each request contains a custom payload type as well, which may - /// be simply a unit struct - pub enum Request { - InitChain(tm_request::InitChain), - Info(tm_request::Info), - Query(tm_request::Query), - PrepareProposal(tm_request::PrepareProposal), - #[allow(dead_code)] - VerifyHeader(request::VerifyHeader), - ProcessProposal(tm_request::ProcessProposal), - #[allow(dead_code)] - RevertProposal(request::RevertProposal), - FinalizeBlock(request::FinalizeBlock), - Commit, - Flush, - Echo(tm_request::Echo), - CheckTx(tm_request::CheckTx), - ListSnapshots, - OfferSnapshot(tm_request::OfferSnapshot), - LoadSnapshotChunk(tm_request::LoadSnapshotChunk), - ApplySnapshotChunk(tm_request::ApplySnapshotChunk), - } - - /// Attempt to convert a tower-abci request to an internal one - impl TryFrom for Request { - type Error = Error; - - fn try_from(req: Req) -> Result { - match req { - Req::InitChain(inner) => Ok(Request::InitChain(inner)), - Req::Info(inner) => Ok(Request::Info(inner)), - Req::Query(inner) => Ok(Request::Query(inner)), - Req::Commit => Ok(Request::Commit), - Req::Flush => Ok(Request::Flush), - Req::Echo(inner) => Ok(Request::Echo(inner)), - Req::CheckTx(inner) => Ok(Request::CheckTx(inner)), - Req::ListSnapshots => Ok(Request::ListSnapshots), - Req::OfferSnapshot(inner) => Ok(Request::OfferSnapshot(inner)), - Req::LoadSnapshotChunk(inner) => { - Ok(Request::LoadSnapshotChunk(inner)) - } - Req::ApplySnapshotChunk(inner) => { - Ok(Request::ApplySnapshotChunk(inner)) - } - Req::PrepareProposal(inner) => { - Ok(Request::PrepareProposal(inner)) - } - _ => Err(Error::ConvertReq(req)), - } - } - } - - /// Custom response types. - /// - /// These will be returned by the shell along with - /// custom payload types (which may be unit structs). It is the duty of - /// the shim to convert these to responses understandable to tower-abci - #[derive(Debug)] - pub enum Response { - InitChain(tm_response::InitChain), - Info(tm_response::Info), - Query(tm_response::Query), - PrepareProposal(response::PrepareProposal), - VerifyHeader(response::VerifyHeader), - ProcessProposal(response::ProcessProposal), - RevertProposal(response::RevertProposal), - FinalizeBlock(response::FinalizeBlock), - EndBlock(tm_response::EndBlock), - Commit(tm_response::Commit, TakeSnapshot), - Flush, - Echo(tm_response::Echo), - CheckTx(tm_response::CheckTx), - ListSnapshots(tm_response::ListSnapshots), - OfferSnapshot(tm_response::OfferSnapshot), - LoadSnapshotChunk(tm_response::LoadSnapshotChunk), - ApplySnapshotChunk(tm_response::ApplySnapshotChunk), - } - - /// Attempt to convert response from shell to a tower-abci response type - impl TryFrom for Resp { - type Error = Error; - - fn try_from(resp: Response) -> Result { - match resp { - Response::InitChain(inner) => Ok(Resp::InitChain(inner)), - Response::Info(inner) => Ok(Resp::Info(inner)), - Response::Query(inner) => Ok(Resp::Query(inner)), - Response::Commit(inner, _) => Ok(Resp::Commit(inner)), - Response::Flush => Ok(Resp::Flush), - Response::Echo(inner) => Ok(Resp::Echo(inner)), - Response::CheckTx(inner) => Ok(Resp::CheckTx(inner)), - Response::ListSnapshots(inner) => { - Ok(Resp::ListSnapshots(inner)) - } - Response::OfferSnapshot(inner) => { - Ok(Resp::OfferSnapshot(inner)) - } - Response::LoadSnapshotChunk(inner) => { - Ok(Resp::LoadSnapshotChunk(inner)) - } - Response::ApplySnapshotChunk(inner) => { - Ok(Resp::ApplySnapshotChunk(inner)) - } - Response::PrepareProposal(inner) => { - Ok(Resp::PrepareProposal(inner)) - } - Response::ProcessProposal(inner) => { - Ok(Resp::ProcessProposal(inner)) - } - _ => Err(Error::ConvertResp(resp)), - } - } - } - - /// Custom types for request payloads - pub mod request { - - use bytes::Bytes; - use namada_sdk::hash::Hash; - use namada_sdk::storage::BlockHeader; - use namada_sdk::tendermint::abci::types::CommitInfo; - use namada_sdk::tendermint::account::Id; - use namada_sdk::tendermint::block::Height; - use namada_sdk::tendermint::time::Time; - use namada_sdk::time::DateTimeUtc; - - use crate::tendermint::abci::request as tm_request; - use crate::tendermint::abci::types::Misbehavior; - - pub struct VerifyHeader; - - pub struct RevertProposal; - - /// A Tx and the result of calling Process Proposal on it - #[derive(Debug, Clone)] - pub struct ProcessedTx { - pub tx: super::TxBytes, - pub result: super::response::TxResult, - } - - #[derive(Debug, Clone)] - pub struct FinalizeBlock { - pub header: BlockHeader, - pub block_hash: Hash, - pub byzantine_validators: Vec, - pub txs: Vec, - pub proposer_address: Vec, - pub height: Height, - pub decided_last_commit: CommitInfo, - } - - // Type to run process proposal checks outside of the CometBFT call - pub(crate) struct CheckProcessProposal { - proposed_last_commit: Option, - misbehavior: Vec, - hash: namada_sdk::tendermint::Hash, - height: Height, - time: Time, - next_validators_hash: namada_sdk::tendermint::Hash, - proposer_address: Id, - } - - impl From for FinalizeBlock { - fn from(req: tm_request::BeginBlock) -> FinalizeBlock { - let header = req.header; - FinalizeBlock { - header: BlockHeader { - #[allow(clippy::disallowed_methods)] - hash: Hash::try_from(header.app_hash.as_bytes()) - .unwrap_or_default(), - time: DateTimeUtc::try_from(header.time).unwrap(), - next_validators_hash: header - .next_validators_hash - .try_into() - .unwrap(), - }, - block_hash: req.hash.try_into().unwrap(), - byzantine_validators: req.byzantine_validators, - txs: vec![], - proposer_address: header.proposer_address.into(), - height: header.height, - decided_last_commit: req.last_commit_info, - } - } - } - - impl From for CheckProcessProposal { - fn from(req: tm_request::BeginBlock) -> CheckProcessProposal { - let header = req.header; - CheckProcessProposal { - proposed_last_commit: Some(req.last_commit_info), - misbehavior: req.byzantine_validators, - hash: req.hash, - height: header.height, - time: header.time, - next_validators_hash: header.next_validators_hash, - proposer_address: header.proposer_address, - } - } - } - - impl CheckProcessProposal { - pub(crate) fn cast_to_tendermint_req( - self, - txs: Vec, - ) -> tm_request::ProcessProposal { - let Self { - proposed_last_commit, - misbehavior, - hash, - height, - time, - next_validators_hash, - proposer_address, - } = self; - - tm_request::ProcessProposal { - txs, - proposed_last_commit, - misbehavior, - hash, - height, - time, - next_validators_hash, - proposer_address, - } - } - } - - impl FinalizeBlock { - #[allow(clippy::result_large_err)] - pub(crate) fn cast_to_process_proposal_req( - self, - ) -> Result { - let header = self.header; - Ok(tm_request::ProcessProposal { - txs: self.txs.into_iter().map(|tx| tx.tx).collect(), - proposed_last_commit: Some(self.decided_last_commit), - misbehavior: self.byzantine_validators, - hash: self.block_hash.into(), - height: self.height, - time: header.time.try_into().map_err(|_| { - super::Error::Shell( - super::shell::Error::InvalidBlockProposal, - ) - })?, - next_validators_hash: header.next_validators_hash.into(), - proposer_address: self - .proposer_address - .try_into() - .map_err(|_| { - super::Error::Shell( - super::shell::Error::InvalidBlockProposal, - ) - })?, - }) - } - } - } - - /// Custom types for response payloads - pub mod response { - use namada_sdk::events::Event; - - pub use crate::tendermint::abci::response::{ - PrepareProposal, ProcessProposal, - }; - use crate::tendermint_proto::abci::{ - Event as TmEvent, ValidatorUpdate, - }; - use crate::tendermint_proto::types::ConsensusParams; - - #[derive(Debug, Default)] - pub struct VerifyHeader; - - #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] - pub struct TxResult { - pub code: u32, - pub info: String, - } - - #[derive(Debug, Default)] - pub struct RevertProposal; - - #[derive(Debug, Default)] - pub struct FinalizeBlock { - pub events: Vec, - pub validator_updates: Vec, - pub consensus_param_updates: Option, - } - - impl From for crate::tendermint_proto::abci::ResponseEndBlock { - fn from(resp: FinalizeBlock) -> Self { - Self { - events: resp - .events - .into_iter() - .map(TmEvent::from) - .collect(), - validator_updates: resp.validator_updates, - consensus_param_updates: resp.consensus_param_updates, - } - } - } - - impl From<(u32, String)> for TxResult { - fn from(value: (u32, String)) -> Self { - Self { - code: value.0, - info: value.1, - } - } - } - - impl From for (u32, String) { - fn from(value: TxResult) -> Self { - (value.code, value.info) - } - } - } -} diff --git a/crates/node/src/shims/mod.rs b/crates/node/src/shims/mod.rs deleted file mode 100644 index 4558a543e94..00000000000 --- a/crates/node/src/shims/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod abcipp_shim; -pub mod abcipp_shim_types; diff --git a/crates/node/src/storage/mod.rs b/crates/node/src/storage/mod.rs index b00111b2a29..2db2137a5ce 100644 --- a/crates/node/src/storage/mod.rs +++ b/crates/node/src/storage/mod.rs @@ -59,13 +59,9 @@ mod tests { use namada_sdk::borsh::BorshDeserialize; use namada_sdk::chain::{BlockHeight, ChainId}; use namada_sdk::collections::HashMap; - use namada_sdk::eth_bridge::storage::bridge_pool; - use namada_sdk::eth_bridge::storage::proof::BridgePoolRootProof; - use namada_sdk::ethereum_events::Uint; use namada_sdk::gas::STORAGE_ACCESS_GAS_PER_BYTE; use namada_sdk::hash::Hash; use namada_sdk::ibc::storage::{client_counter_key, ibc_key, is_ibc_key}; - use namada_sdk::keccak::KeccakHash; use namada_sdk::parameters::Parameters; use namada_sdk::state::merkle_tree::NO_DIFF_KEY_PREFIX; use namada_sdk::state::{ @@ -478,11 +474,6 @@ mod tests { let value_bytes = encode(&state.in_mem().block.height); state.db_write(&key, value_bytes)?; } - let key = bridge_pool::get_signed_root_key(); - let root_proof = - BridgePoolRootProof::new((KeccakHash::default(), Uint::default())); - let bytes = encode(&root_proof); - state.db_write(&key, bytes)?; // Update and commit let height = BlockHeight(1); @@ -509,22 +500,42 @@ mod tests { state.in_mem_mut().begin_block(next_height)?; batch = PersistentState::batch(); } + let persist_diffs = (state.diff_key_filter)(&key); + let tree_key = if !persist_diffs { + let prefix = + Key::from(NO_DIFF_KEY_PREFIX.to_string().to_db_key()); + prefix.join(&key) + } else { + key.clone() + }; match write_type { 0 => { // no update } 1 => { + state.in_mem_mut().block.tree.delete(&tree_key)?; state.db_delete(&key)?; } 2 => { let value_bytes = encode(&state.in_mem().block.height); + state + .in_mem_mut() + .block + .tree + .update(&tree_key, &value_bytes)?; state.db_write(&key, value_bytes)?; } 3 => { + state.in_mem_mut().block.tree.delete(&tree_key)?; state.batch_delete_subspace_val(&mut batch, &key)?; } _ => { let value_bytes = encode(&state.in_mem().block.height); + state + .in_mem_mut() + .block + .tree + .update(&tree_key, &value_bytes)?; state.batch_write_subspace_val( &mut batch, &key, @@ -648,9 +659,6 @@ mod tests { is_key_diff_storable, ); let new_epoch_start = BlockHeight(1); - let signed_root_key = bridge_pool::get_signed_root_key(); - // the first nonce isn't written for a test skipping pruning - let nonce = Uint::default(); state .in_mem_mut() @@ -679,9 +687,6 @@ mod tests { let value: u64 = 2; state.db_write(&key, encode(&value)).expect("write failed"); - // the second nonce isn't written for a test skipping pruning - let nonce = nonce + 1; - state.in_mem_mut().block.epoch = state.in_mem().block.epoch.next(); state .in_mem_mut() @@ -700,12 +705,6 @@ mod tests { .begin_block(new_epoch_start) .expect("begin_block failed"); - let nonce = nonce + 1; - let root_proof = - BridgePoolRootProof::new((KeccakHash::default(), nonce)); - let bytes = encode(&root_proof); - state.db_write(&signed_root_key, bytes).unwrap(); - state.in_mem_mut().block.epoch = state.in_mem().block.epoch.next(); state .in_mem_mut() @@ -724,20 +723,12 @@ mod tests { ); let result = state.get_merkle_tree(6.into(), Some(StoreType::Ibc)); assert!(result.is_ok(), "The ibc tree should be restored"); - let result = - state.get_merkle_tree(6.into(), Some(StoreType::BridgePool)); - assert!(result.is_ok(), "The bridge pool tree should be restored"); state .in_mem_mut() .begin_block(BlockHeight(12)) .expect("begin_block failed"); - let nonce = nonce + 1; - let root_proof = - BridgePoolRootProof::new((KeccakHash::default(), nonce)); - let bytes = encode(&root_proof); - state.db_write(&signed_root_key, bytes).unwrap(); state.in_mem_mut().block.epoch = state.in_mem().block.epoch.next(); state .in_mem_mut() @@ -751,9 +742,6 @@ mod tests { let result = state.get_merkle_tree(6.into(), Some(StoreType::Ibc)); assert!(result.is_ok(), "The ibc tree should be restored"); // bridge pool tree should be pruned because of the nonce - let result = - state.get_merkle_tree(6.into(), Some(StoreType::BridgePool)); - assert!(result.is_err(), "The bridge pool tree should be pruned"); let result = state.get_merkle_tree(10.into(), Some(StoreType::NoDiff)); assert!(result.is_err(), "The tree at Height 10 should be pruned"); @@ -911,6 +899,7 @@ mod tests { assert_eq!(res, val2); // Commit block and storage changes + state.pre_commit_block().unwrap(); state.commit_block().unwrap(); state.in_mem_mut().block.height = state.in_mem_mut().block.height.next_height(); @@ -970,6 +959,7 @@ mod tests { // Delete the data then commit the block state.delete(&key1).unwrap(); state.delete(&key2).unwrap(); + state.pre_commit_block().unwrap(); state.commit_block().unwrap(); state.in_mem_mut().block.height = state.in_mem().block.height.next_height(); diff --git a/crates/node/src/storage/rocksdb.rs b/crates/node/src/storage/rocksdb.rs index 35e77f85e65..d0c48fc6304 100644 --- a/crates/node/src/storage/rocksdb.rs +++ b/crates/node/src/storage/rocksdb.rs @@ -2,10 +2,6 @@ //! //! The current storage tree is: //! - `state`: the latest ledger state -//! - `ethereum_height`: the height of the last eth block processed by the -//! oracle -//! - `eth_events_queue`: a queue of confirmed ethereum events to be processed -//! in order //! - `height`: the last committed block height //! - `next_epoch_min_start_height`: minimum block height from which the next //! epoch can start @@ -57,8 +53,6 @@ use namada_replay_protection as replay_protection; use namada_sdk::arith::checked; use namada_sdk::borsh::{BorshDeserialize, BorshSerialize, BorshSerializeExt}; use namada_sdk::collections::HashSet; -use namada_sdk::eth_bridge::storage::bridge_pool; -use namada_sdk::eth_bridge::storage::proof::BridgePoolRootProof; use namada_sdk::gas::Gas; use namada_sdk::hash::Hash; use namada_sdk::state::merkle_tree::{ @@ -74,7 +68,7 @@ use namada_sdk::storage::{ Epoch, Key, KeySeg, REPLAY_PROTECTION_CF, ROLLBACK_CF, STATE_CF, SUBSPACE_CF, }; -use namada_sdk::{decode, encode, ethereum_events}; +use namada_sdk::{decode, encode}; use rayon::prelude::*; use regex::Regex; use rocksdb::{ @@ -97,8 +91,6 @@ const NEXT_EPOCH_MIN_START_TIME_KEY: &str = "next_epoch_min_start_time"; const UPDATE_EPOCH_BLOCKS_DELAY_KEY: &str = "update_epoch_blocks_delay"; const COMMIT_ONLY_DATA_KEY: &str = "commit_only_data_commitment"; const CONVERSION_STATE_KEY: &str = "conversion_state"; -const ETHEREUM_HEIGHT_KEY: &str = "ethereum_height"; -const ETH_EVENTS_QUEUE_KEY: &str = "eth_events_queue"; const RESULTS_KEY_PREFIX: &str = "results"; const PRED_KEY_PREFIX: &str = "pred"; @@ -1189,18 +1181,6 @@ impl DB for RocksDB { None => return Ok(None), }; - let ethereum_height = - match self.read_value(state_cf, ETHEREUM_HEIGHT_KEY)? { - Some(h) => h, - None => return Ok(None), - }; - - let eth_events_queue = - match self.read_value(state_cf, ETH_EVENTS_QUEUE_KEY)? { - Some(q) => q, - None => return Ok(None), - }; - // Block results let results_key = format!("{RESULTS_KEY_PREFIX}/{}", height.raw()); let results = match self.read_value(block_cf, results_key)? { @@ -1250,8 +1230,6 @@ impl DB for RocksDB { next_epoch_min_start_time, update_epoch_blocks_delay, address_gen, - ethereum_height, - eth_events_queue, commit_only_data, })) } @@ -1275,8 +1253,6 @@ impl DB for RocksDB { address_gen, results, conversion_state, - ethereum_height, - eth_events_queue, commit_only_data, }: BlockStateWrite<'_> = state; @@ -1320,19 +1296,6 @@ impl DB for RocksDB { )?; } - self.add_value_to_batch( - state_cf, - ETHEREUM_HEIGHT_KEY, - ðereum_height, - batch, - ); - self.add_value_to_batch( - state_cf, - ETH_EVENTS_QUEUE_KEY, - ð_events_queue, - batch, - ); - let block_cf = self.get_column_family(BLOCK_CF)?; let prefix = height.raw(); @@ -1691,27 +1654,6 @@ impl DB for RocksDB { Ok(()) } - fn read_bridge_pool_signed_nonce( - &self, - height: BlockHeight, - last_height: BlockHeight, - ) -> Result> { - let nonce_key = bridge_pool::get_signed_root_key(); - let bytes = if height == BlockHeight(0) || height >= last_height { - self.read_subspace_val(&nonce_key)? - } else { - self.read_subspace_val_with_height(&nonce_key, height, last_height)? - }; - match bytes { - Some(bytes) => { - let bp_root_proof = BridgePoolRootProof::try_from_slice(&bytes) - .map_err(Error::BorshCodingError)?; - Ok(Some(bp_root_proof.data.1)) - } - None => Ok(None), - } - } - fn write_replay_protection_entry( &mut self, batch: &mut Self::WriteBatch, @@ -2278,7 +2220,7 @@ mod test { use namada_sdk::state::{MerkleTree, Sha256Hasher}; use namada_sdk::storage::conversion_state::ConversionState; use namada_sdk::storage::types::CommitOnlyData; - use namada_sdk::storage::{BlockResults, Epochs, EthEventsQueue}; + use namada_sdk::storage::{BlockResults, Epochs}; use namada_sdk::time::DateTimeUtc; use tempfile::tempdir; @@ -2810,7 +2752,6 @@ mod test { let update_epoch_blocks_delay = None; let address_gen = EstablishedAddressGen::new("whatever"); let results = BlockResults::default(); - let eth_events_queue = EthEventsQueue::default(); let commit_only_data = CommitOnlyData::default(); let block = BlockStateWrite { merkle_tree_stores, @@ -2825,8 +2766,6 @@ mod test { next_epoch_min_start_time, update_epoch_blocks_delay, address_gen: &address_gen, - ethereum_height: None, - eth_events_queue: ð_events_queue, commit_only_data: &commit_only_data, }; diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 80ef5190ca7..adc949382fb 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -23,7 +23,6 @@ mainnet = [ multicore = ["namada_token/multicore"] std = ["fd-lock", "download-params", "namada_token/std", "namada_wallet/std"] async-send = ["namada_io/async-send"] -namada-eth-bridge = ["namada_ethereum_bridge/namada-eth-bridge"] benches = ["namada_core/benches", "namada_core/testing", "namada_state/benches"] wasm-runtime = ["namada_vm/wasm-runtime"] historic-masp = ["namada_token/historic-masp"] @@ -32,7 +31,6 @@ testing = [ "masp_primitives/test-dependencies", "namada_account/testing", "namada_core/testing", - "namada_ethereum_bridge/testing", "namada_governance/testing", "namada_ibc/testing", "namada_parameters/testing", @@ -57,7 +55,6 @@ migrations = [ "namada_migrations", "namada_account/migrations", "namada_core/migrations", - "namada_ethereum_bridge/migrations", "namada_events/migrations", "namada_governance/migrations", "namada_proof_of_stake/migrations", @@ -65,7 +62,6 @@ migrations = [ "namada_storage/migrations", "namada_token/migrations", "namada_tx/migrations", - "namada_vote_ext/migrations", "namada_gas/migrations", "linkme", ] @@ -73,7 +69,6 @@ migrations = [ [dependencies] namada_account.workspace = true namada_core.workspace = true -namada_ethereum_bridge.workspace = true namada_events.workspace = true namada_gas.workspace = true namada_governance.workspace = true @@ -88,7 +83,6 @@ namada_storage.workspace = true namada_token = { workspace = true, features = ["masp", "masp-validation"] } namada_tx.workspace = true namada_vm = { workspace = true, default-features = false } -namada_vote_ext.workspace = true namada_vp.workspace = true namada_wallet.workspace = true @@ -150,7 +144,6 @@ getrandom = { workspace = true, features = ["wasm_js"] } [dev-dependencies] namada_account = { path = "../account", features = ["testing"] } namada_core = { path = "../core", features = ["rand", "testing"] } -namada_ethereum_bridge = { path = "../ethereum_bridge", features = ["testing"] } namada_governance = { path = "../governance", features = ["testing"] } namada_ibc = { path = "../ibc", features = ["testing"] } namada_parameters.path = "../parameters" @@ -160,7 +153,6 @@ namada_storage = { path = "../storage", features = ["testing"] } namada_token = { path = "../token", features = ["testing", "masp"] } namada_tx = { path = "../tx", features = ["testing"] } namada_vm.path = "../vm" -namada_vote_ext.path = "../vote_ext" namada_vp.path = "../vp" assert_matches.workspace = true diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index d70e035bc58..3d40147d09e 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -11,10 +11,7 @@ use masp_primitives::transaction::components::sapling::builder::BuildParams; use masp_primitives::zip32::PseudoExtendedKey; use namada_core::address::{Address, MASP}; use namada_core::chain::{BlockHeight, ChainId, Epoch}; -use namada_core::collections::HashMap; use namada_core::dec::Dec; -use namada_core::ethereum_events::EthAddress; -use namada_core::keccak::KeccakHash; use namada_core::key::{SchemeType, common}; use namada_core::masp::{DiversifierIndex, MaspEpoch, PaymentAddress}; use namada_core::string_encoding::StringEncoded; @@ -33,7 +30,6 @@ use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; use crate::error::Error; -use crate::eth_bridge::bridge_pool; use crate::ibc::core::host::types::identifiers::{ChannelId, PortId}; use crate::ibc::{NamadaMemo, NamadaMemoData}; use crate::rpc::{ @@ -74,8 +70,6 @@ pub trait NamadaTypes: Clone + std::fmt::Debug { type ConfigRpcTendermintAddress: Clone + std::fmt::Debug + From; - /// Represents the address of an Ethereum endpoint - type EthereumAddress: Clone + std::fmt::Debug; /// Represents a shielded viewing key type ViewingKey: Clone + std::fmt::Debug; /// Represents a shielded spending key @@ -96,8 +90,6 @@ pub trait NamadaTypes: Clone + std::fmt::Debug { type TransferTarget: Clone + std::fmt::Debug; /// Represents some data that is used in a transaction type Data: Clone + std::fmt::Debug; - /// Bridge pool recommendations conversion rates table. - type BpConversionTable: Clone + std::fmt::Debug; /// Address of a `namada-masp-indexer` live instance type MaspIndexerAddress: Clone + std::fmt::Debug; /// Represents a block height @@ -108,28 +100,15 @@ pub trait NamadaTypes: Clone + std::fmt::Debug { #[derive(Clone, Debug)] pub struct SdkTypes; -/// An entry in the Bridge pool recommendations conversion -/// rates table. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BpConversionTableEntry { - /// An alias for the token, or the string representation - /// of its address if none is available. - pub alias: String, - /// Conversion rate from the given token to gwei. - pub conversion_rate: f64, -} - impl NamadaTypes for SdkTypes { type AddrOrNativeToken = Address; type Address = Address; type BalanceOwner = namada_core::masp::BalanceOwner; type BlockHeight = namada_core::chain::BlockHeight; - type BpConversionTable = HashMap; type ConfigRpcTendermintAddress = tendermint_rpc::Url; type Data = Vec; type DatedSpendingKey = DatedSpendingKey; type DatedViewingKey = DatedViewingKey; - type EthereumAddress = (); type Keypair = namada_core::key::common::SecretKey; type MaspIndexerAddress = String; type PaymentAddress = namada_core::masp::PaymentAddress; @@ -2982,235 +2961,6 @@ pub struct PayAddressGen { pub diversifier_index: Option, } -/// Bridge pool batch recommendation. -#[derive(Clone, Debug)] -pub struct RecommendBatch { - /// The query parameters. - pub query: Query, - /// The maximum amount of gas to spend. - pub max_gas: Option, - /// An optional parameter indicating how much net - /// gas the relayer is willing to pay. - pub gas: Option, - /// Bridge pool recommendations conversion rates table. - pub conversion_table: C::BpConversionTable, -} - -/// A transfer to be added to the Ethereum bridge pool. -#[derive(Clone, Debug)] -pub struct EthereumBridgePool { - /// Whether the transfer is for a NUT. - /// - /// By default, we add wrapped ERC20s onto the - /// Bridge pool. - pub nut: bool, - /// The args for building a tx to the bridge pool - pub tx: Tx, - /// The type of token - pub asset: EthAddress, - /// The recipient address - pub recipient: EthAddress, - /// The sender of the transfer - pub sender: C::Address, - /// The amount to be transferred - pub amount: InputAmount, - /// The amount of gas fees - pub fee_amount: InputAmount, - /// The account of fee payer. - /// - /// If unset, it is the same as the sender. - pub fee_payer: Option, - /// The token in which the gas is being paid - pub fee_token: C::AddrOrNativeToken, - /// Path to the tx WASM code file - pub code_path: PathBuf, -} - -impl TxBuilder for EthereumBridgePool { - fn tx(self, func: F) -> Self - where - F: FnOnce(Tx) -> Tx, - { - EthereumBridgePool { - tx: func(self.tx), - ..self - } - } -} - -impl EthereumBridgePool { - /// Whether the transfer is for a NUT. - /// - /// By default, we add wrapped ERC20s onto the - /// Bridge pool. - pub fn nut(self, nut: bool) -> Self { - Self { nut, ..self } - } - - /// The type of token - pub fn asset(self, asset: EthAddress) -> Self { - Self { asset, ..self } - } - - /// The recipient address - pub fn recipient(self, recipient: EthAddress) -> Self { - Self { recipient, ..self } - } - - /// The sender of the transfer - pub fn sender(self, sender: C::Address) -> Self { - Self { sender, ..self } - } - - /// The amount to be transferred - pub fn amount(self, amount: InputAmount) -> Self { - Self { amount, ..self } - } - - /// The amount of gas fees - pub fn fee_amount(self, fee_amount: InputAmount) -> Self { - Self { fee_amount, ..self } - } - - /// The account of fee payer. - /// - /// If unset, it is the same as the sender. - pub fn fee_payer(self, fee_payer: C::Address) -> Self { - Self { - fee_payer: Some(fee_payer), - ..self - } - } - - /// The token in which the gas is being paid - pub fn fee_token(self, fee_token: C::Address) -> Self { - Self { - fee_token: fee_token.into(), - ..self - } - } - - /// Path to the tx WASM code file - pub fn code_path(self, code_path: PathBuf) -> Self { - Self { code_path, ..self } - } -} - -impl EthereumBridgePool { - /// Build a transaction from this builder - pub async fn build( - self, - context: &impl Namada, - ) -> crate::error::Result<(namada_tx::Tx, SigningData)> { - bridge_pool::build_bridge_pool_tx(context, self).await - } -} - -/// Bridge pool proof arguments. -#[derive(Debug, Clone)] -pub struct BridgePoolProof { - /// The address of the ledger node as host:port - pub ledger_address: C::TendermintAddress, - /// The keccak hashes of transfers to - /// acquire a proof of. - pub transfers: Vec, - /// The address of the node responsible for relaying - /// the transfers. - /// - /// This node will receive the gas fees escrowed in - /// the Bridge pool, to compensate the Ethereum relay - /// procedure. - pub relayer: Address, -} - -/// Arguments to an Ethereum Bridge pool relay operation. -#[derive(Debug, Clone)] -pub struct RelayBridgePoolProof { - /// The address of the ledger node as host:port - pub ledger_address: C::TendermintAddress, - /// The hashes of the transfers to be relayed - pub transfers: Vec, - /// The Namada address for receiving fees for relaying - pub relayer: Address, - /// The number of confirmations to wait for on Ethereum - pub confirmations: u64, - /// The Ethereum RPC endpoint. - pub eth_rpc_endpoint: C::EthereumAddress, - /// The Ethereum gas that can be spent during - /// the relay call. - pub gas: Option, - /// The price of Ethereum gas, during the - /// relay call. - pub gas_price: Option, - /// The address of the Ethereum wallet to pay the gas fees. - /// If unset, the default wallet is used. - pub eth_addr: Option, - /// Synchronize with the network, or exit immediately, - /// if the Ethereum node has fallen behind. - pub sync: bool, -} - -/// Bridge validator set arguments. -#[derive(Debug, Clone)] -pub struct BridgeValidatorSet { - /// The address of the ledger node as host:port - pub ledger_address: C::TendermintAddress, - /// The epoch to query. - pub epoch: Option, -} - -/// Governance validator set arguments. -#[derive(Debug, Clone)] -pub struct GovernanceValidatorSet { - /// The address of the ledger node as host:port - pub ledger_address: C::TendermintAddress, - /// The epoch to query. - pub epoch: Option, -} - -/// Validator set proof arguments. -#[derive(Debug, Clone)] -pub struct ValidatorSetProof { - /// The address of the ledger node as host:port - pub ledger_address: C::TendermintAddress, - /// The epoch to query. - pub epoch: Option, -} - -/// Validator set update relayer arguments. -#[derive(Debug, Clone)] -pub struct ValidatorSetUpdateRelay { - /// Run in daemon mode, which will continuously - /// perform validator set updates. - pub daemon: bool, - /// The address of the ledger node as host:port - pub ledger_address: C::TendermintAddress, - /// The number of block confirmations on Ethereum. - pub confirmations: u64, - /// The Ethereum RPC endpoint. - pub eth_rpc_endpoint: C::EthereumAddress, - /// The epoch of the validator set to relay. - pub epoch: Option, - /// The Ethereum gas that can be spent during - /// the relay call. - pub gas: Option, - /// The price of Ethereum gas, during the - /// relay call. - pub gas_price: Option, - /// The address of the Ethereum wallet to pay the gas fees. - /// If unset, the default wallet is used. - pub eth_addr: Option, - /// Synchronize with the network, or exit immediately, - /// if the Ethereum node has fallen behind. - pub sync: bool, - /// The amount of time to sleep between failed - /// daemon mode relays. - pub retry_dur: Option, - /// The amount of time to sleep between successful - /// daemon mode relays. - pub success_dur: Option, -} - /// IBC shielding transfer generation arguments #[derive(Clone, Debug)] pub struct GenIbcShieldingTransfer { diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs index 281932a9a59..c2975788531 100644 --- a/crates/sdk/src/error.rs +++ b/crates/sdk/src/error.rs @@ -3,7 +3,6 @@ use namada_core::address::Address; use namada_core::chain::Epoch; use namada_core::dec::Dec; -use namada_core::ethereum_events::EthAddress; use namada_core::{arith, storage}; use namada_events::EventError; use namada_tx::Tx; @@ -35,9 +34,6 @@ pub enum Error { /// Errors that handle querying from storage #[error("Querying error: {0}")] Query(#[from] QueryError), - /// Ethereum bridge related errors - #[error("{0}")] - EthereumBridge(#[from] EthereumBridgeError), /// Arithmetic error #[error("Arithmetic {0}")] Arith(#[from] arith::Error), @@ -318,53 +314,3 @@ pub enum TxSubmitError { #[error("{0}")] Other(String), } - -/// Ethereum bridge related errors. -#[derive(Error, Debug, Clone)] -pub enum EthereumBridgeError { - /// Error invoking smart contract function. - #[error("Smart contract call failed: {0}")] - ContractCall(String), - /// Ethereum RPC error. - #[error("RPC error: {0}")] - Rpc(String), - /// Error reading the signed Bridge pool. - #[error("Failed to read signed Bridge pool: {0}")] - ReadSignedBridgePool(String), - /// Error reading the Bridge pool. - #[error("Failed to read Bridge pool: {0}")] - ReadBridgePool(String), - /// Error querying transfer to Ethereum progress. - #[error("Failed to query transfer to Ethereum progress: {0}")] - TransferToEthProgress(String), - /// Error querying Ethereum voting powers. - #[error("Failed to query Ethereum voting powers: {0}")] - QueryVotingPowers(String), - /// Ethereum node timeout error. - #[error("Timed out while attempting to communicate with the Ethereum node")] - NodeTimeout, - /// Error generating Bridge pool proof. - #[error("Failed to generate Bridge pool proof: {0}")] - GenBridgePoolProof(String), - /// Error retrieving contract address. - #[error("Failed to retrieve contract address: {0}")] - RetrieveContract(String), - /// Error calculating relay cost. - #[error("Failed to calculate relay cost: {0}")] - RelayCost(String), - /// Invalid Bridge pool nonce error. - #[error("The Bridge pool nonce is invalid")] - InvalidBpNonce, - /// Invalid fee token error. - #[error("An invalid fee token was provided: {0}")] - InvalidFeeToken(Address), - /// Not whitelisted error. - #[error("ERC20 is not whitelisted: {0}")] - Erc20NotWhitelisted(EthAddress), - /// Exceeded token caps error. - #[error("ERC20 token caps exceeded: {0}")] - Erc20TokenCapsExceeded(EthAddress), - /// Transfer already in pool error. - #[error("An identical transfer is already present in the Bridge pool")] - TransferAlreadyInPool, -} diff --git a/crates/sdk/src/eth_bridge/bridge_pool.rs b/crates/sdk/src/eth_bridge/bridge_pool.rs deleted file mode 100644 index 4bbb8c5c934..00000000000 --- a/crates/sdk/src/eth_bridge/bridge_pool.rs +++ /dev/null @@ -1,1638 +0,0 @@ -//! Bridge pool SDK functionality. - -#![allow(clippy::result_large_err)] - -use std::borrow::Cow; -use std::cmp::Ordering; -use std::sync::Arc; - -use ethbridge_bridge_contract::Bridge; -use ethers::providers::Middleware; -use futures::future::FutureExt; -use namada_core::address::{Address, InternalAddress}; -use namada_core::arith::checked; -use namada_core::collections::{HashMap, HashSet}; -use namada_core::eth_abi::Encode; -use namada_core::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, - erc20_token_address, -}; -use namada_core::ethereum_events::EthAddress; -use namada_core::keccak::KeccakHash; -use namada_core::voting_power::FractionalVotingPower; -use namada_ethereum_bridge::storage::bridge_pool::get_pending_key; -use namada_io::{Client, Io, display, display_line, edisplay_line}; -use namada_token::Amount; -use namada_token::storage_key::balance_key; -use namada_tx::Tx; -use namada_tx::data::Fee; -use owo_colors::OwoColorize; -use serde::Serialize; - -use super::{BlockOnEthSync, block_on_eth_sync, eth_sync_or_exit}; -use crate::borsh::BorshSerializeExt; -use crate::control_flow::time::{Duration, Instant}; -use crate::error::{ - EncodingError, Error, EthereumBridgeError, QueryError, TxSubmitError, -}; -use crate::eth_bridge::ethers::abi::AbiDecode; -use crate::internal_macros::echo_error; -use crate::queries::{ - GenBridgePoolProofReq, GenBridgePoolProofRsp, RPC, TransferToErcArgs, - TransferToEthereumStatus, -}; -use crate::rpc::{query_storage_value, query_wasm_code_hash, validate_amount}; -use crate::signing::SigningData; -use crate::tx::{ExtendedWrapperArgs, WrapArgs, derive_build_data}; -use crate::{MaybeSync, Namada, args}; - -/// Craft a transaction that adds a transfer to the Ethereum bridge pool. -pub async fn build_bridge_pool_tx( - context: &impl Namada, - args::EthereumBridgePool { - tx: tx_args, - nut, - asset, - recipient, - sender, - amount, - fee_amount, - fee_payer, - fee_token, - code_path, - }: args::EthereumBridgePool, -) -> Result<(Tx, SigningData), Error> { - let sender_ = sender.clone(); - let (signing_data, wrap_args, transfer, tx_code_hash) = { - let (transfer, tx_code_hash, (signing_data, wrap_args, _)) = futures::try_join!( - validate_bridge_pool_tx( - context, - tx_args.force, - nut, - asset, - recipient, - sender, - amount, - fee_amount, - fee_payer, - fee_token, - ), - query_wasm_code_hash(context, code_path.to_string_lossy()), - derive_build_data( - context, - tx_args - .wrap_tx - .as_ref() - .map(|wrap_args| ExtendedWrapperArgs { - wrap_args, - disposable_gas_payer: false - }), - tx_args.force, - // tx signer - Some(sender_), - vec![], - vec![], - ), - )?; - - (signing_data, wrap_args, transfer, tx_code_hash) - }; - - let chain_id = tx_args - .chain_id - .clone() - .ok_or_else(|| Error::Other("No chain id available".into()))?; - - let mut tx = Tx::new(chain_id, tx_args.expiration.to_datetime()); - if let Some(memo) = &tx_args.memo { - tx.add_memo(memo); - } - tx.add_code_from_hash( - tx_code_hash, - Some(code_path.to_string_lossy().into_owned()), - ) - .add_data(transfer); - - if let Some(WrapArgs { - fee_amount, - fee_payer, - fee_token, - gas_limit, - }) = wrap_args - { - tx.add_wrapper( - Fee { - amount_per_gas_unit: fee_amount, - token: fee_token, - }, - fee_payer, - gas_limit, - ); - } - - Ok((tx, signing_data)) -} - -/// Perform client validation checks on a Bridge pool transfer. -#[allow(clippy::too_many_arguments)] -async fn validate_bridge_pool_tx( - context: &impl Namada, - force: bool, - nut: bool, - asset: EthAddress, - recipient: EthAddress, - sender: Address, - amount: args::InputAmount, - fee_amount: args::InputAmount, - fee_payer: Option
, - fee_token: Address, -) -> Result { - let token_addr = erc20_token_address(&asset); - let validate_token_amount = - validate_amount(context, amount, &token_addr, force).map(|result| { - result.map_err(|e| { - Error::Other(format!( - "Failed to validate Bridge pool transfer amount: {e}" - )) - }) - }); - - let validate_fee_amount = - validate_amount(context, fee_amount, &fee_token, force).map(|result| { - result.map_err(|e| { - Error::Other(format!( - "Failed to validate Bridge pool fee amount: {e}", - )) - }) - }); - - // validate amounts - let (tok_denominated, fee_denominated) = - futures::try_join!(validate_token_amount, validate_fee_amount)?; - - // build pending Bridge pool transfer - let fee_payer = fee_payer.unwrap_or_else(|| sender.clone()); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - asset, - recipient, - sender, - amount: tok_denominated.amount(), - kind: if nut { - TransferToEthereumKind::Nut - } else { - TransferToEthereumKind::Erc20 - }, - }, - gas_fee: GasFee { - token: fee_token, - amount: fee_denominated.amount(), - payer: fee_payer, - }, - }; - - if force { - return Ok(transfer); - } - - //====================================================== - // XXX: the following validations should be kept in sync - // with the validations performed by the Bridge pool VP! - //====================================================== - - // check if an identical transfer is already in the Bridge pool - let transfer_in_pool = RPC - .shell() - .storage_has_key(context.client(), &get_pending_key(&transfer)) - .await - .map_err(|e| Error::Query(QueryError::General(e.to_string())))?; - if transfer_in_pool { - return Err(Error::EthereumBridge( - EthereumBridgeError::TransferAlreadyInPool, - )); - } - - let wnam_addr = RPC - .shell() - .eth_bridge() - .read_native_erc20_contract(context.client()) - .await - .map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::RetrieveContract( - e.to_string(), - )) - })?; - - // validate gas fee token - match &transfer.gas_fee.token { - Address::Internal(InternalAddress::Nut(_)) => { - return Err(Error::EthereumBridge( - EthereumBridgeError::InvalidFeeToken(transfer.gas_fee.token), - )); - } - fee_token if fee_token == &erc20_token_address(&wnam_addr) => { - return Err(Error::EthereumBridge( - EthereumBridgeError::InvalidFeeToken(transfer.gas_fee.token), - )); - } - _ => {} - } - - // validate wnam token caps + whitelist - if transfer.transfer.asset == wnam_addr { - let flow_control = RPC - .shell() - .eth_bridge() - .get_erc20_flow_control(context.client(), &wnam_addr) - .await - .map_err(|e| { - Error::Query(QueryError::General(format!( - "Failed to read wrapped NAM flow control data: {e}" - ))) - })?; - - if !flow_control.whitelisted { - return Err(Error::EthereumBridge( - EthereumBridgeError::Erc20NotWhitelisted(wnam_addr), - )); - } - - if flow_control.exceeds_token_caps(transfer.transfer.amount)? { - return Err(Error::EthereumBridge( - EthereumBridgeError::Erc20TokenCapsExceeded(wnam_addr), - )); - } - } - - // validate balances - let maybe_balance_error = if token_addr == transfer.gas_fee.token { - let expected_debit = - checked!(transfer.transfer.amount + transfer.gas_fee.amount)?; - let balance: Amount = query_storage_value( - context.client(), - &balance_key(&token_addr, &transfer.transfer.sender), - ) - .await?; - - balance - .checked_sub(expected_debit) - .is_none() - .then_some((token_addr, tok_denominated)) - } else { - let check_tokens = async { - let balance: Amount = query_storage_value( - context.client(), - &balance_key(&token_addr, &transfer.transfer.sender), - ) - .await?; - Result::<_, Error>::Ok( - balance - .checked_sub(transfer.transfer.amount) - .is_none() - .then_some((token_addr, tok_denominated)), - ) - }; - let check_fees = async { - let balance: Amount = query_storage_value( - context.client(), - &balance_key( - &transfer.gas_fee.token, - &transfer.transfer.sender, - ), - ) - .await?; - Result::<_, Error>::Ok( - balance - .checked_sub(transfer.gas_fee.amount) - .is_none() - .then_some(( - transfer.gas_fee.token.clone(), - fee_denominated, - )), - ) - }; - - let (err_tokens, err_fees) = - futures::try_join!(check_tokens, check_fees)?; - err_tokens.or(err_fees) - }; - if let Some((token, amount)) = maybe_balance_error { - return Err(Error::Tx(TxSubmitError::NegativeBalanceAfterTransfer( - Box::new(transfer.transfer.sender), - amount.to_string(), - Box::new(token), - ))); - } - - Ok(transfer) -} - -/// A json serializable representation of the Ethereum -/// bridge pool. -#[derive(Serialize)] -struct BridgePoolResponse<'pool> { - bridge_pool_contents: &'pool HashMap, -} - -/// Query the contents of the Ethereum bridge pool. -/// Prints out a json payload. -pub async fn query_bridge_pool( - client: &(impl Client + Sync), - io: &impl Io, -) -> Result, Error> { - let response: Vec = RPC - .shell() - .eth_bridge() - .read_ethereum_bridge_pool(client) - .await - .map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::ReadBridgePool( - e.to_string(), - )) - })?; - let pool_contents: HashMap = response - .into_iter() - .map(|transfer| (transfer.keccak256().to_string(), transfer)) - .collect(); - if pool_contents.is_empty() { - display_line!(io, "Bridge pool is empty."); - return Ok(pool_contents); - } - let contents = BridgePoolResponse { - bridge_pool_contents: &pool_contents, - }; - display_line!( - io, - "{}", - serde_json::to_string_pretty(&contents) - .map_err(|e| EncodingError::Serde(e.to_string()))? - ); - Ok(pool_contents) -} - -/// Query the contents of the Ethereum bridge pool that -/// is covered by the latest signed root. -/// Prints out a json payload. -pub async fn query_signed_bridge_pool( - client: &(impl Client + Sync), - io: &impl Io, -) -> Result, Error> { - let response: Vec = RPC - .shell() - .eth_bridge() - .read_signed_ethereum_bridge_pool(client) - .await - .map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::ReadSignedBridgePool( - e.to_string(), - )) - })?; - let pool_contents: HashMap = response - .into_iter() - .map(|transfer| (transfer.keccak256().to_string(), transfer)) - .collect(); - if pool_contents.is_empty() { - display_line!(io, "Bridge pool is empty."); - return Ok(pool_contents); - } - let contents = BridgePoolResponse { - bridge_pool_contents: &pool_contents, - }; - display_line!( - io, - "{}", - serde_json::to_string_pretty(&contents) - .map_err(|e| EncodingError::Serde(e.to_string()))? - ); - Ok(pool_contents) -} - -/// Iterates over all ethereum events -/// and returns the amount of voting power -/// backing each `TransferToEthereum` event. -/// -/// Prints a json payload. -pub async fn query_relay_progress( - client: &(impl Client + Sync), - io: &impl Io, -) -> Result<(), Error> { - let resp = RPC - .shell() - .eth_bridge() - .transfer_to_ethereum_progress(client) - .await - .map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::TransferToEthProgress( - e.to_string(), - )) - })?; - display_line!( - io, - "{}", - serde_json::to_string_pretty(&resp) - .map_err(|e| EncodingError::Serde(e.to_string()))? - ); - Ok(()) -} - -/// Internal method to construct a proof that a set of transfers are in the -/// bridge pool. -async fn construct_bridge_pool_proof( - client: &(impl Client + Sync), - io: &(impl Io + MaybeSync), - args: GenBridgePoolProofReq<'_, '_>, -) -> Result { - let in_progress = RPC - .shell() - .eth_bridge() - .transfer_to_ethereum_progress(client) - .await - .map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::TransferToEthProgress( - e.to_string(), - )) - })?; - - let warnings: Vec<_> = in_progress - .into_iter() - .filter_map(|(transfer, voting_power)| { - if voting_power >= FractionalVotingPower::ONE_THIRD { - let hash = transfer.keccak256(); - args.transfers.contains(&hash).then_some(hash) - } else { - None - } - }) - .collect(); - - if !warnings.is_empty() { - let warning = "Warning".on_yellow(); - let warning = warning.bold(); - let warning = warning.blink(); - display_line!( - io, - "{warning}: The following hashes correspond to transfers that \ - have surpassed the security threshold in Namada, therefore have \ - likely been relayed to Ethereum, but do not yet have a quorum of \ - validator signatures behind them in Namada; thus they are still \ - in the Bridge pool:\n{warnings:?}", - ); - display!(io, "\nDo you wish to proceed? (y/n): "); - io.flush(); - loop { - let resp = io.read().await.map_err(|e| { - Error::Other(echo_error!( - io, - "Encountered error reading from STDIN: {e:?}" - )) - })?; - match resp.trim() { - "y" => break, - "n" => { - return Err(Error::Other( - "Aborted generating Bridge pool proof".into(), - )); - } - _ => { - display!(io, "Expected 'y' or 'n'. Please try again: "); - io.flush(); - } - } - } - } - - let data = args.serialize_to_vec(); - let response = RPC - .shell() - .eth_bridge() - .generate_bridge_pool_proof(client, Some(data), None, false) - .await - .map_err(|e| { - edisplay_line!( - io, - "Encountered error constructing proof:\n{:?}", - e - ); - Error::EthereumBridge(EthereumBridgeError::GenBridgePoolProof( - e.to_string(), - )) - })?; - - Ok(response.data) -} - -/// A response from construction a bridge pool proof. -#[derive(Serialize)] -struct BridgePoolProofResponse { - hashes: Vec, - relayer_address: Address, - total_fees: HashMap, - abi_encoded_args: Vec, -} - -/// Construct a merkle proof of a batch of transfers in -/// the bridge pool and return it to the user (as opposed -/// to relaying it to ethereum). -pub async fn construct_proof( - client: &(impl Client + Sync), - io: &(impl Io + MaybeSync), - args: args::BridgePoolProof, -) -> Result<(), Error> { - let GenBridgePoolProofRsp { - abi_encoded_args, - appendices, - } = construct_bridge_pool_proof( - client, - io, - GenBridgePoolProofReq { - transfers: args.transfers.as_slice().into(), - relayer: Cow::Borrowed(&args.relayer), - with_appendix: true, - }, - ) - .await?; - let resp = BridgePoolProofResponse { - hashes: args.transfers, - relayer_address: args.relayer, - total_fees: appendices - .map(|appendices| { - appendices.into_iter().try_fold( - HashMap::new(), - |mut total_fees, app| { - let GasFee { token, amount, .. } = - app.gas_fee.into_owned(); - let fees = total_fees - .entry(token) - .or_insert_with(Amount::zero); - fees.receive(&amount) - .map_err(|e| Error::Other(e.to_string()))?; - Ok::<_, Error>(total_fees) - }, - ) - }) - .transpose()? - .unwrap_or_default(), - abi_encoded_args, - }; - display_line!( - io, - "{}", - serde_json::to_string_pretty(&resp) - .map_err(|e| EncodingError::Serde(e.to_string()))? - ); - Ok(()) -} - -/// Relay a validator set update, signed off for a given epoch. -pub async fn relay_bridge_pool_proof( - eth_client: Arc, - client: &(impl Client + Sync), - io: &(impl Io + MaybeSync), - args: args::RelayBridgePoolProof, -) -> Result<(), Error> -where - E: Middleware, - E::Error: std::fmt::Debug + std::fmt::Display, -{ - if args.sync { - block_on_eth_sync( - &*eth_client, - io, - BlockOnEthSync { - deadline: { - #[allow(clippy::disallowed_methods)] - Instant::now() - } + Duration::from_secs(60), - delta_sleep: Duration::from_secs(1), - }, - ) - .await?; - } else { - eth_sync_or_exit(&*eth_client, io).await?; - } - - let GenBridgePoolProofRsp { - abi_encoded_args, .. - } = construct_bridge_pool_proof( - client, - io, - GenBridgePoolProofReq { - transfers: Cow::Owned(args.transfers), - relayer: Cow::Owned(args.relayer), - with_appendix: false, - }, - ) - .await?; - let bridge = - match RPC.shell().eth_bridge().read_bridge_contract(client).await { - Ok(address) => Bridge::new(address.address, eth_client), - Err(err_msg) => { - let error = "Error".on_red(); - let error = error.bold(); - let error = error.blink(); - display_line!( - io, - "Unable to decode the generated proof: {:?}", - error - ); - return Err(Error::EthereumBridge( - EthereumBridgeError::RetrieveContract(err_msg.to_string()), - )); - } - }; - - let (validator_set, signatures, bp_proof): TransferToErcArgs = - AbiDecode::decode(&abi_encoded_args).map_err(|error| { - EncodingError::Decoding(echo_error!( - io, - "Unable to decode the generated proof: {:?}", - error - )) - })?; - - // NOTE: this operation costs no gas on Ethereum - let contract_nonce = bridge - .transfer_to_erc_20_nonce() - .call() - .await - .map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::ContractCall( - e.to_string(), - )) - })?; - - match bp_proof.batch_nonce.cmp(&contract_nonce) { - Ordering::Equal => {} - Ordering::Less => { - let error = "Error".on_red(); - let error = error.bold(); - let error = error.blink(); - display_line!( - io, - "{error}: The Bridge pool nonce in the smart contract is \ - {contract_nonce}, while the nonce in Namada is still {}. A \ - relay of the former one has already happened, but a proof \ - has yet to be crafted in Namada.", - bp_proof.batch_nonce - ); - return Err(Error::EthereumBridge( - EthereumBridgeError::InvalidBpNonce, - )); - } - Ordering::Greater => { - let error = "Error".on_red(); - let error = error.bold(); - let error = error.blink(); - display_line!( - io, - "{error}: The Bridge pool nonce in the smart contract is \ - {contract_nonce}, while the nonce in Namada is still {}. \ - Somehow, Namada's nonce is ahead of the contract's nonce!", - bp_proof.batch_nonce - ); - return Err(Error::EthereumBridge( - EthereumBridgeError::InvalidBpNonce, - )); - } - } - - let mut relay_op = - bridge.transfer_to_erc(validator_set, signatures, bp_proof); - if let Some(gas) = args.gas { - relay_op.tx.set_gas(gas); - } - if let Some(gas_price) = args.gas_price { - relay_op.tx.set_gas_price(gas_price); - } - if let Some(eth_addr) = args.eth_addr { - relay_op.tx.set_from(eth_addr.into()); - } - - let pending_tx = relay_op.send().await.map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::ContractCall(e.to_string())) - })?; - let transf_result = pending_tx - .confirmations(args.confirmations as usize) - .await - .map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::Rpc(e.to_string())) - })?; - - display_line!(io, "{transf_result:?}"); - Ok(()) -} - -/// Query the status of a set of transfers to Ethreum, indexed -/// by their keccak hash. -/// -/// Any unrecognized hashes (i.e. not pertaining to transfers -/// in Namada's event log nor the Bridge pool) will be flagged -/// as such. Unrecognized transfers could have been relayed to -/// Ethereum, or could have expired from the Bridge pool. If -/// these scenarios verify, it should be possible to retrieve -/// the status of these transfers by querying CometBFT's block -/// data. -pub async fn query_eth_transfer_status( - client: &C, - transfers: T, -) -> Result -where - C: Client + Sync, - T: Into>, -{ - RPC.shell() - .eth_bridge() - .pending_eth_transfer_status( - client, - Some(transfers.into().serialize_to_vec()), - None, - false, - ) - .await - .map_err(|e| Error::Query(QueryError::General(e.to_string()))) - .map(|result| result.data) -} - -mod recommendations { - use std::collections::BTreeSet; - - use borsh::BorshDeserialize; - use namada_core::chain::BlockHeight; - use namada_core::ethereum_events::Uint as EthUint; - use namada_core::uint::{self, I256, Uint}; - use namada_ethereum_bridge::storage::proof::BridgePoolRootProof; - use namada_io::edisplay_line; - use namada_vote_ext::validator_set_update::{ - EthAddrBook, VotingPowersMap, VotingPowersMapExt, - }; - - use super::*; - use crate::eth_bridge::storage::bridge_pool::{ - get_nonce_key, get_signed_root_key, - }; - - const fn unsigned_transfer_fee() -> Uint { - Uint::from_u64(37_500_u64) - } - - const fn transfer_fee() -> I256 { - I256(unsigned_transfer_fee()) - } - - const fn signature_fee() -> Uint { - Uint::from_u64(24_500) - } - - const fn valset_fee() -> Uint { - Uint::from_u64(2000) - } - - /// The different states while trying to solve - /// for a recommended batch of transfers. - struct AlgorithState { - /// We are scanning transfers that increase - /// net profits to the relayer. However, we - /// are not in the feasible region. - profitable: bool, - /// We are scanning solutions that satisfy the - /// requirements of the input. - feasible_region: bool, - } - - /// The algorithm exhibits two different remmondation strategies - /// depending on whether the user is will to accept a positive cost - /// for relaying. - #[derive(PartialEq)] - enum AlgorithmMode { - /// Only keep profitable transactions - Greedy, - /// Allow transactions which are not profitable - Generous, - } - - /// Transfer to Ethereum that is eligible to be recommended - /// for a relay operation, generating a profit. - /// - /// This means that the underlying Ethereum event has not - /// been "seen" yet, and that the user provided appropriate - /// conversion rates to gwei for the gas fee token in - /// the transfer. - #[derive(Debug, Eq, PartialEq)] - struct EligibleRecommendation { - /// Pending transfer to Ethereum. - pending_transfer: PendingTransfer, - /// Hash of the [`PendingTransfer`]. - transfer_hash: String, - /// Cost of relaying the transfer, in gwei. - cost: I256, - } - - /// Batch of recommended transfers to Ethereum that generate - /// a profit after a relay operation. - #[derive(Debug, Eq, PartialEq)] - struct RecommendedBatch { - /// Hashes of the recommended transfers to be relayed. - transfer_hashes: Vec, - /// Estimate of the total fees, measured in gwei, that will be paid - /// on Ethereum. - ethereum_gas_fees: Uint, - /// Net profitt in gwei, based on the conversion rates provided - /// to the algorithm. - net_profit: I256, - /// Gas fees paid by the transfers considered for relaying, - /// paid in various token types. - bridge_pool_gas_fees: HashMap, - } - - /// Recommend the most economical batch of transfers to relay based - /// on a conversion rate estimates from NAM to ETH and gas usage - /// heuristics. - pub async fn recommend_batch( - context: &impl Namada, - args: args::RecommendBatch, - ) -> Result<(), Error> { - // get transfers that can already been relayed but are awaiting a quorum - // of backing votes. - let in_progress = RPC - .shell() - .eth_bridge() - .transfer_to_ethereum_progress(context.client()) - .await - .map_err(|e| { - Error::EthereumBridge( - EthereumBridgeError::TransferToEthProgress(e.to_string()), - ) - })? - .into_keys() - .map(|pending| pending.keccak256().to_string()) - .collect::>(); - - // get the signed bridge pool root so we can analyze the signatures - // the estimate the gas cost of verifying them. - let (bp_root, height) = - <(BridgePoolRootProof, BlockHeight)>::try_from_slice( - &RPC.shell() - .storage_value( - context.client(), - None, - None, - false, - &get_signed_root_key(), - ) - .await - .map_err(|err| { - Error::Query(QueryError::General(echo_error!( - context.io(), - "Failed to query Bridge pool proof: {err}" - ))) - })? - .data, - ) - .map_err(|err| { - Error::Encode(EncodingError::Decoding(echo_error!( - context.io(), - "Failed to decode Bridge pool proof: {err}" - ))) - })?; - - // get the latest bridge pool nonce - let latest_bp_nonce = EthUint::try_from_slice( - &RPC.shell() - .storage_value( - context.client(), - None, - None, - false, - &get_nonce_key(), - ) - .await - .map_err(|err| { - Error::Query(QueryError::General(echo_error!( - context.io(), - "Failed to query Bridge pool nonce: {err}" - ))) - })? - .data, - ) - .map_err(|err| { - Error::Encode(EncodingError::Decoding(echo_error!( - context.io(), - "Failed to decode Bridge pool nonce: {err}" - ))) - })?; - - if latest_bp_nonce != bp_root.data.1 { - edisplay_line!( - context.io(), - "The signed Bridge pool nonce is not up to date, repeat this \ - query at a later time" - ); - return Err(Error::EthereumBridge( - EthereumBridgeError::InvalidBpNonce, - )); - } - - // Get the voting powers of each of validator who signed - // the above root. - let voting_powers = RPC - .shell() - .eth_bridge() - .voting_powers_at_height(context.client(), &height) - .await - .map_err(|e| { - Error::EthereumBridge(EthereumBridgeError::QueryVotingPowers( - e.to_string(), - )) - })?; - let valset_size = Uint::from_u64(voting_powers.len() as u64); - - // This is the gas cost for hashing the validator set and - // checking a quorum of signatures (in gwei). - let validator_gas = signature_fee() - * signature_checks(voting_powers, &bp_root.signatures)? - + valset_fee() * valset_size; - - // we don't recommend transfers that have already been relayed - let eligible = generate_eligible( - context.io(), - &args.conversion_table, - &in_progress, - query_signed_bridge_pool(context.client(), context.io()).await?, - )?; - - let max_gas = - args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); - let max_cost = args.gas.map(I256::from).unwrap_or_default(); - - generate_recommendations( - context.io(), - eligible, - &args.conversion_table, - validator_gas, - max_gas, - max_cost, - )? - .map( - |RecommendedBatch { - transfer_hashes, - ethereum_gas_fees, - net_profit, - bridge_pool_gas_fees, - }| { - display_line!( - context.io(), - "Recommended batch: {transfer_hashes:#?}" - ); - display_line!( - context.io(), - "Estimated Ethereum transaction gas (in gwei): \ - {ethereum_gas_fees}", - ); - display_line!( - context.io(), - "Estimated net profit (in gwei): {net_profit}" - ); - display_line!( - context.io(), - "Total fees: {bridge_pool_gas_fees:#?}" - ); - }, - ) - .unwrap_or_else(|| { - display_line!( - context.io(), - "Unable to find a recommendation satisfying the input \ - parameters." - ); - }); - - Ok(()) - } - - /// Given an ordered list of signatures, figure out the size of the first - /// subset constituting a 2 / 3 majority. - /// - /// The function is generic to make unit testing easier (otherwise a dev - /// dependency needs to be added). - fn signature_checks( - voting_powers: VotingPowersMap, - sigs: &HashMap, - ) -> Result { - let voting_powers = voting_powers.get_sorted(); - let total_power = Amount::sum(voting_powers.iter().map(|(_, y)| **y)) - .ok_or_else(|| { - Error::Other("Voting power sum overflow".to_owned()) - })?; - - // Find the total number of signature checks Ethereum will make - let mut power = FractionalVotingPower::NULL; - Ok(Uint::from_u64( - voting_powers - .iter() - .filter_map(|(a, p)| sigs.get(*a).map(|_| **p)) - .take_while(|p| { - if power <= FractionalVotingPower::TWO_THIRDS { - power += FractionalVotingPower::new( - (*p).into(), - total_power.into(), - ) - // NB: this unwrap is infallible, since we calculate - // the total voting power beforehand. the fraction's - // value will never exceed 1.0 - .unwrap(); - true - } else { - false - } - }) - .count() as u64, - )) - } - - /// Generate eligible recommendations. - fn generate_eligible( - io: &IO, - conversion_table: &HashMap, - in_progress: &BTreeSet, - signed_pool: HashMap, - ) -> Result, Error> { - let mut eligible: Vec<_> = signed_pool - .into_iter() - .filter_map(|(pending_hash, pending)| { - if in_progress.contains(&pending_hash) { - return None; - } - - let conversion_rate = conversion_table - .get(&pending.gas_fee.token) - .and_then(|entry| match entry.conversion_rate { - 0.0f64 => { - edisplay_line!( - io, - "{}: Ignoring null conversion rate", - pending.gas_fee.token, - ); - None - } - r if r < 0.0f64 => { - edisplay_line!( - io, - "{}: Ignoring negative conversion rate: {r:.1}", - pending.gas_fee.token, - ); - None - } - r if r > 1e9 => { - edisplay_line!( - io, - "{}: Ignoring high conversion rate: {r:.1} > \ - 10^9", - pending.gas_fee.token, - ); - None - } - r => Some(r), - })?; - - // This is the amount of gwei a single gas token is worth - let gwei_per_gas_token = - Uint::from_u64((1e9 / conversion_rate).floor() as u64); - - Some( - Uint::from(pending.gas_fee.amount) - .checked_mul(gwei_per_gas_token) - .ok_or_else(|| { - "Overflowed calculating earned gwei".into() - }) - .and_then(I256::try_from) - .map_err(|err| err.to_string()) - .and_then(|amt_of_earned_gwei| { - transfer_fee() - .checked_sub(amt_of_earned_gwei) - .ok_or_else(|| { - "Underflowed calculating relaying cost" - .into() - }) - }) - .map(|cost| EligibleRecommendation { - cost, - pending_transfer: pending, - transfer_hash: pending_hash, - }), - ) - }) - .collect::, _>>() - .map_err(|err| { - Error::EthereumBridge(EthereumBridgeError::RelayCost( - echo_error!(io, "Failed to calculate relaying cost: {err}"), - )) - })?; - - // sort transfers in increasing amounts of profitability - eligible.sort_by_key(|EligibleRecommendation { cost, .. }| *cost); - - Ok(eligible) - } - - /// Generates the actual recommendation from restrictions given by the - /// input parameters. - fn generate_recommendations( - io: &IO, - contents: Vec, - conversion_table: &HashMap, - validator_gas: Uint, - max_gas: Uint, - max_cost: I256, - ) -> Result, Error> { - let mut state = AlgorithState { - profitable: true, - feasible_region: false, - }; - - let mode = if max_cost <= I256::zero() { - AlgorithmMode::Greedy - } else { - AlgorithmMode::Generous - }; - - let mut total_gas = validator_gas; - let mut total_cost = I256::try_from(validator_gas).map_err(|err| { - Error::Encode(EncodingError::Conversion(echo_error!( - io, - "Failed to convert value to I256: {err}" - ))) - })?; - let mut total_fees = HashMap::new(); - let mut recommendation = vec![]; - for EligibleRecommendation { - cost, - transfer_hash: hash, - pending_transfer: transfer, - } in contents.into_iter() - { - let next_total_gas = checked!(total_gas + unsigned_transfer_fee())?; - let next_total_cost = checked!(total_cost + cost)?; - if cost.is_negative() { - if next_total_gas <= max_gas && next_total_cost <= max_cost { - state.feasible_region = true; - } else if state.feasible_region { - // once we leave the feasible region, we will never re-enter - // it. - break; - } - recommendation.push(hash); - } else if mode == AlgorithmMode::Generous { - state.profitable = false; - let is_feasible = - next_total_gas <= max_gas && next_total_cost <= max_cost; - // once we leave the feasible region, we will never re-enter it. - if state.feasible_region && !is_feasible { - break; - } else { - recommendation.push(hash); - } - } else { - break; - } - total_cost = next_total_cost; - total_gas = next_total_gas; - update_total_fees(&mut total_fees, transfer, conversion_table); - } - - Ok(if state.feasible_region && !recommendation.is_empty() { - Some(RecommendedBatch { - transfer_hashes: recommendation, - ethereum_gas_fees: total_gas, - net_profit: checked!(-total_cost)?, - bridge_pool_gas_fees: total_fees, - }) - } else { - edisplay_line!( - io, - "Unable to find a recommendation satisfying the input \ - parameters." - ); - None - }) - } - - fn update_total_fees( - total_fees: &mut HashMap, - transfer: PendingTransfer, - conversion_table: &HashMap, - ) { - let GasFee { token, amount, .. } = transfer.gas_fee; - let fees = total_fees - .entry( - conversion_table - .get(&token) - .map(|entry| entry.alias.clone()) - .unwrap_or_else(|| token.to_string()), - ) - .or_insert(uint::ZERO); - *fees += Uint::from(amount); - } - - #[cfg(test)] - mod test_recommendations { - use namada_core::address; - use namada_io::StdIo; - - use super::*; - - /// An established user address for testing & development - pub fn bertha_address() -> Address { - Address::decode("tnam1qyctxtpnkhwaygye0sftkq28zedf774xc5a2m7st") - .expect("The token address decoding shouldn't fail") - } - - /// Generate a pending transfer with the specified gas - /// fee. - pub fn transfer(gas_amount: u64) -> PendingTransfer { - PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([1; 20]), - recipient: EthAddress([2; 20]), - sender: bertha_address(), - amount: Default::default(), - }, - gas_fee: GasFee { - token: address::testing::nam(), - amount: gas_amount.into(), - payer: bertha_address(), - }, - } - } - - /// Convert transfers into a format that the - /// [`generate_recommendations`] function understands. - fn process_transfers( - transfers: Vec, - ) -> Vec { - transfers - .into_iter() - .map(|t| EligibleRecommendation { - cost: transfer_fee() - t.gas_fee.amount.change(), - transfer_hash: t.keccak256().to_string(), - pending_transfer: t, - }) - .collect() - } - - fn address_book(i: u8) -> EthAddrBook { - EthAddrBook { - hot_key_addr: EthAddress([i; 20]), - cold_key_addr: EthAddress([i; 20]), - } - } - - /// Data to pass to the [`test_generate_eligible_aux`] callback. - struct TestGenerateEligible<'a> { - pending: &'a PendingTransfer, - conversion_table: - &'a mut HashMap, - in_progress: &'a mut BTreeSet, - signed_pool: &'a mut HashMap, - expected_eligible: &'a mut Vec, - } - - impl TestGenerateEligible<'_> { - /// Add ETH to a conversion table. - fn add_eth_to_conversion_table(&mut self) { - self.conversion_table.insert( - address::testing::eth(), - args::BpConversionTableEntry { - alias: "ETH".into(), - conversion_rate: 1e9, // 1 ETH = 1e9 GWEI - }, - ); - } - } - - /// Helper function to test [`generate_eligible`]. - fn test_generate_eligible_aux( - mut callback: F, - ) -> Vec - where - F: FnMut(TestGenerateEligible<'_>), - { - let pending = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([1; 20]), - recipient: EthAddress([2; 20]), - sender: bertha_address(), - amount: Default::default(), - }, - gas_fee: GasFee { - token: address::testing::eth(), - amount: 1_000_000_000_u64.into(), // 1 GWEI - payer: bertha_address(), - }, - }; - let mut table = HashMap::new(); - let mut in_progress = BTreeSet::new(); - let mut signed_pool = HashMap::new(); - let mut expected = vec![]; - callback(TestGenerateEligible { - pending: &pending, - conversion_table: &mut table, - in_progress: &mut in_progress, - signed_pool: &mut signed_pool, - expected_eligible: &mut expected, - }); - let eligible = - generate_eligible(&StdIo, &table, &in_progress, signed_pool) - .unwrap(); - assert_eq!(eligible, expected); - eligible - } - - /// Test the happy path of generating eligible recommendations - /// for Bridge pool relayed transfers. - #[test] - fn test_generate_eligible_happy_path() { - test_generate_eligible_aux(|mut ctx| { - ctx.add_eth_to_conversion_table(); - ctx.signed_pool.insert( - ctx.pending.keccak256().to_string(), - ctx.pending.clone(), - ); - ctx.expected_eligible.push(EligibleRecommendation { - transfer_hash: ctx.pending.keccak256().to_string(), - cost: transfer_fee() - - I256::from(ctx.pending.gas_fee.amount), - pending_transfer: ctx.pending.clone(), - }); - }); - } - - /// Test that a transfer is not recommended if it - /// is in the process of being relayed (has >0 voting - /// power behind it). - #[test] - fn test_generate_eligible_with_in_progress() { - test_generate_eligible_aux(|mut ctx| { - ctx.add_eth_to_conversion_table(); - ctx.signed_pool.insert( - ctx.pending.keccak256().to_string(), - ctx.pending.clone(), - ); - ctx.in_progress.insert(ctx.pending.keccak256().to_string()); - }); - } - - /// Test that a transfer is not recommended if its gas - /// token is not found in the conversion table. - #[test] - fn test_generate_eligible_no_gas_token() { - test_generate_eligible_aux(|ctx| { - ctx.signed_pool.insert( - ctx.pending.keccak256().to_string(), - ctx.pending.clone(), - ); - }); - } - - #[test] - fn test_signature_count() { - let voting_powers = VotingPowersMap::from([ - (address_book(1), Amount::from(5)), - (address_book(2), Amount::from(1)), - (address_book(3), Amount::from(1)), - ]); - let signatures = HashMap::from([ - (address_book(1), 0), - (address_book(2), 0), - (address_book(3), 0), - ]); - let checks = signature_checks(voting_powers, &signatures).unwrap(); - assert_eq!(checks, uint::ONE) - } - - #[test] - fn test_signature_count_with_skips() { - let voting_powers = VotingPowersMap::from([ - (address_book(1), Amount::from(5)), - (address_book(2), Amount::from(5)), - (address_book(3), Amount::from(1)), - (address_book(4), Amount::from(1)), - ]); - let signatures = HashMap::from([ - (address_book(1), 0), - (address_book(3), 0), - (address_book(4), 0), - ]); - let checks = signature_checks(voting_powers, &signatures).unwrap(); - assert_eq!(checks, Uint::from_u64(3)) - } - - #[test] - fn test_only_profitable() { - let profitable = vec![transfer(100_000); 17]; - let hash = profitable[0].keccak256().to_string(); - let expected = vec![hash; 17]; - let recommendation = generate_recommendations( - &StdIo, - process_transfers(profitable), - &Default::default(), - Uint::from_u64(800_000), - uint::MAX_VALUE, - I256::zero(), - ) - .unwrap() - .expect("Test failed") - .transfer_hashes; - assert_eq!(recommendation, expected); - } - - #[test] - fn test_non_profitable_removed() { - let mut transfers = vec![transfer(100_000); 17]; - let hash = transfers[0].keccak256().to_string(); - transfers.push(transfer(0)); - let expected: Vec<_> = vec![hash; 17]; - let recommendation = generate_recommendations( - &StdIo, - process_transfers(transfers), - &Default::default(), - Uint::from_u64(800_000), - uint::MAX_VALUE, - I256::zero(), - ) - .unwrap() - .expect("Test failed") - .transfer_hashes; - assert_eq!(recommendation, expected); - } - - #[test] - fn test_max_gas() { - let transfers = vec![transfer(75_000); 4]; - let hash = transfers[0].keccak256().to_string(); - let expected = vec![hash; 2]; - let recommendation = generate_recommendations( - &StdIo, - process_transfers(transfers), - &Default::default(), - Uint::from_u64(50_000), - Uint::from_u64(150_000), - I256(uint::MAX_SIGNED_VALUE), - ) - .unwrap() - .expect("Test failed") - .transfer_hashes; - assert_eq!(recommendation, expected); - } - - #[test] - fn test_net_loss() { - let mut transfers = vec![transfer(75_000); 4]; - transfers.extend([transfer(17_500), transfer(17_500)]); - let expected: Vec<_> = transfers - .iter() - .map(|t| t.keccak256().to_string()) - .take(5) - .collect(); - let recommendation = generate_recommendations( - &StdIo, - process_transfers(transfers), - &Default::default(), - Uint::from_u64(150_000), - uint::MAX_VALUE, - I256::from(20_000), - ) - .unwrap() - .expect("Test failed") - .transfer_hashes; - assert_eq!(recommendation, expected); - } - - #[test] - fn test_net_loss_max_gas() { - let mut transfers = vec![transfer(75_000); 4]; - let hash = transfers[0].keccak256().to_string(); - let expected = vec![hash; 4]; - transfers.extend([transfer(17_500), transfer(17_500)]); - let recommendation = generate_recommendations( - &StdIo, - process_transfers(transfers), - &Default::default(), - Uint::from_u64(150_000), - Uint::from_u64(330_000), - I256::from(20_000), - ) - .unwrap() - .expect("Test failed") - .transfer_hashes; - assert_eq!(recommendation, expected); - } - - #[test] - fn test_wholly_infeasible() { - let transfers = vec![transfer(75_000); 4]; - let recommendation = generate_recommendations( - &StdIo, - process_transfers(transfers), - &Default::default(), - Uint::from_u64(300_000), - uint::MAX_VALUE, - I256::from(20_000), - ) - .unwrap(); - assert!(recommendation.is_none()) - } - - /// Test the profit margin obtained from relaying two - /// Bridge pool transfers with two distinct token types, - /// whose relation is 1:2 in value. - #[test] - fn test_conversion_table_profit_margin() { - // apfel is worth twice as much as schnitzel - const APF_RATE: f64 = 5e8; - const SCH_RATE: f64 = 1e9; - const APFEL: &str = "APF"; - const SCHNITZEL: &str = "SCH"; - - let conversion_table = { - let mut t = HashMap::new(); - t.insert( - address::testing::apfel(), - args::BpConversionTableEntry { - alias: APFEL.into(), - conversion_rate: APF_RATE, - }, - ); - t.insert( - address::testing::schnitzel(), - args::BpConversionTableEntry { - alias: SCHNITZEL.into(), - conversion_rate: SCH_RATE, - }, - ); - t - }; - - let eligible = test_generate_eligible_aux(|ctx| { - ctx.conversion_table.clone_from(&conversion_table); - // tune the pending transfer provided by the ctx - let transfer_paid_in_apfel = { - let mut pending = ctx.pending.clone(); - pending.transfer.amount = 1.into(); - pending.gas_fee.token = address::testing::apfel(); - pending - }; - let transfer_paid_in_schnitzel = { - let mut pending = ctx.pending.clone(); - pending.transfer.amount = 2.into(); - pending.gas_fee.token = address::testing::schnitzel(); - pending - }; - // add the transfers to the pool, and expect them to - // be eligible transfers - for (pending, rate) in [ - (transfer_paid_in_apfel, APF_RATE), - (transfer_paid_in_schnitzel, SCH_RATE), - ] { - ctx.signed_pool.insert( - pending.keccak256().to_string(), - pending.clone(), - ); - ctx.expected_eligible.push(EligibleRecommendation { - transfer_hash: pending.keccak256().to_string(), - cost: transfer_fee() - - I256::from((1e9 / rate).floor() as u64) - * I256::from(pending.gas_fee.amount), - pending_transfer: pending, - }); - } - }); - - const VALIDATOR_GAS_FEE: Uint = Uint::from_u64(100_000); - - let recommended_batch = generate_recommendations( - &StdIo, - eligible, - &conversion_table, - // gas spent by validator signature checks - VALIDATOR_GAS_FEE, - // unlimited amount of gas - uint::MAX_VALUE, - // only profitable - I256::zero(), - ) - .unwrap() - .expect("Test failed"); - - assert_eq!( - recommended_batch.net_profit, - I256::from(1_000_000_000_u64) + I256::from(2_000_000_000_u64) - - I256(VALIDATOR_GAS_FEE) - - transfer_fee() * I256::from(2_u64) - ); - } - } -} - -pub use recommendations::recommend_batch; diff --git a/crates/sdk/src/eth_bridge/mod.rs b/crates/sdk/src/eth_bridge/mod.rs deleted file mode 100644 index 5909c7d3d8b..00000000000 --- a/crates/sdk/src/eth_bridge/mod.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Ethereum bridge utilities shared between `wasm` and the `cli`. - -pub mod bridge_pool; -pub mod validator_set; - -use std::ops::ControlFlow; - -pub use ethers; -use ethers::providers::Middleware; -use itertools::Either; -pub use namada_core::ethereum_structs as structs; -pub use namada_ethereum_bridge::storage::eth_bridge_queries::*; -pub use namada_ethereum_bridge::storage::parameters::*; -pub use namada_ethereum_bridge::storage::wrapped_erc20s; -pub use namada_ethereum_bridge::{ADDRESS, *}; -use namada_io::{Io, display_line, edisplay_line}; -use num256::Uint256; - -use crate::control_flow::time::{ - Constant, Duration, Instant, LinearBackoff, Sleep, -}; -use crate::error::{Error, EthereumBridgeError}; - -const DEFAULT_BACKOFF: Duration = std::time::Duration::from_millis(500); -const DEFAULT_CEILING: Duration = std::time::Duration::from_secs(30); - -/// The result of querying an Ethereum nodes syncing status. -pub enum SyncStatus { - /// The fullnode is syncing. - Syncing, - /// The fullnode is synced up to the given block height. - AtHeight(Uint256), -} - -impl SyncStatus { - /// Returns true if [`SyncStatus`] reflects a synchronized node. - pub fn is_synchronized(&self) -> bool { - matches!(self, SyncStatus::AtHeight(_)) - } -} - -/// Fetch the sync status of an Ethereum node. -#[inline] -pub async fn eth_syncing_status(client: &C) -> Result -where - C: Middleware, -{ - eth_syncing_status_timeout( - client, - DEFAULT_BACKOFF, - { - #[allow(clippy::disallowed_methods)] - Instant::now() - } + DEFAULT_CEILING, - ) - .await -} - -/// Fetch the sync status of an Ethereum node, with a custom time -/// out duration. -/// -/// Queries to the Ethereum node are interspersed with constant backoff -/// sleeps of `backoff_duration`, before ultimately timing out at `deadline`. -pub async fn eth_syncing_status_timeout( - client: &C, - backoff_duration: Duration, - deadline: Instant, -) -> Result -where - C: Middleware, -{ - Sleep { - strategy: Constant(backoff_duration), - } - .timeout(deadline, || async { - let fut_syncing = client.syncing(); - let fut_block_num = client.get_block_number(); - let Ok(status) = futures::try_join!(fut_syncing, fut_block_num,) else { - return ControlFlow::Continue(()); - }; - ControlFlow::Break(match status { - (ethers::types::SyncingStatus::IsFalse, height) - if height != 0u64.into() => - { - SyncStatus::AtHeight(height.as_u64().into()) - } - _ => SyncStatus::Syncing, - }) - }) - .await - .map_err(|_| Error::EthereumBridge(EthereumBridgeError::NodeTimeout)) -} - -/// Arguments to [`block_on_eth_sync`]. -pub struct BlockOnEthSync { - /// The deadline before we timeout in the CLI. - pub deadline: Instant, - /// The duration of sleep calls between each RPC timeout. - pub delta_sleep: Duration, -} - -/// Block until Ethereum finishes synchronizing. -pub async fn block_on_eth_sync( - client: &C, - io: &IO, - args: BlockOnEthSync, -) -> Result<(), Error> -where - C: Middleware, -{ - let BlockOnEthSync { - deadline, - delta_sleep, - } = args; - display_line!(io, "Attempting to synchronize with the Ethereum network"); - Sleep { - strategy: LinearBackoff { delta: delta_sleep }, - } - .timeout(deadline, || async { - let Ok(status) = eth_syncing_status(client).await else { - return ControlFlow::Continue(()); - }; - if status.is_synchronized() { - ControlFlow::Break(()) - } else { - ControlFlow::Continue(()) - } - }) - .await - .map_err(|_| { - edisplay_line!( - io, - "Timed out while waiting for Ethereum to synchronize" - ); - Error::EthereumBridge(EthereumBridgeError::NodeTimeout) - })?; - display_line!(io, "The Ethereum node is up to date"); - Ok(()) -} - -/// Check if Ethereum has finished synchronizing. In case it has -/// not, perform `action`. -pub async fn eth_sync_or( - client: &C, - io: &IO, - mut action: F, -) -> Result, Error> -where - C: Middleware, - F: FnMut() -> T, -{ - let is_synchronized = eth_syncing_status(client) - .await - .map(|status| status.is_synchronized()) - .map_err(|err| { - edisplay_line!( - io, - "An error occurred while fetching the Ethereum \ - synchronization status: {err}" - ); - err - })?; - if is_synchronized { - Ok(Either::Right(())) - } else { - Ok(Either::Left(action())) - } -} - -/// Check if Ethereum has finished synchronizing. In case it has -/// not, end execution. -pub async fn eth_sync_or_exit( - client: &C, - io: &IO, -) -> Result<(), Error> -where - C: Middleware, -{ - eth_sync_or(client, io, || { - edisplay_line!(io, "The Ethereum node has not finished synchronizing"); - }) - .await?; - Ok(()) -} diff --git a/crates/sdk/src/eth_bridge/validator_set.rs b/crates/sdk/src/eth_bridge/validator_set.rs deleted file mode 100644 index a33cab0f53f..00000000000 --- a/crates/sdk/src/eth_bridge/validator_set.rs +++ /dev/null @@ -1,816 +0,0 @@ -//! Validator set updates SDK functionality. - -use std::cmp::Ordering; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; - -use data_encoding::HEXLOWER; -use ethbridge_bridge_contract::Bridge; -use ethers::providers::Middleware; -use futures::future::FutureExt; -use namada_core::chain::Epoch; -use namada_core::eth_abi::EncodeCell; -use namada_core::ethereum_events::EthAddress; -use namada_core::hints; -use namada_ethereum_bridge::storage::proof::EthereumProof; -use namada_io::{Client, Io, display_line, edisplay_line}; -use namada_vote_ext::validator_set_update::{ - ValidatorSetArgs, VotingPowersMap, -}; - -use super::{BlockOnEthSync, block_on_eth_sync, eth_sync_or, eth_sync_or_exit}; -use crate::args; -use crate::control_flow::time::{self, Duration, Instant}; -use crate::error::{Error as SdkError, EthereumBridgeError, QueryError}; -use crate::eth_bridge::ethers::abi::{AbiDecode, AbiType, Tokenizable}; -use crate::eth_bridge::ethers::types::TransactionReceipt; -use crate::eth_bridge::structs::Signature; -use crate::internal_macros::{echo_error, trace_error}; -use crate::queries::RPC; - -/// Relayer related errors. -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Default)] -enum Error { - /// An error, with no further context. - /// - /// This is usually because context was already - /// provided in the form of `tracing!()` calls. - #[default] - NoContext, - /// An error message with a reason and an associated - /// `tracing` log level. - WithReason { - /// The reason of the error. - reason: SdkError, - /// The log level where to display the error message. - level: tracing::Level, - /// If critical, exit the relayer. - critical: bool, - }, -} - -impl Error { - /// Create a new error message. - /// - /// The error is recoverable. - fn recoverable(msg: M) -> Self - where - M: Into, - { - Error::WithReason { - level: tracing::Level::DEBUG, - reason: msg.into(), - critical: false, - } - } - - /// Create a new error message. - /// - /// The error is not recoverable. - fn critical(msg: M) -> Self - where - M: Into, - { - Error::WithReason { - level: tracing::Level::ERROR, - reason: msg.into(), - critical: true, - } - } - - /// Display the error message, and return a new [`Result`], - /// with the error already handled appropriately. - #[allow(clippy::result_large_err)] - fn handle(self) -> Result<(), SdkError> { - let (critical, reason) = match self { - Error::WithReason { - reason, - critical, - level: tracing::Level::ERROR, - .. - } => { - tracing::error!( - %reason, - "An error occurred during the relay" - ); - (critical, reason) - } - Error::WithReason { - reason, - critical, - level: tracing::Level::DEBUG, - } => { - tracing::debug!( - %reason, - "An error occurred during the relay" - ); - (critical, reason) - } - // all log levels we care about are DEBUG and ERROR - _ => { - hints::cold(); - return Ok(()); - } - }; - if hints::unlikely(critical) { - Err(reason) - } else { - Ok(()) - } - } -} - -/// Get the status of a relay result. -trait GetStatus { - /// Return whether a relay result is successful or not. - fn is_successful(&self) -> bool; -} - -impl GetStatus for TransactionReceipt { - fn is_successful(&self) -> bool { - self.status.map(|s| s.as_u64() == 1).unwrap_or(false) - } -} - -impl GetStatus for Option { - fn is_successful(&self) -> bool { - self.as_ref() - .map(|receipt| receipt.is_successful()) - .unwrap_or(false) - } -} - -impl GetStatus for RelayResult { - fn is_successful(&self) -> bool { - use RelayResult::*; - match self { - BridgeCallError(_) | NonceError { .. } | NoReceipt => false, - Receipt { receipt } => receipt.is_successful(), - } - } -} - -/// Check the nonce of a relay. -enum CheckNonce {} - -/// Do not check the nonce of a relay. -enum DoNotCheckNonce {} - -/// Determine if the nonce in the Bridge smart contract prompts -/// a relay operation or not. -trait ShouldRelay { - /// The result of a relay operation. - type RelayResult: GetStatus + From>; - - /// The type of the future to be returned. - type Future<'gov>: Future> + 'gov; - - /// Returns [`Ok`] if the relay should happen. - fn should_relay(_: Epoch, _: &Bridge) -> Self::Future<'_> - where - E: Middleware, - E::Error: std::fmt::Display; - - /// Try to recover from an error that has happened. - fn try_recover>(err: E) -> Error; -} - -impl ShouldRelay for DoNotCheckNonce { - type Future<'gov> = std::future::Ready>; - type RelayResult = Option; - - #[inline] - fn should_relay(_: Epoch, _: &Bridge) -> Self::Future<'_> - where - E: Middleware, - E::Error: std::fmt::Display, - { - std::future::ready(Ok(())) - } - - #[inline] - fn try_recover>(err: E) -> Error { - Error::recoverable(err) - } -} - -impl ShouldRelay for CheckNonce { - type Future<'gov> = - Pin> + 'gov>>; - type RelayResult = RelayResult; - - fn should_relay(epoch: Epoch, bridge: &Bridge) -> Self::Future<'_> - where - E: Middleware, - E::Error: std::fmt::Display, - { - Box::pin(async move { - let bridge_epoch_prep_call = bridge.validator_set_nonce(); - let bridge_epoch_fut = - bridge_epoch_prep_call.call().map(|result| { - result - .map_err(|err| { - RelayResult::BridgeCallError(err.to_string()) - }) - .map(|e| Epoch(e.as_u64())) - }); - - let gov_current_epoch = bridge_epoch_fut.await?; - if epoch == gov_current_epoch.next() { - Ok(()) - } else { - Err(RelayResult::NonceError { - argument: epoch, - contract: gov_current_epoch, - }) - } - }) - } - - #[inline] - fn try_recover>(err: E) -> Error { - Error::critical(err) - } -} - -/// Relay result for [`CheckNonce`]. -#[allow(clippy::large_enum_variant)] // TODO Box Receipt's TransactionReceipt -enum RelayResult { - /// The call to Bridge failed. - BridgeCallError(String), - /// Some nonce related error occurred. - /// - /// The following comparison must hold: `contract + 1 = argument`. - NonceError { - /// The value of the [`Epoch`] argument passed via CLI. - argument: Epoch, - /// The value of the [`Epoch`] in the bridge contract. - contract: Epoch, - }, - /// No receipt was returned from the relay operation. - NoReceipt, - /// The relay operation returned a transfer receipt. - Receipt { - /// The receipt of the transaction. - receipt: TransactionReceipt, - }, -} - -impl From> for RelayResult { - #[inline] - fn from(maybe_receipt: Option) -> Self { - if let Some(receipt) = maybe_receipt { - Self::Receipt { receipt } - } else { - Self::NoReceipt - } - } -} - -/// Query an ABI encoding of the validator set to be installed -/// at the given epoch, and its associated proof. -pub async fn query_validator_set_update_proof( - client: &(impl Client + Sync), - io: &impl Io, - args: args::ValidatorSetProof, -) -> Result>, SdkError> { - let epoch = if let Some(epoch) = args.epoch { - epoch - } else { - RPC.shell().epoch(client).await.unwrap().next() - }; - - let encoded_proof = RPC - .shell() - .eth_bridge() - .read_valset_upd_proof(client, &epoch) - .await - .map_err(|err| { - SdkError::Query(QueryError::General(echo_error!( - io, - "Failed to fetch validator set update proof: {err}" - ))) - })?; - - display_line!(io, "0x{}", HEXLOWER.encode(encoded_proof.as_ref())); - Ok(encoded_proof) -} - -/// Query an ABI encoding of the Bridge validator set at a given epoch. -pub async fn query_bridge_validator_set( - client: &(impl Client + Sync), - io: &impl Io, - args: args::BridgeValidatorSet, -) -> Result { - let epoch = if let Some(epoch) = args.epoch { - epoch - } else { - RPC.shell().epoch(client).await.unwrap() - }; - - let args = RPC - .shell() - .eth_bridge() - .read_bridge_valset(client, &epoch) - .await - .map_err(|err| { - SdkError::Query(QueryError::General(echo_error!( - io, - "Failed to fetch Bridge validator set: {err}" - ))) - })?; - - display_validator_set(io, args.clone()); - Ok(args) -} - -/// Query an ABI encoding of the Governance validator set at a given epoch. -pub async fn query_governnace_validator_set( - client: &(impl Client + Sync), - io: &impl Io, - args: args::GovernanceValidatorSet, -) -> Result { - let epoch = if let Some(epoch) = args.epoch { - epoch - } else { - RPC.shell().epoch(client).await.unwrap() - }; - - let args = RPC - .shell() - .eth_bridge() - .read_governance_valset(client, &epoch) - .await - .map_err(|err| { - SdkError::Query(QueryError::General(echo_error!( - io, - "Failed to fetch Governance validator set: {err}" - ))) - })?; - - display_validator_set(io, args.clone()); - Ok(args) -} - -/// Display the given [`ValidatorSetArgs`]. -fn display_validator_set(io: &IO, args: ValidatorSetArgs) { - use serde::Serialize; - - #[derive(Serialize)] - struct Validator { - addr: EthAddress, - voting_power: u128, - } - - #[derive(Serialize)] - struct ValidatorSet { - set: Vec, - } - - let ValidatorSetArgs { - validators, - voting_powers, - .. - } = args; - let validator_set = ValidatorSet { - set: validators - .into_iter() - .zip(voting_powers.into_iter().map(u128::from)) - .map(|(addr, voting_power)| Validator { addr, voting_power }) - .collect(), - }; - - display_line!( - io, - "{}", - serde_json::to_string_pretty(&validator_set).unwrap() - ); -} - -/// Relay a validator set update, signed off for a given epoch. -pub async fn relay_validator_set_update( - eth_client: Arc, - client: &(impl Client + Sync), - io: &impl Io, - args: args::ValidatorSetUpdateRelay, -) -> Result<(), SdkError> -where - E: Middleware, - E::Error: std::fmt::Debug + std::fmt::Display, -{ - if args.sync { - block_on_eth_sync( - &*eth_client, - io, - BlockOnEthSync { - #[allow(clippy::disallowed_methods)] - deadline: Instant::now() + Duration::from_secs(60), - delta_sleep: Duration::from_secs(1), - }, - ) - .await?; - } else { - eth_sync_or_exit(&*eth_client, io).await?; - } - - if args.daemon { - relay_validator_set_update_daemon(args, eth_client, client, io).await - } else { - relay_validator_set_update_once::( - &args, - eth_client, - client, - |relay_result| match relay_result { - RelayResult::BridgeCallError(reason) => { - edisplay_line!( - io, - "Calling Bridge failed due to: {reason}" - ); - } - RelayResult::NonceError { argument, contract } => { - let whence = match argument.cmp(&contract) { - Ordering::Less => "behind", - Ordering::Equal => "identical to", - Ordering::Greater => "too far ahead of", - }; - edisplay_line!( - io, - "Argument nonce <{argument}> is {whence} contract \ - nonce <{contract}>" - ); - } - RelayResult::NoReceipt => { - edisplay_line!( - io, - "No transfer receipt received from the Ethereum node" - ); - } - RelayResult::Receipt { receipt } => { - if receipt.is_successful() { - display_line!( - io, - "Ethereum transfer succeeded: {:?}", - receipt - ); - } else { - display_line!( - io, - "Ethereum transfer failed: {:?}", - receipt - ); - } - } - }, - ) - .await - } - .or_else(|err| err.handle()) -} - -async fn relay_validator_set_update_daemon( - mut args: args::ValidatorSetUpdateRelay, - eth_client: Arc, - client: &(impl Client + Sync), - io: &impl Io, -) -> Result<(), Error> -where - E: Middleware, - E::Error: std::fmt::Debug + std::fmt::Display, -{ - const DEFAULT_RETRY_DURATION: Duration = Duration::from_secs(1); - const DEFAULT_SUCCESS_DURATION: Duration = Duration::from_secs(10); - - let retry_duration = args.retry_dur.unwrap_or(DEFAULT_RETRY_DURATION); - let success_duration = args.success_dur.unwrap_or(DEFAULT_SUCCESS_DURATION); - - let mut last_call_succeeded = true; - - tracing::info!("The validator set update relayer daemon has started"); - - loop { - let sleep_for = if last_call_succeeded { - success_duration - } else { - retry_duration - }; - - tracing::debug!(?sleep_for, "Sleeping"); - time::sleep(sleep_for).await; - - let is_synchronizing = - eth_sync_or(&*eth_client, io, || ()).await.is_err(); - if is_synchronizing { - tracing::debug!("The Ethereum node is synchronizing"); - last_call_succeeded = false; - continue; - } - - // we could be racing against governance updates, - // so it is best to always fetch the latest Bridge - // contract address - let bridge = - get_bridge_contract(client, Arc::clone(ð_client)).await?; - let bridge_epoch_prep_call = bridge.validator_set_nonce(); - let bridge_epoch_fut = bridge_epoch_prep_call.call().map(|result| { - result - .map_err(|err| { - Error::critical(QueryError::General(trace_error!( - error, - "Failed to fetch latest validator set nonce: {err}" - ))) - }) - .map(|e| e.as_u64() as i128) - }); - - let shell = RPC.shell(); - let nam_current_epoch_fut = shell.epoch(client).map(|result| { - result - .map_err(|err| { - Error::critical(QueryError::General(trace_error!( - error, - "Failed to fetch the latest epoch in Namada: {err}" - ))) - }) - .map(|Epoch(e)| e as i128) - }); - - let (nam_current_epoch, gov_current_epoch) = - futures::try_join!(nam_current_epoch_fut, bridge_epoch_fut)?; - - tracing::debug!( - ?nam_current_epoch, - ?gov_current_epoch, - "Fetched the latest epochs" - ); - - let new_epoch = match nam_current_epoch - gov_current_epoch { - // NB: a namada epoch should always be one behind the nonce - // in the bridge contract, for the latter to be considered - // up to date - -1 => { - tracing::debug!( - "Nothing to do, since the validator set in the Bridge \ - contract is up to date", - ); - last_call_succeeded = false; - continue; - } - 0.. => { - let e = gov_current_epoch + 1; - // consider only the lower 64-bits - Epoch((e & (u64::MAX as i128)) as u64) - } - // NB: if the nonce difference is lower than 0, somehow the state - // of namada managed to fall behind the state of the smart contract - _ => { - tracing::error!("The Bridge contract is ahead of Namada!"); - last_call_succeeded = false; - continue; - } - }; - - // update epoch in the contract - args.epoch = Some(new_epoch); - - let result = - relay_validator_set_update_once::( - &args, - Arc::clone(ð_client), - client, - |transf_result| { - let Some(receipt) = transf_result else { - tracing::warn!( - "No transfer receipt received from the Ethereum \ - node" - ); - last_call_succeeded = false; - return; - }; - last_call_succeeded = receipt.is_successful(); - if last_call_succeeded { - tracing::info!(?receipt, "Ethereum transfer succeeded"); - tracing::info!(?new_epoch, "Updated the validator set"); - } else { - tracing::error!(?receipt, "Ethereum transfer failed"); - } - }, - ) - .await; - - if let Err(err) = result { - // only print errors, do not exit - _ = err.handle(); - last_call_succeeded = false; - } - } -} - -async fn get_bridge_contract( - nam_client: &C, - eth_client: Arc, -) -> Result, Error> -where - C: Client + Sync, - E: Middleware, -{ - let bridge_contract = RPC - .shell() - .eth_bridge() - .read_bridge_contract(nam_client) - .await - .map_err(|err| { - Error::critical(EthereumBridgeError::RetrieveContract( - err.to_string(), - )) - })?; - Ok(Bridge::new(bridge_contract.address, eth_client)) -} - -async fn relay_validator_set_update_once( - args: &args::ValidatorSetUpdateRelay, - eth_client: Arc, - nam_client: &C, - mut action: F, -) -> Result<(), Error> -where - C: Client + Sync, - E: Middleware, - E::Error: std::fmt::Debug + std::fmt::Display, - R: ShouldRelay, - F: FnMut(R::RelayResult), -{ - let epoch_to_relay = if let Some(epoch) = args.epoch { - epoch - } else { - RPC.shell() - .epoch(nam_client) - .await - .map_err(|e| Error::critical(QueryError::General(e.to_string())))? - .next() - }; - - if hints::unlikely(epoch_to_relay == Epoch(0)) { - return Err(Error::critical(SdkError::Other( - "There is no validator set update proof for epoch 0".into(), - ))); - } - - let shell = RPC.shell().eth_bridge(); - let encoded_proof_fut = shell - .read_valset_upd_proof(nam_client, &epoch_to_relay) - .map(|result| { - result.map_err(|err| { - let msg = format!( - "Failed to fetch validator set update proof: {err}" - ); - SdkError::Query(QueryError::General(msg)) - }) - }); - - let bridge_current_epoch = epoch_to_relay - .prev() - .expect("Epoch to relay must have prev"); - let shell = RPC.shell().eth_bridge(); - let validator_set_args_fut = shell - .read_bridge_valset(nam_client, &bridge_current_epoch) - .map(|result| { - result.map_err(|err| { - let msg = - format!("Failed to fetch Bridge validator set: {err}"); - SdkError::Query(QueryError::General(msg)) - }) - }); - - let shell = RPC.shell().eth_bridge(); - let bridge_address_fut = - shell.read_bridge_contract(nam_client).map(|result| { - result.map_err(|err| { - SdkError::EthereumBridge(EthereumBridgeError::RetrieveContract( - err.to_string(), - )) - }) - }); - - let (encoded_proof, validator_set_args, bridge_contract) = - futures::try_join!( - encoded_proof_fut, - validator_set_args_fut, - bridge_address_fut - ) - .map_err(|err| R::try_recover(err))?; - - let (bridge_hash, gov_hash, signatures): ( - [u8; 32], - [u8; 32], - Vec, - ) = abi_decode_struct(encoded_proof); - - let bridge = Bridge::new(bridge_contract.address, eth_client); - - if let Err(result) = R::should_relay(epoch_to_relay, &bridge).await { - action(result); - return Err(Error::NoContext); - } - - let mut relay_op = bridge.update_validator_set( - validator_set_args.into(), - bridge_hash, - gov_hash, - signatures, - ); - if let Some(gas) = args.gas { - relay_op.tx.set_gas(gas); - } - if let Some(gas_price) = args.gas_price { - relay_op.tx.set_gas_price(gas_price); - } - if let Some(eth_addr) = args.eth_addr { - relay_op.tx.set_from(eth_addr.into()); - } - - let pending_tx = relay_op.send().await.map_err(|e| { - Error::critical(EthereumBridgeError::ContractCall(e.to_string())) - })?; - let transf_result = pending_tx - .confirmations(args.confirmations as usize) - .await - .map_err(|e| { - Error::critical(EthereumBridgeError::Rpc(e.to_string())) - })?; - - let transf_result: R::RelayResult = transf_result.into(); - let status = if transf_result.is_successful() { - Ok(()) - } else { - Err(Error::NoContext) - }; - - action(transf_result); - status -} - -// NOTE: there's a bug (or feature?!) in ethers, where -// `EthAbiCodec` derived `AbiDecode` implementations -// have a decode method that expects a tuple, but -// passes invalid param types to `abi::decode()` -fn abi_decode_struct(data: T) -> D -where - T: AsRef<[u8]>, - D: Tokenizable + AbiDecode + AbiType, -{ - let decoded: (D,) = AbiDecode::decode(data).unwrap(); - decoded.0 -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Test [`GetStatus`] on various values. - #[test] - fn test_relay_op_statuses() { - // failure cases - assert!(!Option::::None.is_successful()); - assert!( - !Some(TransactionReceipt { - status: Some(0.into()), - ..Default::default() - }) - .is_successful() - ); - assert!(!RelayResult::BridgeCallError("".into()).is_successful()); - assert!( - !RelayResult::NonceError { - contract: 0.into(), - argument: 0.into(), - } - .is_successful() - ); - assert!(!RelayResult::NoReceipt.is_successful()); - assert!( - !TransactionReceipt { - status: Some(0.into()), - ..Default::default() - } - .is_successful() - ); - - // success cases - assert!( - Some(TransactionReceipt { - status: Some(1.into()), - ..Default::default() - }) - .is_successful() - ); - assert!( - TransactionReceipt { - status: Some(1.into()), - ..Default::default() - } - .is_successful() - ); - } -} diff --git a/crates/sdk/src/events/log.rs b/crates/sdk/src/events/log.rs index ed1db4935f9..5fba688e55f 100644 --- a/crates/sdk/src/events/log.rs +++ b/crates/sdk/src/events/log.rs @@ -144,9 +144,6 @@ impl<'log> WithMatcher<'log> { #[cfg(test)] mod event_log_tests { use namada_core::hash::Hash; - use namada_core::keccak::KeccakHash; - use namada_ethereum_bridge::event::BridgePoolTxHash; - use namada_ethereum_bridge::event::types::BRIDGE_POOL_RELAYED; use super::*; use crate::events::EventLevel; @@ -163,31 +160,16 @@ mod event_log_tests { }; } - /// An applied tx hash query. - macro_rules! bridge_pool_relayed { - ($hash:expr) => { - dumb_queries::QueryMatcher::bridge_pool_relayed( - &KeccakHash::try_from($hash).unwrap(), - ) - }; - } - /// Return a mock `FinalizeBlock` event. fn mock_event(event_type: EventType, hash: impl AsRef) -> Event { Event::new(event_type, EventLevel::Tx) .with(TxHash(Hash::try_from(hash.as_ref()).unwrap())) - .with(BridgePoolTxHash( - &KeccakHash::try_from(hash.as_ref()).unwrap(), - )) .into() } /// Return a vector of mock `FinalizeBlock` events. fn mock_tx_events(hash: &str) -> Vec { - vec![ - mock_event(BRIDGE_POOL_RELAYED, hash), - mock_event(APPLIED_TX, hash), - ] + vec![mock_event(APPLIED_TX, hash)] } /// Test adding a couple of events to the event log, and @@ -208,25 +190,13 @@ mod event_log_tests { // inspect log assert_eq!(log.iter().count(), NUM_HEIGHTS * events.len()); - let events_in_log: Vec<_> = log - .with_matcher(bridge_pool_relayed!(HASH)) - .iter() - .cloned() - .collect(); - - assert_eq!(events_in_log.len(), NUM_HEIGHTS); - - for event in events_in_log { - assert_eq!(events[0], event); - } - let events_in_log: Vec<_> = log.with_matcher(applied!(HASH)).iter().cloned().collect(); assert_eq!(events_in_log.len(), NUM_HEIGHTS); for event in events_in_log { - assert_eq!(events[1], event); + assert_eq!(events[0], event); } } diff --git a/crates/sdk/src/events/log/dumb_queries.rs b/crates/sdk/src/events/log/dumb_queries.rs index ff890cc4ba2..58d3bf0efe3 100644 --- a/crates/sdk/src/events/log/dumb_queries.rs +++ b/crates/sdk/src/events/log/dumb_queries.rs @@ -3,11 +3,6 @@ use namada_core::chain::BlockHeight; use namada_core::collections::HashMap; use namada_core::hash::Hash; -use namada_core::keccak::KeccakHash; -use namada_ethereum_bridge::event::BridgePoolTxHash; -use namada_ethereum_bridge::event::types::{ - BRIDGE_POOL_EXPIRED, BRIDGE_POOL_RELAYED, -}; use namada_ibc::event::types::UPDATE_CLIENT; use namada_ibc::event::{ ClientId as ClientIdAttr, ConsensusHeights, IbcEvent, IbcEventType, @@ -116,18 +111,6 @@ impl QueryMatcher { event.has_subset_of_attrs(&self.attributes) } - /// Returns a query matching the given relayed Bridge pool transaction hash. - pub fn bridge_pool_relayed(tx_hash: &KeccakHash) -> Self { - Self::with_event_type(BRIDGE_POOL_RELAYED) - .and_attribute(BridgePoolTxHash(tx_hash)) - } - - /// Returns a query matching the given expired Bridge pool transaction hash. - pub fn bridge_pool_expired(tx_hash: &KeccakHash) -> Self { - Self::with_event_type(BRIDGE_POOL_EXPIRED) - .and_attribute(BridgePoolTxHash(tx_hash)) - } - /// Returns a query matching the given applied transaction hash. pub fn applied(tx_hash: Hash) -> Self { Self::with_event_type(APPLIED_TX).and_attribute(TxHashAttr(tx_hash)) @@ -174,7 +157,12 @@ impl QueryMatcher { #[cfg(test)] mod tests { - use namada_ethereum_bridge::event::EthBridgeEvent; + use namada_core::address::testing::established_address_1; + use namada_core::token; + use namada_proof_of_stake::event::types::SLASH; + use namada_proof_of_stake::event::{ + PosEvent, SlashedAmount, SlashedValidator, + }; use namada_token::event::types::TRANSFER; use namada_tx::event::masp_types::TRANSFER as MASP_TRANSFER; @@ -188,34 +176,24 @@ mod tests { /// Test if matching the prefix of an event type works as expected. #[test] fn test_query_matching_prefix() { - let matcher = QueryMatcher::of_event_type::(); + let matcher = QueryMatcher::of_event_type::(); let tests = { - let bp_hash: KeccakHash = HASH.parse().unwrap(); let tx_hash: Hash = HASH.parse().unwrap(); + let amount = token::Amount::from_u64(1000); - let event_1: Event = - Event::new(BRIDGE_POOL_RELAYED, EventLevel::Tx) - .with(BridgePoolTxHash(&bp_hash)) - .into(); + let event_1: Event = Event::new(SLASH, EventLevel::Tx) + .with(SlashedValidator(established_address_1())) + .with(SlashedAmount(&amount.into())) + .into(); let matches_1 = true; - let event_2: Event = - Event::new(BRIDGE_POOL_EXPIRED, EventLevel::Tx) - .with(BridgePoolTxHash(&bp_hash)) - .into(); - let matches_2 = true; - - let event_3: Event = Event::new(UPDATE_CLIENT, EventLevel::Tx) + let event_2: Event = Event::new(UPDATE_CLIENT, EventLevel::Tx) .with(TxHashAttr(tx_hash)) .into(); - let matches_3 = false; + let matches_2 = false; - [ - (event_1, matches_1), - (event_2, matches_2), - (event_3, matches_3), - ] + [(event_1, matches_1), (event_2, matches_2)] }; for (ev, status) in tests { diff --git a/crates/sdk/src/internal_macros.rs b/crates/sdk/src/internal_macros.rs index d7ab2bf9591..3fca4f13cbf 100644 --- a/crates/sdk/src/internal_macros.rs +++ b/crates/sdk/src/internal_macros.rs @@ -6,12 +6,4 @@ macro_rules! echo_error { }} } -macro_rules! trace_error { - ($level:ident, $($arg:tt)*) => {{ - let msg = ::alloc::format!($($arg)*); - ::tracing::$level!("{msg}"); - msg - }} -} - -pub(crate) use {echo_error, trace_error}; +pub(crate) use echo_error; diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 677f0bdffe3..f842ff4e4a7 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -22,8 +22,6 @@ pub use { tendermint_rpc, zeroize, }; -pub mod eth_bridge; - pub mod rpc; pub mod args; @@ -48,7 +46,6 @@ use args::{DeviceTransport, InputAmount, SdkTypes}; use masp_primitives::zip32::PseudoExtendedKey; use namada_core::address::Address; use namada_core::dec::Dec; -use namada_core::ethereum_events::EthAddress; use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; use namada_core::key::*; pub use namada_core::masp::{ @@ -61,16 +58,16 @@ pub use namada_io::{MaybeSend, MaybeSync}; pub use namada_token::masp::{ShieldedUtils, ShieldedWallet}; use namada_tx::Tx; use rpc::{denominate_amount, format_denominated_amount, query_native_token}; -use token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; +use token::DenominatedAmount; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tx::{ ProcessTxResponse, TX_BECOME_VALIDATOR_WASM, TX_BOND_WASM, - TX_BRIDGE_POOL_WASM, TX_CHANGE_COMMISSION_WASM, - TX_CHANGE_CONSENSUS_KEY_WASM, TX_CHANGE_METADATA_WASM, - TX_CLAIM_REWARDS_WASM, TX_DEACTIVATE_VALIDATOR_WASM, TX_IBC_WASM, - TX_INIT_ACCOUNT_WASM, TX_INIT_PROPOSAL, TX_REACTIVATE_VALIDATOR_WASM, - TX_REDELEGATE_WASM, TX_RESIGN_STEWARD, TX_REVEAL_PK, TX_TRANSFER_WASM, - TX_UNBOND_WASM, TX_UNJAIL_VALIDATOR_WASM, TX_UPDATE_ACCOUNT_WASM, + TX_CHANGE_COMMISSION_WASM, TX_CHANGE_CONSENSUS_KEY_WASM, + TX_CHANGE_METADATA_WASM, TX_CLAIM_REWARDS_WASM, + TX_DEACTIVATE_VALIDATOR_WASM, TX_IBC_WASM, TX_INIT_ACCOUNT_WASM, + TX_INIT_PROPOSAL, TX_REACTIVATE_VALIDATOR_WASM, TX_REDELEGATE_WASM, + TX_RESIGN_STEWARD, TX_REVEAL_PK, TX_TRANSFER_WASM, TX_UNBOND_WASM, + TX_UNJAIL_VALIDATOR_WASM, TX_UPDATE_ACCOUNT_WASM, TX_UPDATE_STEWARD_COMMISSION, TX_VOTE_PROPOSAL, TX_WITHDRAW_WASM, VP_USER_WASM, }; @@ -534,33 +531,6 @@ pub trait Namada: NamadaIo { } } - /// Make a Withdraw builder from the given minimum set of arguments - fn new_add_erc20_transfer( - &self, - sender: Address, - recipient: EthAddress, - asset: EthAddress, - amount: InputAmount, - ) -> args::EthereumBridgePool { - args::EthereumBridgePool { - sender, - recipient, - asset, - amount, - fee_amount: InputAmount::Unvalidated( - token::DenominatedAmount::new( - token::Amount::default(), - NATIVE_MAX_DECIMAL_PLACES.into(), - ), - ), - fee_payer: None, - fee_token: self.native_token(), - nut: false, - code_path: PathBuf::from(TX_BRIDGE_POOL_WASM), - tx: self.tx_builder(), - } - } - /// Make a ResignSteward builder from the given minimum set of arguments fn new_resign_steward(&self, steward: Address) -> args::ResignSteward { args::ResignSteward { @@ -847,7 +817,6 @@ pub mod testing { use namada_account::{InitAccount, UpdateAccount}; use namada_core::address::testing::arb_non_internal_address; use namada_core::collections::{HashMap, HashSet}; - use namada_core::eth_bridge_pool::PendingTransfer; use namada_core::hash::testing::arb_hash; use namada_core::key::testing::arb_common_keypair; use namada_core::masp::AssetData; @@ -880,7 +849,6 @@ pub mod testing { BorshDeserialize, BorshSchema, BorshSerialize, BorshSerializeExt, }; use crate::chain::ChainId; - use crate::eth_bridge_pool::testing::arb_pending_transfer; use crate::key::testing::arb_common_pk; use crate::time::{DateTime, DateTimeUtc, TimeZone, Utc}; use crate::tx::data::pgf::tests::arb_update_steward_commission; @@ -920,7 +888,6 @@ pub mod testing { Redelegation(Redelegation), UpdateStewardCommission(UpdateStewardCommission), ResignSteward(Address), - PendingTransfer(PendingTransfer), IbcMsgTransfer( MsgTransfer, Option<(StoredBuildParams, String)>, @@ -1498,22 +1465,6 @@ pub mod testing { } } - prop_compose! { - /// Generate an arbitrary pending transfer transaction - pub fn arb_pending_transfer_tx()( - mut header in arb_header(0), - wrapper in arb_wrapper_tx(), - pending_transfer in arb_pending_transfer(), - code_hash in arb_hash(), - ) -> (Tx, TxData) { - header.tx_type = TxType::Wrapper(Box::new(wrapper)); - let mut tx = Tx { header, sections: vec![] }; - tx.add_data(pending_transfer.clone()); - tx.add_code_from_hash(code_hash, Some(TX_BRIDGE_POOL_WASM.to_owned())); - (tx, TxData::PendingTransfer(pending_transfer)) - } - } - prop_compose! { /// Generate an arbitrary IBC transfer message pub fn arb_msg_transfer()( @@ -1637,7 +1588,6 @@ pub mod testing { arb_redelegation_tx(), arb_update_steward_commission_tx(), arb_resign_steward_tx(), - arb_pending_transfer_tx(), arb_ibc_msg_transfer_tx(), arb_ibc_msg_nft_transfer_tx(), ] diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index 7677bd9a870..aad22aac3fe 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -102,8 +102,7 @@ async fn get_indexed_masp_events_at_height( .block_results(height.0) .await .map_err(|e| Error::from(QueryError::General(e.to_string())))? - .end_block_events - .unwrap_or_default() + .finalize_block_events .into_iter() .map(|event| { // Check if the event is a Masp one diff --git a/crates/sdk/src/queries/mod.rs b/crates/sdk/src/queries/mod.rs index 72d7e04b096..50e90a1e0eb 100644 --- a/crates/sdk/src/queries/mod.rs +++ b/crates/sdk/src/queries/mod.rs @@ -11,11 +11,6 @@ pub use types::{ }; use vp::{VP, Vp}; -pub use self::shell::eth_bridge::{ - Erc20FlowControl, GenBridgePoolProofReq, GenBridgePoolProofRsp, - TransferToErcArgs, TransferToEthereumStatus, -}; - #[macro_use] mod router; mod shell; diff --git a/crates/sdk/src/queries/shell.rs b/crates/sdk/src/queries/shell.rs index 6ed17d266f2..33df843528b 100644 --- a/crates/sdk/src/queries/shell.rs +++ b/crates/sdk/src/queries/shell.rs @@ -1,7 +1,5 @@ use std::collections::BTreeMap; -pub(super) mod eth_bridge; - use borsh::BorshDeserialize; use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; @@ -26,7 +24,6 @@ use namada_token::storage_key::masp_token_map_key; use namada_tx::data::DryRunResult; use namada_tx::event::types::APPLIED; -use self::eth_bridge::{ETH_BRIDGE, EthBridge}; use crate::borsh::BorshSerializeExt; use crate::events::Event; use crate::events::log::dumb_queries; @@ -58,9 +55,6 @@ type Conversion = ( router! {SHELL, // Shell provides storage read access, block metadata and can dry-run a tx - // Ethereum bridge specific queries - ( "eth_bridge" ) = (sub ETH_BRIDGE), - // Epoch of the last committed block ( "epoch" ) -> Epoch = epoch, diff --git a/crates/sdk/src/queries/shell/eth_bridge.rs b/crates/sdk/src/queries/shell/eth_bridge.rs deleted file mode 100644 index 93b33f85988..00000000000 --- a/crates/sdk/src/queries/shell/eth_bridge.rs +++ /dev/null @@ -1,1792 +0,0 @@ -//! Ethereum bridge related shell queries. - -use std::borrow::Cow; -use std::str::FromStr; - -use borsh::{BorshDeserialize, BorshSerialize}; -use namada_core::address::Address; -use namada_core::arith::checked; -use namada_core::chain::{BlockHeight, Epoch}; -use namada_core::collections::{HashMap, HashSet}; -use namada_core::eth_abi::{Encode, EncodeCell}; -use namada_core::eth_bridge_pool::{PendingTransfer, PendingTransferAppendix}; -use namada_core::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, -}; -use namada_core::keccak::KeccakHash; -use namada_core::storage::{DbKeySeg, Key}; -use namada_core::token::Amount; -use namada_core::voting_power::FractionalVotingPower; -use namada_core::{ethereum_structs, hints}; -use namada_ethereum_bridge::event::{BpTransferStatus, BridgePoolTxHash}; -use namada_ethereum_bridge::protocol::transactions::votes::{ - EpochedVotingPower, EpochedVotingPowerExt, -}; -use namada_ethereum_bridge::storage::bridge_pool::get_key_from_hash; -use namada_ethereum_bridge::storage::eth_bridge_queries::EthBridgeQueries; -use namada_ethereum_bridge::storage::parameters::UpgradeableContract; -use namada_ethereum_bridge::storage::proof::{EthereumProof, sort_sigs}; -use namada_ethereum_bridge::storage::vote_tallies::{Keys, eth_msgs_prefix}; -use namada_ethereum_bridge::storage::{ - bridge_contract_key, native_erc20_key, vote_tallies, -}; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_state::MembershipProof::BridgePool; -use namada_state::{DB, DBIter, StorageHasher, StoreRef, StoreType}; -use namada_storage::{CustomError, ResultExt, StorageRead}; -use namada_vote_ext::validator_set_update::{ - ValidatorSetArgs, VotingPowersMap, -}; -use serde::{Deserialize, Serialize}; - -use crate::borsh::BorshSerializeExt; -use crate::eth_bridge::ethers::abi::AbiDecode; -use crate::governance; -use crate::queries::{EncodedResponseQuery, RequestCtx, RequestQuery}; - -/// Container for the status of queried transfers to Ethereum. -#[derive( - Default, - Debug, - Clone, - Eq, - PartialEq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - Serialize, - Deserialize, -)] -pub struct TransferToEthereumStatus { - /// The block height at which the query was performed. - /// - /// This value may be used to busy wait while a Bridge pool - /// proof is being constructed for it, such that clients can - /// safely perform additional actions. - pub queried_height: BlockHeight, - /// Transfers in the query whose status it was determined - /// to be `pending`. - pub pending: HashSet, - /// Transfers in the query whose status it was determined - /// to be `relayed`. - pub relayed: HashSet, - /// Transfers in the query whose status it was determined - /// to be `expired`. - pub expired: HashSet, - /// Hashes pertaining to bogus data that might have been queried, - /// or transfers that were not in the event log, despite having - /// been relayed to Ethereum or expiring from the Bridge pool. - pub unrecognized: HashSet, -} - -/// Contains information about the flow control of some ERC20 -/// wrapped asset. -#[derive( - Debug, - Copy, - Clone, - Eq, - PartialEq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -pub struct Erc20FlowControl { - /// Whether the wrapped asset is whitelisted. - pub whitelisted: bool, - /// Total minted supply of some wrapped asset. - pub supply: Amount, - /// The token cap of some wrapped asset. - pub cap: Amount, -} - -impl Erc20FlowControl { - /// Check if the `transferred_amount` exceeds the token caps of some ERC20 - /// asset. - #[inline] - #[allow(clippy::result_large_err)] - pub fn exceeds_token_caps( - &self, - transferred_amount: Amount, - ) -> crate::error::Result { - Ok(checked!(self.supply + transferred_amount)? > self.cap) - } -} - -/// Request data to pass to `generate_bridge_pool_proof`. -#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] -pub struct GenBridgePoolProofReq<'transfers, 'relayer> { - /// The hashes of the transfers to be relayed. - pub transfers: Cow<'transfers, [KeccakHash]>, - /// The address of the relayer to compensate. - pub relayer: Cow<'relayer, Address>, - /// Whether to return the appendix of a [`PendingTransfer`]. - pub with_appendix: bool, -} - -/// Arguments to pass to `transfer_to_erc`. -pub type TransferToErcArgs = ( - ethereum_structs::ValidatorSetArgs, - Vec, - ethereum_structs::RelayProof, -); - -/// Response data returned by `generate_bridge_pool_proof`. -#[derive( - Debug, - Clone, - Eq, - PartialEq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, -)] -pub struct GenBridgePoolProofRsp { - /// Ethereum ABI encoded arguments to pass to `transfer_to_erc`. - pub abi_encoded_args: Vec, - /// Appendix data of all requested pending transfers. - pub appendices: Option>>, -} - -impl GenBridgePoolProofRsp { - /// Retrieve all [`PendingTransfer`] instances returned from the RPC server. - pub fn pending_transfers(self) -> impl Iterator { - TransferToErcArgs::decode(&self.abi_encoded_args) - .into_iter() - .flat_map(|(_, _, proof)| proof.transfers) - .zip(self.appendices.into_iter().flatten()) - .map(|(event, appendix)| { - let event: TransferToEthereum = event.into(); - PendingTransfer::from_parts(&event, appendix) - }) - } -} - -router! {ETH_BRIDGE, - // Get the current contents of the Ethereum bridge pool - ( "pool" / "contents" ) - -> Vec = read_ethereum_bridge_pool, - - // Get the contents of the Ethereum bridge pool covered by - // the latest signed Merkle tree root. - ( "pool" / "signed_contents" ) - -> Vec = read_signed_ethereum_bridge_pool, - - // Generate a merkle proof for the inclusion of requested - // transfers in the Ethereum bridge pool - ( "pool" / "proof" ) - -> GenBridgePoolProofRsp = (with_options generate_bridge_pool_proof), - - // Iterates over all ethereum events and returns the amount of - // voting power backing each `TransferToEthereum` event. - ( "pool" / "transfer_to_eth_progress" ) - -> HashMap - = transfer_to_ethereum_progress, - - // Given a list of keccak hashes, check whether they have been - // relayed, expired or if they are still pending. - ( "pool" / "transfer_status" ) - -> TransferToEthereumStatus = (with_options pending_eth_transfer_status), - - // Request a proof of a validator set signed off for - // the given epoch. - // - // The request may fail if a proof is not considered complete yet. - ( "validator_set" / "proof" / [epoch: Epoch] ) - -> EncodeCell> - = read_valset_upd_proof, - - // Request the set of bridge validators at the given epoch. - // - // The request may fail if no validator set exists at that epoch. - ( "validator_set" / "bridge" / [epoch: Epoch] ) - -> ValidatorSetArgs = read_bridge_valset, - - // Request the set of governance validators at the given epoch. - // - // The request may fail if no validator set exists at that epoch. - ( "validator_set" / "governance" / [epoch: Epoch] ) - -> ValidatorSetArgs = read_governance_valset, - - // Read the address and version of the Ethereum bridge's Bridge - // smart contract. - ( "contracts" / "bridge" ) - -> UpgradeableContract = read_bridge_contract, - - // Read the address of the Ethereum bridge's native ERC20 - // smart contract. - ( "contracts" / "native_erc20" ) - -> EthAddress = read_native_erc20_contract, - - // Read the voting powers map for the requested validator set - // at the given block height. - ( "voting_powers" / "height" / [height: BlockHeight] ) - -> VotingPowersMap = voting_powers_at_height, - - // Read the voting powers map for the requested validator set - // at the given block height. - ( "voting_powers" / "epoch" / [epoch: Epoch] ) - -> VotingPowersMap = voting_powers_at_epoch, - - // Read the total supply and respective cap of some wrapped - // ERC20 token in Namada. - ( "erc20" / "flow_control" / [asset: EthAddress] ) - -> Erc20FlowControl = get_erc20_flow_control, -} - -/// Given a list of keccak hashes, check whether they have been -/// relayed, expired or if they are still pending. -fn pending_eth_transfer_status( - ctx: RequestCtx<'_, D, H, V, T>, - request: &RequestQuery, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut transfer_hashes: HashSet = - BorshDeserialize::try_from_slice(&request.data) - .into_storage_result()?; - - if transfer_hashes.is_empty() { - return Ok(Default::default()); - } - - let last_committed_height = ctx.state.in_mem().get_last_block_height(); - let mut status = TransferToEthereumStatus { - queried_height: last_committed_height, - ..Default::default() - }; - - // check which transfers in the Bridge pool match the requested hashes - let merkle_tree = ctx - .state - .get_merkle_tree(last_committed_height, Some(StoreType::BridgePool)) - .expect("We should always be able to read the database"); - let stores = merkle_tree.stores(); - let store = match stores.store(&StoreType::BridgePool) { - StoreRef::BridgePool(store) => store, - _ => unreachable!(), - }; - if hints::likely(store.len() > transfer_hashes.len()) { - transfer_hashes.retain(|hash| { - let transfer_in_pool = store.contains_key(hash); - if transfer_in_pool { - status.pending.insert(hash.clone()); - } - !transfer_in_pool - }); - } else { - for hash in store.keys() { - if transfer_hashes.swap_remove(hash) { - status.pending.insert(hash.clone()); - } - if transfer_hashes.is_empty() { - break; - } - } - } - - if transfer_hashes.is_empty() { - let data = status.serialize_to_vec(); - return Ok(EncodedResponseQuery { - data, - height: last_committed_height, - ..Default::default() - }); - } - - // INVARIANT: transfers that are in the event log will have already - // been processed and therefore removed from the Bridge pool at the - // time of this query - let completed_transfers = ctx.event_log.iter().filter_map(|ev| { - let Ok(transfer_status) = BpTransferStatus::try_from(ev.kind()) else { - return None; - }; - let tx_hash: KeccakHash = ev - .read_attribute::>() - .expect("The transfer hash must be available"); - if !transfer_hashes.swap_remove(&tx_hash) { - return None; - } - Some((tx_hash, transfer_status, transfer_hashes.is_empty())) - }); - for (hash, transfer_status, early_exit) in completed_transfers { - if hints::likely(matches!(transfer_status, BpTransferStatus::Relayed)) { - status.relayed.insert(hash.clone()); - } else { - status.expired.insert(hash.clone()); - } - if early_exit { - // early drop of the transfer hashes, in - // case its storage capacity was big - transfer_hashes = Default::default(); - break; - } - } - - let status = { - // any remaining transfers are returned as - // unrecognized hashes - status.unrecognized = transfer_hashes; - status - }; - Ok(EncodedResponseQuery { - data: status.serialize_to_vec(), - height: last_committed_height, - ..Default::default() - }) -} - -/// Read the total supply and respective cap of some wrapped -/// ERC20 token in Namada. -fn get_erc20_flow_control( - ctx: RequestCtx<'_, D, H, V, T>, - asset: EthAddress, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let ethbridge_queries = ctx.state.ethbridge_queries(); - - let whitelisted = ethbridge_queries.is_token_whitelisted(&asset); - let supply = ethbridge_queries - .get_token_supply(&asset) - .unwrap_or_default(); - let cap = ethbridge_queries.get_token_cap(&asset).unwrap_or_default(); - - Ok(Erc20FlowControl { - whitelisted, - supply, - cap, - }) -} - -/// Helper function to read a smart contract from storage. -fn read_contract( - key: &Key, - ctx: RequestCtx<'_, D, H, V, U>, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - T: BorshDeserialize, -{ - let Some(contract) = StorageRead::read(ctx.state, key)? else { - return Err(namada_storage::Error::SimpleMessage( - "Failed to read contract: The Ethereum bridge storage is not \ - initialized", - )); - }; - Ok(contract) -} - -/// Read the address and version of the Ethereum bridge's Bridge -/// smart contract. -#[inline] -fn read_bridge_contract( - ctx: RequestCtx<'_, D, H, V, T>, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - read_contract(&bridge_contract_key(), ctx) -} - -/// Read the address of the Ethereum bridge's native ERC20 -/// smart contract. -#[inline] -fn read_native_erc20_contract( - ctx: RequestCtx<'_, D, H, V, T>, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - read_contract(&native_erc20_key(), ctx) -} - -/// Read the current contents of the Ethereum bridge -/// pool. -fn read_ethereum_bridge_pool( - ctx: RequestCtx<'_, D, H, V, T>, -) -> namada_storage::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - Ok(read_ethereum_bridge_pool_at_height( - ctx.state.in_mem().get_last_block_height(), - ctx, - )) -} - -/// Read the contents of the Ethereum bridge -/// pool covered by the latest signed root. -fn read_signed_ethereum_bridge_pool( - ctx: RequestCtx<'_, D, H, V, T>, -) -> namada_storage::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - // get the latest signed merkle root of the Ethereum bridge pool - let (_, height) = ctx - .state - .ethbridge_queries() - .get_signed_bridge_pool_root() - .ok_or(namada_storage::Error::SimpleMessage( - "No signed root for the Ethereum bridge pool exists in storage.", - )) - .into_storage_result()?; - Ok(read_ethereum_bridge_pool_at_height(height, ctx)) -} - -/// Read the Ethereum bridge pool contents at a specified height. -fn read_ethereum_bridge_pool_at_height( - height: BlockHeight, - ctx: RequestCtx<'_, D, H, V, T>, -) -> Vec -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - // get the backing store of the merkle tree corresponding - // at the specified height. - let merkle_tree = ctx - .state - .get_merkle_tree(height, Some(StoreType::BridgePool)) - .expect("We should always be able to read the database"); - let stores = merkle_tree.stores(); - let store = match stores.store(&StoreType::BridgePool) { - StoreRef::BridgePool(store) => store, - _ => unreachable!(), - }; - - let transfers: Vec = store - .keys() - .map(|hash| { - let value = ctx - .state - .db_read_with_height(&get_key_from_hash(hash), height) - .unwrap() - .0 - .unwrap(); - PendingTransfer::try_from_slice(&value).unwrap() - }) - .collect(); - transfers -} - -/// Generate a merkle proof for the inclusion of the -/// requested transfers in the Ethereum bridge pool. -fn generate_bridge_pool_proof( - ctx: RequestCtx<'_, D, H, V, T>, - request: &RequestQuery, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - if let Ok(GenBridgePoolProofReq { - transfers: transfer_hashes, - relayer, - with_appendix, - }) = BorshDeserialize::try_from_slice(&request.data) - { - // get the latest signed merkle root of the Ethereum bridge pool - let (signed_root, height) = ctx - .state - .ethbridge_queries() - .get_signed_bridge_pool_root() - .ok_or(namada_storage::Error::SimpleMessage( - "No signed root for the Ethereum bridge pool exists in \ - storage.", - )) - .into_storage_result()?; - - // make sure a relay attempt won't happen before the new signed - // root has had time to be generated - let latest_bp_nonce = - ctx.state.ethbridge_queries().get_bridge_pool_nonce(); - if latest_bp_nonce != signed_root.data.1 { - return Err(namada_storage::Error::Custom(CustomError( - format!( - "Mismatch between the nonce in the Bridge pool root proof \ - ({}) and the latest Bridge pool nonce in storage ({})", - signed_root.data.1, latest_bp_nonce, - ) - .into(), - ))); - } - - // get the merkle tree corresponding to the above root. - let tree = ctx - .state - .get_merkle_tree(height, Some(StoreType::BridgePool)) - .into_storage_result()?; - // from the hashes of the transfers, get the actual values. - let mut missing_hashes = vec![]; - let (keys, values): (Vec<_>, Vec>) = transfer_hashes - .iter() - .filter_map(|hash| { - let key = get_key_from_hash(hash); - match ctx.state.read_bytes(&key) { - Ok(Some(bytes)) => Some((key, bytes)), - _ => { - missing_hashes.push(hash); - None - } - } - }) - .unzip(); - if !missing_hashes.is_empty() { - return Err(namada_storage::Error::Custom(CustomError( - format!( - "One or more of the provided hashes had no corresponding \ - transfer in storage: {:?}", - missing_hashes - ) - .into(), - ))); - } - let (transfers, appendices) = values.iter().fold( - (vec![], vec![]), - |(mut transfers, mut appendices), bytes| { - let pending = PendingTransfer::try_from_slice(bytes) - .expect("Deserializing storage shouldn't fail"); - let eth_transfer = (&pending).into(); - if with_appendix { - appendices.push(pending.into_appendix()); - } - transfers.push(eth_transfer); - (transfers, appendices) - }, - ); - // get the membership proof - match tree.get_sub_tree_existence_proof( - &keys, - values.iter().map(|v| v.as_slice()).collect(), - ) { - Ok(BridgePool(proof)) => { - let (validator_args, voting_powers) = ctx - .state - .ethbridge_queries() - .get_bridge_validator_set::>(None); - let relay_proof = ethereum_structs::RelayProof { - transfers, - pool_root: signed_root.data.0.0, - proof: proof.proof.into_iter().map(|hash| hash.0).collect(), - proof_flags: proof.flags, - batch_nonce: signed_root.data.1.into(), - relayer_address: relayer.to_string(), - }; - let validator_set: ethereum_structs::ValidatorSetArgs = - validator_args.into(); - let signatures = - sort_sigs(&voting_powers, &signed_root.signatures); - let rsp = GenBridgePoolProofRsp { - abi_encoded_args: ethers::abi::AbiEncode::encode(( - validator_set, - signatures, - relay_proof, - )), - appendices: with_appendix.then_some(appendices), - }; - let data = rsp.serialize_to_vec(); - Ok(EncodedResponseQuery { - data, - height, - ..Default::default() - }) - } - Ok(_) => unreachable!(), - Err(e) => Err(namada_storage::Error::new(e)), - } - } else { - Err(namada_storage::Error::SimpleMessage( - "Could not deserialize transfers", - )) - } -} - -/// Iterates over all ethereum events -/// and returns the amount of voting power -/// backing each `TransferToEthereum` event. -fn transfer_to_ethereum_progress( - ctx: RequestCtx<'_, D, H, V, T>, -) -> namada_storage::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let mut pending_events = HashMap::new(); - for (mut key, value) in ctx - .state - .iter_prefix(ð_msgs_prefix())? - .filter_map(|(k, v, _)| { - let key = Key::from_str(&k).expect( - "Iterating over keys from storage shouldn't not yield \ - un-parsable keys.", - ); - match key.segments.last() { - Some(DbKeySeg::StringSeg(seg)) - if seg == Keys::segments().body => - { - Some((key, v)) - } - _ => None, - } - }) - { - // we checked above that key is not empty, so this write is fine - *key.segments.last_mut().unwrap() = - DbKeySeg::StringSeg(Keys::segments().seen.into()); - // check if the event has been seen - let is_seen = - ctx.state.read::(&key).into_storage_result()?.expect( - "Iterating over storage should not yield keys without values.", - ); - if is_seen { - continue; - } - - if let Ok(EthereumEvent::TransfersToEthereum { transfers, .. }) = - EthereumEvent::try_from_slice(&value) - { - // read the voting power behind the event - *key.segments.last_mut().unwrap() = - DbKeySeg::StringSeg(Keys::segments().voting_power.into()); - let voting_power = ctx - .state - .read::(&key) - .into_storage_result()? - .expect( - "Iterating over storage should not yield keys without \ - values.", - ) - .fractional_stake::<_, _, governance::Store<_>>(ctx.state); - for transfer in transfers { - let key = get_key_from_hash(&transfer.keccak256()); - let transfer = ctx - .state - .read::(&key) - .into_storage_result()? - .expect("The transfer must be present in storage"); - pending_events.insert(transfer, voting_power); - } - } - } - Ok(pending_events) -} - -/// Read a validator set update proof from storage. -/// -/// This method may fail if a complete proof (i.e. with more than -/// 2/3 of the total voting power behind it) is not available yet. -fn read_valset_upd_proof( - ctx: RequestCtx<'_, D, H, V, T>, - epoch: Epoch, -) -> namada_storage::Result>> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - if epoch.0 == 0 { - return Err(namada_storage::Error::Custom(CustomError( - "Validator set update proofs should only be requested from epoch \ - 1 onwards" - .into(), - ))); - } - let current_epoch = ctx.state.in_mem().last_epoch; - if epoch > current_epoch.next() { - return Err(namada_storage::Error::Custom(CustomError( - format!( - "Requesting validator set update proof for {epoch:?}, but the \ - last installed epoch is still {current_epoch:?}" - ) - .into(), - ))); - } - - if !ctx.state.ethbridge_queries().valset_upd_seen(epoch) { - return Err(namada_storage::Error::Custom(CustomError( - format!( - "Validator set update proof is not yet available for the \ - queried epoch: {epoch:?}" - ) - .into(), - ))); - } - - let valset_upd_keys = vote_tallies::Keys::from(&epoch); - let proof: EthereumProof = - StorageRead::read(ctx.state, &valset_upd_keys.body())?.expect( - "EthereumProof is seen in storage, therefore it must exist", - ); - - // NOTE: we pass the epoch of the new set of validators - Ok(proof.map(|set| (epoch, set)).encode()) -} - -/// Request the set of bridge validators at the given epoch. -/// -/// This method may fail if no set of validators exists yet, -/// at that [`Epoch`]. -fn read_bridge_valset( - ctx: RequestCtx<'_, D, H, V, T>, - epoch: Epoch, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let current_epoch = ctx.state.in_mem().last_epoch; - if epoch > current_epoch.next() { - Err(namada_storage::Error::Custom(CustomError( - format!( - "Requesting Bridge validator set at {epoch:?}, but the last \ - installed epoch is still {current_epoch:?}" - ) - .into(), - ))) - } else { - Ok(ctx - .state - .ethbridge_queries() - .get_bridge_validator_set::>(Some(epoch)) - .0) - } -} - -/// Request the set of governance validators at the given epoch. -/// -/// This method may fail if no set of validators exists yet, -/// at that [`Epoch`]. -fn read_governance_valset( - ctx: RequestCtx<'_, D, H, V, T>, - epoch: Epoch, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let current_epoch = ctx.state.in_mem().last_epoch; - if epoch > current_epoch.next() { - Err(namada_storage::Error::Custom(CustomError( - format!( - "Requesting Governance validator set at {epoch:?}, but the \ - last installed epoch is still {current_epoch:?}" - ) - .into(), - ))) - } else { - Ok(ctx - .state - .ethbridge_queries() - .get_governance_validator_set::>(Some(epoch)) - .0) - } -} - -/// Retrieve the consensus validator voting powers at the -/// given [`BlockHeight`]. -fn voting_powers_at_height( - ctx: RequestCtx<'_, D, H, V, T>, - height: BlockHeight, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let maybe_epoch = ctx.state.get_epoch_at_height(height).unwrap(); - let Some(epoch) = maybe_epoch else { - return Err(namada_storage::Error::SimpleMessage( - "The epoch of the requested height does not exist", - )); - }; - voting_powers_at_epoch(ctx, epoch) -} - -/// Retrieve the consensus validator voting powers at the -/// given [`Epoch`]. -fn voting_powers_at_epoch( - ctx: RequestCtx<'_, D, H, V, T>, - epoch: Epoch, -) -> namada_storage::Result -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let current_epoch = ctx.state.in_mem().get_current_epoch().0; - if epoch > checked!(current_epoch + 1u64)? { - return Err(namada_storage::Error::SimpleMessage( - "The requested epoch cannot be queried", - )); - } - let (_, voting_powers) = ctx - .state - .ethbridge_queries() - .get_bridge_validator_set::>(Some(epoch)); - Ok(voting_powers) -} - -#[cfg(test)] -mod test_ethbridge_router { - use std::collections::BTreeMap; - - use assert_matches::assert_matches; - use namada_core::address::testing::{established_address_1, nam}; - use namada_core::eth_bridge_pool::{ - GasFee, TransferToEthereum, TransferToEthereumKind, - }; - use namada_core::voting_power::EthBridgeVotingPower; - use namada_ethereum_bridge::protocol::transactions::validator_set_update::aggregate_votes; - use namada_ethereum_bridge::storage::bridge_pool::{ - BridgePoolTree, get_pending_key, get_signed_root_key, - }; - use namada_ethereum_bridge::storage::proof::BridgePoolRootProof; - use namada_ethereum_bridge::storage::whitelist; - use namada_ethereum_bridge::test_utils::GovStore; - use namada_proof_of_stake::queries::get_total_voting_power; - use namada_storage::StorageWrite; - use namada_storage::mockdb::MockDBWriteBatch; - use namada_vote_ext::validator_set_update; - use namada_vote_ext::validator_set_update::{ - EthAddrBook, VotingPowersMapExt, - }; - - use super::test_utils::bertha_address; - use super::*; - use crate::queries::RPC; - use crate::queries::testing::TestClient; - - /// Test that reading the bridge validator set works. - #[tokio::test] - async fn test_read_consensus_valset() { - let mut client = TestClient::new(RPC); - let epoch = Epoch(0); - assert_eq!(client.state.in_mem().last_epoch, epoch); - - // write validator to storage - test_utils::init_default_storage(&mut client.state); - - // commit the changes - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - - // check the response - let validator_set = RPC - .shell() - .eth_bridge() - .read_bridge_valset(&client, &epoch) - .await - .unwrap(); - let expected = { - let total_power = - get_total_voting_power::<_, GovStore<_>>(&client.state, epoch) - .into(); - - let voting_powers_map: VotingPowersMap = client - .state - .ethbridge_queries() - .get_consensus_eth_addresses::>(epoch) - .map(|(addr_book, _, power)| (addr_book, power)) - .collect(); - let (validators, voting_powers) = voting_powers_map - .get_sorted() - .into_iter() - .map(|(&EthAddrBook { hot_key_addr, .. }, &power)| { - let voting_power: EthBridgeVotingPower = - FractionalVotingPower::new(power.into(), total_power) - .expect("Fractional voting power should be >1") - .try_into() - .unwrap(); - (hot_key_addr, voting_power) - }) - .unzip(); - - ValidatorSetArgs { - epoch, - validators, - voting_powers, - } - }; - - assert_eq!(validator_set, expected); - } - - /// Test that when reading an consensus validator set too far ahead, - /// RPC clients are met with an error. - #[tokio::test] - async fn test_read_consensus_valset_too_far_ahead() { - let mut client = TestClient::new(RPC); - assert_eq!(client.state.in_mem().last_epoch.0, 0); - - // write validator to storage - test_utils::init_default_storage(&mut client.state); - - // commit the changes - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - - // check the response - let result = RPC - .shell() - .eth_bridge() - .read_bridge_valset(&client, &Epoch(999_999)) - .await; - let Err(err) = result else { - panic!("Test failed"); - }; - - assert!( - err.to_string() - .split_once("but the last installed epoch is still") - .is_some() - ); - } - - /// Test that reading a validator set proof works. - #[tokio::test] - async fn test_read_valset_upd_proof() { - let mut client = TestClient::new(RPC); - assert_eq!(client.state.in_mem().last_epoch.0, 0); - - // write validator to storage - let keys = test_utils::init_default_storage(&mut client.state); - - // write proof to storage - let vext = validator_set_update::Vext { - voting_powers: VotingPowersMap::new(), - validator_addr: established_address_1(), - signing_epoch: 0.into(), - } - .sign( - &keys - .get(&established_address_1()) - .expect("Test failed") - .eth_bridge, - ); - let tx_result = aggregate_votes::<_, _, GovStore<_>>( - &mut client.state, - validator_set_update::VextDigest::singleton(vext.clone()), - 0.into(), - ) - .expect("Test failed"); - assert!(!tx_result.changed_keys.is_empty()); - - // commit the changes - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - - // check the response - let proof = RPC - .shell() - .eth_bridge() - .read_valset_upd_proof(&client, &Epoch(1)) - .await - .unwrap(); - let expected = { - let mut proof = - EthereumProof::new((1.into(), vext.0.data.voting_powers)); - proof.attach_signature( - client - .state - .ethbridge_queries() - .get_eth_addr_book::>( - &established_address_1(), - Some(0.into()), - ) - .expect("Test failed"), - vext.0.sig, - ); - proof.encode() - }; - - assert_eq!(proof, expected); - } - - /// Test that when reading a validator set proof too far ahead, - /// RPC clients are met with an error. - #[tokio::test] - async fn test_read_valset_upd_proof_too_far_ahead() { - let mut client = TestClient::new(RPC); - assert_eq!(client.state.in_mem().last_epoch.0, 0); - - // write validator to storage - test_utils::init_default_storage(&mut client.state); - - // commit the changes - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - - // check the response - let result = RPC - .shell() - .eth_bridge() - .read_valset_upd_proof(&client, &Epoch(999_999)) - .await; - let Err(err) = result else { - panic!("Test failed"); - }; - - assert!( - err.to_string() - .split_once("but the last installed epoch is still") - .is_some() - ); - } - - /// Test that reading the bridge pool works - #[tokio::test] - async fn test_read_bridge_pool() { - let mut client = TestClient::new(RPC); - - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - - // write a transfer into the bridge pool - client.state.in_mem_mut().block.height = 1.into(); - client - .state - .write(&get_pending_key(&transfer), &transfer) - .expect("Test failed"); - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // check the response - let pool = RPC - .shell() - .eth_bridge() - .read_ethereum_bridge_pool(&client) - .await - .unwrap(); - assert_eq!(pool, Vec::from([transfer])); - } - - /// Test that reading the bridge pool always gets - /// the latest pool - #[tokio::test] - async fn test_bridge_pool_updates() { - let mut client = TestClient::new(RPC); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - - // write a transfer into the bridge pool - client - .state - .write(&get_pending_key(&transfer), &transfer) - .expect("Test failed"); - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // update the pool - client - .state - .delete(&get_pending_key(&transfer)) - .expect("Test failed"); - let mut transfer2 = transfer; - transfer2.transfer.amount = 1.into(); - client - .state - .write(&get_pending_key(&transfer2), &transfer2) - .expect("Test failed"); - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // check the response - let pool = RPC - .shell() - .eth_bridge() - .read_ethereum_bridge_pool(&client) - .await - .unwrap(); - assert_eq!(pool, Vec::from([transfer2])); - } - - /// Test that we can get a merkle proof even if the signed - /// merkle roots is lagging behind the pool - #[tokio::test] - async fn test_get_merkle_proof() { - let mut client = TestClient::new(RPC); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - - // write validator to storage - test_utils::init_default_storage(&mut client.state); - - // write a transfer into the bridge pool - client - .state - .write(&get_pending_key(&transfer), &transfer) - .expect("Test failed"); - - // create a signed Merkle root for this pool - let signed_root = BridgePoolRootProof { - signatures: Default::default(), - data: (transfer.keccak256(), 0.into()), - }; - let written_height = client.state.in_mem().block.height; - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // update the pool - let mut transfer2 = transfer.clone(); - transfer2.transfer.amount = 1.into(); - client - .state - .write(&get_pending_key(&transfer2), transfer2) - .expect("Test failed"); - - // add the signature for the pool at the previous block height - client - .state - .write( - &get_signed_root_key(), - (signed_root.clone(), written_height), - ) - .expect("Test failed"); - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - let resp = RPC - .shell() - .eth_bridge() - .generate_bridge_pool_proof( - &client, - Some( - GenBridgePoolProofReq { - transfers: vec![transfer.keccak256()].into(), - relayer: Cow::Owned(bertha_address()), - with_appendix: false, - } - .serialize_to_vec(), - ), - None, - false, - ) - .await - .unwrap(); - - let tree = BridgePoolTree::new( - transfer.keccak256(), - BTreeMap::from([(transfer.keccak256(), 1.into())]), - ); - let proof = tree - .get_membership_proof(vec![transfer.clone()]) - .expect("Test failed"); - - let (validator_args, voting_powers) = - client - .state - .ethbridge_queries() - .get_bridge_validator_set::>(None); - let relay_proof = ethereum_structs::RelayProof { - transfers: vec![(&transfer).into()], - pool_root: signed_root.data.0.0, - proof: proof.proof.into_iter().map(|hash| hash.0).collect(), - proof_flags: proof.flags, - batch_nonce: Default::default(), - relayer_address: bertha_address().to_string(), - }; - let signatures = sort_sigs(&voting_powers, &signed_root.signatures); - let validator_set: ethereum_structs::ValidatorSetArgs = - validator_args.into(); - let encoded = ethers::abi::AbiEncode::encode(( - validator_set, - signatures, - relay_proof, - )); - assert_eq!(encoded, resp.data.abi_encoded_args); - } - - /// Test if the merkle tree including a transfer has not had its - /// root signed, then we cannot generate a proof. - #[tokio::test] - async fn test_cannot_get_proof() { - let mut client = TestClient::new(RPC); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - // write validator to storage - test_utils::init_default_storage(&mut client.state); - - // write a transfer into the bridge pool - client - .state - .write(&get_pending_key(&transfer), &transfer) - .expect("Test failed"); - - // create a signed Merkle root for this pool - let signed_root = BridgePoolRootProof { - signatures: Default::default(), - data: (transfer.keccak256(), 0.into()), - }; - - // commit the changes and increase block height - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // update the pool - let mut transfer2 = transfer; - transfer2.transfer.amount = 1.into(); - client - .state - .write(&get_pending_key(&transfer2), &transfer2) - .expect("Test failed"); - - // add the signature for the pool at the previous block height - client - .state - .write(&get_signed_root_key(), (signed_root, BlockHeight::from(0))) - .expect("Test failed"); - - // commit the changes and increase block height - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // this is in the pool, but its merkle root has not been signed yet - let resp = RPC - .shell() - .eth_bridge() - .generate_bridge_pool_proof( - &client, - Some( - GenBridgePoolProofReq { - transfers: vec![transfer2.keccak256()].into(), - relayer: Cow::Owned(bertha_address()), - with_appendix: false, - } - .serialize_to_vec(), - ), - None, - false, - ) - .await; - // thus proof generation should fail - assert!(resp.is_err()); - } - - /// Test that the RPC call for bridge pool transfers - /// covered by a signed merkle root behaves correctly. - #[tokio::test] - async fn test_read_signed_bp_transfers() { - let mut client = TestClient::new(RPC); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - // write validator to storage - test_utils::init_default_storage(&mut client.state); - - // write a transfer into the bridge pool - client - .state - .write(&get_pending_key(&transfer), &transfer) - .expect("Test failed"); - - // create a signed Merkle root for this pool - let signed_root = BridgePoolRootProof { - signatures: Default::default(), - data: (transfer.keccak256(), 0.into()), - }; - let written_height = client.state.in_mem().block.height; - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // update the pool - let mut transfer2 = transfer.clone(); - transfer2.transfer.amount = 1.into(); - client - .state - .write(&get_pending_key(&transfer2), transfer2) - .expect("Test failed"); - - // add the signature for the pool at the previous block height - client - .state - .write(&get_signed_root_key(), (signed_root, written_height)) - .expect("Test failed"); - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - let resp = RPC - .shell() - .eth_bridge() - .read_signed_ethereum_bridge_pool(&client) - .await - .unwrap(); - assert_eq!(resp, vec![transfer]); - } - - /// Test that we can get the backing voting power for - /// each pending TransferToEthereum event. - #[tokio::test] - async fn test_transfer_to_eth_progress() { - let mut client = TestClient::new(RPC); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - // write validator to storage - let (_, dummy_validator_stake) = test_utils::default_validator(); - test_utils::init_default_storage(&mut client.state); - - // write a transfer into the bridge pool - client - .state - .write(&get_pending_key(&transfer), &transfer) - .expect("Test failed"); - - let event_transfer: namada_core::ethereum_events::TransferToEthereum = - (&transfer).into(); - let eth_event = EthereumEvent::TransfersToEthereum { - nonce: Default::default(), - transfers: vec![event_transfer.clone()], - relayer: bertha_address(), - }; - let eth_msg_key = vote_tallies::Keys::from(ð_event); - let voting_power = FractionalVotingPower::HALF; - client - .state - .write(ð_msg_key.body(), eth_event) - .expect("Test failed"); - client - .state - .write( - ð_msg_key.voting_power(), - EpochedVotingPower::from([( - 0.into(), - voting_power * dummy_validator_stake, - )]), - ) - .expect("Test failed"); - client - .state - .write(ð_msg_key.seen(), false) - .expect("Test failed"); - // commit the changes and increase block height - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // update the pool - let mut transfer2 = transfer.clone(); - transfer2.transfer.amount = 1.into(); - client - .state - .write(&get_pending_key(&transfer2), transfer2) - .expect("Test failed"); - - // commit the changes and increase block height - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - let resp = RPC - .shell() - .eth_bridge() - .transfer_to_ethereum_progress(&client) - .await - .unwrap(); - let expected: HashMap = - [(transfer, voting_power)].into_iter().collect(); - assert_eq!(expected, resp); - } - - /// Test if the a transfer has been removed from the - /// pool (either because it was transferred or timed out), - /// a proof is not generated for it, even if it was - /// covered by a signed merkle root at a previous block - /// height. - #[tokio::test] - async fn test_cannot_get_proof_for_removed_transfer() { - let mut client = TestClient::new(RPC); - // write validator to storage - test_utils::init_default_storage(&mut client.state); - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - - // write a transfer into the bridge pool - client - .state - .write(&get_pending_key(&transfer), &transfer) - .expect("Test failed"); - - // create a signed Merkle root for this pool - let signed_root = BridgePoolRootProof { - signatures: Default::default(), - data: (transfer.keccak256(), 0.into()), - }; - let written_height = client.state.in_mem().block.height; - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - - // update the pool - let mut transfer2 = transfer.clone(); - transfer2.transfer.amount = 1.into(); - client - .state - .write(&get_pending_key(&transfer2), transfer2) - .expect("Test failed"); - - // add the signature for the pool at the previous block height - client - .state - .write(&get_signed_root_key(), (signed_root, written_height)) - .expect("Test failed"); - - // commit the changes and increase block height - client.state.commit_block().expect("Test failed"); - client.state.in_mem_mut().block.height += 1; - // this was in the pool, covered by an old signed Merkle root. - let resp = RPC - .shell() - .eth_bridge() - .generate_bridge_pool_proof( - &client, - Some( - GenBridgePoolProofReq { - transfers: vec![transfer.keccak256()].into(), - relayer: Cow::Owned(bertha_address()), - with_appendix: false, - } - .serialize_to_vec(), - ), - None, - false, - ) - .await; - assert!(resp.is_ok()); - - // remove a transfer from the pool. - client - .state - .delete(&get_pending_key(&transfer)) - .expect("Test failed"); - - // this was in the pool, covered by an old signed Merkle root. - let resp = RPC - .shell() - .eth_bridge() - .generate_bridge_pool_proof( - &client, - Some( - GenBridgePoolProofReq { - transfers: vec![transfer.keccak256()].into(), - relayer: Cow::Owned(bertha_address()), - with_appendix: false, - } - .serialize_to_vec(), - ), - None, - false, - ) - .await; - // thus proof generation should fail - assert!(resp.is_err()); - } - - /// Test reading the supply and cap of an ERC20 token. - #[tokio::test] - async fn test_get_erc20_flow_control() { - const ERC20_TOKEN: EthAddress = EthAddress([0; 20]); - - let mut client = TestClient::new(RPC); - assert_eq!(client.state.in_mem().last_epoch.0, 0); - - // initialize storage - test_utils::init_default_storage(&mut client.state); - - // check supply - should be 0 - let result = RPC - .shell() - .eth_bridge() - .get_erc20_flow_control(&client, &ERC20_TOKEN) - .await; - assert_matches!( - result, - Ok(f) if f.supply.is_zero() && f.cap.is_zero() - ); - - // write tokens to storage - let supply_amount = Amount::native_whole(123); - let cap_amount = Amount::native_whole(12345); - let key = whitelist::Key { - asset: ERC20_TOKEN, - suffix: whitelist::KeyType::WrappedSupply, - } - .into(); - client - .state - .write(&key, supply_amount) - .expect("Test failed"); - let key = whitelist::Key { - asset: ERC20_TOKEN, - suffix: whitelist::KeyType::Cap, - } - .into(); - client.state.write(&key, cap_amount).expect("Test failed"); - - // check that the supply was updated - let result = RPC - .shell() - .eth_bridge() - .get_erc20_flow_control(&client, &ERC20_TOKEN) - .await; - assert_matches!( - result, - Ok(f) if f.supply == supply_amount && f.cap == cap_amount - ); - } - - /// Test that querying the status of the Bridge pool - /// returns the expected keccak hashes. - #[tokio::test] - async fn test_bridge_pool_status() { - let mut client = TestClient::new(RPC); - - // write a transfer into the bridge pool - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: EthAddress([0; 20]), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: 0.into(), - }, - gas_fee: GasFee { - token: nam(), - amount: 0.into(), - payer: bertha_address(), - }, - }; - client - .state - .write(&get_pending_key(&transfer), transfer.clone()) - .expect("Test failed"); - - // write transfers into the event log - let mut transfer2 = transfer.clone(); - transfer2.transfer.amount = 1.into(); - let mut transfer3 = transfer.clone(); - transfer3.transfer.amount = 2.into(); - client.event_log.log_events(vec![ - crate::eth_bridge::event::EthBridgeEvent::BridgePool { - tx_hash: transfer2.keccak256(), - status: crate::eth_bridge::event::BpTransferStatus::Expired, - } - .into(), - crate::eth_bridge::event::EthBridgeEvent::BridgePool { - tx_hash: transfer3.keccak256(), - status: crate::eth_bridge::event::BpTransferStatus::Relayed, - } - .into(), - ]); - - // some arbitrary transfer - since it's neither in the - // Bridge pool nor in the event log, it is assumed it has - // either been relayed or that it has expired - let mut transfer4 = transfer.clone(); - transfer4.transfer.amount = 3.into(); - - // change block height - client.state.in_mem_mut().block.height = 1.into(); - - // write bridge pool signed root - { - let signed_root = BridgePoolRootProof { - signatures: Default::default(), - data: (KeccakHash([0; 32]), 0.into()), - }; - let written_height = client.state.in_mem().block.height; - client - .state - .write(&get_signed_root_key(), (signed_root, written_height)) - .expect("Test failed"); - client - .state - .commit_block_from_batch(MockDBWriteBatch) - .expect("Test failed"); - } - - // commit storage changes - client.state.commit_block().expect("Test failed"); - - // check transfer statuses - let status = RPC - .shell() - .eth_bridge() - .pending_eth_transfer_status( - &client, - Some( - { - let mut req = HashSet::new(); - req.insert(transfer.keccak256()); - req.insert(transfer2.keccak256()); - req.insert(transfer3.keccak256()); - req.insert(transfer4.keccak256()); - req - } - .serialize_to_vec(), - ), - None, - false, - ) - .await - .unwrap() - .data; - - assert_eq!( - status.pending, - HashSet::from([transfer.keccak256()]), - "unexpected pending transfers" - ); - assert_eq!( - status.expired, - HashSet::from([transfer2.keccak256()]), - "unexpected expired transfers" - ); - assert_eq!( - status.relayed, - HashSet::from([transfer3.keccak256()]), - "unexpected relayed transfers" - ); - assert_eq!( - status.unrecognized, - HashSet::from([transfer4.keccak256()]), - "unexpected unrecognized transfers" - ); - } -} - -#[cfg(any(feature = "testing", test))] -#[allow(dead_code)] -mod test_utils { - use namada_core::address::Address; - #[allow(unused_imports)] - pub use namada_ethereum_bridge::test_utils::*; - - /// An established user address for testing & development - pub fn bertha_address() -> Address { - Address::decode("tnam1qyctxtpnkhwaygye0sftkq28zedf774xc5a2m7st") - .expect("The token address decoding shouldn't fail") - } -} diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index 924eee24d10..e46e543e6c7 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -59,7 +59,7 @@ use namada_proof_of_stake::types::{ use namada_state::{BlockHeader, LastBlock}; use namada_token::masp::MaspTokenRewardData; use namada_tx::data::{BatchedTxResult, DryRunResult, ResultCode, TxResult}; -use namada_tx::event::{Batch as BatchAttr, Code as CodeAttr}; +use namada_tx::event::{Batch as BatchAttr, Code as CodeAttr, CometTxHash}; use serde::{Deserialize, Serialize}; use crate::args::{InputAmount, OsmosisPoolHop, Slippage}; @@ -706,6 +706,8 @@ pub struct TxResponse { pub code: ResultCode, /// Gas used. pub gas_used: WholeGas, + /// CometBFT matching tx hash + pub comet_tx_hash: Hash, } /// Determines a result of an inner tx from @@ -747,6 +749,9 @@ impl TryFrom for TxResponse { let gas_used = applied_event .read_attribute::() .map_err(|err| err.to_string())?; + let comet_tx_hash = applied_event + .read_attribute::() + .map_err(|err| err.to_string())?; // Reconstruct the inner txs' events if let Some(batch) = &mut batch { @@ -776,6 +781,7 @@ impl TryFrom for TxResponse { height, code, gas_used, + comet_tx_hash, }) } } diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index f8ca8722fb4..a94bbfdee74 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -48,11 +48,10 @@ use tokio::sync::RwLock; use crate::args::SdkTypes; use crate::borsh::BorshSerializeExt; use crate::error::{EncodingError, Error, TxSubmitError}; -use crate::eth_bridge_pool::PendingTransfer; use crate::governance::storage::proposal::{AddRemove, PGFAction, PGFTarget}; use crate::rpc::validate_amount; use crate::tx::{ - Commitment, TX_BECOME_VALIDATOR_WASM, TX_BOND_WASM, TX_BRIDGE_POOL_WASM, + Commitment, TX_BECOME_VALIDATOR_WASM, TX_BOND_WASM, TX_CHANGE_COMMISSION_WASM, TX_CHANGE_CONSENSUS_KEY_WASM, TX_CHANGE_METADATA_WASM, TX_CLAIM_REWARDS_WASM, TX_DEACTIVATE_VALIDATOR_WASM, TX_IBC_WASM, TX_INIT_ACCOUNT_WASM, @@ -2273,39 +2272,6 @@ pub async fn to_ledger_vector( ]); tv.output_expert.push(format!("Steward : {}", address)); - } else if code_sec.tag == Some(TX_BRIDGE_POOL_WASM.to_string()) { - let transfer = PendingTransfer::try_from_slice( - &tx.data(cmt) - .ok_or_else(|| Error::Other("Invalid Data".to_string()))?, - ) - .map_err(|err| { - Error::from(EncodingError::Conversion(err.to_string())) - })?; - - tv.name = "Bridge_Pool_Transfer_0".to_string(); - - tv.output.extend(vec![ - format!("Type : Bridge Pool Transfer"), - format!("Transfer Kind : {}", transfer.transfer.kind), - format!("Transfer Sender : {}", transfer.transfer.sender), - format!("Transfer Recipient : {}", transfer.transfer.recipient), - format!("Transfer Asset : {}", transfer.transfer.asset), - format!("Transfer Amount : {}", transfer.transfer.amount), - format!("Gas Payer : {}", transfer.gas_fee.payer), - format!("Gas Token : {}", transfer.gas_fee.token), - format!("Gas Amount : {}", transfer.gas_fee.amount), - ]); - - tv.output_expert.extend(vec![ - format!("Transfer Kind : {}", transfer.transfer.kind), - format!("Transfer Sender : {}", transfer.transfer.sender), - format!("Transfer Recipient : {}", transfer.transfer.recipient), - format!("Transfer Asset : {}", transfer.transfer.asset), - format!("Transfer Amount : {}", transfer.transfer.amount), - format!("Gas Payer : {}", transfer.gas_fee.payer), - format!("Gas Token : {}", transfer.gas_fee.token), - format!("Gas Amount : {}", transfer.gas_fee.amount), - ]); } else { tv.name = "Custom_0".to_string(); tv.output.push("Type : Custom".to_string()); @@ -2982,7 +2948,6 @@ mod test_signing { TX_REDELEGATE_WASM, TX_UPDATE_STEWARD_COMMISSION, TX_RESIGN_STEWARD, - TX_BRIDGE_POOL_WASM, ] { let mut tx_malformed = tx.clone(); let cmts = std::mem::take(&mut tx_malformed.header.batch); diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 93f643562da..2e2ad02caea 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -126,8 +126,6 @@ pub const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; /// Claim-rewards WASM path pub const TX_CLAIM_REWARDS_WASM: &str = "tx_claim_rewards.wasm"; /// Bridge pool WASM path -pub const TX_BRIDGE_POOL_WASM: &str = "tx_bridge_pool.wasm"; -/// Change commission WASM path pub const TX_CHANGE_COMMISSION_WASM: &str = "tx_change_validator_commission.wasm"; /// Change consensus key WASM path @@ -477,6 +475,7 @@ pub fn display_batch_resp(context: &impl Namada, resp: &TxResponse) { ResultCode::InvalidVoteExtension => "invalid vote extension", ResultCode::TooLarge => "transaction too large", ResultCode::TxNotAllowlisted => "transaction not allowlisted", + ResultCode::DeprecatedProtocolTx => "protocol txs are deprecated", }; let err_msg = if resp.info.is_empty() { err.to_string() @@ -607,6 +606,8 @@ pub fn display_batch_resp(context: &impl Namada, resp: &TxResponse) { ); } + display_line!(context.io(), "CometBFT tx hash: {}", resp.comet_tx_hash); + tracing::debug!( "Full result: {}", serde_json::to_string_pretty(&resp).unwrap() diff --git a/crates/sdk/src/validation.rs b/crates/sdk/src/validation.rs index f4237424967..9e553ea1a80 100644 --- a/crates/sdk/src/validation.rs +++ b/crates/sdk/src/validation.rs @@ -7,7 +7,7 @@ use namada_vp::VpEnv; use namada_vp::native_vp::{self, CtxPostStorageRead, CtxPreStorageRead}; use crate::state::StateRead; -use crate::{eth_bridge, governance, ibc, parameters, proof_of_stake, token}; +use crate::{governance, ibc, parameters, proof_of_stake, token}; /// Native VP context pub type NativeVpCtx<'a, S, CA> = @@ -86,18 +86,6 @@ pub type MaspVp<'ctx, CTX> = token::vp::MaspVp< token::Transfer, >; -/// Native ETH bridge VP -pub type EthBridgeVp<'ctx, CTX> = - eth_bridge::vp::EthBridge<'ctx, CTX, TokenKeys>; - -/// Native ETH bridge pool VP -pub type EthBridgePoolVp<'ctx, CTX> = - eth_bridge::vp::BridgePool<'ctx, CTX, TokenKeys>; - -/// Native ETH bridge NUT VP -pub type EthBridgeNutVp<'ctx, CTX> = - eth_bridge::vp::NonUsableTokens<'ctx, CTX, TokenKeys>; - /// Governance store implementation over the native prior context pub type GovPreStore<'a, S, CA> = governance::Store, Eval>>; diff --git a/crates/state/src/in_memory.rs b/crates/state/src/in_memory.rs index 64237553f88..f316e7a7357 100644 --- a/crates/state/src/in_memory.rs +++ b/crates/state/src/in_memory.rs @@ -4,21 +4,20 @@ use clru::CLruCache; use namada_core::address::{Address, EstablishedAddressGen, InternalAddress}; use namada_core::borsh::{BorshDeserialize, BorshSerialize}; use namada_core::chain::{BLOCK_HEIGHT_LENGTH, CHAIN_ID_LENGTH, ChainId}; +use namada_core::encode; use namada_core::hash::Hash; use namada_core::parameters::{EpochDuration, Parameters}; use namada_core::time::DateTimeUtc; -use namada_core::{encode, ethereum_structs}; use namada_gas::{Gas, MEMORY_ACCESS_GAS_PER_BYTE}; use namada_macros::BorshDeserializer; use namada_merkle_tree::{MerkleRoot, MerkleTree}; #[cfg(feature = "migrations")] use namada_migrations::*; use namada_storage::conversion_state::ConversionState; -use namada_storage::tx_queue::ExpiredTxsQueue; use namada_storage::types::CommitOnlyData; use namada_storage::{ BlockHeader, BlockHeight, BlockResults, EPOCH_TYPE_LENGTH, Epoch, Epochs, - EthEventsQueue, Key, KeySeg, StorageHasher, TxIndex, + Key, KeySeg, StorageHasher, TxIndex, }; use crate::Result; @@ -61,17 +60,6 @@ where pub tx_index: TxIndex, /// The currently saved conversion state pub conversion_state: ConversionState, - /// Queue of expired transactions that need to be retransmitted. - /// - /// These transactions do not need to be persisted, as they are - /// retransmitted at the **COMMIT** phase immediately following - /// the block when they were queued. - pub expired_txs_queue: ExpiredTxsQueue, - /// The latest block height on Ethereum processed, if - /// the bridge is enabled. - pub ethereum_height: Option, - /// The queue of Ethereum events to be processed in order. - pub eth_events_queue: EthEventsQueue, /// How many block heights in the past can the storage be queried pub storage_read_past_height_limit: Option, /// Data that needs to be committed to the merkle tree @@ -159,10 +147,7 @@ where update_epoch_blocks_delay: None, tx_index: TxIndex::default(), conversion_state: ConversionState::default(), - expired_txs_queue: ExpiredTxsQueue::default(), native_token, - ethereum_height: None, - eth_events_queue: EthEventsQueue::default(), storage_read_past_height_limit, commit_only_data: CommitOnlyData::default(), block_proposals_cache: CLruCache::new( diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 6bcc9abf46a..f06f2ec4fdc 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -35,17 +35,15 @@ pub use namada_core::chain::{ BLOCK_HASH_LENGTH, BLOCK_HEIGHT_LENGTH, BlockHash, BlockHeader, BlockHeight, Epoch, Epochs, }; -use namada_core::eth_bridge_pool::is_pending_transfer_key; use namada_core::hash::Hash; pub use namada_core::hash::Sha256Hasher; pub use namada_core::storage::{ - BlockResults, EPOCH_TYPE_LENGTH, EthEventsQueue, Key, KeySeg, TxIndex, + BlockResults, EPOCH_TYPE_LENGTH, Key, KeySeg, TxIndex, }; use namada_core::tendermint::merkle::proof::ProofOps; use namada_gas::{ Gas, MEMORY_ACCESS_GAS_PER_BYTE, STORAGE_ACCESS_GAS_PER_BYTE, }; -use namada_merkle_tree::Error as MerkleTreeError; pub use namada_merkle_tree::{ self as merkle_tree, MembershipProof, MerkleTree, MerkleTreeStoresRead, MerkleTreeStoresWrite, StoreRef, StoreType, ics23_specs, @@ -59,7 +57,7 @@ pub use namada_storage::{ BlockStateRead, BlockStateWrite, DB, DBIter, DBWriteBatch, DbError, DbResult, Error, OptionExt, Result, ResultExt, StorageHasher, StorageRead, StorageWrite, collections, iter_prefix, iter_prefix_bytes, - iter_prefix_with_filter, iter_prefix_with_filter_map, mockdb, tx_queue, + iter_prefix_with_filter, iter_prefix_with_filter_map, mockdb, }; use namada_systems::parameters; use thiserror::Error; @@ -591,7 +589,6 @@ pub mod testing { use namada_core::address::EstablishedAddressGen; use namada_core::time::DateTimeUtc; pub use namada_storage::testing::{PrefixIter, *}; - use namada_storage::tx_queue::ExpiredTxsQueue; use storage::types::CommitOnlyData; use super::mockdb::MockDB; @@ -644,10 +641,7 @@ pub mod testing { update_epoch_blocks_delay: None, tx_index: TxIndex::default(), conversion_state: ConversionState::default(), - expired_txs_queue: ExpiredTxsQueue::default(), native_token: address::testing::nam(), - ethereum_height: None, - eth_events_queue: EthEventsQueue::default(), storage_read_past_height_limit: Some(1000), commit_only_data: CommitOnlyData::default(), block_proposals_cache: CLruCache::new( @@ -669,11 +663,8 @@ mod tests { use chrono::{TimeZone, Utc}; use merkle_tree::NO_DIFF_KEY_PREFIX; - use namada_core::address::InternalAddress; use namada_core::borsh::{BorshDeserialize, BorshSerializeExt}; - use namada_core::keccak::KeccakHash; use namada_core::parameters::{EpochDuration, Parameters}; - use namada_core::storage::DbKeySeg; use namada_core::time::{self, DateTimeUtc, Duration}; use proptest::prelude::*; use proptest::test_runner::Config; @@ -921,6 +912,7 @@ mod tests { assert_eq!(res, val2); // Commit block and storage changes + state.pre_commit_block().unwrap(); state.commit_block().unwrap(); state.in_mem_mut().block.height = state.in_mem().block.height.next_height(); @@ -986,6 +978,7 @@ mod tests { state.delete(&key2).unwrap(); // Commit the block again + state.pre_commit_block().unwrap(); state.commit_block().unwrap(); state.in_mem_mut().block.height = state.in_mem().block.height.next_height(); @@ -1260,23 +1253,7 @@ mod tests { ) .prop_map(|kvs| { kvs.into_iter() - .map(|(mut key, val)| { - if let DbKeySeg::AddressSeg(Address::Internal( - InternalAddress::EthBridgePool, - )) = key.segments[0] - { - // This is needed to be able to write this key to DB - - // the merkle tree's `BridgePoolTree::parse_key` - // requires a `KeccakHash` on the 2nd segment - key.segments.insert( - 1, - DbKeySeg::StringSeg( - KeccakHash::default().to_string(), - ), - ); - } - (key, Level::Storage(val)) - }) + .map(|(key, val)| (key, Level::Storage(val))) .collect::>() }); diff --git a/crates/state/src/wl_state.rs b/crates/state/src/wl_state.rs index 26e13869fa2..846b01b316d 100644 --- a/crates/state/src/wl_state.rs +++ b/crates/state/src/wl_state.rs @@ -4,7 +4,6 @@ use std::ops::{Deref, DerefMut}; use itertools::Either; use namada_core::address::Address; use namada_core::arith::checked; -use namada_core::borsh::BorshSerializeExt; use namada_core::chain::ChainId; use namada_core::masp::MaspEpoch; use namada_core::parameters::{EpochDuration, Parameters}; @@ -25,9 +24,9 @@ use crate::in_memory::InMemory; use crate::write_log::{StorageModification, WriteLog}; use crate::{ DB, DBIter, EPOCH_SWITCH_BLOCKS_DELAY, Epoch, Error, Hash, Key, KeySeg, - LastBlock, MembershipProof, MerkleTree, MerkleTreeError, ProofOps, Result, + LastBlock, MembershipProof, MerkleTree, ProofOps, Result, STORAGE_ACCESS_GAS_PER_BYTE, State, StateError, StateRead, StorageHasher, - StoreType, TxWrites, is_pending_transfer_key, + StoreType, TxWrites, }; /// Owned state with full R/W access. @@ -282,12 +281,6 @@ where /// Commit the current block's write log to the storage and commit the block /// to DB. Starts a new block write log. pub fn commit_block(&mut self) -> Result<()> { - if self.in_mem.last_epoch != self.in_mem.block.epoch { - self.in_mem_mut() - .update_epoch_in_merkle_tree() - .into_storage_result()?; - } - let mut batch = D::batch(); self.commit_write_log_block(&mut batch) .into_storage_result()?; @@ -299,6 +292,20 @@ where Ok(()) } + /// Pre-commit the current block's write log to the pre-commit merkle tree. + pub fn pre_commit_block(&mut self) -> Result<()> { + if self.in_mem.last_epoch != self.in_mem.block.epoch { + self.in_mem_mut() + .update_epoch_in_merkle_tree() + .into_storage_result()?; + } + + self.pre_commit_write_log_block().into_storage_result()?; + self.commit_only_data()?; + + Ok(()) + } + /// Commit the current block's write log to the storage. Starts a new block /// write log. pub fn commit_write_log_block( @@ -352,6 +359,59 @@ where Ok(()) } + /// Pre-commit the current block's write log to the pre-commit merkle tree + pub fn pre_commit_write_log_block(&mut self) -> Result<()> { + for (key, entry) in self.0.write_log.block_write_log.iter() { + let persist_diffs = (self.diff_key_filter)(key); + match entry { + StorageModification::Write { value } => { + Self::pre_commit_write_subspace_val( + &mut self.0.in_mem.block.tree, + key, + value, + persist_diffs, + )?; + } + StorageModification::Delete => { + Self::pre_commit_delete_subspace_val( + &mut self.0.in_mem.block.tree, + key, + persist_diffs, + )?; + } + StorageModification::InitAccount { vp_code_hash } => { + Self::pre_commit_write_subspace_val( + &mut self.0.in_mem.block.tree, + key, + vp_code_hash, + persist_diffs, + )?; + } + } + } + + let replay_prot_key = replay_protection::commitment_key(); + let commitment: Hash = self + .read(&replay_prot_key) + .expect("Could not read db") + .unwrap_or_default(); + let new_commitment = self + .0 + .write_log + .replay_protection + .iter() + .fold(commitment, |acc, hash| acc.concat(hash)); + let persist_diffs = (self.diff_key_filter)(&replay_prot_key); + Self::pre_commit_write_subspace_val( + &mut self.0.in_mem.block.tree, + &replay_prot_key, + new_commitment, + persist_diffs, + )?; + + Ok(()) + } + /// Start write batch. pub fn batch() -> D::WriteBatch { D::batch() @@ -373,22 +433,6 @@ where ) -> Result { let value = value.as_ref(); let persist_diffs = (self.diff_key_filter)(key); - - if is_pending_transfer_key(key) { - // The tree of the bridge pool stores the current height for the - // pending transfer - let height = self.in_mem.block.height.serialize_to_vec(); - self.in_mem.block.tree.update(key, height)?; - } else { - // Update the merkle tree - if !persist_diffs { - let prefix = - Key::from(NO_DIFF_KEY_PREFIX.to_string().to_db_key()); - self.in_mem.block.tree.update(&prefix.join(key), value)?; - } else { - self.in_mem.block.tree.update(key, value)?; - }; - } Ok(self.db.batch_write_subspace_val( batch, self.in_mem.block.height, @@ -407,13 +451,6 @@ where key: &Key, ) -> Result { let persist_diffs = (self.diff_key_filter)(key); - // Update the merkle tree - if !persist_diffs { - let prefix = Key::from(NO_DIFF_KEY_PREFIX.to_string().to_db_key()); - self.in_mem.block.tree.delete(&prefix.join(key))?; - } else { - self.in_mem.block.tree.delete(key)?; - } Ok(self.db.batch_delete_subspace_val( batch, self.in_mem.block.height, @@ -422,6 +459,43 @@ where )?) } + /// Pre-commit write the value with the given height and account subspace + /// key to the pre-commit merkle tree. + pub fn pre_commit_write_subspace_val( + tree: &mut MerkleTree, + key: &Key, + value: impl AsRef<[u8]>, + persist_diffs: bool, + ) -> Result<()> { + let value = value.as_ref(); + + // Update the merkle tree + if !persist_diffs { + let prefix = Key::from(NO_DIFF_KEY_PREFIX.to_string().to_db_key()); + tree.update(&prefix.join(key), value)?; + } else { + tree.update(key, value)?; + }; + Ok(()) + } + + /// Pre-commit delete the value with the given height and account subspace + /// key from the pre-commit merkle tree. + pub fn pre_commit_delete_subspace_val( + tree: &mut MerkleTree, + key: &Key, + persist_diffs: bool, + ) -> Result<()> { + // Update the merkle tree + if !persist_diffs { + let prefix = Key::from(NO_DIFF_KEY_PREFIX.to_string().to_db_key()); + tree.delete(&prefix.join(key))?; + } else { + tree.delete(key)?; + } + Ok(()) + } + // Prune merkle tree stores. Use after updating self.block.height in the // commit. fn prune_merkle_tree_stores( @@ -480,20 +554,6 @@ where ), )?; } - - // Prune the BridgePool subtree stores with invalid nonce - let mut epoch = match self.get_oldest_epoch_with_valid_nonce()? { - Some(epoch) => epoch, - None => return Ok(()), - }; - while oldest_epoch < epoch { - epoch = epoch.prev().unwrap(); - self.db.prune_merkle_tree_store( - batch, - &StoreType::BridgePool, - Either::Right(epoch), - )?; - } } Ok(()) @@ -522,49 +582,6 @@ where Ok(self.db.move_current_replay_protection_entries(batch)?) } - /// Get oldest epoch which has the valid signed nonce of the bridge pool - fn get_oldest_epoch_with_valid_nonce(&self) -> Result> { - let last_height = self.in_mem.get_last_block_height(); - let current_nonce = match self - .db - .read_bridge_pool_signed_nonce(last_height, last_height)? - { - Some(nonce) => nonce, - None => return Ok(None), - }; - let (mut epoch, _) = self.in_mem.get_last_epoch(); - // We don't need to check the older epochs because their Merkle tree - // snapshots have been already removed - let oldest_epoch = self.in_mem.get_oldest_epoch(); - // Look up the last valid epoch which has the previous nonce of the - // current one. It has the previous nonce, but it was - // incremented during the epoch. - while 0 < epoch.0 && oldest_epoch <= epoch { - epoch = epoch.prev().unwrap(); - let height = match self - .in_mem - .block - .pred_epochs - .get_start_height_of_epoch(epoch) - { - Some(h) => h, - None => continue, - }; - let nonce = match self - .db - .read_bridge_pool_signed_nonce(height, last_height)? - { - Some(nonce) => nonce, - // skip pruning when the old epoch doesn't have the signed nonce - None => break, - }; - if nonce < current_nonce { - break; - } - } - Ok(Some(epoch)) - } - /// Rebuild full Merkle tree after [`read_last_block()`] fn rebuild_full_merkle_tree( &self, @@ -587,8 +604,6 @@ where results, address_gen, conversion_state, - ethereum_height, - eth_events_queue, commit_only_data: _, /* Ignore the last tx gas map - we clear it * out after commit */ }) = self @@ -622,8 +637,6 @@ where let in_mem = &mut self.0.in_mem; in_mem.block.tree = tree; in_mem.conversion_state = conversion_state; - in_mem.ethereum_height = ethereum_height; - in_mem.eth_events_queue = eth_events_queue; tracing::debug!("Loaded storage from DB"); } else { tracing::info!("No state could be found"); @@ -664,8 +677,6 @@ where } } - self.commit_only_data()?; - let state = BlockStateWrite { merkle_tree_stores: self.in_mem.block.tree.stores(), header: self.in_mem.header.as_ref(), @@ -686,8 +697,6 @@ where update_epoch_blocks_delay: self.in_mem.update_epoch_blocks_delay, address_gen: &self.in_mem.address_gen, conversion_state: &self.in_mem.conversion_state, - ethereum_height: self.in_mem.ethereum_height.as_ref(), - eth_events_queue: &self.in_mem.eth_events_queue, commit_only_data: &self.in_mem.commit_only_data, }; self.db @@ -897,20 +906,12 @@ where let value = value.as_ref(); let persist_diffs = (self.diff_key_filter)(key); - if is_pending_transfer_key(key) { - // The tree of the bright pool stores the current height for the - // pending transfer - let height = self.in_mem.block.height.serialize_to_vec(); - self.in_mem.block.tree.update(key, height)?; + // Update the merkle tree + if !persist_diffs { + let prefix = Key::from(NO_DIFF_KEY_PREFIX.to_string().to_db_key()); + self.in_mem.block.tree.update(&prefix.join(key), value)?; } else { - // Update the merkle tree - if !persist_diffs { - let prefix = - Key::from(NO_DIFF_KEY_PREFIX.to_string().to_db_key()); - self.in_mem.block.tree.update(&prefix.join(key), value)?; - } else { - self.in_mem.block.tree.update(key, value)?; - } + self.in_mem.block.tree.update(key, value)?; } let len = value.len(); @@ -958,11 +959,6 @@ where } /// Get a Tendermint-compatible existence proof. - /// - /// Proofs from the Ethereum bridge pool are not - /// Tendermint-compatible. Requesting for a key - /// belonging to the bridge pool will cause this - /// method to error. pub fn get_existence_proof( &self, key: &Key, @@ -979,36 +975,28 @@ where }; if height > self.in_mem.get_last_block_height() { - if let MembershipProof::ICS23(proof) = + let MembershipProof::ICS23(proof) = self.in_mem.block.tree.get_sub_tree_existence_proof( array::from_ref(key), vec![value], - )? - { - self.in_mem - .block - .tree - .get_sub_tree_proof(key, proof) - .map(Into::into) - .map_err(Into::into) - } else { - Err(Error::from(MerkleTreeError::TendermintProof)) - } + )?; + self.in_mem + .block + .tree + .get_sub_tree_proof(key, proof) + .map(Into::into) + .map_err(Into::into) } else { let (store_type, _) = StoreType::sub_key(key)?; let tree = self.get_merkle_tree(height, Some(store_type))?; - if let MembershipProof::ICS23(proof) = tree + let MembershipProof::ICS23(proof) = tree .get_sub_tree_existence_proof( array::from_ref(key), vec![value], - )? - { - tree.get_sub_tree_proof(key, proof) - .map(Into::into) - .map_err(Into::into) - } else { - Err(Error::from(MerkleTreeError::TendermintProof)) - } + )?; + tree.get_sub_tree_proof(key, proof) + .map(Into::into) + .map_err(Into::into) } } @@ -1106,14 +1094,7 @@ where match old.0.cmp(&new.0) { Ordering::Equal => { // the value was updated - tree.update( - &new_key, - if is_pending_transfer_key(&new_key) { - target_height.serialize_to_vec() - } else { - new.1.clone() - }, - )?; + tree.update(&new_key, new.1.clone())?; old_diff = old_diff_iter.next(); new_diff = new_diff_iter.next(); } @@ -1124,14 +1105,7 @@ where } Ordering::Greater => { // the value was inserted - tree.update( - &new_key, - if is_pending_transfer_key(&new_key) { - target_height.serialize_to_vec() - } else { - new.1.clone() - }, - )?; + tree.update(&new_key, new.1.clone())?; new_diff = new_diff_iter.next(); } } @@ -1149,14 +1123,7 @@ where let key = Key::parse(new.0.clone()) .expect("the key should be parsable"); - tree.update( - &key, - if is_pending_transfer_key(&key) { - target_height.serialize_to_vec() - } else { - new.1.clone() - }, - )?; + tree.update(&key, new.1.clone())?; new_diff = new_diff_iter.next(); } @@ -1579,6 +1546,7 @@ where key: &storage::Key, val: T, ) -> Result<()> { + use namada_core::borsh::BorshSerializeExt; let _ = self .write_log_mut() .write_temp(key, val.serialize_to_vec())?; diff --git a/crates/storage/src/db.rs b/crates/storage/src/db.rs index 7e9699456da..26752e682c9 100644 --- a/crates/storage/src/db.rs +++ b/crates/storage/src/db.rs @@ -3,11 +3,11 @@ use std::num::TryFromIntError; use itertools::Either; use namada_core::address::EstablishedAddressGen; +use namada_core::arith; use namada_core::chain::{BlockHeader, BlockHeight, Epoch, Epochs}; use namada_core::hash::{Error as HashError, Hash}; -use namada_core::storage::{BlockResults, DbColFam, EthEventsQueue, Key}; +use namada_core::storage::{BlockResults, DbColFam, Key}; use namada_core::time::DateTimeUtc; -use namada_core::{arith, ethereum_events, ethereum_structs}; use namada_gas::Gas; use namada_merkle_tree::{ Error as MerkleTreeError, MerkleTreeStoresRead, MerkleTreeStoresWrite, @@ -71,11 +71,6 @@ pub struct BlockStateRead { pub results: BlockResults, /// The conversion state pub conversion_state: ConversionState, - /// The latest block height on Ethereum processed, if - /// the bridge is enabled. - pub ethereum_height: Option, - /// The queue of Ethereum events to be processed in order. - pub eth_events_queue: EthEventsQueue, /// Structure holding data that needs to be added to the merkle tree pub commit_only_data: CommitOnlyData, } @@ -106,11 +101,6 @@ pub struct BlockStateWrite<'a> { pub results: &'a BlockResults, /// The conversion state pub conversion_state: &'a ConversionState, - /// The latest block height on Ethereum processed, if - /// the bridge is enabled. - pub ethereum_height: Option<&'a ethereum_structs::BlockHeight>, - /// The queue of Ethereum events to be processed in order. - pub eth_events_queue: &'a EthEventsQueue, /// Structure holding data that needs to be added to the merkle tree pub commit_only_data: &'a CommitOnlyData, } @@ -265,13 +255,6 @@ pub trait DB: Debug { pruned_target: Either, ) -> Result<()>; - /// Read the signed nonce of Bridge Pool - fn read_bridge_pool_signed_nonce( - &self, - height: BlockHeight, - last_height: BlockHeight, - ) -> Result>; - /// Write a replay protection entry fn write_replay_protection_entry( &mut self, diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 4ad58590328..7496b222593 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -29,7 +29,6 @@ pub mod conversion_state; mod db; mod error; pub mod mockdb; -pub mod tx_queue; pub mod types; pub use db::{Error as DbError, Result as DbResult, *}; @@ -180,10 +179,10 @@ pub trait StorageWrite { } /// Iterate items matching the given prefix, ordered by the storage keys. -pub fn iter_prefix_bytes<'a>( - storage: &'a impl StorageRead, +pub fn iter_prefix_bytes( + storage: &impl StorageRead, prefix: Key, -) -> Result)>> + 'a> { +) -> Result)>> + '_> { let mut iter = storage.iter_prefix(&prefix)?; let iter = std::iter::from_fn(move || { match storage.iter_next(&mut iter) { @@ -209,10 +208,10 @@ pub fn iter_prefix_bytes<'a>( /// Iterate Borsh encoded items matching the given prefix, ordered by the /// storage keys. -pub fn iter_prefix<'a, T>( - storage: &'a impl StorageRead, +pub fn iter_prefix( + storage: &impl StorageRead, prefix: Key, -) -> Result> + 'a> +) -> Result> + '_> where T: BorshDeserialize, { diff --git a/crates/storage/src/mockdb.rs b/crates/storage/src/mockdb.rs index ffc75d9d765..acbb27d32e6 100644 --- a/crates/storage/src/mockdb.rs +++ b/crates/storage/src/mockdb.rs @@ -11,7 +11,7 @@ use namada_core::borsh::{BorshDeserialize, BorshSerialize}; use namada_core::chain::{BlockHeader, BlockHeight, Epoch}; use namada_core::hash::Hash; use namada_core::storage::{DbColFam, KEY_SEGMENT_SEPARATOR, Key, KeySeg}; -use namada_core::{decode, encode, ethereum_events}; +use namada_core::{decode, encode}; use namada_gas::Gas; use namada_merkle_tree::{ MerkleTreeStoresRead, MerkleTreeStoresWrite, StoreType, @@ -34,8 +34,6 @@ const NEXT_EPOCH_MIN_START_TIME_KEY: &str = "next_epoch_min_start_time"; const UPDATE_EPOCH_BLOCKS_DELAY_KEY: &str = "update_epoch_blocks_delay"; const COMMIT_ONLY_DATA_KEY: &str = "commit_only_data_commitment"; const CONVERSION_STATE_KEY: &str = "conversion_state"; -const ETHEREUM_HEIGHT_KEY: &str = "ethereum_height"; -const ETH_EVENTS_QUEUE_KEY: &str = "eth_events_queue"; const RESULTS_KEY_PREFIX: &str = "results"; const MERKLE_TREE_ROOT_KEY_SEGMENT: &str = "root"; @@ -153,16 +151,6 @@ impl DB for MockDB { None => return Ok(None), }; - let ethereum_height = match self.read_value(ETHEREUM_HEIGHT_KEY)? { - Some(h) => h, - None => return Ok(None), - }; - - let eth_events_queue = match self.read_value(ETH_EVENTS_QUEUE_KEY)? { - Some(q) => q, - None => return Ok(None), - }; - // Block results let results_key = format!("{RESULTS_KEY_PREFIX}/{}", height.raw()); let results = match self.read_value(results_key)? { @@ -207,8 +195,6 @@ impl DB for MockDB { next_epoch_min_start_time, update_epoch_blocks_delay, address_gen, - ethereum_height, - eth_events_queue, commit_only_data, })) } @@ -232,8 +218,6 @@ impl DB for MockDB { address_gen, results, conversion_state, - ethereum_height, - eth_events_queue, commit_only_data, }: BlockStateWrite<'_> = state; @@ -249,8 +233,6 @@ impl DB for MockDB { UPDATE_EPOCH_BLOCKS_DELAY_KEY, &update_epoch_blocks_delay, ); - self.write_value(ETHEREUM_HEIGHT_KEY, ðereum_height); - self.write_value(ETH_EVENTS_QUEUE_KEY, ð_events_queue); self.write_value(CONVERSION_STATE_KEY, &conversion_state); self.write_value(COMMIT_ONLY_DATA_KEY, &commit_only_data); @@ -600,14 +582,6 @@ impl DB for MockDB { Ok(()) } - fn read_bridge_pool_signed_nonce( - &self, - _height: BlockHeight, - _last_height: BlockHeight, - ) -> Result> { - Ok(None) - } - fn write_replay_protection_entry( &mut self, _batch: &mut Self::WriteBatch, diff --git a/crates/storage/src/tx_queue.rs b/crates/storage/src/tx_queue.rs deleted file mode 100644 index 547dfc47429..00000000000 --- a/crates/storage/src/tx_queue.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Transaction queue - -use namada_core::borsh::{BorshDeserialize, BorshSerialize}; -pub use namada_core::ethereum_events::EthereumEvent; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; - -/// Expired transaction kinds. -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshDeserializer)] -pub enum ExpiredTx { - /// Broadcast the given Ethereum event. - EthereumEvent(EthereumEvent), -} - -/// Queue of expired transactions that need to be retransmitted. -#[derive( - Default, Clone, Debug, BorshSerialize, BorshDeserialize, BorshDeserializer, -)] -pub struct ExpiredTxsQueue { - inner: Vec, -} - -impl ExpiredTxsQueue { - /// Push a new transaction to the back of the queue. - #[inline] - pub fn push(&mut self, tx: ExpiredTx) { - self.inner.push(tx); - } - - /// Consume all the transactions in the queue. - #[inline] - pub fn drain(&mut self) -> impl Iterator + '_ { - self.inner.drain(..) - } -} diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index e0603280b6a..ec453bb8e8a 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -17,10 +17,6 @@ rust-version.workspace = true default = ["namada_sdk/std"] mainnet = ["namada_sdk/mainnet"] migrations = ["namada_sdk/migrations", "namada_core/migrations"] -namada-eth-bridge = [ - "namada_sdk/namada-eth-bridge", - "namada_apps_lib/namada-eth-bridge", -] historic-masp = ["namada_sdk/historic-masp"] [dependencies] diff --git a/crates/tests/src/e2e/ibc_tests.rs b/crates/tests/src/e2e/ibc_tests.rs index 2e5873b4dd6..4ee411f40a3 100644 --- a/crates/tests/src/e2e/ibc_tests.rs +++ b/crates/tests/src/e2e/ibc_tests.rs @@ -20,7 +20,6 @@ use eyre::{WrapErr, eyre}; use ibc_middleware_packet_forward::ForwardMetadata; use itertools::Either; use namada_apps_lib::client::rpc::query_storage_value_bytes; -use namada_apps_lib::config::ethereum_bridge; use namada_apps_lib::config::genesis::templates; use namada_apps_lib::tendermint_rpc::{Client, HttpClient, Url}; use namada_core::masp::PaymentAddress; @@ -62,8 +61,8 @@ use crate::e2e::ledger_tests::{ use crate::e2e::setup::{ self, Bin, CosmosChainType, ENV_VAR_COSMWASM_CONTRACT_DIR, NamadaCmd, Test, TestDir, Who, apply_use_device, osmosis_fixtures_dir, run_cosmos_cmd, - run_cosmos_cmd_homeless, run_hermes_cmd, set_ethereum_bridge_mode, - setup_cosmos, setup_hermes, sleep, working_dir, + run_cosmos_cmd_homeless, run_hermes_cmd, setup_cosmos, setup_hermes, sleep, + working_dir, }; use crate::ibc::primitives::Signer; use crate::strings::TX_APPLIED_SUCCESS; @@ -2512,14 +2511,6 @@ fn run_namada_cosmos( ) -> Result<(NamadaCmd, NamadaCmd, Test, Test)> { let test = setup::network(&mut update_genesis, None)?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - let ledger = start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))?; let (cosmos, test_cosmos) = setup_and_boot_cosmos(chain_type)?; diff --git a/crates/tests/src/e2e/ledger_tests.rs b/crates/tests/src/e2e/ledger_tests.rs index a2d3360e2ee..6a26af43ad1 100644 --- a/crates/tests/src/e2e/ledger_tests.rs +++ b/crates/tests/src/e2e/ledger_tests.rs @@ -25,14 +25,16 @@ use namada_apps_lib::client::utils::PRE_GENESIS_DIR; use namada_apps_lib::config::genesis::chain; use namada_apps_lib::config::genesis::templates::TokenBalances; use namada_apps_lib::config::utils::convert_tm_addr_to_socket_addr; -use namada_apps_lib::config::{self, ethereum_bridge}; +use namada_apps_lib::config::{self}; use namada_apps_lib::tendermint_config::net::Address as TendermintAddress; use namada_apps_lib::wallet::defaults::is_use_device; use namada_apps_lib::wallet::{self, Alias}; use namada_core::chain::ChainId; use namada_core::token::NATIVE_MAX_DECIMAL_PLACES; +use namada_node::tendermint_config::TxIndexConfig; use namada_sdk::address::Address; use namada_sdk::chain::{ChainIdPrefix, Epoch}; +use namada_sdk::tendermint_rpc::Client; use namada_sdk::time::DateTimeUtc; use namada_sdk::token; use namada_test_utils::TestWasms; @@ -45,7 +47,7 @@ use super::helpers::{ epochs_per_year_from_min_duration, get_height, get_pregenesis_wallet, wait_for_block_height, wait_for_wasm_pre_compile, }; -use super::setup::{NamadaCmd, set_ethereum_bridge_mode, working_dir}; +use super::setup::{NamadaCmd, working_dir}; use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, is_debug_mode, parse_reached_epoch, @@ -99,14 +101,6 @@ pub fn start_namada_ledger_node_wait_wasm( fn run_ledger() -> Result<()> { let test = setup::single_node_net()?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - let cmd_combinations = vec![ (Bin::Node, vec!["ledger"]), (Bin::Node, vec!["ledger", "run"]), @@ -156,21 +150,6 @@ fn test_node_connectivity_and_consensus() -> Result<()> { allow_duplicate_ips(&test, &test.net.chain_id, Who::Validator(0)); allow_duplicate_ips(&test, &test.net.chain_id, Who::Validator(1)); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(1), - ethereum_bridge::ledger::Mode::Off, - None, - ); - // 1. Run 2 genesis validator ledger nodes and 1 non-validator node let bg_validator_0 = start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))? @@ -249,14 +228,6 @@ fn test_node_connectivity_and_consensus() -> Result<()> { fn test_namada_shuts_down_if_tendermint_dies() -> Result<()> { let test = setup::single_node_net()?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - // 1. Run the ledger node let mut ledger = start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))?; @@ -293,14 +264,6 @@ fn test_namada_shuts_down_if_tendermint_dies() -> Result<()> { fn run_ledger_load_state_and_reset() -> Result<()> { let test = setup::single_node_net()?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - // 1. Run the ledger node let mut ledger = start_namada_ledger_node(&test, Some(0), Some(40))?; @@ -386,14 +349,6 @@ fn run_ledger_load_state_and_reset() -> Result<()> { fn test_db_migration() -> Result<()> { let test = setup::single_node_net()?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - // 1. Run the ledger node, halting at height 6 let mut ledger = run_as!( test, @@ -574,13 +529,6 @@ fn pos_bonds() -> Result<()> { )?; allow_duplicate_ips(&test, &test.net.chain_id, Who::Validator(0)); allow_duplicate_ips(&test, &test.net.chain_id, Who::Validator(1)); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); // If used, keep Speculos alive for duration of the test let _speculos = if hw_wallet_automation::uses_automation() { @@ -1048,14 +996,6 @@ fn ledger_many_txs_in_a_block() -> Result<()> { Some("10s"), )?); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - // 1. Run the ledger node let bg_ledger = start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))? @@ -1188,20 +1128,6 @@ fn double_signing_gets_slashed() -> Result<()> { allow_duplicate_ips(&test, &test.net.chain_id, Who::Validator(0)); allow_duplicate_ips(&test, &test.net.chain_id, Who::Validator(1)); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(1), - ethereum_bridge::ledger::Mode::Off, - None, - ); println!("pipeline_len: {}", pipeline_len); // 1. Copy the first genesis validator base-dir @@ -1606,20 +1532,6 @@ fn deactivate_and_reactivate_validator() -> Result<()> { )?; allow_duplicate_ips(&test, &test.net.chain_id, Who::Validator(0)); allow_duplicate_ips(&test, &test.net.chain_id, Who::Validator(1)); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(1), - ethereum_bridge::ledger::Mode::Off, - None, - ); // 1. Run the ledger node let _bg_validator_0 = @@ -1779,14 +1691,6 @@ fn test_invalid_validator_txs() -> Result<()> { None, )?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - // 1. Run the ledger node let _bg_validator_0 = start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))? @@ -1945,16 +1849,6 @@ fn change_consensus_key() -> Result<()> { None, )?; - for i in 0..2 { - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(i), - ethereum_bridge::ledger::Mode::Off, - None, - ); - } - // ========================================================================= // 1. Run 2 genesis validator ledger nodes @@ -2053,13 +1947,6 @@ fn proposal_change_shielded_reward() -> Result<()> { }, None, )?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); // 1. Run the ledger node let mut ledger = @@ -2378,13 +2265,6 @@ fn rollback() -> Result<()> { // slow block production rate Some("5s"), )?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); // 1. Run the ledger node once let mut ledger = @@ -2506,13 +2386,6 @@ fn masp_txs_and_queries() -> Result<()> { // Run all cmds on the first validator let who = Who::Validator(0); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - who, - ethereum_bridge::ledger::Mode::Off, - None, - ); // 1. Run the ledger node let _bg_ledger = @@ -2759,14 +2632,6 @@ fn test_genesis_chain_id_change() -> Result<()> { test_genesis_result .exp_string("Genesis files were dry-run successfully")?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - // Unset the chain ID - the transaction signatures have been validated at // init-network so we don't need it anymore unsafe { @@ -2791,14 +2656,6 @@ fn test_genesis_chain_id_change() -> Result<()> { fn test_genesis_manipulation() -> Result<()> { let test = setup::single_node_net().unwrap(); - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - let chain_dir = test.get_chain_dir(Who::Validator(0)); let genesis = chain::Finalized::read_toml_files(&chain_dir).unwrap(); @@ -2852,3 +2709,87 @@ fn test_genesis_manipulation() -> Result<()> { Ok(()) } + +#[test] +fn comet_tx_indexer() -> Result<()> { + use namada_apps_lib::config::Config; + use namada_sdk::{tendermint, tendermint_rpc}; + + let test = Arc::new(setup::network( + |genesis, base_dir: &_| { + setup::set_validators(1, genesis, base_dir, |_| 0, vec![]) + }, + None, + )?); + + // Enable comet tx indexer + let update_config = |mut config: Config| { + config.ledger.cometbft.tx_index = TxIndexConfig { + indexer: namada_node::tendermint_config::TxIndexer::Kv, + }; + config + }; + + let validator_base_dir = test.get_base_dir(Who::Validator(0)); + let validator_config = update_config(Config::load( + &validator_base_dir, + &test.net.chain_id, + None, + )); + validator_config + .write(&validator_base_dir, &test.net.chain_id, true) + .unwrap(); + + // 1. Run the ledger node + let bg_ledger = + start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))? + .background(); + + let validator_rpc = get_actor_rpc(&test, Who::Validator(0)); + + // A token transfer tx args + let tx_args = apply_use_device(vec![ + "transparent-transfer", + "--source", + BERTHA, + "--target", + ALBERT, + "--token", + NAM, + "--amount", + "1.01", + "--signing-keys", + BERTHA_KEY, + "--node", + &validator_rpc, + ]); + let mut client = run!(*test, Bin::Client, tx_args, Some(80))?; + let expected = "CometBFT tx hash: "; + let (_unread, matched) = client.exp_regex(&format!("{expected}.*\n"))?; + let comet_tx_hash = matched.trim().split_once(expected).unwrap().1; + client.assert_success(); + + // Wait to commit a block + let mut ledger = bg_ledger.foreground(); + ledger.exp_regex(r"Committed block hash.*, height: [0-9]+")?; + + // Check the tx result in Comet's indexer + let client = tendermint_rpc::HttpClient::builder( + tendermint_rpc::HttpClientUrl::from_str(&validator_rpc).unwrap(), + ) + .compat_mode(tendermint_rpc::client::CompatMode::V0_38) + .timeout(std::time::Duration::from_secs(30)) + .build() + .unwrap(); + let result = test + .async_runtime() + .block_on( + client + .tx(tendermint::Hash::from_str(comet_tx_hash).unwrap(), false), + ) + .unwrap(); + assert!(result.tx_result.code.is_ok()); + assert!(result.tx_result.gas_used > 0); + + Ok(()) +} diff --git a/crates/tests/src/e2e/setup.rs b/crates/tests/src/e2e/setup.rs index 13e7f347d90..82739103ec2 100644 --- a/crates/tests/src/e2e/setup.rs +++ b/crates/tests/src/e2e/setup.rs @@ -24,7 +24,7 @@ use namada_apps_lib::client::utils::{ }; use namada_apps_lib::config::genesis::utils::read_toml; use namada_apps_lib::config::genesis::{templates, transactions}; -use namada_apps_lib::config::{Config, ethereum_bridge, genesis}; +use namada_apps_lib::config::{Config, genesis}; use namada_apps_lib::wallet::defaults::{derive_template_dir, is_use_device}; use namada_apps_lib::{config, wallet}; use namada_core::address::Address; @@ -120,23 +120,6 @@ pub fn allow_duplicate_ips(test: &Test, chain_id: &ChainId, who: Who) { }); } -/// Configures the Ethereum bridge mode of `who`. This should be done before -/// `who` starts running. -pub fn set_ethereum_bridge_mode( - test: &Test, - chain_id: &ChainId, - who: Who, - mode: ethereum_bridge::ledger::Mode, - rpc_endpoint: Option<&str>, -) { - update_actor_config(test, chain_id, who, |config| { - config.ledger.ethereum_bridge.mode = mode; - if let Some(addr) = rpc_endpoint { - config.ledger.ethereum_bridge.oracle_rpc_endpoint = addr.into(); - } - }); -} - /// Set `num` validators to the genesis config. Note that called from inside /// the [`network`]'s first argument's closure, e.g. `set_validators(2, _)` will /// configure a network with 2 validators. diff --git a/crates/tests/src/integration/ledger_tests.rs b/crates/tests/src/integration/ledger_tests.rs index ac4dfdc4fed..14941e6cb3e 100644 --- a/crates/tests/src/integration/ledger_tests.rs +++ b/crates/tests/src/integration/ledger_tests.rs @@ -1,23 +1,30 @@ use std::collections::BTreeSet; use std::fs; +use std::mem::ManuallyDrop; use std::num::NonZeroU64; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::sync::{Arc, Mutex}; use assert_matches::assert_matches; use borsh::BorshDeserialize; use color_eyre::eyre::Result; use data_encoding::HEXLOWER; +use namada_apps_lib::cli::args; +use namada_apps_lib::config::{self, TendermintMode}; use namada_apps_lib::wallet::defaults::{self, is_use_device}; use namada_core::chain::Epoch; use namada_core::dec::Dec; use namada_core::hash::Hash; use namada_core::storage::{DbColFam, Key}; use namada_core::token::NATIVE_MAX_DECIMAL_PLACES; -use namada_node::shell::SnapshotSync; use namada_node::shell::testing::client::run; -use namada_node::shell::testing::node::NodeResults; -use namada_node::shell::testing::utils::{Bin, CapturedOutput}; +use namada_node::shell::testing::node::{ + InnerMockNode, MockNode, MockServicesPackage, NodeResults, + SalvageableTestDir, mock_services, +}; +use namada_node::shell::testing::utils::{Bin, CapturedOutput, TestDir}; +use namada_node::shell::{Shell, SnapshotSync}; use namada_node::storage::DbSnapshot; use namada_sdk::account::AccountPublicKeysMap; use namada_sdk::borsh::BorshSerializeExt; @@ -32,12 +39,12 @@ use namada_test_utils::TestWasms; use test_log::test; use crate::e2e::ledger_tests::prepare_proposal_data; -use crate::e2e::setup::apply_use_device; use crate::e2e::setup::constants::{ ALBERT, ALBERT_KEY, APFEL, BERTHA, BERTHA_KEY, BTC, CHRISTEL, CHRISTEL_KEY, DAEWON, DOT, ESTER, ETH, GOVERNANCE_ADDRESS, KARTOFFEL, NAM, PGF_ADDRESS, SCHNITZEL, }; +use crate::e2e::setup::{ENV_VAR_KEEP_TEMP, apply_use_device}; use crate::integration::helpers::{ find_address, make_temp_account, prepare_steward_commission_update_data, }; @@ -3345,3 +3352,111 @@ pub fn find_files_with_ext( Ok(result) } + +/// Test that when a node is restarted after FinalizeBlock but before Commit, +/// the merkle tree is restarted correctly and matches the next FinalizeBlock +/// attempt at the same height. +#[test] +fn test_merkle_tree_restore() -> Result<()> { + // NOTE: Force keep temp to avoid clearing the test dir on node restart + let keep_temp = true; + let (node, services) = + setup::initialize_genesis_aux(|genesis| genesis, Some(keep_temp))?; + + // Submit some tx to have non-empty merkle tree + let tx_args = apply_use_device(vec![ + "transparent-transfer", + "--source", + BERTHA, + "--target", + ALBERT, + "--token", + NAM, + "--amount", + "10.1", + "--signing-keys", + BERTHA_KEY, + ]); + let captured = CapturedOutput::of(|| run(&node, Bin::Client, tx_args)); + assert_matches!(captured.result, Ok(_)); + assert!(captured.contains(TX_APPLIED_SUCCESS)); + + // Attempt to finalize block without Commit + node.finalize_block(None); + + let (height_fst, root_fst) = { + let shell = node.shell.lock().unwrap(); + let block = &shell.state.in_mem().block; + (block.height, block.tree.root()) + }; + + // Restart the node before Commit to reload its state + let (mut node, _services) = { + let chain_id = node.shell.lock().unwrap().chain_id.clone(); + let test_dir = node.test_dir.test_dir.path().to_path_buf(); + + drop(node); + drop(services); + + let MockServicesPackage { + services, + controller, + } = mock_services(); + + let global_args = args::Global { + is_pre_genesis: true, + chain_id: Some(chain_id.clone()), + base_dir: test_dir.clone(), + wasm_dir: Some(test_dir.join(chain_id.as_str()).join("wasm")), + }; + let keep_temp = match std::env::var(ENV_VAR_KEEP_TEMP) { + Ok(val) => !val.eq_ignore_ascii_case("false"), + _ => false, + }; + + let node = MockNode(Arc::new(InnerMockNode { + shell: Mutex::new(Shell::new( + config::Ledger::new( + global_args.base_dir, + chain_id.clone(), + TendermintMode::Validator, + ), + global_args.wasm_dir.expect( + "Wasm path not provided to integration test setup.", + ), + None, + None, + 50 * 1024 * 1024, // 50 kiB + 50 * 1024 * 1024, // 50 kiB + )), + test_dir: SalvageableTestDir { + keep_temp, + test_dir: ManuallyDrop::new(TestDir(test_dir)), + }, + services, + tx_result_codes: Mutex::new(vec![]), + tx_results: Mutex::new(vec![]), + blocks: Mutex::new(HashMap::new()), + })); + (node, controller) + }; + + // Finalize the same block again + node.finalize_block(None); + + let (height_snd, root_snd) = { + let shell = node.shell.lock().unwrap(); + let block = &shell.state.in_mem().block; + (block.height, block.tree.root()) + }; + + // Check that the merkle tree root is the same as after the first + // `FinalizeBlock` block attempt + assert_eq!(height_fst, height_snd); + assert_eq!(root_fst, root_snd); + + node.finalize_and_commit(None); + node.next_epoch(); + + Ok(()) +} diff --git a/crates/tests/src/integration/setup.rs b/crates/tests/src/integration/setup.rs index 2479e13d905..332d92a50a4 100644 --- a/crates/tests/src/integration/setup.rs +++ b/crates/tests/src/integration/setup.rs @@ -20,8 +20,8 @@ use namada_core::chain::ChainIdPrefix; use namada_core::collections::HashMap; use namada_node::shell::Shell; use namada_node::shell::testing::node::{ - InnerMockNode, MockNode, MockServicesCfg, MockServicesController, - MockServicesPackage, SalvageableTestDir, mock_services, + InnerMockNode, MockNode, MockServicesController, MockServicesPackage, + SalvageableTestDir, mock_services, }; use namada_node::shell::testing::utils::TestDir; use namada_sdk::dec::Dec; @@ -40,15 +40,27 @@ pub fn setup() -> Result<(MockNode, MockServicesController)> { /// Setup folders with genesis, configs, wasm, etc. pub fn initialize_genesis( + update_genesis: impl FnMut( + templates::All, + ) -> templates::All, +) -> Result<(MockNode, MockServicesController)> { + initialize_genesis_aux(update_genesis, None) +} + +/// Setup folders with genesis, configs, wasm, etc. Allows to override +/// `keep_temp` irrespective of the `ENV_VAR_KEEP_TEMP` env var. +pub fn initialize_genesis_aux( mut update_genesis: impl FnMut( templates::All, ) -> templates::All, + keep_temp: Option, ) -> Result<(MockNode, MockServicesController)> { let working_dir = std::fs::canonicalize("../..").unwrap(); - let keep_temp = match std::env::var(ENV_VAR_KEEP_TEMP) { - Ok(val) => !val.eq_ignore_ascii_case("false"), - _ => false, - }; + let keep_temp = + keep_temp.unwrap_or_else(|| match std::env::var(ENV_VAR_KEEP_TEMP) { + Ok(val) => !val.eq_ignore_ascii_case("false"), + _ => false, + }); let test_dir = TestDir::new(); let template_dir = derive_template_dir(&working_dir); @@ -120,25 +132,8 @@ pub fn initialize_genesis( // Remove release archive fs::remove_file(release_archive_path).unwrap(); - let eth_bridge_params = genesis.get_eth_bridge_params(); - let auto_drive_services = { - // NB: for now, the only condition that - // dictates whether mock services should - // be enabled is if the Ethereum bridge - // is enabled at genesis - eth_bridge_params.is_some() - }; - let enable_eth_oracle = { - // NB: we only enable the oracle if the - // Ethereum bridge is enabled at genesis - eth_bridge_params.is_some() - }; - let services_cfg = MockServicesCfg { - auto_drive_services, - enable_eth_oracle, - }; finalize_wallet(&template_dir, &global_args, genesis); - create_node(test_dir, global_args, keep_temp, services_cfg) + create_node(test_dir, global_args, keep_temp) } /// Add the address from the finalized genesis to the wallet. @@ -183,7 +178,6 @@ fn create_node( test_dir: TestDir, global_args: args::Global, keep_temp: bool, - services_cfg: MockServicesCfg, ) -> Result<(MockNode, MockServicesController)> { // look up the chain id from the global file. let chain_id = global_args.chain_id.unwrap_or_default(); @@ -197,11 +191,9 @@ fn create_node( // instantiate and initialize the ledger node. let MockServicesPackage { - auto_drive_services, services, - shell_handlers, controller, - } = mock_services(services_cfg); + } = mock_services(); let node = MockNode(Arc::new(InnerMockNode { shell: Mutex::new(Shell::new( config::Ledger::new( @@ -212,8 +204,6 @@ fn create_node( global_args .wasm_dir .expect("Wasm path not provided to integration test setup."), - shell_handlers.tx_broadcaster, - shell_handlers.eth_oracle_channels, None, None, 50 * 1024 * 1024, // 50 kiB @@ -227,7 +217,6 @@ fn create_node( tx_result_codes: Mutex::new(vec![]), tx_results: Mutex::new(vec![]), blocks: Mutex::new(HashMap::new()), - auto_drive_services, })); let init_req = namada_apps_lib::tendermint::abci::request::InitChain { diff --git a/crates/tests/src/native_vp/eth_bridge_pool.rs b/crates/tests/src/native_vp/eth_bridge_pool.rs deleted file mode 100644 index 940bc8e1188..00000000000 --- a/crates/tests/src/native_vp/eth_bridge_pool.rs +++ /dev/null @@ -1,314 +0,0 @@ -#[cfg(test)] -mod test_bridge_pool_vp { - use std::cell::RefCell; - use std::path::PathBuf; - - use borsh::BorshDeserialize; - use namada_apps_lib::wallet::defaults::{albert_address, bertha_address}; - use namada_apps_lib::wasm_loader; - use namada_sdk::address::testing::{nam, wnam}; - use namada_sdk::borsh::BorshSerializeExt; - use namada_sdk::chain::ChainId; - use namada_sdk::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; - use namada_sdk::eth_bridge::{ - Contracts, Erc20WhitelistEntry, EthereumBridgeParams, - UpgradeableContract, wrapped_erc20s, - }; - use namada_sdk::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, - }; - use namada_sdk::ethereum_events::EthAddress; - use namada_sdk::gas::VpGasMeter; - use namada_sdk::key::{SecretKey, common, ed25519}; - use namada_sdk::token::Amount; - use namada_sdk::tx::{TX_BRIDGE_POOL_WASM as ADD_TRANSFER_WASM, Tx}; - use namada_sdk::validation::EthBridgePoolVp; - use namada_tx_prelude::BatchedTx; - - use crate::native_vp::TestNativeVpEnv; - use crate::tx::{TestTxEnv, tx_host_env}; - const ASSET: EthAddress = EthAddress([1; 20]); - const BERTHA_WEALTH: u64 = 1_000_000; - const BERTHA_TOKENS: u64 = 10_000; - const GAS_FEE: u64 = 100; - const TOKENS: u64 = 10; - const TOKEN_CAP: u64 = TOKENS; - - /// A signing keypair for good old Bertha. - fn bertha_keypair() -> common::SecretKey { - // generated from - // [`namada_sdk::key::ed25519::gen_keypair`] - let bytes = [ - 240, 3, 224, 69, 201, 148, 60, 53, 112, 79, 80, 107, 101, 127, 186, - 6, 176, 162, 113, 224, 62, 8, 183, 187, 124, 234, 244, 251, 92, 36, - 119, 243, - ]; - let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); - ed_sk.try_to_sk().unwrap() - } - - /// Gets the absolute path to wasm directory - fn wasm_dir() -> PathBuf { - let mut current_path = std::env::current_dir() - .expect("Current directory should exist") - .canonicalize() - .expect("Current directory should exist"); - while current_path.file_name().unwrap() != "tests" { - current_path.pop(); - } - // Two-dirs up to root - current_path.pop(); - current_path.pop(); - current_path.join("wasm") - } - - /// Create necessary accounts and balances for the test. - fn setup_env(batched_tx: BatchedTx) -> TestTxEnv { - let mut env = TestTxEnv { - batched_tx, - ..Default::default() - }; - let config = EthereumBridgeParams { - erc20_whitelist: vec![Erc20WhitelistEntry { - token_address: wnam(), - token_cap: Amount::from_u64(TOKEN_CAP).native_denominated(), - }], - eth_start_height: Default::default(), - min_confirmations: Default::default(), - contracts: Contracts { - native_erc20: wnam(), - bridge: UpgradeableContract { - address: EthAddress([42; 20]), - version: Default::default(), - }, - }, - }; - // initialize Ethereum bridge storage - config.init_storage(&mut env.state); - // initialize Bertha's account - env.spawn_accounts([&albert_address(), &bertha_address(), &nam()]); - // enrich Albert - env.credit_tokens(&albert_address(), &nam(), BERTHA_WEALTH.into()); - // enrich Bertha - env.credit_tokens(&bertha_address(), &nam(), BERTHA_WEALTH.into()); - // Bertha has ERC20 tokens too. - let token = wrapped_erc20s::token(&ASSET); - env.credit_tokens(&bertha_address(), &token, BERTHA_TOKENS.into()); - // Bertha has... NUTs? :D - let nuts = wrapped_erc20s::nut(&ASSET); - env.credit_tokens(&bertha_address(), &nuts, BERTHA_TOKENS.into()); - // give Bertha some wNAM. technically this is impossible to mint, - // but we're testing invalid protocol paths... - let wnam_tok_addr = wrapped_erc20s::token(&wnam()); - env.credit_tokens( - &bertha_address(), - &wnam_tok_addr, - BERTHA_TOKENS.into(), - ); - env - } - - fn run_vp(tx: BatchedTx) -> bool { - let env = setup_env(tx); - tx_host_env::set(env); - let mut tx_env = tx_host_env::take(); - tx_env.execute_tx().expect("Test failed."); - let gas_meter = RefCell::new(VpGasMeter::new_from_meter( - &*tx_env.gas_meter.borrow(), - )); - let vp_env = TestNativeVpEnv::from_tx_env(tx_env, BRIDGE_POOL_ADDRESS); - - let ctx = vp_env.ctx(&gas_meter); - EthBridgePoolVp::validate_tx( - &ctx, - &vp_env.tx_env.batched_tx.to_ref(), - &vp_env.keys_changed, - &vp_env.verifiers, - ) - .is_ok() - } - - fn validate_tx(tx: BatchedTx) { - #[cfg(feature = "namada-eth-bridge")] - { - assert!(run_vp(tx)); - } - #[cfg(not(feature = "namada-eth-bridge"))] - { - // NB: small hack to always check we reject txs - // if the bridge is disabled at compile time - invalidate_tx(tx) - } - } - - fn invalidate_tx(tx: BatchedTx) { - assert!(!run_vp(tx)); - } - - fn create_tx( - transfer: PendingTransfer, - keypair: &common::SecretKey, - ) -> BatchedTx { - let data = transfer.serialize_to_vec(); - let wasm_code = - wasm_loader::read_wasm_or_exit(wasm_dir(), ADD_TRANSFER_WASM); - - let mut tx = Tx::new(ChainId::default(), None); - tx.add_code(wasm_code, None) - .add_serialized_data(data) - .sign_wrapper(keypair.clone()); - tx.batch_first_tx() - } - - #[test] - fn validate_erc20_tx() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: ASSET, - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: Amount::from(TOKENS), - }, - gas_fee: GasFee { - token: nam(), - amount: Amount::from(GAS_FEE), - payer: bertha_address(), - }, - }; - validate_tx(create_tx(transfer, &bertha_keypair())); - } - - #[test] - fn validate_mint_wnam_tx() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: Amount::from(TOKENS), - }, - gas_fee: GasFee { - token: nam(), - amount: Amount::from(GAS_FEE), - payer: bertha_address(), - }, - }; - validate_tx(create_tx(transfer, &bertha_keypair())); - } - - #[test] - fn invalidate_wnam_over_cap_tx() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: Amount::from(TOKEN_CAP + 1), - }, - gas_fee: GasFee { - token: nam(), - amount: Amount::from(GAS_FEE), - payer: bertha_address(), - }, - }; - invalidate_tx(create_tx(transfer, &bertha_keypair())); - } - - #[test] - fn validate_mint_wnam_different_sender_tx() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: Amount::from(TOKENS), - }, - gas_fee: GasFee { - token: nam(), - amount: Amount::from(GAS_FEE), - payer: albert_address(), - }, - }; - validate_tx(create_tx(transfer, &bertha_keypair())); - } - - #[test] - fn invalidate_fees_paid_in_nuts() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: Amount::from(TOKENS), - }, - gas_fee: GasFee { - token: wrapped_erc20s::nut(&ASSET), - amount: Amount::from(GAS_FEE), - payer: bertha_address(), - }, - }; - invalidate_tx(create_tx(transfer, &bertha_keypair())); - } - - #[test] - fn invalidate_fees_paid_in_wnam() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: Amount::from(TOKENS), - }, - gas_fee: GasFee { - token: wrapped_erc20s::token(&wnam()), - amount: Amount::from(GAS_FEE), - payer: bertha_address(), - }, - }; - invalidate_tx(create_tx(transfer, &bertha_keypair())); - } - - #[test] - fn validate_erc20_tx_with_same_gas_token() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: ASSET, - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: Amount::from(TOKENS), - }, - gas_fee: GasFee { - token: wrapped_erc20s::token(&ASSET), - amount: Amount::from(GAS_FEE), - payer: bertha_address(), - }, - }; - validate_tx(create_tx(transfer, &bertha_keypair())); - } - - #[test] - fn validate_wnam_tx_with_diff_gas_token() { - let transfer = PendingTransfer { - transfer: TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - asset: wnam(), - recipient: EthAddress([0; 20]), - sender: bertha_address(), - amount: Amount::from(TOKENS), - }, - gas_fee: GasFee { - token: wrapped_erc20s::token(&ASSET), - amount: Amount::from(GAS_FEE), - payer: bertha_address(), - }, - }; - validate_tx(create_tx(transfer, &bertha_keypair())); - } -} diff --git a/crates/tests/src/native_vp/mod.rs b/crates/tests/src/native_vp/mod.rs index 3fc7f93f863..a65781eb644 100644 --- a/crates/tests/src/native_vp/mod.rs +++ b/crates/tests/src/native_vp/mod.rs @@ -1,4 +1,3 @@ -pub mod eth_bridge_pool; pub mod pos; use std::cell::RefCell; diff --git a/crates/trans_token/src/storage.rs b/crates/trans_token/src/storage.rs index da3f6cb1927..4a4214e332a 100644 --- a/crates/trans_token/src/storage.rs +++ b/crates/trans_token/src/storage.rs @@ -175,13 +175,6 @@ where S: StorageRead, { let (key, is_default_zero) = match token { - Address::Internal(InternalAddress::Nut(erc20)) => { - let token = Address::Internal(InternalAddress::Erc20(*erc20)); - // NB: always use the equivalent ERC20's smallest - // denomination to specify amounts, if we cannot - // find a denom in storage - (denom_key(&token), true) - } Address::Internal(InternalAddress::IbcToken(_)) => { return Ok(Some(0u8.into())); } diff --git a/crates/trans_token/src/vp.rs b/crates/trans_token/src/vp.rs index 36eb7a0f33c..3e7caa70f0d 100644 --- a/crates/trans_token/src/vp.rs +++ b/crates/trans_token/src/vp.rs @@ -199,19 +199,6 @@ where all_tokens.extend(dec_mints.keys().cloned()); all_tokens.iter().try_for_each(|token| { - if token.is_internal() - && matches!(token, Address::Internal(InternalAddress::Nut(_))) - && !verifiers.contains(token) - { - // Established address tokens, IbcToken and Erc20 do not have - // VPs themselves, their validation is handled - // by the `Multitoken` internal address, - // but internal token Nut addresses have to verify the transfer - return Err(Error::new_alloc(format!( - "Token {token} must verify the tx" - ))); - } - let inc_change = inc_changes.get(token).cloned().unwrap_or_default(); let dec_change = diff --git a/crates/tx/src/data/mod.rs b/crates/tx/src/data/mod.rs index 5121d8bf152..47e62d401ee 100644 --- a/crates/tx/src/data/mod.rs +++ b/crates/tx/src/data/mod.rs @@ -1,12 +1,12 @@ //! Data-Types that are used in transactions. +#![allow(deprecated)] // Triggered by deprecated `TxType::Protocol + pub mod eval_vp; /// txs to manage pgf pub mod pgf; /// txs to manage pos pub mod pos; -/// transaction protocols made by validators -pub mod protocol; /// wrapper txs pub mod wrapper; @@ -39,7 +39,6 @@ use sha2::{Digest, Sha256}; pub use wrapper::*; use crate::TxCommitments; -use crate::data::protocol::ProtocolTx; /// The different result codes that the ledger may send back to a client /// indicating the status of their submitted tx. @@ -89,6 +88,8 @@ pub enum ResultCode { TxNotAllowlisted = 12, // ========================================================================= // WARN: These codes shouldn't be changed between version! + /// Protocol txs are deprecated + DeprecatedProtocolTx = 13, } impl ResultCode { @@ -102,7 +103,8 @@ impl ResultCode { Ok | WasmRuntimeError => true, InvalidTx | InvalidSig | AllocationError | ReplayTx | InvalidChainId | ExpiredTx | TxGasLimit | FeeError - | InvalidVoteExtension | TooLarge | TxNotAllowlisted => false, + | InvalidVoteExtension | TooLarge | TxNotAllowlisted + | DeprecatedProtocolTx => false, } } @@ -580,7 +582,8 @@ pub enum TxType { /// A Tx that contains a payload in the form of a raw tx Wrapper(Box), /// Txs issued by validators as part of internal protocols - Protocol(Box), + #[deprecated] + Protocol(Vec), } impl TxType { diff --git a/crates/tx/src/data/protocol.rs b/crates/tx/src/data/protocol.rs deleted file mode 100644 index e52e24e4228..00000000000 --- a/crates/tx/src/data/protocol.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Types for sending and verifying txs -//! used in Namada protocols - -use namada_core::borsh::{ - BorshDeserialize, BorshSchema, BorshSerialize, BorshSerializeExt, -}; -use namada_core::key::*; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; - -use crate::TxError; - -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, - PartialEq, -)] -/// Txs sent by validators as part of internal protocols -pub struct ProtocolTx { - /// we require ProtocolTxs be signed - pub pk: common::PublicKey, - /// The type of protocol message being sent - pub tx: ProtocolTxType, -} - -impl ProtocolTx { - /// Validate the signature of a protocol tx - pub fn validate_sig( - &self, - signed_hash: [u8; 32], - sig: &common::Signature, - ) -> Result<(), TxError> { - common::SigScheme::verify_signature(&self.pk, &signed_hash, sig) - .map_err(|err| { - TxError::SigError(format!( - "ProtocolTx signature verification failed: {}", - err - )) - }) - } - - /// Produce a SHA-256 hash of this section - pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { - hasher.update(self.serialize_to_vec()); - hasher - } -} - -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Copy, - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, - PartialEq, -)] -/// Types of protocol messages to be sent -pub enum ProtocolTxType { - /// Ethereum events contained in vote extensions that - /// are compressed before being included on chain - EthereumEvents, - /// Collection of signatures over the Ethereum bridge - /// pool merkle root and nonce. - BridgePool, - /// Validator set updates contained in vote extensions - ValidatorSetUpdate, - /// Ethereum events seen by some validator - EthEventsVext, - /// Signature over the Ethereum bridge pool merkle root and nonce. - BridgePoolVext, - /// Validator set update signed by some validator - ValSetUpdateVext, -} - -impl ProtocolTxType { - /// Determine if this [`ProtocolTxType`] is an Ethereum - /// protocol tx. - #[inline] - pub fn is_ethereum(&self) -> bool { - matches!( - self, - Self::EthereumEvents - | Self::BridgePool - | Self::ValidatorSetUpdate - | Self::EthEventsVext - | Self::BridgePoolVext - | Self::ValSetUpdateVext - ) - } -} diff --git a/crates/tx/src/event.rs b/crates/tx/src/event.rs index 4240076b4d3..37c39644c42 100644 --- a/crates/tx/src/event.rs +++ b/crates/tx/src/event.rs @@ -4,6 +4,7 @@ use std::fmt::Display; use std::str::FromStr; use namada_core::borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::hash::Hash; use namada_core::ibc::IbcTxDataHash; use namada_core::masp::MaspTxId; use namada_events::extend::{ @@ -58,15 +59,15 @@ pub mod types { /// already filled in. pub fn new_tx_event(tx: &Tx, height: u64) -> Event { let base_event = match tx.header().tx_type { - TxType::Wrapper(_) | TxType::Protocol(_) => { - Event::new(types::APPLIED, EventLevel::Tx) - .with(TxHash(tx.header_hash())) - } + TxType::Wrapper(_) => Event::new(types::APPLIED, EventLevel::Tx) + .with(TxHash(tx.header_hash())), _ => unreachable!(), }; + let comet_tx_hash = tx.comet_tx_hash(); base_event .with(Height(height.into())) .with(Log(String::new())) + .with(CometTxHash(comet_tx_hash)) .into() } @@ -225,3 +226,17 @@ impl EventAttributeEntry<'static> for IndexedTx { self } } + +/// Extend an [`Event`] with CometBFT tx hash. +pub struct CometTxHash(pub Hash); + +impl EventAttributeEntry<'static> for CometTxHash { + type Value = Hash; + type ValueOwned = Self::Value; + + const KEY: &'static str = "comet-tx-hash"; + + fn into_value(self) -> Self::Value { + self.0 + } +} diff --git a/crates/tx/src/lib.rs b/crates/tx/src/lib.rs index 94dff9d2015..200bf2eed3d 100644 --- a/crates/tx/src/lib.rs +++ b/crates/tx/src/lib.rs @@ -29,7 +29,6 @@ mod types; use data::TxType; pub use either; pub use event::new_tx_event; -pub use namada_core::key::SignableEthMessage; pub use section::{ Authorization, Code, Commitment, CompressedAuthorization, Data, Header, MaspBuilder, Memo, Section, Signer, TxCommitments, diff --git a/crates/tx/src/section.rs b/crates/tx/src/section.rs index 19728e58d65..d7055d7c7cd 100644 --- a/crates/tx/src/section.rs +++ b/crates/tx/src/section.rs @@ -22,7 +22,6 @@ use serde::de::Error as SerdeError; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use crate::data::protocol::ProtocolTx; use crate::data::{TxType, WrapperTx, hash_tx}; use crate::sign::VerifySigError; use crate::{SALT_LENGTH, Tx, hex_data_serde, hex_salt_serde}; @@ -120,15 +119,6 @@ impl Header { None } } - - /// Get the protocol header if it is present - pub fn protocol(&self) -> Option { - if let TxType::Protocol(protocol) = &self.tx_type { - Some(*protocol.clone()) - } else { - None - } - } } impl Section { diff --git a/crates/tx/src/types.rs b/crates/tx/src/types.rs index 7a3539642f5..fe786ed67c7 100644 --- a/crates/tx/src/types.rs +++ b/crates/tx/src/types.rs @@ -59,6 +59,8 @@ pub enum TxError { Deserialization(String), #[error("Tx contains repeated sections")] RepeatedSections, + #[error("Protocol txs are deprecated")] + DeprecatedProtocolTx, } /// A Namada transaction is represented as a header followed by a series of @@ -234,6 +236,12 @@ impl Tx { Section::Header(raw_header).get_hash() } + /// CometBFT matching tx hash (applicable for wrapper txs only) + pub fn comet_tx_hash(&self) -> namada_core::hash::Hash { + let bytes = self.to_bytes(); + namada_core::hash::Hash::sha256(bytes) + } + /// Get hashes of all the sections in this transaction pub fn sechashes(&self) -> Vec { let mut hashes = @@ -679,7 +687,7 @@ impl Tx { /// /// If it is a raw Tx, signed or not, we return `None`. /// - /// If it is a WrapperTx or ProtocolTx, we extract the signed data of + /// If it is a WrapperTx, we extract the signed data of /// the Tx and verify it is of the appropriate form. This means /// 1. The wrapper tx is indeed signed /// 2. The signature is valid @@ -702,18 +710,10 @@ impl Tx { )) }) } - // verify signature and extract signed data - TxType::Protocol(protocol) => self - .verify_signature(&protocol.pk, &self.unique_sechashes()) - .map(Option::Some) - .map_err(|err| { - TxError::SigError(format!( - "ProtocolTx signature verification failed: {}", - err - )) - }), // return as is TxType::Raw => Ok(None), + #[allow(deprecated)] + TxType::Protocol(_) => Err(TxError::DeprecatedProtocolTx), } } @@ -1165,7 +1165,6 @@ mod test { use super::*; use crate::data; - use crate::data::protocol::{ProtocolTx, ProtocolTxType}; /// Test that the BorshSchema for Tx gets generated without any name /// conflicts @@ -1184,7 +1183,8 @@ mod test { let serialized_txs: Vec = serde_json::from_reader(file).expect("file should be proper JSON"); - for serialized_tx in serialized_txs { + for (ix, serialized_tx) in serialized_txs.iter().enumerate() { + dbg!(ix); let raw_bytes = HEXLOWER.decode(serialized_tx.as_bytes()).unwrap(); let tx = Tx::try_from_bytes(raw_bytes.as_ref()).unwrap(); @@ -1255,48 +1255,6 @@ mod test { } } - #[test] - fn test_protocol_tx_signing() { - let sk1 = key::testing::keypair_1(); - let sk2 = key::testing::keypair_2(); - let pk1 = sk1.to_public(); - let tx = Tx::from_type(TxType::Protocol(Box::new(ProtocolTx { - pk: pk1, - tx: ProtocolTxType::BridgePool, - }))); - - // Unsigned tx should fail validation - tx.validate_tx().expect_err("Unsigned"); - - { - let mut tx = tx.clone(); - // Sign the tx - tx.add_section(Section::Authorization(Authorization::new( - tx.sechashes(), - BTreeMap::from_iter([(0, sk1)]), - None, - ))); - - // Signed tx should pass validation - tx.validate_tx() - .expect("valid tx") - .expect("with authorization"); - } - - { - let mut tx = tx.clone(); - // Sign the tx with a wrong key - tx.add_section(Section::Authorization(Authorization::new( - tx.sechashes(), - BTreeMap::from_iter([(0, sk2)]), - None, - ))); - - // Should be rejected - tx.validate_tx().expect_err("invalid signature - wrong key"); - } - } - #[test] fn test_inner_tx_signing() { let sk1 = key::testing::keypair_1(); diff --git a/crates/tx_prelude/src/lib.rs b/crates/tx_prelude/src/lib.rs index e4e26dc1f35..8d3248c1c35 100644 --- a/crates/tx_prelude/src/lib.rs +++ b/crates/tx_prelude/src/lib.rs @@ -33,11 +33,10 @@ use namada_core::chain::CHAIN_ID_LENGTH; pub use namada_core::chain::{ BLOCK_HASH_LENGTH, BlockHash, BlockHeader, BlockHeight, Epoch, }; -pub use namada_core::ethereum_events::EthAddress; use namada_core::internal::HostEnvResult; use namada_core::key::common; use namada_core::storage::TxIndex; -pub use namada_core::{address, encode, eth_bridge_pool, storage, *}; +pub use namada_core::{address, encode, storage, *}; pub use namada_events::extend::Log; pub use namada_events::{ EmitEvents, Event, EventLevel, EventToEmit, EventType, diff --git a/crates/vote_ext/Cargo.toml b/crates/vote_ext/Cargo.toml deleted file mode 100644 index 766ea29a477..00000000000 --- a/crates/vote_ext/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "namada_vote_ext" -description = "Namada vote extensions" -resolver = "2" -authors.workspace = true -edition.workspace = true -documentation.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true -rust-version.workspace = true - -[features] -migrations = ["namada_migrations", "linkme"] - -[dependencies] -namada_core.workspace = true -namada_macros.workspace = true -namada_migrations = { workspace = true, optional = true } -namada_tx.workspace = true - -borsh.workspace = true -linkme = { workspace = true, optional = true } -serde.workspace = true - -[dev-dependencies] -namada_core = { path = "../core", features = ["testing"] } - -data-encoding.workspace = true diff --git a/crates/vote_ext/src/bridge_pool_roots.rs b/crates/vote_ext/src/bridge_pool_roots.rs deleted file mode 100644 index f5c72255bf1..00000000000 --- a/crates/vote_ext/src/bridge_pool_roots.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Vote extension types for adding a signature -//! of the bridge pool merkle root to be added -//! to storage. This will be used to generate -//! bridge pool inclusion proofs for Ethereum. -use std::ops::{Deref, DerefMut}; - -use namada_core::address::Address; -use namada_core::borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use namada_core::chain::BlockHeight; -use namada_core::collections::HashSet; -use namada_core::key::common; -use namada_core::key::common::Signature; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_tx::Signed; - -/// A vote extension containing a validator's signature -/// of the current root and nonce of the -/// Ethereum bridge pool. -#[derive( - Debug, - Clone, - PartialEq, - PartialOrd, - Ord, - Eq, - Hash, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct BridgePoolRootVext { - /// The address of the validator who submitted the vote extension. - // NOTE: The validator's established address was included as a workaround - // for `namada#200`, which prevented us from mapping a CometBFT validator - // address to a Namada address. Since then, we have committed to keeping - // this `validator_addr` field. - pub validator_addr: Address, - /// The block height at which the vote extensions was - /// sent. - /// - /// This can be used as replay protection as well - /// as allowing validators to query the epoch with - /// the appropriate validator set to verify signatures - pub block_height: BlockHeight, - /// The actual signature being submitted. - /// This is a signature over `keccak(eth_header || keccak(root || nonce))`. - pub sig: Signature, -} - -/// Alias for [`BridgePoolRootVext`]. -pub type Vext = BridgePoolRootVext; - -/// A signed [`BridgePoolRootVext`]. -/// -/// Note that this is serialized with Ethereum's -/// ABI encoding schema. -#[derive( - Clone, - Debug, - BorshSerialize, - BorshSchema, - BorshDeserialize, - BorshDeserializer, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, -)] -pub struct SignedVext(pub Signed); - -impl Deref for SignedVext { - type Target = Signed; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From> for SignedVext { - fn from(value: Signed) -> Self { - Self(value) - } -} - -impl Vext { - /// Creates a new signed [`Vext`]. - #[inline] - pub fn sign(&self, sk: &common::SecretKey) -> SignedVext { - SignedVext(Signed::new(sk, self.clone())) - } -} - -/// A collection of validator signatures over the -/// Ethereum bridge pool Merkle root and nonce. -#[derive( - Debug, - Default, - Clone, - PartialEq, - Eq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct MultiSignedVext(pub HashSet); - -impl Deref for MultiSignedVext { - type Target = HashSet; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for MultiSignedVext { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl IntoIterator for MultiSignedVext { - type IntoIter = namada_core::collections::hash_set::IntoIter; - type Item = SignedVext; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl From for MultiSignedVext { - fn from(vext: SignedVext) -> Self { - Self(HashSet::from([vext])) - } -} diff --git a/crates/vote_ext/src/ethereum_events.rs b/crates/vote_ext/src/ethereum_events.rs deleted file mode 100644 index 56db6606ed3..00000000000 --- a/crates/vote_ext/src/ethereum_events.rs +++ /dev/null @@ -1,314 +0,0 @@ -//! Contains types necessary for processing Ethereum events -//! in vote extensions. - -use std::collections::BTreeSet; -use std::ops::Deref; - -use namada_core::address::Address; -use namada_core::borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use namada_core::chain::BlockHeight; -use namada_core::collections::HashMap; -use namada_core::ethereum_events::EthereumEvent; -use namada_core::key::common::{self, Signature}; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_tx::Signed; - -/// Type alias for an [`EthereumEventsVext`]. -pub type Vext = EthereumEventsVext; - -/// Represents a [`Vext`] signed by some validator, with -/// a Namada protocol key. -#[derive( - Clone, - Debug, - BorshSerialize, - BorshSchema, - BorshDeserialize, - BorshDeserializer, -)] -pub struct SignedVext(pub Signed); - -impl Deref for SignedVext { - type Target = Signed; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From> for SignedVext { - fn from(value: Signed) -> Self { - Self(value) - } -} - -/// Represents a set of [`EthereumEvent`] instances seen by some validator. -/// -/// This struct will be created and signed over by each consensus validator, -/// to be included as a vote extension at the end of a Tendermint PreCommit -/// phase. -#[derive( - Debug, - Clone, - PartialEq, - Eq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct EthereumEventsVext { - /// The block height for which this [`Vext`] was made. - pub block_height: BlockHeight, - /// The address of the validator who submitted the vote extension. - // NOTE: The validator's established address was included as a workaround - // for `namada#200`, which prevented us from mapping a CometBFT validator - // address to a Namada address. Since then, we have committed to keeping - // this `validator_addr` field. - pub validator_addr: Address, - /// The new ethereum events seen. These should be - /// deterministically ordered. - pub ethereum_events: Vec, -} - -impl Vext { - /// Creates a [`Vext`] without any Ethereum events. - pub fn empty(block_height: BlockHeight, validator_addr: Address) -> Self { - Self { - block_height, - ethereum_events: Vec::new(), - validator_addr, - } - } - - /// Sign a [`Vext`] with a validator's `signing_key`, - /// and return the signed data. - pub fn sign(self, signing_key: &common::SecretKey) -> Signed { - Signed::new(signing_key, self) - } -} - -/// Aggregates an Ethereum event with the corresponding -/// validators who saw this event. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct MultiSignedEthEvent { - /// The Ethereum event that was signed. - pub event: EthereumEvent, - /// List of addresses of validators who signed this event - /// and block height at which they signed it - pub signers: BTreeSet<(Address, BlockHeight)>, -} - -/// Type alias for an [`EthereumEventsVextDigest`]. -pub type VextDigest = EthereumEventsVextDigest; - -/// Compresses a set of signed [`Vext`] instances, to save -/// space on a block. -#[derive( - Debug, - Clone, - PartialEq, - Eq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct EthereumEventsVextDigest { - /// The signatures, signing address, and signing block height - /// of each [`Vext`] - pub signatures: HashMap<(Address, BlockHeight), Signature>, - /// The events that were reported - pub events: Vec, -} - -impl VextDigest { - /// Build a singleton [`VextDigest`], from the provided [`Vext`]. - #[inline] - pub fn singleton(ext: Signed) -> VextDigest { - VextDigest { - signatures: HashMap::from([( - (ext.data.validator_addr.clone(), ext.data.block_height), - ext.sig, - )]), - events: ext - .data - .ethereum_events - .into_iter() - .map(|event| MultiSignedEthEvent { - event, - signers: BTreeSet::from([( - ext.data.validator_addr.clone(), - ext.data.block_height, - )]), - }) - .collect(), - } - } - - /// Decompresses a set of signed [`Vext`] instances. - pub fn decompress(self, _: BlockHeight) -> Vec> { - let VextDigest { signatures, events } = self; - - let mut extensions = vec![]; - - for (validator, sig) in signatures.into_iter() { - let mut ext = Vext::empty(validator.1, validator.0.clone()); - - for event in events.iter() { - if event.signers.contains(&validator) { - ext.ethereum_events.push(event.event.clone()); - } - } - - ext.ethereum_events.sort(); - - let signed = Signed::new_from(ext, sig); - extensions.push(signed); - } - extensions - } -} - -#[cfg(test)] -mod tests { - use namada_core::ethereum_events::Uint; - use namada_core::hash::Hash; - use namada_core::{address, key}; - - use super::*; - - /// Test the hashing of an Ethereum event - #[test] - fn test_ethereum_event_hash() { - let nonce = Uint::from(123u64); - let event = EthereumEvent::TransfersToNamada { - nonce, - transfers: vec![], - }; - let hash = event.hash().unwrap(); - - assert_eq!( - hash, - Hash([ - 94, 131, 116, 129, 41, 204, 178, 144, 24, 8, 185, 16, 103, 236, - 209, 191, 20, 89, 145, 17, 41, 233, 31, 98, 185, 6, 217, 204, - 80, 38, 224, 23 - ]) - ); - } - - /// Test decompression of a set of Ethereum events - #[test] - fn test_decompress_ethereum_events() { - // we need to construct a `Vec>` - let sk_1 = key::testing::keypair_1(); - let sk_2 = key::testing::keypair_2(); - - let last_block_height = BlockHeight(123); - - let ev_1 = EthereumEvent::TransfersToNamada { - nonce: 1u64.into(), - transfers: vec![], - }; - let ev_2 = EthereumEvent::TransfersToEthereum { - nonce: 2u64.into(), - transfers: vec![], - relayer: address::testing::established_address_1(), - }; - - let validator_1 = address::testing::established_address_1(); - let validator_2 = address::testing::established_address_2(); - - let ext = |validator: Address| -> Vext { - let mut ext = Vext::empty(last_block_height, validator); - - ext.ethereum_events.push(ev_1.clone()); - ext.ethereum_events.push(ev_2.clone()); - ext.ethereum_events.sort(); - - ext - }; - - // assume both v1 and v2 saw the same events, - // so each of them signs `ext` with their respective sk - let ext_1 = Signed::new(&sk_1, ext(validator_1.clone())); - let ext_2 = Signed::new(&sk_2, ext(validator_2.clone())); - let ext_3 = Signed::new(&sk_1, { - let mut ext = Vext::empty( - BlockHeight(last_block_height.0 - 1), - validator_1.clone(), - ); - ext.ethereum_events.push(ev_1.clone()); - ext.ethereum_events.push(ev_2.clone()); - ext.ethereum_events.sort(); - ext - }); - - let ext = vec![ext_1, ext_2, ext_3]; - - // we have the `Signed` instances we need, - // let us now compress them into a single `VextDigest` - let signatures: HashMap<_, _> = [ - ((validator_1.clone(), last_block_height), ext[0].sig.clone()), - ((validator_2.clone(), last_block_height), ext[1].sig.clone()), - ( - (validator_1.clone(), BlockHeight(last_block_height.0 - 1)), - ext[2].sig.clone(), - ), - ] - .into_iter() - .collect(); - - let signers = { - let mut s = BTreeSet::new(); - s.insert((validator_1.clone(), last_block_height)); - s.insert(( - validator_1.clone(), - BlockHeight(last_block_height.0 - 1), - )); - s.insert((validator_2, last_block_height)); - s - }; - let events = vec![ - MultiSignedEthEvent { - event: ev_1, - signers: signers.clone(), - }, - MultiSignedEthEvent { - event: ev_2, - signers, - }, - ]; - - let digest = VextDigest { events, signatures }; - - // finally, decompress the `VextDigest` back into a - // `Vec>` - let decompressed = digest - .decompress(last_block_height) - .into_iter() - .collect::>>(); - - assert_eq!(decompressed.len(), ext.len()); - for vext in decompressed.into_iter() { - assert!(ext.contains(&vext)); - if vext.data.validator_addr == validator_1 { - assert!(vext.verify(&sk_1.to_public()).is_ok()) - } else { - assert!(vext.verify(&sk_2.to_public()).is_ok()) - } - } - } -} diff --git a/crates/vote_ext/src/lib.rs b/crates/vote_ext/src/lib.rs deleted file mode 100644 index c8cdaaaf8f3..00000000000 --- a/crates/vote_ext/src/lib.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! This module contains types necessary for processing vote extensions. - -#![doc(html_favicon_url = "https://dev.namada.net/master/favicon.png")] -#![doc(html_logo_url = "https://dev.namada.net/master/rustdoc-logo.png")] -#![deny(rustdoc::broken_intra_doc_links)] -#![deny(rustdoc::private_intra_doc_links)] -#![warn( - missing_docs, - rust_2018_idioms, - clippy::cast_sign_loss, - clippy::cast_possible_truncation, - clippy::cast_possible_wrap, - clippy::cast_lossless, - clippy::arithmetic_side_effects, - clippy::dbg_macro, - clippy::print_stdout, - clippy::print_stderr -)] - -pub mod bridge_pool_roots; -pub mod ethereum_events; -pub mod validator_set_update; - -use namada_core::borsh::{ - BorshDeserialize, BorshSchema, BorshSerialize, BorshSerializeExt, -}; -use namada_core::chain::ChainId; -use namada_core::key::common; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_tx::data::TxType; -use namada_tx::data::protocol::{ProtocolTx, ProtocolTxType}; -use namada_tx::{Authorization, Signed, Tx, TxError}; - -/// This type represents the data we pass to the extension of -/// a vote at the PreCommit phase of Tendermint. -#[derive( - Debug, - Clone, - PartialEq, - Eq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct VoteExtension { - /// Vote extension data related with Ethereum events. - pub ethereum_events: Option>, - /// A signature of the Ethereum bridge pool root and nonce. - pub bridge_pool_root: Option, - /// Vote extension data related with validator set updates. - pub validator_set_update: Option, -} - -macro_rules! ethereum_tx_data_deserialize_inner { - ($variant:ty) => { - impl TryFrom<&Tx> for $variant { - type Error = TxError; - - fn try_from(tx: &Tx) -> Result { - let tx_data = tx - .data(tx.commitments().first().ok_or_else(|| { - TxError::Deserialization( - "Missing inner protocol tx commitments".into(), - ) - })?) - .ok_or_else(|| { - TxError::Deserialization( - "Expected protocol tx type associated data".into(), - ) - })?; - Self::try_from_slice(&tx_data) - .map_err(|err| TxError::Deserialization(err.to_string())) - } - } - }; -} - -macro_rules! ethereum_tx_data_declare { - ( - $( #[$outer_attrs:meta] )* - { - $( - $(#[$inner_attrs:meta])* - $variant:ident ($inner_ty:ty) - ),* $(,)? - } - ) => { - $( #[$outer_attrs] )* - pub enum EthereumTxData { - $( - $(#[$inner_attrs])* - $variant ( $inner_ty ) - ),* - } - - /// All the variants of [`EthereumTxData`], stored - /// in a trait. - #[allow(missing_docs)] - pub trait EthereumTxDataVariants { - $( type $variant; )* - } - - impl EthereumTxDataVariants for EthereumTxData { - $( type $variant = $inner_ty; )* - } - - #[allow(missing_docs)] - pub mod ethereum_tx_data_variants { - //! All the variants of [`EthereumTxData`], stored - //! in a module. - use super::*; - - $( pub type $variant = $inner_ty; )* - } - - $( ethereum_tx_data_deserialize_inner!($inner_ty); )* - }; - } - -ethereum_tx_data_declare! { - /// Data associated with Ethereum protocol transactions. - #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshDeserializer, BorshSchema)] - { - /// Ethereum events contained in vote extensions that - /// are compressed before being included on chain - EthereumEvents(ethereum_events::VextDigest), - /// Collection of signatures over the Ethereum bridge - /// pool merkle root and nonce. - BridgePool(bridge_pool_roots::MultiSignedVext), - /// Validator set updates contained in vote extensions - ValidatorSetUpdate(validator_set_update::VextDigest), - /// Ethereum events seen by some validator - EthEventsVext(ethereum_events::SignedVext), - /// Signature over the Ethereum bridge pool merkle root and nonce. - BridgePoolVext(bridge_pool_roots::SignedVext), - /// Validator set update signed by some validator - ValSetUpdateVext(validator_set_update::SignedVext), - } -} - -impl TryFrom<&Tx> for EthereumTxData { - type Error = TxError; - - fn try_from(tx: &Tx) -> Result { - let TxType::Protocol(protocol_tx) = tx.header().tx_type else { - return Err(TxError::Deserialization( - "Expected protocol tx type".into(), - )); - }; - let Some(tx_data) = - tx.data(tx.commitments().first().ok_or_else(|| { - TxError::Deserialization( - "Missing inner protocol tx commitments".into(), - ) - })?) - else { - return Err(TxError::Deserialization( - "Expected protocol tx type associated data".into(), - )); - }; - Self::deserialize(&protocol_tx.tx, &tx_data) - } -} - -impl EthereumTxData { - /// Sign transaction Ethereum data and wrap it in a [`Tx`]. - pub fn sign( - &self, - signing_key: &common::SecretKey, - chain_id: ChainId, - ) -> Tx { - let (tx_data, tx_type) = self.serialize(); - let mut outer_tx = - Tx::from_type(TxType::Protocol(Box::new(ProtocolTx { - pk: signing_key.to_public(), - tx: tx_type, - }))); - outer_tx.header.chain_id = chain_id; - outer_tx.set_data(namada_tx::Data::new(tx_data)); - outer_tx.add_section(namada_tx::Section::Authorization( - Authorization::new( - outer_tx.sechashes(), - [(0, signing_key.clone())].into_iter().collect(), - None, - ), - )); - outer_tx - } - - /// Serialize Ethereum protocol transaction data. - pub fn serialize(&self) -> (Vec, ProtocolTxType) { - macro_rules! match_of_type { - ( $( $type:ident ),* $(,)?) => { - match self { - $( EthereumTxData::$type(x) => - (x.serialize_to_vec(), ProtocolTxType::$type)),* - } - } - } - match_of_type! { - EthereumEvents, - BridgePool, - ValidatorSetUpdate, - EthEventsVext, - BridgePoolVext, - ValSetUpdateVext, - } - } - - /// Deserialize Ethereum protocol transaction data. - pub fn deserialize( - tx_type: &ProtocolTxType, - data: &[u8], - ) -> Result { - let deserialize: fn(&[u8]) -> _ = match tx_type { - ProtocolTxType::EthereumEvents => |data| { - BorshDeserialize::try_from_slice(data) - .map(EthereumTxData::EthereumEvents) - }, - ProtocolTxType::BridgePool => |data| { - BorshDeserialize::try_from_slice(data) - .map(EthereumTxData::BridgePool) - }, - ProtocolTxType::ValidatorSetUpdate => |data| { - BorshDeserialize::try_from_slice(data) - .map(EthereumTxData::ValidatorSetUpdate) - }, - ProtocolTxType::EthEventsVext => |data| { - BorshDeserialize::try_from_slice(data) - .map(EthereumTxData::EthEventsVext) - }, - ProtocolTxType::BridgePoolVext => |data| { - BorshDeserialize::try_from_slice(data) - .map(EthereumTxData::BridgePoolVext) - }, - ProtocolTxType::ValSetUpdateVext => |data| { - BorshDeserialize::try_from_slice(data) - .map(EthereumTxData::ValSetUpdateVext) - }, - }; - deserialize(data) - .map_err(|err| TxError::Deserialization(err.to_string())) - } -} diff --git a/crates/vote_ext/src/validator_set_update.rs b/crates/vote_ext/src/validator_set_update.rs deleted file mode 100644 index 254a6958ff1..00000000000 --- a/crates/vote_ext/src/validator_set_update.rs +++ /dev/null @@ -1,602 +0,0 @@ -//! Contains types necessary for processing validator set updates -//! in vote extensions. -use std::cmp::Ordering; -use std::ops::Deref; - -use namada_core::address::Address; -use namada_core::borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use namada_core::chain::Epoch; -use namada_core::collections::HashMap; -use namada_core::eth_abi::{AbiEncode, Encode, Token}; -use namada_core::ethereum_events::EthAddress; -use namada_core::keccak::KeccakHash; -use namada_core::key::common::{self, Signature}; -use namada_core::voting_power::{EthBridgeVotingPower, FractionalVotingPower}; -use namada_core::{ethereum_structs, token}; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use namada_tx::Signed; - -// the contract versions and namespaces plugged into validator set hashes -// TODO(namada#249): ideally, these values should not be hardcoded -const BRIDGE_CONTRACT_VERSION: u8 = 1; -const BRIDGE_CONTRACT_NAMESPACE: &str = "bridge"; -const GOVERNANCE_CONTRACT_VERSION: u8 = 1; -const GOVERNANCE_CONTRACT_NAMESPACE: &str = "governance"; - -/// Type alias for a [`ValidatorSetUpdateVextDigest`]. -pub type VextDigest = ValidatorSetUpdateVextDigest; - -/// Contains the digest of all signatures from a quorum of -/// validators for a [`Vext`]. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct ValidatorSetUpdateVextDigest { - /// A mapping from a consensus validator address to a [`Signature`]. - pub signatures: HashMap, - /// The addresses of the validators in the new [`Epoch`], - /// and their respective voting power. - pub voting_powers: VotingPowersMap, -} - -impl VextDigest { - /// Build a singleton [`VextDigest`], from the provided [`Vext`]. - #[inline] - pub fn singleton(SignedVext(ext): SignedVext) -> VextDigest { - VextDigest { - signatures: HashMap::from([( - ext.data.validator_addr.clone(), - ext.sig, - )]), - voting_powers: ext.data.voting_powers, - } - } - - /// Decompresses a set of signed [`Vext`] instances. - pub fn decompress(self, signing_epoch: Epoch) -> Vec { - let VextDigest { - signatures, - voting_powers, - } = self; - - let mut extensions = vec![]; - - for (validator_addr, signature) in signatures.into_iter() { - let voting_powers = voting_powers.clone(); - let data = Vext { - validator_addr, - voting_powers, - signing_epoch, - }; - extensions.push(SignedVext(Signed::new_from(data, signature))); - } - extensions - } -} - -/// Represents a [`Vext`] signed by some validator, with -/// an Ethereum key. -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - PartialEq, - Eq, -)] -pub struct SignedVext(pub Signed); - -impl Deref for SignedVext { - type Target = Signed; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Type alias for a [`ValidatorSetUpdateVext`]. -pub type Vext = ValidatorSetUpdateVext; - -/// Represents a validator set update, for some new [`Epoch`]. -#[derive( - Eq, - PartialEq, - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct ValidatorSetUpdateVext { - /// The addresses of the validators in the new [`Epoch`], - /// and their respective voting power. - /// - /// When signing a [`Vext`], this [`VotingPowersMap`] is converted - /// into two arrays: one for its keys, and another for its - /// values. The arrays are sorted in descending order based - /// on the voting power of each validator. - pub voting_powers: VotingPowersMap, - /// The address of the validator who submitted the vote extension. - // NOTE: The validator's established address was included as a workaround - // for `namada#200`, which prevented us from mapping a CometBFT validator - // address to a Namada address. Since then, we have committed to keeping - // this `validator_addr` field. - pub validator_addr: Address, - /// The value of Namada's [`Epoch`] at the creation of this - /// [`Vext`]. - /// - /// An important invariant is that this [`Epoch`] will always - /// correspond to an [`Epoch`] before the new validator set - /// is installed. - /// - /// Since this is a monotonically growing sequence number, - /// it is signed together with the rest of the data to - /// prevent replay attacks on validator set updates. - /// - /// Additionally, we can use this [`Epoch`] value to query - /// the appropriate validator set to verify signatures with - /// (i.e. the previous validator set). - pub signing_epoch: Epoch, -} - -impl Vext { - /// Creates a new signed [`Vext`]. - /// - /// For more information, read the docs of [`SignedVext`]. - #[inline] - pub fn sign(&self, sk: &common::SecretKey) -> SignedVext { - SignedVext(Signed::new(sk, self.clone())) - } -} - -/// Container type for both kinds of Ethereum bridge addresses: -/// -/// - An address derived from a hot key. -/// - An address derived from a cold key. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct EthAddrBook { - /// Ethereum address derived from a hot key. - pub hot_key_addr: EthAddress, - /// Ethereum address derived from a cold key. - pub cold_key_addr: EthAddress, -} - -/// Provides a mapping between [`EthAddress`] and [`token::Amount`] instances. -pub type VotingPowersMap = HashMap; - -/// This trait contains additional methods for a [`VotingPowersMap`], related -/// with validator set update vote extensions logic. -pub trait VotingPowersMapExt { - /// Returns a [`Vec`] of pairs of validator addresses and voting powers, - /// sorted in descending order by voting power. - fn get_sorted(&self) -> Vec<(&EthAddrBook, &token::Amount)>; - - /// Returns the list of Ethereum validator hot and cold addresses and their - /// respective voting powers (in this order), with an Ethereum ABI - /// compatible encoding. Implementations of this method must be - /// deterministic based on `self`. In addition, the returned `Vec`s must be - /// sorted in descending order by voting power, as this is more efficient to - /// deal with on the Ethereum side when working out if there is enough - /// voting power for a given validator set update. - fn get_abi_encoded(&self) -> (Vec, Vec) { - let sorted = self.get_sorted(); - - let total_voting_power: token::Amount = token::Amount::sum( - sorted.iter().map(|&(_, &voting_power)| voting_power), - ) - .expect("Voting power sum must not overflow"); - - // split the vec into two portions - sorted - .into_iter() - .map(|(addr_book, &voting_power)| { - let voting_power: EthBridgeVotingPower = - FractionalVotingPower::new( - voting_power.into(), - total_voting_power.into(), - ) - .expect( - "Voting power in map can't be larger than the total \ - voting power", - ) - .try_into() - .expect( - "Must be able to convert to eth bridge voting power", - ); - - let &EthAddrBook { - hot_key_addr, - cold_key_addr, - } = addr_book; - - ( - Token::FixedBytes( - encode_validator_data(hot_key_addr, voting_power) - .into(), - ), - Token::FixedBytes( - encode_validator_data(cold_key_addr, voting_power) - .into(), - ), - ) - }) - .unzip() - } - - /// Returns the bridge and governance keccak hashes of - /// this [`VotingPowersMap`]. - #[inline] - fn get_bridge_and_gov_hashes( - &self, - next_epoch: Epoch, - ) -> (KeccakHash, KeccakHash) { - let (bridge_validators, governance_validators) = self.get_abi_encoded(); - valset_upd_toks_to_hashes( - next_epoch, - bridge_validators, - governance_validators, - ) - } -} - -/// Returns the bridge and governance keccak hashes calculated from -/// the given hot and cold key addresses, and their respective validator's -/// voting powers, normalized to `2^32`. -pub fn valset_upd_toks_to_hashes( - next_epoch: Epoch, - bridge_validators: Vec, - governance_validators: Vec, -) -> (KeccakHash, KeccakHash) { - let bridge_hash = compute_hash( - next_epoch, - BRIDGE_CONTRACT_VERSION, - BRIDGE_CONTRACT_NAMESPACE, - bridge_validators, - ); - let governance_hash = compute_hash( - next_epoch, - GOVERNANCE_CONTRACT_VERSION, - GOVERNANCE_CONTRACT_NAMESPACE, - governance_validators, - ); - (bridge_hash, governance_hash) -} - -/// Compare two items of [`VotingPowersMap`]. This comparison operation must -/// match the equivalent comparison operation in Ethereum bridge code. -fn compare_voting_powers_map_items( - first: &(&EthAddrBook, &token::Amount), - second: &(&EthAddrBook, &token::Amount), -) -> Ordering { - let (first_power, second_power) = (first.1, second.1); - let (first_addr, second_addr) = (first.0, second.0); - match second_power.cmp(first_power) { - Ordering::Equal => first_addr.cmp(second_addr), - ordering => ordering, - } -} - -impl VotingPowersMapExt for VotingPowersMap { - fn get_sorted(&self) -> Vec<(&EthAddrBook, &token::Amount)> { - let mut pairs: Vec<_> = self.iter().collect(); - pairs.sort_by(compare_voting_powers_map_items); - pairs - } -} - -/// Convert an [`Epoch`] to a [`Token`]. -#[inline] -fn epoch_to_token(Epoch(e): Epoch) -> Token { - Token::Uint(e.into()) -} - -/// Compute the keccak hash of a validator set update. -/// -/// For more information, check the specs of the Ethereum bridge smart -/// contracts. -#[inline] -fn compute_hash( - next_epoch: Epoch, - contract_version: u8, - contract_namespace: &str, - validators: Vec, -) -> KeccakHash { - AbiEncode::keccak256(&[ - Token::Uint(contract_version.into()), - Token::String(contract_namespace.into()), - Token::Array(validators), - epoch_to_token(next_epoch), - ]) -} - -/// Given a validator's [`EthAddress`] and its respective -/// [`EthBridgeVotingPower`], return an encoded representation -/// of this data, understood by the smart contract. -#[inline] -fn encode_validator_data( - address: EthAddress, - voting_power: EthBridgeVotingPower, -) -> [u8; 32] { - let address = address.0; - let voting_power = u128::from(voting_power).to_be_bytes(); - - let mut buffer = [0u8; 32]; - buffer[..20].copy_from_slice(&address); - buffer[20..].copy_from_slice(&voting_power[4..]); - - buffer -} - -/// Struct for serializing validator set -/// arguments with ABI for Ethereum smart -/// contracts. -#[derive( - Debug, - Clone, - Default, - Eq, - PartialEq, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, -)] -pub struct ValidatorSetArgs { - /// Ethereum addresses of the validators. - pub validators: Vec, - /// The voting powers of the validators. - pub voting_powers: Vec, - /// The epoch when the validators were part of - /// the consensus set of validators. - /// - /// Serves as a nonce. - pub epoch: Epoch, -} - -impl From for ethereum_structs::ValidatorSetArgs { - fn from(valset: ValidatorSetArgs) -> Self { - let ValidatorSetArgs { - validators, - voting_powers, - epoch, - } = valset; - ethereum_structs::ValidatorSetArgs { - validator_set: validators - .into_iter() - .zip(voting_powers) - .map(|(addr, power)| encode_validator_data(addr, power)) - .collect(), - nonce: epoch.0.into(), - } - } -} - -impl Encode<1> for ValidatorSetArgs { - fn tokenize(&self) -> [Token; 1] { - let validator_set = Token::Array( - self.validators - .iter() - .zip(self.voting_powers.iter()) - .map(|(&addr, &power)| { - Token::FixedBytes(encode_validator_data(addr, power).into()) - }) - .collect(), - ); - let nonce = Token::Uint(self.epoch.0.into()); - [Token::Tuple(vec![validator_set, nonce])] - } -} - -// this is only here so we don't pollute the -// outer namespace with serde traits -mod tag { - use namada_core::eth_abi::{AbiEncode, Encode, Token}; - use namada_core::hash::KeccakHasher; - use namada_core::keccak::KeccakHash; - use namada_core::key::Signable; - use serde::{Deserialize, Serialize}; - - use super::{ - GOVERNANCE_CONTRACT_VERSION, Vext, VotingPowersMapExt, epoch_to_token, - }; - - /// Tag type that indicates we should use [`AbiEncode`] - /// to sign data in a [`namada_tx::Signed`] wrapper. - #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] - pub struct SerializeWithAbiEncode; - - impl Signable for SerializeWithAbiEncode { - type Hasher = KeccakHasher; - type Output = KeccakHash; - - fn as_signable(ext: &Vext) -> Self::Output { - // NOTE: the smart contract expects us to sign - // against the next nonce (i.e. the new epoch) - let next_epoch = ext.signing_epoch.next(); - let (KeccakHash(bridge_hash), KeccakHash(gov_hash)) = - ext.voting_powers.get_bridge_and_gov_hashes(next_epoch); - AbiEncode::signable_keccak256(&[ - Token::Uint(GOVERNANCE_CONTRACT_VERSION.into()), - Token::String("updateValidatorSet".into()), - Token::FixedBytes(bridge_hash.to_vec()), - Token::FixedBytes(gov_hash.to_vec()), - epoch_to_token(next_epoch), - ]) - } - } -} - -#[doc(inline)] -pub use tag::SerializeWithAbiEncode; - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use data_encoding::HEXLOWER; - - use super::*; - - /// Test the keccak hash of a validator set update - #[test] - fn test_validator_set_update_keccak_hash() { - // ```js - // const ethers = require('ethers'); - // const keccak256 = require('keccak256') - // - // const abiEncoder = new ethers.AbiCoder(); - // - // const output = abiEncoder.encode( - // ['uint256', 'string', 'bytes32[]', 'uint256'], - // [1, 'bridge', [], 1], - // ); - // - // const hash = keccak256(output).toString('hex'); - // - // console.log(hash); - // ``` - const EXPECTED: &str = - "b97454f4c266c0d223651a52a705d76f3be337ace04be4590d9aedab9818dabc"; - - let KeccakHash(got) = compute_hash( - 1u64.into(), - BRIDGE_CONTRACT_VERSION, - BRIDGE_CONTRACT_NAMESPACE, - vec![], - ); - - assert_eq!(&HEXLOWER.encode(&got[..]), EXPECTED); - } - - /// Checks that comparing two [`VotingPowersMap`] items which have the same - /// voting powers but different [`EthAddrBook`]s does not result in them - /// being regarded as equal. - #[test] - fn test_compare_voting_powers_map_items_identical_voting_powers() { - let same_voting_power = 200.into(); - - let validator_a = EthAddrBook { - hot_key_addr: EthAddress([0; 20]), - cold_key_addr: EthAddress([0; 20]), - }; - let validator_b = EthAddrBook { - hot_key_addr: EthAddress([1; 20]), - cold_key_addr: EthAddress([1; 20]), - }; - - assert_eq!( - compare_voting_powers_map_items( - &(&validator_a, &same_voting_power), - &(&validator_b, &same_voting_power), - ), - Ordering::Less - ); - } - - /// Checks that comparing two [`VotingPowersMap`] items with different - /// voting powers results in the item with the lesser voting power being - /// regarded as "greater". - #[test] - fn test_compare_voting_powers_map_items_different_voting_powers() { - let validator_a = EthAddrBook { - hot_key_addr: EthAddress([0; 20]), - cold_key_addr: EthAddress([0; 20]), - }; - let validator_a_voting_power = 200.into(); - let validator_b = EthAddrBook { - hot_key_addr: EthAddress([1; 20]), - cold_key_addr: EthAddress([1; 20]), - }; - let validator_b_voting_power = 100.into(); - - assert_eq!( - compare_voting_powers_map_items( - &(&validator_a, &validator_a_voting_power), - &(&validator_b, &validator_b_voting_power), - ), - Ordering::Less - ); - } - - /// Checks that [`VotingPowersMapExt::get_abi_encoded`] gives a - /// deterministic result in the case where there are multiple validators - /// with the same voting power. - /// - /// NB: this test may pass even if the implementation is not - /// deterministic unless the test is run with the `--release` profile, as it - /// is implicitly relying on how iterating over a [`HashMap`] seems to - /// return items in the order in which they were inserted, at least for this - /// very small 2-item example. - #[test] - fn test_voting_powers_map_get_abi_encoded_deterministic_with_identical_voting_powers() - { - let validator_a = EthAddrBook { - hot_key_addr: EthAddress([0; 20]), - cold_key_addr: EthAddress([0; 20]), - }; - let validator_b = EthAddrBook { - hot_key_addr: EthAddress([1; 20]), - cold_key_addr: EthAddress([1; 20]), - }; - let same_voting_power = 200.into(); - - let mut voting_powers_1 = VotingPowersMap::default(); - voting_powers_1.insert(validator_a.clone(), same_voting_power); - voting_powers_1.insert(validator_b.clone(), same_voting_power); - - let mut voting_powers_2 = VotingPowersMap::default(); - voting_powers_2.insert(validator_b, same_voting_power); - voting_powers_2.insert(validator_a, same_voting_power); - - let x = voting_powers_1.get_abi_encoded(); - let y = voting_powers_2.get_abi_encoded(); - assert_eq!(x, y); - } - - #[test] - fn test_abi_encode_valset_args() { - let valset_update = ValidatorSetArgs { - validators: vec![ - EthAddress::from_str( - "0x241D37B7Cf5233b3b0b204321420A86e8f7bfdb5", - ) - .expect("Test failed"), - ], - voting_powers: vec![8828299u64.into()], - epoch: 0.into(), - }; - let encoded = valset_update.encode().into_inner(); - let encoded = HEXLOWER.encode(&encoded); - let expected = "000000000000000000000000000000000000000000000000000000000000002\ - 000000000000000000000000000000000000000000000000000000000000000\ - 400000000000000000000000000000000000000000000000000000000000000\ - 000000000000000000000000000000000000000000000000000000000000000\ - 0001241d37b7cf5233b3b0b204321420a86e8f7bfdb50000000000000000008\ - 6b58b"; - assert_eq!(expected, encoded); - } -} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 4a75e333369..5cabaf685d0 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -31,7 +31,6 @@ path = "make-db-migration.rs" [features] default = ["migrations"] migrations = [] -namada-eth-bridge = ["namada_sdk/namada-eth-bridge"] [dev-dependencies] masp_primitives = { workspace = true, features = ["transparent-inputs"] } diff --git a/fuzz/fuzz_targets/txs_finalize_block.rs b/fuzz/fuzz_targets/txs_finalize_block.rs index e41a5fd1c23..55b4ab483da 100644 --- a/fuzz/fuzz_targets/txs_finalize_block.rs +++ b/fuzz/fuzz_targets/txs_finalize_block.rs @@ -4,16 +4,14 @@ use data_encoding::HEXUPPER; use libfuzzer_sys::fuzz_target; -use namada_apps_lib::wallet; +use namada_apps_lib::{tendermint, wallet}; use namada_core::address::Address; use namada_core::key::PublicKeyTmRawHash; use namada_core::time::DateTimeUtc; use namada_node::shell; +use namada_node::shell::FinalizeBlockRequest; +use namada_node::shell::abci::{ProcessedTx, TxBytes}; use namada_node::shell::test_utils::TestShell; -use namada_node::shims::abcipp_shim_types::shim::TxBytes; -use namada_node::shims::abcipp_shim_types::shim::request::{ - FinalizeBlock, ProcessedTx, -}; use namada_tx::Tx; use namada_tx::data::TxType; @@ -37,7 +35,7 @@ fuzz_target!(|txs: Vec| { match SHELL.as_mut() { Some(shell) => shell, None => { - let (shell, _recv, _, _) = shell::test_utils::setup(); + let shell = shell::test_utils::setup(); SHELL = Some(shell); SHELL.as_mut().unwrap() } @@ -59,9 +57,12 @@ fuzz_target!(|txs: Vec| { let proposer_address_bytes = HEXUPPER .decode(proposer_pk.tm_raw_hash().as_bytes()) .unwrap(); - let req = FinalizeBlock { + let req = FinalizeBlockRequest { txs, - proposer_address: proposer_address_bytes, + proposer_address: tendermint::account::Id::try_from( + proposer_address_bytes, + ) + .unwrap(), ..Default::default() }; let _events = shell.finalize_block(req).unwrap(); diff --git a/fuzz/fuzz_targets/txs_mempool.rs b/fuzz/fuzz_targets/txs_mempool.rs index e653ec2f5dd..b5ea765ce96 100644 --- a/fuzz/fuzz_targets/txs_mempool.rs +++ b/fuzz/fuzz_targets/txs_mempool.rs @@ -8,10 +8,7 @@ use namada_node::shell::test_utils::TestShell; use namada_tx::Tx; lazy_static! { - static ref SHELL: TestShell = { - let (shell, _recv, _, _) = shell::test_utils::setup(); - shell - }; + static ref SHELL: TestShell = shell::test_utils::setup(); } fuzz_target!(|tx: Tx| { diff --git a/fuzz/fuzz_targets/txs_prepare_proposal.rs b/fuzz/fuzz_targets/txs_prepare_proposal.rs index 66a68dcbfa2..4970eb24ef3 100644 --- a/fuzz/fuzz_targets/txs_prepare_proposal.rs +++ b/fuzz/fuzz_targets/txs_prepare_proposal.rs @@ -3,16 +3,13 @@ use lazy_static::lazy_static; use libfuzzer_sys::fuzz_target; use namada_node::shell; +use namada_node::shell::abci::TxBytes; use namada_node::shell::test_utils::TestShell; -use namada_node::shims::abcipp_shim_types::shim::TxBytes; use namada_node::tendermint_proto::abci::RequestPrepareProposal; use namada_tx::Tx; lazy_static! { - static ref SHELL: TestShell = { - let (shell, _recv, _, _) = shell::test_utils::setup(); - shell - }; + static ref SHELL: TestShell = shell::test_utils::setup(); } fuzz_target!(|txs: Vec| { diff --git a/fuzz/fuzz_targets/txs_process_proposal.rs b/fuzz/fuzz_targets/txs_process_proposal.rs index f355d15efab..36363ddc503 100644 --- a/fuzz/fuzz_targets/txs_process_proposal.rs +++ b/fuzz/fuzz_targets/txs_process_proposal.rs @@ -7,10 +7,7 @@ use namada_node::shell::test_utils::{ProcessProposal, TestShell}; use namada_tx::Tx; lazy_static! { - static ref SHELL: TestShell = { - let (shell, _recv, _, _) = shell::test_utils::setup(); - shell - }; + static ref SHELL: TestShell = shell::test_utils::setup(); } fuzz_target!(|txs: Vec| { diff --git a/fuzz/fuzz_targets/txs_wasm_run.rs b/fuzz/fuzz_targets/txs_wasm_run.rs index 851990cf72e..6cfe203c24f 100644 --- a/fuzz/fuzz_targets/txs_wasm_run.rs +++ b/fuzz/fuzz_targets/txs_wasm_run.rs @@ -9,17 +9,13 @@ use arbitrary::Arbitrary; use data_encoding::HEXUPPER; use lazy_static::lazy_static; use libfuzzer_sys::fuzz_target; -use namada_apps_lib::wallet; +use namada_apps_lib::{tendermint, wallet}; use namada_core::key::PublicKeyTmRawHash; use namada_node::shell; +use namada_node::shell::FinalizeBlockRequest; +use namada_node::shell::abci::{ProcessedTx, TxBytes, TxResult}; use namada_node::shell::test_utils::TestShell; -use namada_node::shims::abcipp_shim_types::shim::TxBytes; -use namada_node::shims::abcipp_shim_types::shim::request::{ - FinalizeBlock, ProcessedTx, -}; -use namada_node::shims::abcipp_shim_types::shim::response::TxResult; use namada_sdk::address::Address; -use namada_sdk::eth_bridge_pool::PendingTransfer; use namada_sdk::ibc::apps::nft_transfer::types::msgs::transfer::MsgTransfer as IbcMsgNftTransfer; use namada_sdk::ibc::apps::transfer::types::msgs::transfer::MsgTransfer as IbcMsgTransfer; use namada_sdk::ibc::core::handler::types::msgs::MsgEnvelope; @@ -31,7 +27,7 @@ use namada_tx::data::{pgf, pos}; lazy_static! { static ref SHELL: Mutex = { - let (shell, _recv, _, _) = shell::test_utils::setup(); + let shell = shell::test_utils::setup(); Mutex::new(shell) }; } @@ -58,7 +54,6 @@ enum TxKind { ChangeCommission(pos::CommissionChange), ChangeConsensusKey(pos::ConsensusKeyChange), ChangeMetadata(pos::MetaDataChange), - BridgePool(PendingTransfer), ResignSteward(Address), UpdateStewardCommission(pgf::UpdateStewardCommission), } @@ -162,10 +157,6 @@ fn run(kinds: NonEmptyVec) { tx.add_data(data); tx::TX_CHANGE_METADATA_WASM } - BridgePool(data) => { - tx.add_data(data); - tx::TX_BRIDGE_POOL_WASM - } ResignSteward(data) => { tx.add_data(data); tx::TX_RESIGN_STEWARD @@ -211,9 +202,12 @@ fn run(kinds: NonEmptyVec) { let proposer_address_bytes = HEXUPPER .decode(proposer_pk.tm_raw_hash().as_bytes()) .unwrap(); - let req = FinalizeBlock { + let req = FinalizeBlockRequest { txs, - proposer_address: proposer_address_bytes, + proposer_address: tendermint::account::Id::try_from( + proposer_address_bytes, + ) + .unwrap(), ..Default::default() }; let _event = shell.finalize_block(req).unwrap(); diff --git a/scripts/get_cometbft.sh b/scripts/get_cometbft.sh index 52224762147..c1c81d43623 100755 --- a/scripts/get_cometbft.sh +++ b/scripts/get_cometbft.sh @@ -5,8 +5,8 @@ set -Eo pipefail # an example download-url # https://github.com/tendermint/tendermint/releases/download/v0.34.13/tendermint_0.34.13_linux_amd64.tar.gz # https://github.com/heliaxdev/tendermint/releases/download/v0.1.1-abcipp/tendermint_0.1.0-abcipp_darwin_amd64.tar.gz -CMT_MAJORMINOR="0.37" -CMT_PATCH="15" +CMT_MAJORMINOR="0.38" +CMT_PATCH="17" CMT_REPO="https://github.com/cometbft/cometbft" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 73ada894dc2..0f995e5831f 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -4676,35 +4676,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "namada_ethereum_bridge" -version = "0.251.0" -dependencies = [ - "borsh", - "ethers", - "eyre", - "itertools 0.14.0", - "konst", - "namada_account", - "namada_core", - "namada_events", - "namada_governance", - "namada_macros", - "namada_parameters", - "namada_proof_of_stake", - "namada_state", - "namada_storage", - "namada_systems", - "namada_trans_token", - "namada_tx", - "namada_vote_ext", - "namada_vp_env", - "serde", - "smooth-operator", - "thiserror 2.0.12", - "tracing", -] - [[package]] name = "namada_events" version = "0.251.0" @@ -4897,7 +4868,6 @@ dependencies = [ "nam-jubjub", "namada_account", "namada_core", - "namada_ethereum_bridge", "namada_events", "namada_gas", "namada_governance", @@ -4911,7 +4881,6 @@ dependencies = [ "namada_token", "namada_tx", "namada_vm", - "namada_vote_ext", "namada_vp", "namada_wallet", "num-traits", @@ -5212,17 +5181,6 @@ dependencies = [ "namada_core", ] -[[package]] -name = "namada_vote_ext" -version = "0.251.0" -dependencies = [ - "borsh", - "namada_core", - "namada_macros", - "namada_tx", - "serde", -] - [[package]] name = "namada_vp" version = "0.251.0" @@ -8145,15 +8103,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "tx_bridge_pool" -version = "0.251.0" -dependencies = [ - "getrandom 0.2.15", - "namada_tx_prelude", - "rlsf", -] - [[package]] name = "tx_change_consensus_key" version = "0.251.0" diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index a008a27d276..2701d9df8fb 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -4,7 +4,6 @@ resolver = "2" members = [ "tx_become_validator", "tx_bond", - "tx_change_bridge_pool", "tx_change_consensus_key", "tx_change_validator_commission", "tx_change_validator_metadata", diff --git a/wasm/tx_change_bridge_pool/Cargo.toml b/wasm/tx_change_bridge_pool/Cargo.toml deleted file mode 100644 index 4618f30a5c7..00000000000 --- a/wasm/tx_change_bridge_pool/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "tx_bridge_pool" -description = "WASM transaction to interact with the bridge pool" -authors.workspace = true -edition.workspace = true -license.workspace = true -version.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[features] -debug-panic-hook = [] - -[dependencies] -namada_tx_prelude.workspace = true - -rlsf.workspace = true -getrandom.workspace = true - -[lib] -crate-type = ["cdylib"] diff --git a/wasm/tx_change_bridge_pool/src/lib.rs b/wasm/tx_change_bridge_pool/src/lib.rs deleted file mode 100644 index 377ed0a2ff9..00000000000 --- a/wasm/tx_change_bridge_pool/src/lib.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! A tx for adding a transfer request across the Ethereum bridge -//! into the bridge pool. -use namada_tx_prelude::eth_bridge_pool::{ - BRIDGE_POOL_ADDRESS, GasFee, PendingTransfer, TransferToEthereum, - get_pending_key, -}; -use namada_tx_prelude::parameters::native_erc20_key; -use namada_tx_prelude::*; - -#[transaction] -fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { - let data = ctx.get_tx_data(&tx_data)?; - let transfer = PendingTransfer::try_from_slice(&data[..]) - .map_err(|e| Error::wrap("Error deserializing PendingTransfer", e))?; - debug_log!("Received transfer to add to Bridge pool"); - // pay the gas fees - let GasFee { - token: ref fee_token_addr, - amount, - ref payer, - } = transfer.gas_fee; - token::transfer(ctx, payer, &BRIDGE_POOL_ADDRESS, fee_token_addr, amount)?; - debug_log!("Bridge pool token transfer succeeded"); - let TransferToEthereum { - asset, - ref sender, - amount, - .. - } = transfer.transfer; - // if minting wNam, escrow the correct amount - if asset == native_erc20_address(ctx)? { - let nam_addr = ctx.get_native_token()?; - token::transfer(ctx, sender, &address::ETH_BRIDGE, &nam_addr, amount)?; - } else { - // Otherwise we escrow ERC20 tokens. - let token = transfer.token_address(); - token::transfer(ctx, sender, &BRIDGE_POOL_ADDRESS, &token, amount)?; - } - debug_log!("Bridge pool escrow succeeded"); - // add transfer into the pool - let pending_key = get_pending_key(&transfer); - ctx.write(&pending_key, transfer) - .wrap_err("Could not write transfer to bridge pool")?; - Ok(()) -} - -fn native_erc20_address(ctx: &mut Ctx) -> Result { - debug_log!("Trying to get wnam key for Bridge pool transfer"); - let addr = ctx - .read(&native_erc20_key()) - .wrap_err("Could not read wrapped NAM address")? - .ok_or_err_msg("Wrapped NAM address must be present in storage")?; - debug_log!("Got wnam key for Bridge pool transfer: {addr}"); - Ok(addr) -}