diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14fa7231d88..7b26283d9a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ env: AWS_ROLE_ARN: arn:aws:iam::024848458133:role/github_oidc_FuelLabs_fuel-core AWS_ECR_ORG: q2z3y3a7 CARGO_TERM_COLOR: always - RUST_VERSION: 1.75.0 + RUST_VERSION: 1.79.0 RUST_VERSION_FMT: nightly-2023-10-29 RUST_VERSION_COV: nightly-2024-06-05 RUSTFLAGS: -D warnings @@ -180,9 +180,6 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} slack_webhook: ${{ secrets.SLACK_WEBHOOK_NOTIFY_BUILD }} - env: - RUSTFLAGS: -D warnings - publish-crates-check: runs-on: buildjet-4vcpu-ubuntu-2204 steps: @@ -199,6 +196,43 @@ jobs: check-repo: false ignore-unpublished-changes: true + cargo-test-kms: + if: github.event.repository.fork == false + needs: + - cargo-verifications + - rustfmt + - check-changelog + runs-on: buildjet-4vcpu-ubuntu-2204 + env: + RUSTFLAGS: -D warnings + FUEL_CORE_TEST_AWS_KMS_ARN: "arn:aws:kms:us-east-1:249945542445:key/mrk-e13c7118ce544f7da66294f6e87c8790" + timeout-minutes: 45 + permissions: + contents: read + id-token: write + steps: + - name: Configure AWS credentials for integration testing + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::249945542445:role/github_oidc_FuelLabs_fuel-core + aws-region: us-east-1 + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} + - uses: rui314/setup-mold@v1 + - uses: buildjet/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: cargo-test-kms-${{ hashFiles('**/Cargo.lock') }} + - name: Run integration tests for kms only + run: cargo test -p fuel-core-tests --features aws-kms -- kms + publish-codecov: name: Publish code coverage report on GitHub pages branch runs-on: buildjet-4vcpu-ubuntu-2204 diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 71ca1a71b41..80f9a6079f3 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -16,7 +16,7 @@ defaults: env: # So cargo doesn't complain about unstable features RUSTC_BOOTSTRAP: 1 - RUST_VERSION: 1.75.0 + RUST_VERSION: 1.79.0 PR_TITLE: Weekly `cargo update` PR_MESSAGE: | Automation to keep dependencies in `Cargo.lock` current. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a256c49e6f..e43e2fe45ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- [2051](https://github.com/FuelLabs/fuel-core/pull/2051): Add support for AWS KMS signing for the PoA consensus module. The new key can be specified with `--consensus-aws-kms AWS_KEY_ARN`. + +### Changed + +#### Breaking +- [2051](https://github.com/FuelLabs/fuel-core/pull/2051): Misdocumented `CONSENSUS_KEY` environ variable has been removed, use `CONSENSUS_KEY_SECRET` instead. Also raises MSRV to `1.79.0`. + ## [Version 0.33.0] ### Added @@ -21,7 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [2080](https://github.com/FuelLabs/fuel-core/pull/2080): Reject Upgrade txs with invalid wasm on txpool level. - [2082](https://github.com/FuelLabs/fuel-core/pull/2088): Move `TxPoolError` from `fuel-core-types` to `fuel-core-txpool`. - [2086](https://github.com/FuelLabs/fuel-core/pull/2086): Added support for PoA key rotation. -- [2086](https://github.com/FuelLabs/fuel-core/pull/2086): Support overriding of the non consensus parameters in the chan config. +- [2086](https://github.com/FuelLabs/fuel-core/pull/2086): Support overriding of the non consensus parameters in the chain config. ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 8b081c59842..c096b47de1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,6 +669,327 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-config" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.0", + "hex", + "http 0.2.12", + "ring 0.17.8", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-runtime" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42c2d4218de4dcd890a109461e2f799a1a2ba3bcd2cde9af88360f5df9266c6" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.0", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid 1.10.0", +] + +[[package]] +name = "aws-sdk-kms" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bc44afa1eaa1d4cae1fb61647b142083c3ae1b77c225b5d61be3d5b64bd93d3" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca5e0b9fb285638f1007e9d961d963b9e504ab968fe5a3807cce94070bd0ce3" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc3e48ec239bb734db029ceef83599f4c9b3ce5d25c961b5bcd3f031c15bed54" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede095dfcc5c92b224813c24a82b65005a475c98d737e2726a898cf583e2e8bd" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac 0.12.1", + "http 0.2.12", + "http 1.1.0", + "once_cell", + "percent-encoding", + "sha2 0.10.8", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abbf454960d0db2ad12684a1640120e7557294b0ff8e2f11236290a1b293225" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand 2.1.0", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "http-body 1.0.1", + "httparse", + "hyper", + "hyper-rustls", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls 0.21.12", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.1.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cee7cadb433c781d3299b916fbf620fea813bf38f49db282fb6858141a05cc8" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.1.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.5.17" @@ -681,7 +1002,7 @@ dependencies = [ "bytes", "futures-util", "http 0.2.12", - "http-body", + "http-body 0.4.6", "hyper", "itoa", "matchit", @@ -710,7 +1031,7 @@ dependencies = [ "bytes", "futures-util", "http 0.2.12", - "http-body", + "http-body 0.4.6", "mime", "tower-layer", "tower-service", @@ -762,6 +1083,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -934,6 +1265,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "bzip2" version = "0.4.4" @@ -2881,12 +3222,15 @@ name = "fuel-core-bin" version = "0.33.0" dependencies = [ "anyhow", + "aws-config", + "aws-sdk-kms", "clap 4.5.13", "const_format", "dirs 4.0.0", "dotenvy", "fuel-core", "fuel-core-chain-config", + "fuel-core-poa", "fuel-core-storage", "fuel-core-types", "hex", @@ -3149,11 +3493,13 @@ version = "0.33.0" dependencies = [ "anyhow", "async-trait", + "aws-sdk-kms", "fuel-core-chain-config", "fuel-core-poa", "fuel-core-services", "fuel-core-storage", "fuel-core-types", + "k256", "mockall", "rand", "serde", @@ -3278,6 +3624,8 @@ version = "0.0.0" dependencies = [ "anyhow", "async-trait", + "aws-config", + "aws-sdk-kms", "clap 4.5.13", "cynic", "ethers", @@ -3299,6 +3647,7 @@ dependencies = [ "hyper", "insta", "itertools 0.12.1", + "k256", "pretty_assertions", "primitive-types", "proptest", @@ -3306,6 +3655,7 @@ dependencies = [ "reqwest", "rstest", "serde_json", + "spki", "tempfile", "test-case", "test-helpers", @@ -4074,6 +4424,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + [[package]] name = "http-range-header" version = "0.3.1" @@ -4120,7 +4493,7 @@ dependencies = [ "futures-util", "h2", "http 0.2.12", - "http-body", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -5941,6 +6314,12 @@ version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + [[package]] name = "overload" version = "0.1.1" @@ -6916,6 +7295,12 @@ dependencies = [ "regex-syntax 0.8.4", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -6943,7 +7328,7 @@ dependencies = [ "futures-util", "h2", "http 0.2.12", - "http-body", + "http-body 0.4.6", "hyper", "hyper-rustls", "ipnet", @@ -8536,7 +8921,7 @@ dependencies = [ "futures-core", "futures-util", "http 0.2.12", - "http-body", + "http-body 0.4.6", "http-range-header", "pin-project-lite", "tokio", @@ -8820,6 +9205,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -8881,6 +9272,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -9528,6 +9925,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xmltree" version = "0.10.3" diff --git a/bin/fuel-core/Cargo.toml b/bin/fuel-core/Cargo.toml index b3f457c3711..157e341a135 100644 --- a/bin/fuel-core/Cargo.toml +++ b/bin/fuel-core/Cargo.toml @@ -17,12 +17,17 @@ path = "src/main.rs" [dependencies] anyhow = { workspace = true } +aws-config = { version = "1.1.7", features = [ + "behavior-version-latest", +], optional = true } +aws-sdk-kms = { version = "1.37.0", optional = true } clap = { workspace = true, features = ["derive", "env", "string"] } const_format = { version = "0.2", optional = true } dirs = "4.0" dotenvy = { version = "0.15", optional = true } fuel-core = { workspace = true, features = ["wasm-executor"] } fuel-core-chain-config = { workspace = true } +fuel-core-poa = { workspace = true } fuel-core-types = { workspace = true } hex = "0.4" humantime = "2.1" @@ -52,6 +57,7 @@ test-case = { workspace = true } [features] default = ["env", "relayer", "rocksdb"] +aws-kms = ["dep:aws-config", "dep:aws-sdk-kms", "fuel-core-poa/aws-kms"] env = ["dep:dotenvy"] p2p = ["fuel-core/p2p", "const_format"] relayer = ["fuel-core/relayer", "dep:url"] @@ -59,4 +65,11 @@ parquet = ["fuel-core-chain-config/parquet", "fuel-core-types/serde"] rocksdb = ["fuel-core/rocksdb"] rocksdb-production = ["fuel-core/rocksdb-production", "rocksdb"] # features to enable in production, but increase build times -production = ["env", "relayer", "rocksdb-production", "p2p", "parquet"] +production = [ + "env", + "relayer", + "rocksdb-production", + "p2p", + "parquet", + "aws-kms", +] diff --git a/bin/fuel-core/src/cli/run.rs b/bin/fuel-core/src/cli/run.rs index 739ed56a420..e620a0f9f78 100644 --- a/bin/fuel-core/src/cli/run.rs +++ b/bin/fuel-core/src/cli/run.rs @@ -35,7 +35,6 @@ use fuel_core::{ Config as TxPoolConfig, }, types::{ - blockchain::primitives::SecretKeyWrapper, fuel_tx::ContractId, fuel_vm::SecretKey, secrecy::Secret, @@ -45,6 +44,7 @@ use fuel_core_chain_config::{ SnapshotMetadata, SnapshotReader, }; +use fuel_core_poa::signer::SignMode; use fuel_core_types::blockchain::header::StateTransitionBytecodeVersion; use pyroscope::{ pyroscope::PyroscopeAgentRunning, @@ -72,8 +72,6 @@ use fuel_core::state::historical_rocksdb::StateRewindPolicy; use super::DEFAULT_DATABASE_CACHE_SIZE; -pub const CONSENSUS_KEY_ENV: &str = "CONSENSUS_KEY_SECRET"; - #[cfg(feature = "p2p")] mod p2p; @@ -182,9 +180,16 @@ pub struct Command { /// The signing key used when producing blocks. /// Setting via the `CONSENSUS_KEY_SECRET` ENV var is preferred. - #[arg(long = "consensus-key", env)] + #[arg(long = "consensus-key", env = "CONSENSUS_KEY_SECRET")] pub consensus_key: Option, + /// Use [AWS KMS](https://docs.aws.amazon.com/kms/latest/APIReference/Welcome.html)for signing blocks. + /// Loads the AWS credentials and configuration from the environment. + /// Takes key_id as an argument, e.g. key ARN works. + #[arg(long = "consensus-aws-kms", env, conflicts_with = "consensus_key")] + #[cfg(feature = "aws-kms")] + pub consensus_aws_kms: Option, + /// A new block is produced instantly when transactions are available. #[clap(flatten)] pub poa_trigger: PoATriggerArgs, @@ -245,7 +250,7 @@ pub struct Command { } impl Command { - pub fn get_config(self) -> anyhow::Result { + pub async fn get_config(self) -> anyhow::Result { let Command { service_name: name, max_database_cache_size, @@ -265,6 +270,8 @@ impl Command { min_gas_price, gas_price_threshold_percent, consensus_key, + #[cfg(feature = "aws-kms")] + consensus_aws_kms, poa_trigger, predefined_blocks_path, coinbase_recipient, @@ -310,24 +317,44 @@ impl Command { info!("Block production disabled"); } - let consensus_key = load_consensus_key(consensus_key)?; - if consensus_key.is_some() && trigger == Trigger::Never { - warn!("Consensus key configured but block production is disabled!"); + let mut consensus_signer = SignMode::Unavailable; + + #[cfg(feature = "aws-kms")] + if let Some(key_id) = consensus_aws_kms { + let config = aws_config::load_from_env().await; + let client = aws_sdk_kms::Client::new(&config); + // Ensure that the key is accessible and has the correct type + let key = client + .get_public_key() + .key_id(&key_id) + .send() + .await? + .key_spec; + if key != Some(aws_sdk_kms::types::KeySpec::EccSecgP256K1) { + anyhow::bail!("The key is not of the correct type, got {:?}", key); + } + consensus_signer = SignMode::Kms { key_id, client }; } - // if consensus key is not configured, fallback to dev consensus key - let consensus_key = consensus_key.or_else(|| { - if debug { + if matches!(consensus_signer, SignMode::Unavailable) { + if let Some(consensus_key) = consensus_key { + let key = SecretKey::from_str(&consensus_key) + .context("failed to parse consensus signing key")?; + consensus_signer = SignMode::Key(Secret::new(key.into())); + } else if debug { + // if consensus key is not configured, fallback to dev consensus key let key = default_consensus_dev_key(); warn!( "Fuel Core is using an insecure test key for consensus. Public key: {}", key.public_key() ); - Some(Secret::new(key.into())) - } else { - None + consensus_signer = SignMode::Key(Secret::new(key.into())); } - }); + } + + if consensus_signer.is_available() && trigger == Trigger::Never { + warn!("Consensus key configured but block production is disabled!"); + } let coinbase_recipient = if let Some(coinbase_recipient) = coinbase_recipient { Some(coinbase_recipient) @@ -439,7 +466,7 @@ impl Command { p2p: p2p_cfg, #[cfg(feature = "p2p")] sync: sync_args.into(), - consensus_key, + consensus_signer, name, relayer_consensus_config: verifier, min_connected_reserved_peers, @@ -450,7 +477,7 @@ impl Command { } } -pub fn get_service_with_shutdown_listeners( +pub async fn get_service_with_shutdown_listeners( command: Command, ) -> anyhow::Result<(FuelService, ShutdownListener)> { #[cfg(feature = "rocksdb")] @@ -459,7 +486,7 @@ pub fn get_service_with_shutdown_listeners( } let profiling = command.profiling.clone(); - let config = command.get_config()?; + let config = command.get_config().await?; // start profiling agent if url is configured let _profiling_agent = start_pyroscope_agent(profiling, &config)?; @@ -478,13 +505,14 @@ pub fn get_service_with_shutdown_listeners( )) } -pub fn get_service(command: Command) -> anyhow::Result { - let (service, _) = get_service_with_shutdown_listeners(command)?; +pub async fn get_service(command: Command) -> anyhow::Result { + let (service, _) = get_service_with_shutdown_listeners(command).await?; Ok(service) } pub async fn exec(command: Command) -> anyhow::Result<()> { - let (service, shutdown_listener) = get_service_with_shutdown_listeners(command)?; + let (service, shutdown_listener) = + get_service_with_shutdown_listeners(command).await?; // Genesis could take a long time depending on the snapshot size. Start needs to be // interruptible by the shutdown_signal @@ -510,26 +538,6 @@ pub async fn exec(command: Command) -> anyhow::Result<()> { Ok(()) } -// Attempt to load the consensus key from cli arg first, otherwise check the env. -fn load_consensus_key( - cli_arg: Option, -) -> anyhow::Result>> { - let secret_string = if let Some(cli_arg) = cli_arg { - warn!("Consensus key configured insecurely using cli args. Consider setting the {} env var instead.", CONSENSUS_KEY_ENV); - Some(cli_arg) - } else { - env::var(CONSENSUS_KEY_ENV).ok() - }; - - if let Some(key) = secret_string { - let key = - SecretKey::from_str(&key).context("failed to parse consensus signing key")?; - Ok(Some(Secret::new(key.into()))) - } else { - Ok(None) - } -} - fn start_pyroscope_agent( profiling_args: profiling::ProfilingArgs, config: &Config, diff --git a/ci_checks.sh b/ci_checks.sh index e9c28efa95f..246ef0b3f5c 100755 --- a/ci_checks.sh +++ b/ci_checks.sh @@ -6,7 +6,7 @@ # The script runs almost all CI checks locally. # # Requires installed: -# - Rust `1.75.0` +# - Rust `1.79.0` # - Nightly rust formatter # - `cargo install cargo-sort` # - `cargo install cargo-make` diff --git a/crates/chain-config/src/config/snapshot_metadata.rs b/crates/chain-config/src/config/snapshot_metadata.rs index 55479de99a2..e0f8b01af36 100644 --- a/crates/chain-config/src/config/snapshot_metadata.rs +++ b/crates/chain-config/src/config/snapshot_metadata.rs @@ -19,6 +19,7 @@ pub enum TableEncoding { }, } impl TableEncoding { + #[allow(clippy::assigning_clones)] // TODO: clean up the following code fn strip_prefix(&mut self, dir: &Path) -> anyhow::Result<()> { match self { TableEncoding::Json { filepath } => { @@ -40,6 +41,7 @@ impl TableEncoding { Ok(()) } + #[allow(clippy::needless_borrows_for_generic_args)] // False positive fn prepend_path(&mut self, dir: &Path) { match self { TableEncoding::Json { filepath } => { @@ -80,6 +82,7 @@ impl SnapshotMetadata { Ok(snapshot) } + #[allow(clippy::assigning_clones)] // TODO: clean up the following code fn strip_prefix(&mut self, dir: &Path) -> anyhow::Result<&mut Self> { self.chain_config = self.chain_config.strip_prefix(dir)?.to_owned(); self.table_encoding.strip_prefix(dir)?; diff --git a/crates/fuel-core/src/p2p_test_helpers.rs b/crates/fuel-core/src/p2p_test_helpers.rs index ed45f6c529e..d5bb4af1976 100644 --- a/crates/fuel-core/src/p2p_test_helpers.rs +++ b/crates/fuel-core/src/p2p_test_helpers.rs @@ -25,6 +25,7 @@ use fuel_core_p2p::{ }; use fuel_core_poa::{ ports::BlockImporter, + signer::SignMode, Trigger, }; use fuel_core_storage::{ @@ -322,7 +323,7 @@ pub async fn make_nodes( node_config.utxo_validation = utxo_validation; update_signing_key(&mut node_config, Input::owner(&secret.public_key())); - node_config.consensus_key = Some(Secret::new(secret.into())); + node_config.consensus_signer = SignMode::Key(Secret::new(secret.into())); test_txs = txs; } diff --git a/crates/fuel-core/src/service/adapters.rs b/crates/fuel-core/src/service/adapters.rs index 7001b2dd200..2caa2edfb68 100644 --- a/crates/fuel-core/src/service/adapters.rs +++ b/crates/fuel-core/src/service/adapters.rs @@ -1,18 +1,25 @@ -use std::sync::Arc; - use fuel_core_consensus_module::{ block_verifier::Verifier, RelayerConsensusConfig, }; use fuel_core_importer::ImporterResult; +use fuel_core_poa::{ + ports::BlockSigner, + signer::SignMode, +}; use fuel_core_services::stream::BoxStream; #[cfg(feature = "p2p")] use fuel_core_types::services::p2p::peer_reputation::AppScore; use fuel_core_types::{ + blockchain::{ + block::Block, + consensus::Consensus, + }, fuel_types::BlockHeight, services::block_importer::SharedImportResult, }; use fuel_core_upgradable_executor::executor::Executor; +use std::sync::Arc; use crate::{ database::{ @@ -180,6 +187,26 @@ impl BlockImporterAdapter { } } +pub struct FuelBlockSigner { + mode: SignMode, +} +impl FuelBlockSigner { + pub fn new(mode: SignMode) -> Self { + Self { mode } + } +} + +#[async_trait::async_trait] +impl BlockSigner for FuelBlockSigner { + async fn seal_block(&self, block: &Block) -> anyhow::Result { + self.mode.seal_block(block).await + } + + fn is_available(&self) -> bool { + self.mode.is_available() + } +} + #[cfg(feature = "p2p")] #[derive(Clone)] pub struct P2PAdapter { diff --git a/crates/fuel-core/src/service/config.rs b/crates/fuel-core/src/service/config.rs index de0fa611a2b..f9514b73cbe 100644 --- a/crates/fuel-core/src/service/config.rs +++ b/crates/fuel-core/src/service/config.rs @@ -4,6 +4,7 @@ use std::{ }; use clap::ValueEnum; +use fuel_core_poa::signer::SignMode; use strum_macros::{ Display, EnumString, @@ -26,13 +27,7 @@ use fuel_core_p2p::config::{ pub use fuel_core_poa::Trigger; #[cfg(feature = "relayer")] use fuel_core_relayer::Config as RelayerConfig; -use fuel_core_types::{ - blockchain::{ - header::StateTransitionBytecodeVersion, - primitives::SecretKeyWrapper, - }, - secrecy::Secret, -}; +use fuel_core_types::blockchain::header::StateTransitionBytecodeVersion; use crate::{ combined_database::CombinedDatabaseConfig, @@ -69,7 +64,7 @@ pub struct Config { pub p2p: Option>, #[cfg(feature = "p2p")] pub sync: fuel_core_sync::Config, - pub consensus_key: Option>, + pub consensus_signer: SignMode, pub name: String, pub relayer_consensus_config: fuel_core_consensus_module::RelayerConsensusConfig, /// The number of reserved peers to connect to before starting to sync. @@ -172,7 +167,7 @@ impl Config { p2p: Some(P2PConfig::::default("test_network")), #[cfg(feature = "p2p")] sync: fuel_core_sync::Config::default(), - consensus_key: Some(Secret::new( + consensus_signer: SignMode::Key(fuel_core_types::secrecy::Secret::new( fuel_core_chain_config::default_consensus_dev_key().into(), )), name: String::default(), @@ -205,7 +200,7 @@ impl From<&Config> for fuel_core_poa::Config { fn from(config: &Config) -> Self { fuel_core_poa::Config { trigger: config.block_production, - signing_key: config.consensus_key.clone(), + signer: config.consensus_signer.clone(), metrics: false, min_connected_reserved_peers: config.min_connected_reserved_peers, time_until_synced: config.time_until_synced, diff --git a/crates/fuel-core/src/service/sub_services.rs b/crates/fuel-core/src/service/sub_services.rs index d7106e9cf3b..bf57148791e 100644 --- a/crates/fuel-core/src/service/sub_services.rs +++ b/crates/fuel-core/src/service/sub_services.rs @@ -1,7 +1,10 @@ #![allow(clippy::let_unit_value)] use super::{ - adapters::P2PAdapter, + adapters::{ + FuelBlockSigner, + P2PAdapter, + }, genesis::create_genesis_block, }; #[cfg(feature = "relayer")] @@ -43,7 +46,10 @@ use fuel_core_gas_price_service::fuel_gas_price_updater::{ UpdaterMetadata, V0Metadata, }; -use fuel_core_poa::Trigger; +use fuel_core_poa::{ + signer::SignMode, + Trigger, +}; use fuel_core_services::{ RunnableService, ServiceRunner, @@ -63,6 +69,7 @@ pub type PoAService = fuel_core_poa::Service< TxPoolAdapter, BlockProducerAdapter, BlockImporterAdapter, + SignMode, InDirectoryPredefinedBlocks, >; #[cfg(feature = "p2p")] @@ -250,6 +257,7 @@ pub fn init_sub_services( producer_adapter.clone(), importer_adapter.clone(), p2p_adapter.clone(), + FuelBlockSigner::new(config.consensus_signer.clone()), predefined_blocks, ) }); diff --git a/crates/fuel-core/src/state/rocks_db.rs b/crates/fuel-core/src/state/rocks_db.rs index 26f9e4a5937..00d9fc92a75 100644 --- a/crates/fuel-core/src/state/rocks_db.rs +++ b/crates/fuel-core/src/state/rocks_db.rs @@ -412,6 +412,8 @@ where // because we are not going to use it after the RocksDb is dropped. // We control the lifetime of the `Self` - RocksDb, so we can guarantee that // the snapshot will be dropped before the RocksDb. + #[allow(clippy::missing_transmute_annotations)] + // Remove this and see for yourself let snapshot = unsafe { let snapshot = db.snapshot(); core::mem::transmute(snapshot) diff --git a/crates/services/consensus_module/poa/Cargo.toml b/crates/services/consensus_module/poa/Cargo.toml index 3ae865c7de2..03d80d521ae 100644 --- a/crates/services/consensus_module/poa/Cargo.toml +++ b/crates/services/consensus_module/poa/Cargo.toml @@ -12,10 +12,12 @@ version = { workspace = true } [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } +aws-sdk-kms = { version = "1.37.0", optional = true } fuel-core-chain-config = { workspace = true } fuel-core-services = { workspace = true } fuel-core-storage = { workspace = true } fuel-core-types = { workspace = true } +k256 = { version = "0.13.3", features = ["ecdsa-core"], optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["full"] } @@ -33,6 +35,7 @@ test-case = { workspace = true } tokio = { workspace = true, features = ["full", "test-util"] } [features] +aws-kms = ["dep:aws-sdk-kms", "dep:k256"] test-helpers = [ "fuel-core-storage/test-helpers", "fuel-core-types/test-helpers", diff --git a/crates/services/consensus_module/poa/src/config.rs b/crates/services/consensus_module/poa/src/config.rs index 7da6cad5b80..c2a11ff1fca 100644 --- a/crates/services/consensus_module/poa/src/config.rs +++ b/crates/services/consensus_module/poa/src/config.rs @@ -1,14 +1,12 @@ -use fuel_core_types::{ - blockchain::primitives::SecretKeyWrapper, - fuel_types::ChainId, - secrecy::Secret, -}; +use fuel_core_types::fuel_types::ChainId; use tokio::time::Duration; +use crate::signer::SignMode; + #[derive(Debug, Clone)] pub struct Config { pub trigger: Trigger, - pub signing_key: Option>, + pub signer: SignMode, pub metrics: bool, pub min_connected_reserved_peers: usize, pub time_until_synced: Duration, @@ -20,7 +18,7 @@ impl Default for Config { fn default() -> Self { Config { trigger: Trigger::default(), - signing_key: None, + signer: SignMode::Unavailable, metrics: false, min_connected_reserved_peers: 0, time_until_synced: Duration::ZERO, diff --git a/crates/services/consensus_module/poa/src/lib.rs b/crates/services/consensus_module/poa/src/lib.rs index 1be9c9ad009..477a342c01f 100644 --- a/crates/services/consensus_module/poa/src/lib.rs +++ b/crates/services/consensus_module/poa/src/lib.rs @@ -3,6 +3,7 @@ #![deny(unused_crate_dependencies)] #![deny(unused_must_use)] #![deny(warnings)] +#![allow(clippy::blocks_in_conditions)] // False positives with tracing macros mod deadline_clock; mod sync; @@ -13,6 +14,7 @@ mod service_test; pub mod config; pub mod ports; pub mod service; +pub mod signer; pub mod verifier; pub use config::{ diff --git a/crates/services/consensus_module/poa/src/ports.rs b/crates/services/consensus_module/poa/src/ports.rs index d34d35c4fc9..0bc76d5947f 100644 --- a/crates/services/consensus_module/poa/src/ports.rs +++ b/crates/services/consensus_module/poa/src/ports.rs @@ -6,6 +6,7 @@ use fuel_core_storage::{ use fuel_core_types::{ blockchain::{ block::Block, + consensus::Consensus, header::BlockHeader, primitives::DaBlockHeight, }, @@ -79,6 +80,12 @@ pub trait BlockImporter: Send + Sync { fn block_stream(&self) -> BoxStream; } +#[async_trait::async_trait] +pub trait BlockSigner: Send + Sync { + async fn seal_block(&self, block: &Block) -> anyhow::Result; + fn is_available(&self) -> bool; +} + #[cfg_attr(test, mockall::automock)] /// The port for the database. pub trait Database { diff --git a/crates/services/consensus_module/poa/src/service.rs b/crates/services/consensus_module/poa/src/service.rs index d8bc937dae3..1ccac8e9ca7 100644 --- a/crates/services/consensus_module/poa/src/service.rs +++ b/crates/services/consensus_module/poa/src/service.rs @@ -3,7 +3,6 @@ use anyhow::{ Context, }; use std::{ - ops::Deref, sync::Arc, time::Duration, }; @@ -24,6 +23,7 @@ use crate::{ ports::{ BlockImporter, BlockProducer, + BlockSigner, P2pPort, PredefinedBlocks, TransactionPool, @@ -40,7 +40,7 @@ use fuel_core_services::{ stream::BoxStream, RunnableService, RunnableTask, - Service as _, + Service as OtherService, ServiceRunner, StateWatcher, }; @@ -48,24 +48,14 @@ use fuel_core_storage::transactional::Changes; use fuel_core_types::{ blockchain::{ block::Block, - consensus::{ - poa::PoAConsensus, - Consensus, - }, header::BlockHeader, - primitives::SecretKeyWrapper, SealedBlock, }, - fuel_crypto::Signature, fuel_tx::{ Transaction, TxId, }, fuel_types::BlockHeight, - secrecy::{ - ExposeSecret, - Secret, - }, services::{ block_importer::ImportResult, executor::{ @@ -79,7 +69,8 @@ use fuel_core_types::{ }; use serde::Serialize; -pub type Service = ServiceRunner>; +pub type Service = ServiceRunner>; + #[derive(Clone)] pub struct SharedState { request_sender: mpsc::Sender, @@ -133,8 +124,8 @@ pub(crate) enum RequestType { Trigger, } -pub struct MainTask { - signing_key: Option>, +pub struct MainTask { + signer: S, block_producer: B, block_importer: I, txpool: T, @@ -151,12 +142,13 @@ pub struct MainTask { sync_task_handle: ServiceRunner, } -impl MainTask +impl MainTask where T: TransactionPool, I: BlockImporter, PB: PredefinedBlocks, { + #[allow(clippy::too_many_arguments)] pub fn new( last_block: &BlockHeader, config: Config, @@ -164,6 +156,7 @@ where block_producer: B, block_importer: I, p2p_port: P, + signer: S, predefined_blocks: PB, ) -> Self { let tx_status_update_stream = txpool.transaction_status_events(); @@ -175,7 +168,6 @@ where let peer_connections_stream = p2p_port.reserved_peers_count(); let Config { - signing_key, min_connected_reserved_peers, time_until_synced, trigger, @@ -193,7 +185,7 @@ where let sync_task_handle = ServiceRunner::new(sync_task); Self { - signing_key, + signer, txpool, block_producer, block_importer, @@ -250,11 +242,12 @@ where } } -impl MainTask +impl MainTask where T: TransactionPool, B: BlockProducer, I: BlockImporter, + S: BlockSigner, PB: PredefinedBlocks, { // Request the block producer to make a new block, and return it when ready @@ -323,7 +316,7 @@ where ) -> anyhow::Result<()> { let last_block_created = Instant::now(); // verify signing key is set - if self.signing_key.is_none() { + if !self.signer.is_available() { return Err(anyhow!("unable to produce blocks without a consensus key")) } @@ -357,7 +350,8 @@ where self.txpool.remove_txs(tx_ids_to_remove); // Sign the block and seal it - let seal = seal_block(&self.signing_key, &block)?; + let seal = self.signer.seal_block(&block).await + .expect("Failed to seal block. Panicking for now, TODO: https://github.com/FuelLabs/fuel-core/issues/1917"); let block = SealedBlock { entity: block, consensus: seal, @@ -403,9 +397,8 @@ where ) -> anyhow::Result<()> { tracing::info!("Producing predefined block"); let last_block_created = Instant::now(); - // verify signing key is set - if self.signing_key.is_none() { - return Err(anyhow!("unable to produce blocks without a consensus key")) + if !self.signer.is_available() { + return Err(anyhow!("unable to produce blocks without a signer")) } // Ask the block producer to create the block @@ -436,7 +429,7 @@ where } // Sign the block and seal it - let seal = seal_block(&self.signing_key, &block)?; + let seal = self.signer.seal_block(&block).await?; let sealed_block = SealedBlock { entity: block, consensus: seal, @@ -501,14 +494,14 @@ struct PredefinedBlockWithSkippedTransactions { } #[async_trait::async_trait] -impl RunnableService for MainTask +impl RunnableService for MainTask where Self: RunnableTask, { const NAME: &'static str = "PoA"; type SharedData = SharedState; - type Task = MainTask; + type Task = MainTask; type TaskParams = (); fn shared_data(&self) -> Self::SharedData { @@ -536,11 +529,12 @@ where } #[async_trait::async_trait] -impl RunnableTask for MainTask +impl RunnableTask for MainTask where T: TransactionPool, B: BlockProducer, I: BlockImporter, + S: BlockSigner, PB: PredefinedBlocks, { async fn run(&mut self, watcher: &mut StateWatcher) -> anyhow::Result { @@ -626,19 +620,22 @@ where } } -pub fn new_service( +#[allow(clippy::too_many_arguments)] +pub fn new_service( last_block: &BlockHeader, config: Config, txpool: T, block_producer: B, block_importer: I, p2p_port: P, + block_signer: S, predefined_blocks: PB, -) -> Service +) -> Service where T: TransactionPool + 'static, B: BlockProducer + 'static, I: BlockImporter + 'static, + S: BlockSigner + 'static, PB: PredefinedBlocks + 'static, P: P2pPort, { @@ -649,29 +646,11 @@ where block_producer, block_importer, p2p_port, + block_signer, predefined_blocks, )) } -fn seal_block( - signing_key: &Option>, - block: &Block, -) -> anyhow::Result { - if let Some(key) = signing_key { - let block_hash = block.id(); - let message = block_hash.into_message(); - - // The length of the secret is checked - let signing_key = key.expose_secret().deref(); - - let poa_signature = Signature::sign(signing_key, &message); - let seal = Consensus::PoA(PoAConsensus::new(poa_signature)); - Ok(seal) - } else { - Err(anyhow!("no PoA signing key configured")) - } -} - fn increase_time(time: Tai64, duration: Duration) -> anyhow::Result { let timestamp = time.0; let timestamp = timestamp diff --git a/crates/services/consensus_module/poa/src/service_test.rs b/crates/services/consensus_module/poa/src/service_test.rs index 4c0855d6a4e..afe7eddc2d9 100644 --- a/crates/services/consensus_module/poa/src/service_test.rs +++ b/crates/services/consensus_module/poa/src/service_test.rs @@ -5,6 +5,7 @@ use crate::{ new_service, ports::{ BlockProducer, + BlockSigner, InMemoryPredefinedBlocks, MockBlockImporter, MockBlockProducer, @@ -13,11 +14,13 @@ use crate::{ TransactionsSource, }, service::MainTask, + signer::SignMode, Config, Service, Trigger, }; use async_trait::async_trait; +use fuel_core_chain_config::default_consensus_dev_key; use fuel_core_services::{ stream::pending, Service as StorageTrait, @@ -28,6 +31,7 @@ use fuel_core_storage::transactional::Changes; use fuel_core_types::{ blockchain::{ block::Block, + consensus::Consensus, header::{ BlockHeader, PartialBlockHeader, @@ -166,11 +170,12 @@ impl TestContextBuilder { let service = new_service( &BlockHeader::new_block(BlockHeight::from(1u32), Tai64::now()), - config, + config.clone(), txpool, producer, importer, p2p_port, + FakeBlockSigner { succeeds: true }, predefined_blocks, ); service.start().unwrap(); @@ -178,11 +183,33 @@ impl TestContextBuilder { } } +struct FakeBlockSigner { + succeeds: bool, +} + +#[async_trait::async_trait] +impl BlockSigner for FakeBlockSigner { + async fn seal_block(&self, block: &Block) -> anyhow::Result { + if self.succeeds { + SignMode::Key(Secret::new(default_consensus_dev_key().into())) + .seal_block(block) + .await + } else { + Err(anyhow::anyhow!("failed to sign block")) + } + } + + fn is_available(&self) -> bool { + true + } +} + struct TestContext { service: Service< MockTransactionPool, MockBlockProducer, MockBlockImporter, + FakeBlockSigner, InMemoryPredefinedBlocks, >, } @@ -345,9 +372,11 @@ async fn remove_skipped_transactions() { vec![] }); + let signer = SignMode::Key(Secret::new(secret_key.into())); + let config = Config { trigger: Trigger::Instant, - signing_key: Some(Secret::new(secret_key.into())), + signer: signer.clone(), metrics: false, ..Default::default() }; @@ -363,12 +392,79 @@ async fn remove_skipped_transactions() { block_producer, block_importer, p2p_port, + FakeBlockSigner { succeeds: true }, predefined_blocks, ); assert!(task.produce_next_block().await.is_ok()); } +#[tokio::test] +#[should_panic = "failed to sign block"] +async fn panics_if_signing_fails() { + // The test verifies that if `BlockProducer` returns skipped transactions, they would + // be propagated to `TxPool` for removal. + let mut rng = StdRng::seed_from_u64(2322); + let secret_key = SecretKey::random(&mut rng); + + let mut block_producer = MockBlockProducer::default(); + block_producer + .expect_produce_and_execute_block() + .times(1) + .returning(move |_, _, _| { + Ok(UncommittedResult::new( + ExecutionResult { + block: Default::default(), + skipped_transactions: Default::default(), + tx_status: Default::default(), + events: Default::default(), + }, + Default::default(), + )) + }); + + let mut block_importer = MockBlockImporter::default(); + + block_importer + .expect_commit_result() + .times(1) + .returning(|_| Ok(())); + + block_importer + .expect_block_stream() + .returning(|| Box::pin(tokio_stream::pending())); + + let TxPoolContext { + txpool, + status_sender: _, + txs: _, + } = MockTransactionPool::new_with_txs(vec![make_tx(&mut rng)]); + + let signer = SignMode::Key(Secret::new(secret_key.into())); + + let config = Config { + trigger: Trigger::Instant, + signer: signer.clone(), + metrics: false, + ..Default::default() + }; + + let p2p_port = generate_p2p_port(); + + let mut task = MainTask::new( + &BlockHeader::new_block(BlockHeight::from(1u32), Tai64::now()), + config, + txpool, + block_producer, + block_importer, + p2p_port, + FakeBlockSigner { succeeds: false }, + InMemoryPredefinedBlocks::new(HashMap::new()), + ); + + let _ = task.produce_next_block().await; // This panics for now +} + #[tokio::test] async fn does_not_produce_when_txpool_empty_in_instant_mode() { // verify the PoA service doesn't trigger empty blocks to be produced when there are @@ -395,9 +491,11 @@ async fn does_not_produce_when_txpool_empty_in_instant_mode() { txpool.expect_total_consumable_gas().returning(|| 0); txpool.expect_pending_number().returning(|| 0); + let signer = SignMode::Key(Secret::new(secret_key.into())); + let config = Config { trigger: Trigger::Instant, - signing_key: Some(Secret::new(secret_key.into())), + signer: signer.clone(), metrics: false, ..Default::default() }; @@ -413,6 +511,7 @@ async fn does_not_produce_when_txpool_empty_in_instant_mode() { block_producer, block_importer, p2p_port, + FakeBlockSigner { succeeds: true }, predefined_blocks, ); @@ -502,6 +601,7 @@ async fn consensus_service__run__will_include_sequential_predefined_blocks_befor (3u32.into(), block_for_height(3)), (4u32.into(), block_for_height(4)), ]; + let signer = SignMode::Key(test_signing_key()); let blocks_map: HashMap<_, _> = blocks.clone().into_iter().collect(); let (block_producer, mut block_receiver) = FakeBlockProducer::new(); let last_block = BlockHeader::new_block(BlockHeight::from(1u32), Tai64::now()); @@ -509,7 +609,7 @@ async fn consensus_service__run__will_include_sequential_predefined_blocks_befor trigger: Trigger::Interval { block_time: Duration::from_millis(100), }, - signing_key: Some(test_signing_key()), + signer, metrics: false, ..Default::default() }; @@ -528,6 +628,7 @@ async fn consensus_service__run__will_include_sequential_predefined_blocks_befor block_producer, block_importer, generate_p2p_port(), + FakeBlockSigner { succeeds: true }, InMemoryPredefinedBlocks::new(blocks_map), ); @@ -570,7 +671,7 @@ async fn consensus_service__run__will_insert_predefined_blocks_in_correct_order( trigger: Trigger::Interval { block_time: Duration::from_millis(100), }, - signing_key: Some(test_signing_key()), + signer: SignMode::Key(test_signing_key()), metrics: false, ..Default::default() }; @@ -589,6 +690,7 @@ async fn consensus_service__run__will_insert_predefined_blocks_in_correct_order( block_producer, block_importer, generate_p2p_port(), + FakeBlockSigner { succeeds: true }, InMemoryPredefinedBlocks::new(predefined_blocks_map), ); diff --git a/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs b/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs index 836324272d5..1f6a59f1052 100644 --- a/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs +++ b/crates/services/consensus_module/poa/src/service_test/manually_produce_tests.rs @@ -52,7 +52,7 @@ async fn can_manually_produce_block( let mut ctx_builder = TestContextBuilder::new(); ctx_builder.with_config(Config { trigger, - signing_key: Some(test_signing_key()), + signer: SignMode::Key(test_signing_key()), metrics: false, ..Default::default() }); diff --git a/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs b/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs index f6460b4fd9a..012b8848ec3 100644 --- a/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs +++ b/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs @@ -12,7 +12,7 @@ async fn clean_startup_shutdown_each_trigger() -> anyhow::Result<()> { let mut ctx_builder = TestContextBuilder::new(); ctx_builder.with_config(Config { trigger, - signing_key: Some(test_signing_key()), + signer: SignMode::Key(test_signing_key()), metrics: false, ..Default::default() }); @@ -31,7 +31,7 @@ async fn never_trigger_never_produces_blocks() { let mut ctx_builder = TestContextBuilder::new(); ctx_builder.with_config(Config { trigger: Trigger::Never, - signing_key: Some(test_signing_key()), + signer: SignMode::Key(test_signing_key()), metrics: false, ..Default::default() }); @@ -116,7 +116,7 @@ impl DefaultContext { async fn instant_trigger_produces_block_instantly() { let mut ctx = DefaultContext::new(Config { trigger: Trigger::Instant, - signing_key: Some(test_signing_key()), + signer: SignMode::Key(test_signing_key()), metrics: false, ..Default::default() }); @@ -135,7 +135,7 @@ async fn interval_trigger_produces_blocks_periodically() -> anyhow::Result<()> { trigger: Trigger::Interval { block_time: Duration::new(2, 0), }, - signing_key: Some(test_signing_key()), + signer: SignMode::Key(test_signing_key()), metrics: false, ..Default::default() }); @@ -200,7 +200,7 @@ async fn interval_trigger_doesnt_react_to_full_txpool() -> anyhow::Result<()> { trigger: Trigger::Interval { block_time: Duration::new(2, 0), }, - signing_key: Some(test_signing_key()), + signer: SignMode::Key(test_signing_key()), metrics: false, ..Default::default() }); diff --git a/crates/services/consensus_module/poa/src/signer.rs b/crates/services/consensus_module/poa/src/signer.rs new file mode 100644 index 00000000000..e7fa62da4e2 --- /dev/null +++ b/crates/services/consensus_module/poa/src/signer.rs @@ -0,0 +1,95 @@ +use anyhow::anyhow; +#[cfg(feature = "aws-kms")] +use aws_sdk_kms::{ + primitives::Blob, + types::{ + MessageType, + SigningAlgorithmSpec, + }, +}; +#[cfg(feature = "aws-kms")] +use fuel_core_types::fuel_crypto::Message; +use fuel_core_types::{ + blockchain::{ + block::Block, + consensus::{ + poa::PoAConsensus, + Consensus, + }, + primitives::SecretKeyWrapper, + }, + fuel_vm::Signature, + secrecy::{ + ExposeSecret, + Secret, + }, +}; +use std::ops::Deref; + +/// How the block is signed +#[derive(Clone, Debug)] +pub enum SignMode { + /// Blocks cannot be produced + Unavailable, + /// Sign using a secret key + Key(Secret), + /// Sign using AWS KMS + #[cfg(feature = "aws-kms")] + Kms { + key_id: String, + client: aws_sdk_kms::Client, + }, +} + +impl SignMode { + /// Is block sigining (production) available + pub fn is_available(&self) -> bool { + !matches!(self, SignMode::Unavailable) + } + + /// Sign a block + pub async fn seal_block(&self, block: &Block) -> anyhow::Result { + let block_hash = block.id(); + let message = block_hash.into_message(); + + let poa_signature = match self { + SignMode::Unavailable => return Err(anyhow!("no PoA signing key configured")), + SignMode::Key(key) => { + let signing_key = key.expose_secret().deref(); + Signature::sign(signing_key, &message) + } + #[cfg(feature = "aws-kms")] + SignMode::Kms { key_id, client } => { + sign_with_kms(client, key_id, message).await? + } + }; + Ok(Consensus::PoA(PoAConsensus::new(poa_signature))) + } +} + +#[cfg(feature = "aws-kms")] +async fn sign_with_kms( + client: &aws_sdk_kms::Client, + key_id: &str, + message: Message, +) -> anyhow::Result { + let reply = client + .sign() + .key_id(key_id) + .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) + .message_type(MessageType::Digest) + .message(Blob::new(*message)) + .send() + .await + .inspect_err(|err| tracing::error!("Failed to sign with AWS KMS: {err:?}"))?; + let signature_der = reply + .signature + .ok_or_else(|| anyhow!("no signature returned from AWS KMS"))? + .into_inner(); + // https://stackoverflow.com/a/71475108 + let sig = k256::ecdsa::Signature::from_der(&signature_der) + .map_err(|_| anyhow!("invalid DER signature from AWS KMS"))?; + let sig = sig.normalize_s().unwrap_or(sig); + let sig_bytes = <[u8; 64]>::from(sig.to_bytes()); + Ok(Signature::from_bytes(sig_bytes)) +} diff --git a/crates/services/p2p/src/discovery/discovery_config.rs b/crates/services/p2p/src/discovery/discovery_config.rs index 34eca530bc2..9800ca7e8ea 100644 --- a/crates/services/p2p/src/discovery/discovery_config.rs +++ b/crates/services/p2p/src/discovery/discovery_config.rs @@ -42,7 +42,7 @@ impl Config { reserved_nodes: vec![], reserved_nodes_only_mode: false, random_walk: None, - max_peers_connected: std::usize::MAX, + max_peers_connected: usize::MAX, with_mdns: false, network_name, connection_idle_timeout: Duration::from_secs(10), diff --git a/crates/services/sync/src/lib.rs b/crates/services/sync/src/lib.rs index 8022d33e38f..2516c3e8fad 100644 --- a/crates/services/sync/src/lib.rs +++ b/crates/services/sync/src/lib.rs @@ -3,6 +3,8 @@ #![deny(unused_crate_dependencies)] #![deny(missing_docs)] #![deny(warnings)] +#![allow(clippy::blocks_in_conditions)] // Triggered by tracing macros + //! # Sync Service //! Responsible for syncing the blockchain from the network. diff --git a/crates/services/txpool/src/service/update_sender/tests/test_e2e.rs b/crates/services/txpool/src/service/update_sender/tests/test_e2e.rs index 49ec836fa6e..8b07ec3fa74 100644 --- a/crates/services/txpool/src/service/update_sender/tests/test_e2e.rs +++ b/crates/services/txpool/src/service/update_sender/tests/test_e2e.rs @@ -66,6 +66,7 @@ fn test_update_sender_reg() { /// # Arguments /// /// * `ops` - A vector of `Op` enums that determine the operations to be performed on the UpdateSender. +#[allow(clippy::assigning_clones)] // Test code, we don't care about performance. fn test_update_sender_inner(ops: Vec) { // Setup runtime thread_local! { diff --git a/deployment/Dockerfile b/deployment/Dockerfile index ae5d3b03695..262c95b4b66 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -1,6 +1,6 @@ # Stage 1: Build FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx -FROM --platform=$BUILDPLATFORM rust:1.75.0 AS chef +FROM --platform=$BUILDPLATFORM rust:1.79.0 AS chef ARG TARGETPLATFORM RUN cargo install cargo-chef && rustup target add wasm32-unknown-unknown diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bdbeb14b060..2e3facb684d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.75.0" +channel = "1.79.0" targets = ["wasm32-unknown-unknown"] diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 019b96e394d..1f63bc64d75 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -19,6 +19,10 @@ path = "tests/lib.rs" [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } +aws-config = { version = "1.1.7", features = [ + "behavior-version-latest", +], optional = true } +aws-sdk-kms = { version = "1.37.0", optional = true } clap = { workspace = true } cynic = { workspace = true } ethers = "2" @@ -50,11 +54,13 @@ futures = { workspace = true } hyper = { workspace = true, features = ["server"] } insta = { workspace = true } itertools = { workspace = true } +k256 = { version = "0.13.3", features = ["ecdsa-core"] } primitive-types = { workspace = true, default-features = false } rand = { workspace = true } reqwest = { workspace = true } rstest = "0.15" serde_json = { workspace = true } +spki = "0.7.3" tempfile = { workspace = true } test-case = { workspace = true } test-helpers = { path = "./test-helpers" } @@ -74,3 +80,4 @@ default = ["fuel-core/default", "relayer"] p2p = ["fuel-core/p2p", "fuel-core-p2p"] relayer = ["fuel-core/relayer", "fuel-core-relayer"] wasm-executor = ["fuel-core/wasm-executor"] +aws-kms = ["dep:aws-config", "dep:aws-sdk-kms", "fuel-core-bin/aws-kms"] diff --git a/tests/test-helpers/src/fuel_core_driver.rs b/tests/test-helpers/src/fuel_core_driver.rs index a4cd6efd028..a290712e5d2 100644 --- a/tests/test-helpers/src/fuel_core_driver.rs +++ b/tests/test-helpers/src/fuel_core_driver.rs @@ -62,7 +62,8 @@ impl FuelCoreDriver { let node = fuel_core_bin::cli::run::get_service( fuel_core_bin::cli::run::Command::parse_from(args), - )?; + ) + .await?; node.start_and_await().await?; diff --git a/tests/tests/blocks.rs b/tests/tests/blocks.rs index 479507c56df..53a81f5b039 100644 --- a/tests/tests/blocks.rs +++ b/tests/tests/blocks.rs @@ -17,7 +17,10 @@ use fuel_core_client::client::{ types::TransactionStatus, FuelClient, }; -use fuel_core_poa::Trigger; +use fuel_core_poa::{ + signer::SignMode, + Trigger, +}; use fuel_core_storage::{ tables::{ FuelBlocks, @@ -127,15 +130,15 @@ async fn produce_block() { let block = client.block_by_height(block_height).await.unwrap().unwrap(); let actual_pub_key = block.block_producer().unwrap(); let block_height: u32 = block.header.height; - let expected_pub_key = config - .consensus_key - .unwrap() - .expose_secret() - .deref() - .public_key(); - - assert!(1 == block_height); - assert_eq!(*actual_pub_key, expected_pub_key); + assert_eq!(block_height, 1); + + match config.consensus_signer { + SignMode::Key(key) => { + let expected_pub_key = key.expose_secret().deref().public_key(); + assert_eq!(*actual_pub_key, expected_pub_key); + } + _ => panic!("config must have a consensus signing key"), + } } else { panic!("Wrong tx status"); }; @@ -159,13 +162,13 @@ async fn produce_block_manually() { let block = client.block_by_height(1.into()).await.unwrap().unwrap(); assert_eq!(block.header.height, 1); let actual_pub_key = block.block_producer().unwrap(); - let expected_pub_key = config - .consensus_key - .unwrap() - .expose_secret() - .deref() - .public_key(); - assert_eq!(*actual_pub_key, expected_pub_key); + match config.consensus_signer { + SignMode::Key(key) => { + let expected_pub_key = key.expose_secret().deref().public_key(); + assert_eq!(*actual_pub_key, expected_pub_key); + } + _ => panic!("config must have a consensus signing key"), + } } #[tokio::test] diff --git a/tests/tests/kms.rs b/tests/tests/kms.rs new file mode 100644 index 00000000000..b870522446c --- /dev/null +++ b/tests/tests/kms.rs @@ -0,0 +1,19 @@ +//! E2E tests for AWS KMS + +use test_helpers::fuel_core_driver::FuelCoreDriver; + +#[tokio::test] +async fn can_produce_blocks_with_kms() { + + let args = vec![ + "--debug", + "--poa-instant", + "true", + "--consensus-aws-kms", + kms_arn, + ]; + let driver = FuelCoreDriver::spawn(&args).await.unwrap(); + driver.client.produce_blocks(1, None).await.unwrap(); + let block = driver.client.block_by_height(1u32.into()).await.unwrap(); + let _ = driver.kill().await; +} diff --git a/tests/tests/poa.rs b/tests/tests/poa.rs index a04bc17aa3a..b8f5554749f 100644 --- a/tests/tests/poa.rs +++ b/tests/tests/poa.rs @@ -12,6 +12,7 @@ use fuel_core_client::client::{ types::TransactionStatus, FuelClient, }; +use fuel_core_poa::signer::SignMode; use fuel_core_storage::transactional::AtomicView; use fuel_core_types::{ blockchain::consensus::Consensus, @@ -37,7 +38,7 @@ async fn can_get_sealed_block_from_poa_produced_block() { let db = CombinedDatabase::default(); let mut config = Config::local_node(); - config.consensus_key = Some(Secret::new(poa_secret.into())); + config.consensus_signer = SignMode::Key(Secret::new(poa_secret.into())); let srv = FuelService::from_combined_database(db.clone(), config) .await .unwrap(); @@ -91,6 +92,76 @@ async fn can_get_sealed_block_from_poa_produced_block() { .expect("failed to verify signature"); } +#[tokio::test] +#[cfg(feature = "aws-kms")] +async fn can_get_sealed_block_from_poa_produced_block_when_signing_with_kms() { + use fuel_core_types::fuel_crypto::PublicKey; + + // This test is only enabled if the environment variable is set + let Some(kms_arn) = option_env!("FUEL_CORE_TEST_AWS_KMS_ARN") else { + return; + }; + + // Get the public key for the KMS key + let config = aws_config::load_from_env().await; + let kms_client = aws_sdk_kms::Client::new(&config); + let poa_public_der = kms_client + .get_public_key() + .key_id(kms_arn) + .send() + .await + .expect("Unable to fetch public key from KMS") + .public_key + .unwrap() + .into_inner(); + let poa_public_bytes = spki::SubjectPublicKeyInfoRef::try_from(&*poa_public_der) + .expect("invalid DER signature from AWS KMS") + .subject_public_key + .raw_bytes(); + let poa_public = k256::ecdsa::VerifyingKey::from_sec1_bytes(poa_public_bytes) + .expect("invalid public key"); + let poa_public = PublicKey::from(&poa_public); + + // start node with the kms enabled and produce some blocks + let num_blocks = 100; + let args = vec![ + "--debug", + "--poa-instant", + "true", + "--consensus-aws-kms", + kms_arn, + ]; + let driver = FuelCoreDriver::spawn(&args).await.unwrap(); + let _ = driver + .client + .produce_blocks(num_blocks, None) + .await + .unwrap(); + + // stop the node and just grab the database + let db_path = driver.kill().await; + let db = + CombinedDatabase::open(db_path.path(), 1024 * 1024, Default::default()).unwrap(); + + let view = db.on_chain().latest_view().unwrap(); + + // verify signatures + for height in 1..=num_blocks { + let sealed_block = view + .get_sealed_block_by_height(&height.into()) + .unwrap() + .expect("expected sealed block to be available"); + let block_id = sealed_block.entity.id(); + let signature = match sealed_block.consensus { + Consensus::PoA(poa) => poa.signature, + _ => panic!("Not expected consensus"), + }; + signature + .verify(&poa_public, &block_id.into_message()) + .expect("failed to verify signature"); + } +} + #[tokio::test(flavor = "multi_thread")] async fn starting_node_with_predefined_nodes_produces_these_predefined_blocks( ) -> anyhow::Result<()> { @@ -204,7 +275,7 @@ mod p2p { let mut config = make_config(name.to_string(), config.clone()); config.debug = true; config.block_production = Trigger::Never; - config.consensus_key = Some(Secret::new(secret.into())); + config.consensus_signer = SignMode::Key(Secret::new(secret.into())); config.p2p.as_mut().unwrap().bootstrap_nodes = bootstrap.listeners(); config.p2p.as_mut().unwrap().reserved_nodes = bootstrap.listeners(); config.p2p.as_mut().unwrap().info_interval = Some(Duration::from_millis(100)); diff --git a/tests/tests/trigger_integration/instant.rs b/tests/tests/trigger_integration/instant.rs index b56cc6e8d68..3e9cc773e4d 100644 --- a/tests/tests/trigger_integration/instant.rs +++ b/tests/tests/trigger_integration/instant.rs @@ -12,7 +12,10 @@ use fuel_core_client::client::{ }, FuelClient, }; -use fuel_core_poa::Trigger; +use fuel_core_poa::{ + signer::SignMode, + Trigger, +}; use fuel_core_types::{ fuel_asm::*, fuel_crypto::SecretKey, @@ -30,7 +33,8 @@ async fn poa_instant_trigger_is_produces_instantly() { let db = Database::default(); let mut config = Config::local_node(); - config.consensus_key = Some(Secret::new(SecretKey::random(&mut rng).into())); + config.consensus_signer = + SignMode::Key(Secret::new(SecretKey::random(&mut rng).into())); config.block_production = Trigger::Instant; let srv = FuelService::from_database(db.clone(), config) diff --git a/tests/tests/trigger_integration/interval.rs b/tests/tests/trigger_integration/interval.rs index 1cfef77d1ee..b928e54c35d 100644 --- a/tests/tests/trigger_integration/interval.rs +++ b/tests/tests/trigger_integration/interval.rs @@ -12,7 +12,10 @@ use fuel_core_client::client::{ }, FuelClient, }; -use fuel_core_poa::Trigger; +use fuel_core_poa::{ + signer::SignMode, + Trigger, +}; use fuel_core_types::{ fuel_asm::*, fuel_crypto::SecretKey, @@ -36,7 +39,8 @@ async fn poa_interval_produces_empty_blocks_at_correct_rate() { let db = Database::default(); let mut config = Config::local_node(); config.graphql_config.max_queries_complexity = 1_000_000; - config.consensus_key = Some(Secret::new(SecretKey::random(&mut rng).into())); + config.consensus_signer = + SignMode::Key(Secret::new(SecretKey::random(&mut rng).into())); config.block_production = Trigger::Interval { block_time: Duration::new(round_time_seconds, 0), }; @@ -99,7 +103,8 @@ async fn poa_interval_produces_nonempty_blocks_at_correct_rate() { let db = Database::default(); let mut config = Config::local_node(); config.graphql_config.max_queries_complexity = 1_000_000; - config.consensus_key = Some(Secret::new(SecretKey::random(&mut rng).into())); + config.consensus_signer = + SignMode::Key(Secret::new(SecretKey::random(&mut rng).into())); config.block_production = Trigger::Interval { block_time: Duration::new(round_time_seconds, 0), }; diff --git a/tests/tests/trigger_integration/never.rs b/tests/tests/trigger_integration/never.rs index 03b5e36d579..50cdda4af49 100644 --- a/tests/tests/trigger_integration/never.rs +++ b/tests/tests/trigger_integration/never.rs @@ -12,7 +12,10 @@ use fuel_core_client::client::{ }, FuelClient, }; -use fuel_core_poa::Trigger; +use fuel_core_poa::{ + signer::SignMode, + Trigger, +}; use fuel_core_types::{ fuel_asm::op, fuel_crypto::SecretKey, @@ -30,7 +33,8 @@ async fn poa_never_trigger_doesnt_produce_blocks() { let db = Database::default(); let mut config = Config::local_node(); config.block_production = Trigger::Never; - config.consensus_key = Some(Secret::new(SecretKey::random(&mut rng).into())); + config.consensus_signer = + SignMode::Key(Secret::new(SecretKey::random(&mut rng).into())); let srv = FuelService::from_database(db.clone(), config) .await .unwrap(); diff --git a/version-compatibility/forkless-upgrade/src/tests_helper.rs b/version-compatibility/forkless-upgrade/src/tests_helper.rs index 67dd06e9007..504120353f7 100644 --- a/version-compatibility/forkless-upgrade/src/tests_helper.rs +++ b/version-compatibility/forkless-upgrade/src/tests_helper.rs @@ -25,12 +25,24 @@ use rand::{ }; use std::str::FromStr; +// Awful version compatibility hack. +// `$bin_crate::cli::run::get_service` is async in the later versions of fuel-core-bin. +macro_rules! maybe_await { + (true, $expr:expr) => { + $expr.await + }; + (false, $expr:expr) => { + $expr + }; +} + macro_rules! define_core_driver { - ($bin_crate:ident, $service:ident, $client:ident, $name:ident) => { + ($bin_crate:ident, $service:ident, $client:ident, $name:ident, $bin_crate_get_service_is_async:tt) => { pub struct $name { - /// This must be before the db_dir as the drop order matters here. + /// This must be before the _db_dir as the drop order matters here. pub node: $service, - pub db_dir: tempfile::TempDir, + pub _db_dir: tempfile::TempDir, + #[allow(dead_code)] // Only some of the clients generated by this macro are used pub client: $client, } @@ -51,16 +63,16 @@ macro_rules! define_core_driver { ]; args.extend(extra_args); - let node = $bin_crate::cli::run::get_service( + let node = maybe_await!($bin_crate_get_service_is_async, $bin_crate::cli::run::get_service( $bin_crate::cli::run::Command::parse_from(args), - )?; + ))?; node.start_and_await().await?; let client = $client::from(node.shared.graph_ql.bound_address); Ok(Self { node, - db_dir, + _db_dir: db_dir, client, }) } @@ -72,14 +84,16 @@ define_core_driver!( genesis_fuel_core_bin, GenesisFuelService, GenesisClient, - GenesisFuelCoreDriver + GenesisFuelCoreDriver, + false ); define_core_driver!( latest_fuel_core_bin, LatestFuelService, LatestClient, - LatestFuelCoreDriver + LatestFuelCoreDriver, + true ); pub const IGNITION_SNAPSHOT: &str = "./chain-configurations/ignition";