diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index e5b52109e5..d9cd9e5f19 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -13,7 +13,7 @@ runs: uses: RDXWorks-actions/toolchain@master with: # IMPORTANT: This version should match the version in radixdlt-scrypto on respective branch - toolchain: 1.81.0 + toolchain: 1.92.0 default: true target: ${{inputs.cross-compile-to-windows == 'true' && 'x86_64-pc-windows-msvc' || ''}} diff --git a/.github/workflows/add-artifacts-to-release.yml b/.github/workflows/add-artifacts-to-release.yml index 1e9ae71153..8565526929 100644 --- a/.github/workflows/add-artifacts-to-release.yml +++ b/.github/workflows/add-artifacts-to-release.yml @@ -92,7 +92,7 @@ jobs: - uses: RDXWorks-actions/toolchain@master with: profile: minimal - toolchain: stable + toolchain: 1.92.0 override: true - name: Install Rust Targets run: | diff --git a/Dockerfile b/Dockerfile index df0e2e3a23..f2eecaddcb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,14 +52,14 @@ RUN apt-get update \ docker.io=20.10.24+dfsg1-1+deb12u1+b6 \ libssl-dev=3.0.20-1~deb12u2 \ pkg-config=1.8.1-1 \ - unzip=6.0-28 \ + unzip=6.0-28+deb12u1 \ wget=${WGET_VERSION} \ software-properties-common=0.99.30-4.1~deb12u1 \ && apt-get install -y --no-install-recommends \ - openjdk-17-jdk=17.0.19+10-1~deb12u2 \ - openjdk-17-jre=17.0.19+10-1~deb12u2 \ - openjdk-17-jdk-headless=17.0.19+10-1~deb12u2 \ - openjdk-17-jre-headless=17.0.19+10-1~deb12u2 \ + openjdk-17-jdk=17.0.20.1+1-1~deb12u1 \ + openjdk-17-jre=17.0.20.1+1-1~deb12u1 \ + openjdk-17-jdk-headless=17.0.20.1+1-1~deb12u1 \ + openjdk-17-jre-headless=17.0.20.1+1-1~deb12u1 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -141,7 +141,7 @@ RUN apt-get update \ # We fix the version of Rust here to ensure that we can update it without having # issues with the caching layers containing outdated versions which aren't compatible. RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o rustup.sh \ - && sh rustup.sh -y --target 1.88.0-aarch64-unknown-linux-gnu,1.88.0-x86_64-unknown-linux-gnu --default-toolchain 1.88.0 + && sh rustup.sh -y --target aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu --default-toolchain 1.92.0 # RUN "$HOME/.cargo/bin/cargo" install sccache --version 0.7.4 @@ -262,7 +262,7 @@ LABEL org.opencontainers.image.authors="devops@radixdlt.com" # - https://packages.debian.org/bookworm/libc6 RUN apt-get update -y \ && apt-get -y --no-install-recommends install \ - openjdk-17-jre-headless=17.0.19+10-1~deb12u2 \ + openjdk-17-jre-headless=17.0.20.1+1-1~deb12u1 \ # https://security-tracker.debian.org/tracker/CVE-2023-38545 curl=7.88.1-10+deb12u15 \ gettext-base=0.21-12 \ diff --git a/cli-tools/build.gradle b/cli-tools/build.gradle index 2af999cc0e..9f8a8eea53 100644 --- a/cli-tools/build.gradle +++ b/cli-tools/build.gradle @@ -136,6 +136,7 @@ def nodeNames = project.getProperties().get('nodeNames', "") def validators = project.getProperties().get('validators', '0') def publicKeys = project.getProperties().get('publicKeys', '') def network = project.getProperties().get('network', '') +def stakingAccountPublicKey = project.findProperty('stakingAccountPublicKey') task getClassPathForRadixShell { doLast { @@ -187,6 +188,9 @@ task generateDevGenesis(type: Exec) { "com.radixdlt.cli.GenerateGenesis", "--validator-count=${validators}", "--network=${network}" + if (stakingAccountPublicKey != null) { + args "--staking-account-public-key=${stakingAccountPublicKey}" + } } task generateGenesisFile(type: Exec) { @@ -205,6 +209,9 @@ task generateGenesisFile(type: Exec) { "com.radixdlt.cli.GenerateGenesis", "--public-keys=${publicKeys}", "--network=${network}" + if (stakingAccountPublicKey != null) { + args "--staking-account-public-key=${stakingAccountPublicKey}" + } } task createGenerateGenesisScripts(type: CreateStartScripts) { diff --git a/cli-tools/src/main/java/com/radixdlt/cli/GenerateGenesis.java b/cli-tools/src/main/java/com/radixdlt/cli/GenerateGenesis.java index 47c567ebe7..bd2a4d71a1 100644 --- a/cli-tools/src/main/java/com/radixdlt/cli/GenerateGenesis.java +++ b/cli-tools/src/main/java/com/radixdlt/cli/GenerateGenesis.java @@ -143,6 +143,11 @@ public static void main(String[] args) throws Exception { options.addOption("p", "public-keys", true, "Specify validator keys"); options.addOption("v", "validator-count", true, "Specify number of validators to generate"); options.addOption("n", "network", true, "Specify the network name or ID"); + options.addOption( + "s", + "staking-account-public-key", + true, + "Override the powerful staking account public key (hex)"); CommandLineParser parser = new DefaultParser(); CommandLine cmd = parser.parse(options, args); @@ -188,8 +193,15 @@ public static void main(String[] args) throws Exception { }); final var network = parseNetwork(cmd.getOptionValue("n")); + if (cmd.hasOption("s") && !NETWORKS_TO_USE_POWERFUL_STAKING_ACCOUNT.contains(network)) { + throw new IllegalArgumentException("This network does not use a powerful staking account"); + } + final var stakingAccountPublicKey = + cmd.hasOption("s") + ? ECDSASecp256k1PublicKey.fromHex(cmd.getOptionValue("s")) + : GENESIS_POWERFUL_STAKING_ACCOUNT_PUBLIC_KEY; final var validators = validatorsBuilder.build(); - final var genesisData = createGenesisData(network, validators); + final var genesisData = createGenesisData(network, validators, stakingAccountPublicKey); final var encodedGenesisData = NodeSborCodecs.encode(genesisData, NodeSborCodecs.resolveCodec(new TypeToken<>() {})); final var compressedGenesisData = Compress.compress(encodedGenesisData); @@ -240,7 +252,9 @@ Could not resolve (lower case) logical network name, or network id. } private static GenesisData createGenesisData( - Network network, ImmutableList validators) { + Network network, + ImmutableList validators, + ECDSASecp256k1PublicKey stakingAccountPublicKey) { final var usePowerfulStakingAccount = NETWORKS_TO_USE_POWERFUL_STAKING_ACCOUNT.contains(network); @@ -251,16 +265,14 @@ private static GenesisData createGenesisData( final var stakingAccount = usePowerfulStakingAccount - ? Address.virtualAccountAddress(GENESIS_POWERFUL_STAKING_ACCOUNT_PUBLIC_KEY) + ? Address.virtualAccountAddress(stakingAccountPublicKey) : Address.virtualAccountAddress(PrivateKeys.ofNumeric(1).getPublicKey()); final var stakingAccountOwnsAllValidators = usePowerfulStakingAccount; final Map xrdBalances = usePowerfulStakingAccount - ? Map.of( - GENESIS_POWERFUL_STAKING_ACCOUNT_PUBLIC_KEY, - GENESIS_POWERFUL_STAKING_ACCOUNT_INITIAL_XRD_BALANCE) + ? Map.of(stakingAccountPublicKey, GENESIS_POWERFUL_STAKING_ACCOUNT_INITIAL_XRD_BALANCE) : Map.of(); var consensusConfig = diff --git a/common/src/main/java/com/radixdlt/networks/Network.java b/common/src/main/java/com/radixdlt/networks/Network.java index 44aba9aa45..eed2337637 100644 --- a/common/src/main/java/com/radixdlt/networks/Network.java +++ b/common/src/main/java/com/radixdlt/networks/Network.java @@ -94,9 +94,9 @@ public enum Network { "tdx_2_", FixedNetworkGenesis.constant( HashCode.fromBytes( - Hex.decode("2825a4f1697dd457a27b6f641999d5f64d46ad512a49ee54653e4b866eab86e3")), + Hex.decode("2536c14ce79bb6cf7113aeed00a76666c4693622ee2187fefdf5225a2fd5cd51")), WrappedByteArray.fromBase64String( - "/wYAAHNOYVBwWQCPAgDGlw0JsgsUXCEGCgEACQEABREJICEKCWQAAAAK9A0RCAq4CwkiDArgkwQFChU0JKDsgpDrUQ6PtZoFGBkBIKAAAGSns7bgDRkTCQEVOxUJJKAAABBjLV7HawUJIhUB4CAiAwQBICEBAoDR8n5qke/XQptnD/OrX5rCMZLhJi7+LunGT4EIuc2gAAAAYIqmHv7EbdLVCAAAABUCsAEgIQQGIAchAw5SIf/qpLqorABPAywoLS8tR8jo2w15AUAIpnGOzWFIAQEBAWKzAPBJICECAgwEbmFtZSIAAQwTRGVmYXVsdCB2YWxpZGF0b3IgMQIMCGluZm9fdXJsIg0BDBhodHRwczovL3d3dy5yYWRpeGRsdC5jb216yQAFq3wW0qUtuYiPbyBCBqyKDUokPNOghqTrU7tt5IPo1+DROvarAAQ0Av6rAB2rfJN1Tm8Rj44uiQJFqfg94pPmNJ85dkD9s51S79+5QpbO9qsAADb+qwAuqwB4hKj6wGhwun4klfc+61w0uqJsaYm1e9FHmiKFsm6bV/pWAQA5/qsADasQAQIggAF27QIMICEEAo7SAhAgIQECCWEJMKAAAADoPIDQnzwuOwMBEREBBUd+bgKaRwB+CgKaRwB+pgGGRwAB7iRA6u10RtCcLJ8MEevwPAAAACAMCgx0cmFuc2Zlcl94cmQIcmFkaXN3YXAIbWV0YWRhdGERZnVuZ2libGVfcmVzb3VyY2UVbm9uX2Y+FgC4HWFjY291bnRfYXV0aG9yaXplZF9kZXBvc2l0b3JzDmdsb2JhbF9uX293bmVkJm5OQwBgX3dpdGhfcmVtb3RlX3R5cGUZa3Zfc3RvckYaADwPbWF4X3RyYW5zYWN0aW9u"))), + "/wYAAHNOYVBwWQCPAgASzkEPsgsUXCEGCgEACQEABQkHKAAAIQoJZAAAAAr0DRoICrgLCRsMCuCTBAUKFTQkoOyCkOtRDo+1mgUYGQEgoAAAZKeztuANGRMJARU7FQkgoAAAEGMtXsdrFYENAeAgIgMEASAhAQKA0Z5qm6ckGZ1ekdMsR2Xv3/D9wODYzg5xCVT+dKIzoAAAAGCKph7+xG3S1QgAAAAVArABICEEBiAHIQKVH3QVvG5TGRc1TV/S8YIf1330OtVfC9K4KIz2+Xvv0wEBAQFiswDwSSAhAgIMBG5hbWUiAAEME0RlZmF1bHQgdmFsaWRhdG9yIDECDAhpbmZvX3VybCINAQwYaHR0cHM6Ly93d3cucmFkaXhkbHQuY29teskABat8b8DgUXfyb9oovnwwKanJ+kJ84hDaMSD98J8HYttxhdb2qwAEMgL+qwAdq3x6ZAJPmCNKHV7mLi4dMBIVVa7Ld618ure+IAQ18W+/3varAAAz/qsALqsAfIFIPS1onORL8Ac2G+MyjWEedqx+xuJf5L91gJ+dD5N89qsAADT+qwANqxABAiCAAXbtAgwgIQQCjtICYTgACWEJMKAAAADoPIDQnzwuOwMBEREBBUd+bgKaRwB+CgKaRwB+pgGGRwAB7iRA6u10RtCcLJ8MEevwPAAAACAMCgx0cmFuc2Zlcl94cmQIcmFkaXN3YXAIbWV0YWRhdGERZnVuZ2libGVfcmVzb3VyY2UVbm9uX2Y+FgC4HWFjY291bnRfYXV0aG9yaXplZF9kZXBvc2l0b3JzDmdsb2JhbF9uX293bmVkJm5OQwBgX3dpdGhfcmVtb3RlX3R5cGUZa3Zfc3RvckYaADwPbWF4X3RyYW5zYWN0aW9u"))), // Temporary networks that match Olympia - for genesis testing mostly OLYMPIA_RELEASENET(3, "releasenet", "tdx_3_"), diff --git a/core-rust-bridge/src/main/java/com/radixdlt/protocol/ProtocolConfig.java b/core-rust-bridge/src/main/java/com/radixdlt/protocol/ProtocolConfig.java index 5c834ddd31..c297619702 100644 --- a/core-rust-bridge/src/main/java/com/radixdlt/protocol/ProtocolConfig.java +++ b/core-rust-bridge/src/main/java/com/radixdlt/protocol/ProtocolConfig.java @@ -74,19 +74,22 @@ import com.radixdlt.sbor.codec.CodecMap; import com.radixdlt.sbor.codec.StructCodec; import com.radixdlt.sbor.exceptions.SborDecodeException; +import com.radixdlt.utils.UInt64; import java.util.ArrayList; import java.util.List; import java.util.Map; public record ProtocolConfig( ImmutableList protocolUpdateTriggers, - Map rawProtocolUpdateContentOverrides) { + Map rawProtocolUpdateContentOverrides, + ImmutableList userTransactionMoratoriums) { public static final String GENESIS_PROTOCOL_VERSION_NAME = "babylon-genesis"; public static final String ANEMONE_PROTOCOL_VERSION_NAME = "anemone"; public static final String BOTTLENOSE_PROTOCOL_VERSION_NAME = "bottlenose"; public static final String CUTTLEFISH_PART1_PROTOCOL_VERSION_NAME = "cuttlefish"; public static final String CUTTLEFISH_PART2_PROTOCOL_VERSION_NAME = "cuttlefish-part2"; + public static final String EAGLE_RAY_PROTOCOL_VERSION_NAME = "eagle-ray"; public static ImmutableList VERSION_NAMES = ImmutableList.of( @@ -94,13 +97,36 @@ public record ProtocolConfig( ANEMONE_PROTOCOL_VERSION_NAME, BOTTLENOSE_PROTOCOL_VERSION_NAME, CUTTLEFISH_PART1_PROTOCOL_VERSION_NAME, - CUTTLEFISH_PART2_PROTOCOL_VERSION_NAME); + CUTTLEFISH_PART2_PROTOCOL_VERSION_NAME, + EAGLE_RAY_PROTOCOL_VERSION_NAME); public static final String LATEST_PROTOCOL_VERSION_NAME = VERSION_NAMES.get(VERSION_NAMES.size() - 1); public ProtocolConfig(ImmutableList protocolUpdateTriggers) { - this(protocolUpdateTriggers, Map.of()); + this(protocolUpdateTriggers, Map.of(), ImmutableList.of()); + } + + public ProtocolConfig( + ImmutableList protocolUpdateTriggers, + Map rawProtocolUpdateContentOverrides) { + this(protocolUpdateTriggers, rawProtocolUpdateContentOverrides, ImmutableList.of()); + } + + /** Replaces the moratorium schedule with a single range. */ + public ProtocolConfig withUserTransactionMoratorium(long fromEpoch, long untilEpoch) { + return new ProtocolConfig( + protocolUpdateTriggers, + rawProtocolUpdateContentOverrides, + ImmutableList.of( + new UserTransactionMoratorium( + UInt64.fromNonNegativeLong(fromEpoch), UInt64.fromNonNegativeLong(untilEpoch)))); + } + + public static ProtocolConfig enactAtEpochWithUserTransactionMoratorium( + String version, long fromEpoch, long enactmentEpoch) { + return enactAtEpoch(version, enactmentEpoch) + .withUserTransactionMoratorium(fromEpoch, enactmentEpoch); } public static void registerCodec(CodecMap codecMap) { diff --git a/core-rust-bridge/src/main/java/com/radixdlt/protocol/UserTransactionMoratorium.java b/core-rust-bridge/src/main/java/com/radixdlt/protocol/UserTransactionMoratorium.java new file mode 100644 index 0000000000..7526b6bebd --- /dev/null +++ b/core-rust-bridge/src/main/java/com/radixdlt/protocol/UserTransactionMoratorium.java @@ -0,0 +1,84 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.protocol; + +import com.radixdlt.sbor.codec.CodecMap; +import com.radixdlt.sbor.codec.StructCodec; +import com.radixdlt.utils.UInt64; + +/** + * An epoch range during which user transactions are refused. + * + * @param fromInclusive the first committed epoch during which transactions are refused + * @param toExclusive the first committed epoch from which clients may retry + */ +public record UserTransactionMoratorium(UInt64 fromInclusive, UInt64 toExclusive) { + + public static void registerCodec(CodecMap codecMap) { + codecMap.register( + UserTransactionMoratorium.class, + codecs -> StructCodec.fromRecordComponents(UserTransactionMoratorium.class, codecs)); + } +} diff --git a/core-rust-bridge/src/main/java/com/radixdlt/sbor/NodeSborCodecs.java b/core-rust-bridge/src/main/java/com/radixdlt/sbor/NodeSborCodecs.java index ac96a865a4..4159a78e81 100644 --- a/core-rust-bridge/src/main/java/com/radixdlt/sbor/NodeSborCodecs.java +++ b/core-rust-bridge/src/main/java/com/radixdlt/sbor/NodeSborCodecs.java @@ -135,6 +135,7 @@ public static void registerCodecsWithCodecMap(CodecMap codecMap) { ProtocolUpdateEnactmentCondition.registerCodec(codecMap); ProtocolUpdateEnactmentCondition.SignalledReadinessThreshold.registerCodec(codecMap); ProtocolState.registerCodec(codecMap); + UserTransactionMoratorium.registerCodec(codecMap); ProtocolUpdateResult.registerCodec(codecMap); RawLedgerTransaction.registerCodec(codecMap); RawNotarizedTransaction.registerCodec(codecMap); diff --git a/core-rust-bridge/src/main/java/com/radixdlt/statecomputer/RustStateComputer.java b/core-rust-bridge/src/main/java/com/radixdlt/statecomputer/RustStateComputer.java index 38edc116a9..ca39d1c071 100644 --- a/core-rust-bridge/src/main/java/com/radixdlt/statecomputer/RustStateComputer.java +++ b/core-rust-bridge/src/main/java/com/radixdlt/statecomputer/RustStateComputer.java @@ -68,13 +68,16 @@ import com.google.common.reflect.TypeToken; import com.radixdlt.environment.NodeRustEnvironment; +import com.radixdlt.lang.Option; import com.radixdlt.lang.Result; import com.radixdlt.lang.Tuple; import com.radixdlt.monitoring.LabelledTimer; import com.radixdlt.monitoring.Metrics; import com.radixdlt.monitoring.Metrics.MethodId; +import com.radixdlt.protocol.UserTransactionMoratorium; import com.radixdlt.sbor.Natives; import com.radixdlt.statecomputer.commit.*; +import com.radixdlt.utils.UInt64; import java.util.Objects; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @@ -99,6 +102,11 @@ public RustStateComputer(Metrics metrics, NodeRustEnvironment nodeRustEnvironmen Natives.builder(nodeRustEnvironment, RustStateComputer::protocolState) .measure(timer.label(new MethodId(RustStateComputer.class, "protocolState"))) .build(new TypeToken<>() {}); + this.ensureUserTransactionsAllowedFunc = + Natives.builder(nodeRustEnvironment, RustStateComputer::ensureUserTransactionsAllowed) + .measure( + timer.label(new MethodId(RustStateComputer.class, "ensureUserTransactionsAllowed"))) + .build(new TypeToken<>() {}); } public PrepareResult prepare(PrepareRequest prepareRequest) { @@ -135,4 +143,20 @@ public ProtocolState protocolState() { private static native byte[] protocolState( NodeRustEnvironment nodeRustEnvironment, byte[] payload); + + /** Returns the active moratorium as an error, using Rust's committed ledger epoch. */ + public Result ensureUserTransactionsAllowed() { + return ensureUserTransactionsAllowedFunc.call(Option.none()); + } + + /** Checks the consensus event's epoch independently of the committed ledger epoch. */ + public Result ensureUserTransactionsAllowed(long epoch) { + return ensureUserTransactionsAllowedFunc.call(Option.some(UInt64.fromNonNegativeLong(epoch))); + } + + private final Natives.Call1, Result> + ensureUserTransactionsAllowedFunc; + + private static native byte[] ensureUserTransactionsAllowed( + NodeRustEnvironment nodeRustEnvironment, byte[] payload); } diff --git a/core-rust-bridge/src/test/java/com/radixdlt/protocol/ProtocolConfigTest.java b/core-rust-bridge/src/test/java/com/radixdlt/protocol/ProtocolConfigTest.java new file mode 100644 index 0000000000..11b75a28a6 --- /dev/null +++ b/core-rust-bridge/src/test/java/com/radixdlt/protocol/ProtocolConfigTest.java @@ -0,0 +1,111 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.protocol; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.common.reflect.TypeToken; +import com.radixdlt.sbor.NodeSborCodecs; +import org.junit.Test; + +public final class ProtocolConfigTest { + @Test + public void round_trips_the_separate_moratorium_with_the_ordinary_epoch_trigger() { + // Arrange + final var original = + ProtocolConfig.enactAtEpochWithUserTransactionMoratorium("eagle-ray", 339897, 339898); + final var encoded = + NodeSborCodecs.encode( + original, NodeSborCodecs.resolveCodec(new TypeToken() {})); + + // Act + final var decoded = ProtocolConfig.sborDecode(encoded, "Invalid protocol config"); + + // Assert + assertEquals(original, decoded); + assertEquals( + ProtocolUpdateEnactmentCondition.unconditionallyAtEpoch(339898), + decoded + .protocolUpdateTriggers() + .get(decoded.protocolUpdateTriggers().size() - 1) + .enactmentCondition()); + } + + @Test + public void refuses_malformed_configuration() { + // Arrange + final var encoded = new byte[] {0}; + + // Act + final var error = + assertThrows( + RuntimeException.class, + () -> ProtocolConfig.sborDecode(encoded, "Invalid protocol config")); + + // Assert + assertEquals("Invalid protocol config", error.getMessage()); + } +} diff --git a/core-rust/Cargo.lock b/core-rust/Cargo.lock index 10a73a67c7..d3f82f5177 100644 --- a/core-rust/Cargo.lock +++ b/core-rust/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -188,22 +188,19 @@ checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" [[package]] name = "bindgen" -version = "0.65.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cexpr", "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "prettyplease", + "itertools", "proc-macro2", "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn 2.0.32", ] @@ -336,12 +333,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ + "find-msvc-tools", "jobserver", "libc", + "shlex 2.0.1", ] [[package]] @@ -788,6 +787,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + [[package]] name = "fixedstr" version = "0.2.12" @@ -946,6 +951,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.4.1" @@ -1086,12 +1097,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.15.2", "serde", ] @@ -1176,9 +1187,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] @@ -1207,12 +1218,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leb128" version = "0.2.5" @@ -1243,14 +1248,13 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "librocksdb-sys" -version = "0.11.0+8.1.1" +version = "0.17.3+10.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +checksum = "cef2a00ee60fe526157c9023edab23943fae1ce2ab6f4abb2a807c1746835de9" dependencies = [ "bindgen", "bzip2-sys", "cc", - "glob", "libc", "libz-sys", "lz4-sys", @@ -1298,9 +1302,9 @@ checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909" [[package]] name = "lz4-sys" -version = "1.9.4" +version = "1.11.1+lz4-1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" dependencies = [ "cc", "libc", @@ -1625,12 +1629,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "percent-encoding" version = "2.3.0" @@ -1698,10 +1696,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "prettyplease" -version = "0.2.15" +name = "preinterpret" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "1727fb2b8bcde3f60a0073507de280c5da6494798be79b4ee412b487e91891b4" dependencies = [ "proc-macro2", "syn 2.0.32", @@ -1752,8 +1750,8 @@ dependencies = [ [[package]] name = "radix-blueprint-schema-init" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "bitflags 1.3.2", "radix-common", @@ -1763,8 +1761,8 @@ dependencies = [ [[package]] name = "radix-common" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "bech32", "blake2", @@ -1789,8 +1787,8 @@ dependencies = [ [[package]] name = "radix-common-derive" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "paste", "proc-macro2", @@ -1801,8 +1799,8 @@ dependencies = [ [[package]] name = "radix-engine" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "bitflags 1.3.2", "colored", @@ -1832,8 +1830,8 @@ dependencies = [ [[package]] name = "radix-engine-interface" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "bitflags 1.3.2", "const-sha1", @@ -1853,16 +1851,16 @@ dependencies = [ [[package]] name = "radix-engine-profiling" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "fixedstr", ] [[package]] name = "radix-engine-profiling-derive" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "proc-macro2", "quote", @@ -1872,8 +1870,8 @@ dependencies = [ [[package]] name = "radix-engine-toolkit-common" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "radix-common", "radix-engine", @@ -1886,8 +1884,8 @@ dependencies = [ [[package]] name = "radix-native-sdk" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "radix-common", "radix-engine-interface", @@ -1897,17 +1895,18 @@ dependencies = [ [[package]] name = "radix-rust" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", + "preinterpret", "serde", ] [[package]] name = "radix-sbor-derive" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "proc-macro2", "quote", @@ -1917,8 +1916,8 @@ dependencies = [ [[package]] name = "radix-substate-store-impls" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "hex", "itertools", @@ -1931,8 +1930,8 @@ dependencies = [ [[package]] name = "radix-substate-store-interface" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "hex", "itertools", @@ -1943,8 +1942,8 @@ dependencies = [ [[package]] name = "radix-substate-store-queries" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "hex", "itertools", @@ -1960,8 +1959,8 @@ dependencies = [ [[package]] name = "radix-transaction-scenarios" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "hex", "itertools", @@ -1980,8 +1979,8 @@ dependencies = [ [[package]] name = "radix-transactions" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "annotate-snippets", "bech32", @@ -2088,9 +2087,9 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "rocksdb" -version = "0.21.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +checksum = "ddb7af00d2b17dbd07d82c0063e25411959748ff03e8d4f96134c2ff41fce34f" dependencies = [ "libc", "librocksdb-sys", @@ -2104,9 +2103,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2153,8 +2152,8 @@ dependencies = [ [[package]] name = "sbor" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "const-sha1", "hex", @@ -2167,8 +2166,8 @@ dependencies = [ [[package]] name = "sbor-derive" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "proc-macro2", "sbor-derive-common", @@ -2177,11 +2176,11 @@ dependencies = [ [[package]] name = "sbor-derive-common" -version = "1.3.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=cuttlefish-c3aa4337#c3aa4337b7631b5fb90acef44fa8ffd62bdea821" +version = "1.4.0" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=eagle-ray-25e5fff4#25e5fff46a7037d460f4f135647a8233e2360195" dependencies = [ "const-sha1", - "indexmap 2.2.6", + "indexmap 2.7.0", "itertools", "proc-macro2", "quote", @@ -2256,7 +2255,7 @@ version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "itoa", "ryu", "serde", @@ -2310,7 +2309,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.7.0", "serde", "serde_derive", "serde_json", @@ -2378,6 +2377,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3087,7 +3092,7 @@ version = "0.111.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad71036aada3f6b09251546e97e4f4f176dd6b41cf6fa55e7e0f65e86aec319a" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.7.0", "semver", ] diff --git a/core-rust/Cargo.toml b/core-rust/Cargo.toml index d4955f2944..19cf0d9cde 100644 --- a/core-rust/Cargo.toml +++ b/core-rust/Cargo.toml @@ -26,18 +26,18 @@ resolver = "2" # Then use tag="release_name-BLAH" in the below dependencies. # ================================================================= -sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337", features = ["serde"] } -radix-transactions = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337" } -radix-transaction-scenarios = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337" } -radix-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337", features = ["serde"] } -radix-engine-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337" } -radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337" } -radix-substate-store-impls = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337" } -radix-substate-store-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337" } -radix-substate-store-queries = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337" } -radix-rust = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337", features = ["serde"] } -radix-blueprint-schema-init = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337", features = ["serde"] } -radix-engine-toolkit-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "cuttlefish-c3aa4337" } +sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4", features = ["serde"] } +radix-transactions = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4" } +radix-transaction-scenarios = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4" } +radix-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4", features = ["serde"] } +radix-engine-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4" } +radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4" } +radix-substate-store-impls = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4" } +radix-substate-store-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4" } +radix-substate-store-queries = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4" } +radix-rust = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4", features = ["serde"] } +radix-blueprint-schema-init = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4", features = ["serde"] } +radix-engine-toolkit-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "eagle-ray-25e5fff4" } itertools = { version = "=0.10.5" } jni = { version = "=0.19.0" } @@ -59,6 +59,7 @@ tower = { version = "=0.4.13" } tower-http = { version = "=0.4.3", features = ["catch-panic"]} hyper = { version = "=0.14.27", features = ["server", "http1"] } paste = { version = "=1.0.14", default-features = false } +rocksdb = { version = "=0.24.0" } [profile.dev] opt-level = 3 diff --git a/core-rust/core-api-server/core-api-schema.yaml b/core-rust/core-api-server/core-api-schema.yaml index 6bfd4b2068..faf4b1fec6 100644 --- a/core-rust/core-api-server/core-api-schema.yaml +++ b/core-rust/core-api-server/core-api-schema.yaml @@ -4385,6 +4385,8 @@ components: - V1 - V2 - V3 + - V4 + - V5 SystemParameters: type: object required: diff --git a/core-rust/core-api-server/src/core_api/conversions/substates/boot_loader_module.rs b/core-rust/core-api-server/src/core_api/conversions/substates/boot_loader_module.rs index db7b067721..a322fdc04e 100644 --- a/core-rust/core-api-server/src/core_api/conversions/substates/boot_loader_module.rs +++ b/core-rust/core-api-server/src/core_api/conversions/substates/boot_loader_module.rs @@ -32,6 +32,8 @@ fn to_api_system_version(system_version: &SystemVersion) -> models::SystemVersio SystemVersion::V1 => models::SystemVersion::V1, SystemVersion::V2 => models::SystemVersion::V2, SystemVersion::V3 => models::SystemVersion::V3, + SystemVersion::V4 => models::SystemVersion::V4, + SystemVersion::V5 => models::SystemVersion::V5, } } diff --git a/core-rust/core-api-server/src/core_api/errors.rs b/core-rust/core-api-server/src/core_api/errors.rs index 38d4b1ab21..5a130ceb45 100644 --- a/core-rust/core-api-server/src/core_api/errors.rs +++ b/core-rust/core-api-server/src/core_api/errors.rs @@ -239,6 +239,7 @@ pub(crate) fn detailed_error( } } +#[allow(dead_code)] pub(crate) fn length_limit_error() -> ResponseError { ResponseError { status_code: StatusCode::PAYLOAD_TOO_LARGE, diff --git a/core-rust/core-api-server/src/core_api/extractors.rs b/core-rust/core-api-server/src/core_api/extractors.rs index 7d94555b0b..3ad871ffef 100644 --- a/core-rust/core-api-server/src/core_api/extractors.rs +++ b/core-rust/core-api-server/src/core_api/extractors.rs @@ -14,6 +14,7 @@ use super::{client_error, length_limit_error, ResponseError}; // We define our own `Json` extractor that customizes the error from `axum::Json` +#[allow(dead_code)] #[derive(Debug)] pub(crate) struct Json(pub T); pub use axum::extract::State; // Re-export State so that it can be used easily diff --git a/core-rust/core-api-server/src/core_api/generated/models/system_version.rs b/core-rust/core-api-server/src/core_api/generated/models/system_version.rs index c717c3eb5f..5ba9919fee 100644 --- a/core-rust/core-api-server/src/core_api/generated/models/system_version.rs +++ b/core-rust/core-api-server/src/core_api/generated/models/system_version.rs @@ -19,6 +19,10 @@ pub enum SystemVersion { V2, #[serde(rename = "V3")] V3, + #[serde(rename = "V4")] + V4, + #[serde(rename = "V5")] + V5, } @@ -28,6 +32,8 @@ impl ToString for SystemVersion { Self::V1 => String::from("V1"), Self::V2 => String::from("V2"), Self::V3 => String::from("V3"), + Self::V4 => String::from("V4"), + Self::V5 => String::from("V5"), } } } @@ -40,4 +46,3 @@ impl Default for SystemVersion { - diff --git a/core-rust/core-api-server/src/core_api/handlers/lts/state_account_all_fungible_resource_balances.rs b/core-rust/core-api-server/src/core_api/handlers/lts/state_account_all_fungible_resource_balances.rs index fefe80f48a..037eee1941 100644 --- a/core-rust/core-api-server/src/core_api/handlers/lts/state_account_all_fungible_resource_balances.rs +++ b/core-rust/core-api-server/src/core_api/handlers/lts/state_account_all_fungible_resource_balances.rs @@ -1,5 +1,6 @@ use crate::prelude::*; +#[allow(clippy::iter_kv_map)] #[tracing::instrument(skip(state))] pub(crate) async fn handle_lts_state_account_all_fungible_resource_balances( state: State, diff --git a/core-rust/core-api-server/src/core_api/handlers/transaction_submit.rs b/core-rust/core-api-server/src/core_api/handlers/transaction_submit.rs index e82ec28d99..4d61af116a 100644 --- a/core-rust/core-api-server/src/core_api/handlers/transaction_submit.rs +++ b/core-rust/core-api-server/src/core_api/handlers/transaction_submit.rs @@ -37,7 +37,21 @@ pub(crate) async fn handle_transaction_submit( )), Err(MempoolAddError::Duplicate(_)) => Ok(models::TransactionSubmitResponse::new(true)), Err(MempoolAddError::Rejected(rejection, notarized_transaction_hash)) => { - if let Some(already_committed_error) = rejection.transaction_intent_already_committed_error() { + if let MempoolRejectionReason::UserTransactionMoratorium(moratorium) = &rejection.reason { + Err(detailed_error( + StatusCode::BAD_REQUEST, + "User transactions are temporarily not accepted: a user transaction moratorium is in force until the pending protocol update is enacted", + TransactionSubmitErrorDetails::TransactionSubmitRejectedErrorDetails { + error_message: rejection.reason.to_string(&mapping_context), + is_fresh: true, + is_payload_rejection_permanent: false, + is_intent_rejection_permanent: false, + retry_from_timestamp: None, + retry_from_epoch: Some(to_api_epoch(&mapping_context, moratorium.to_exclusive)?), + invalid_from_epoch: None, + }, + )) + } else if let Some(already_committed_error) = rejection.transaction_intent_already_committed_error() { let is_same_transaction = Some(already_committed_error.committed_notarized_transaction_hash) == notarized_transaction_hash; Err(detailed_error( StatusCode::BAD_REQUEST, diff --git a/core-rust/engine-state-api-server/src/engine_state_api/extractors.rs b/core-rust/engine-state-api-server/src/engine_state_api/extractors.rs index f1c855fc21..2d20643428 100644 --- a/core-rust/engine-state-api-server/src/engine_state_api/extractors.rs +++ b/core-rust/engine-state-api-server/src/engine_state_api/extractors.rs @@ -12,6 +12,7 @@ use serde::Serialize; pub use axum::extract::State; use axum::http::StatusCode; +#[allow(dead_code)] #[derive(Debug)] pub(crate) struct Json(pub T); diff --git a/core-rust/node-common/Cargo.toml b/core-rust/node-common/Cargo.toml index d026182468..df3f70c84a 100644 --- a/core-rust/node-common/Cargo.toml +++ b/core-rust/node-common/Cargo.toml @@ -29,4 +29,4 @@ tracing-opentelemetry = { version = "=0.18.0" } tracing-subscriber = { version = "=0.3.17" } opentelemetry = { version = "=0.18.0", default-features = false, features = ["rt-tokio", "trace"] } opentelemetry-jaeger = { version = "=0.17.0", features = ["rt-tokio"] } -rocksdb = { version = "=0.21.0" } +rocksdb = { workspace = true } diff --git a/core-rust/p2p/src/rocks_db.rs b/core-rust/p2p/src/rocks_db.rs index e91b19a7f0..9bfc105be9 100644 --- a/core-rust/p2p/src/rocks_db.rs +++ b/core-rust/p2p/src/rocks_db.rs @@ -90,7 +90,6 @@ use crate::safety_store_components::SafetyState; /// The `NAME` constants defined by `*Cf` structs (and referenced below) are used as database column /// family names. Any change would effectively mean a ledger wipe. For this reason, we choose to /// define them manually (rather than using the `Into`, which is refactor-sensitive). - const ALL_ADDRESS_BOOK_COLUMN_FAMILIES: [&str; 3] = [ AddressBookCf::NAME, HighPriorityPeersCf::NAME, diff --git a/core-rust/p2p/src/safety_store_components.rs b/core-rust/p2p/src/safety_store_components.rs index 4ba42b4f8f..5a82d98f58 100644 --- a/core-rust/p2p/src/safety_store_components.rs +++ b/core-rust/p2p/src/safety_store_components.rs @@ -85,7 +85,7 @@ define_single_versioned! { /// Safety state components. Note that these structs are intended only for proper encoding/deconding /// of the safety state. They may repeat existing structs defined elsewhere. - +/// /// Timestamp of the various safety state components. // At present it's just an alias for i64. Later we may want to replace it with struct using crono crate and // do something like shown below to transparently convert to/from internal representation diff --git a/core-rust/rust-toolchain.toml b/core-rust/rust-toolchain.toml new file mode 100644 index 0000000000..9eb42f68f4 --- /dev/null +++ b/core-rust/rust-toolchain.toml @@ -0,0 +1,10 @@ +[toolchain] +# IMPORTANT: This should correspond to the channel currently used in radixdlt/scrypto. +# If this changes it will also need updating in: +# - .github/actions/setup-env/action.yml +# - .github/workflows/add-artifacts-to-release.yml +# - Dockerfile +channel = "1.92.0" +components = ["rustfmt"] +targets = ["wasm32-unknown-unknown"] +profile = "default" diff --git a/core-rust/state-manager/src/accumulator_tree/slice_merger.rs b/core-rust/state-manager/src/accumulator_tree/slice_merger.rs index c4abcb060e..6b262824bc 100644 --- a/core-rust/state-manager/src/accumulator_tree/slice_merger.rs +++ b/core-rust/state-manager/src/accumulator_tree/slice_merger.rs @@ -82,6 +82,7 @@ impl AccuTreeSliceMerger { } /// Appends the next `TreeSlice`. + #[allow(clippy::manual_div_ceil)] pub fn append(&mut self, slice: TreeSlice) { let mut merged_levels = self.merged.levels.iter_mut(); let merged_leaves = merged_levels.next(); diff --git a/core-rust/state-manager/src/accumulator_tree/tree_builder.rs b/core-rust/state-manager/src/accumulator_tree/tree_builder.rs index 0c6cf87b5e..955e29a2f5 100644 --- a/core-rust/state-manager/src/accumulator_tree/tree_builder.rs +++ b/core-rust/state-manager/src/accumulator_tree/tree_builder.rs @@ -99,6 +99,7 @@ impl<'s, S: AccuTreeStore, M: Merklizable> AccuTree<'s, S, M> { } /// Appends the given batch of new leaves to the tree. + #[allow(clippy::manual_div_ceil, clippy::manual_is_multiple_of)] pub fn append(&mut self, leaves: Vec) { let new_leaf_count = leaves.len(); if new_leaf_count == 0 { diff --git a/core-rust/state-manager/src/committer.rs b/core-rust/state-manager/src/committer.rs index f32377179f..8473e4fbdc 100644 --- a/core-rust/state-manager/src/committer.rs +++ b/core-rust/state-manager/src/committer.rs @@ -75,6 +75,9 @@ pub struct Committer { protocol_manager: Arc, ledger_metrics: Arc, formatter: Arc, + /// Lets concurrency tests pause a commit before database persistence. + #[cfg(test)] + before_database_commit: Mutex>, } impl Committer { @@ -98,8 +101,16 @@ impl Committer { protocol_manager, ledger_metrics, formatter, + #[cfg(test)] + before_database_commit: LockFactory::new("commit_test_hook").new_mutex(None), } } + + /// Runs the observer once, immediately before database persistence. + #[cfg(test)] + pub(crate) fn before_next_database_commit(&self, observer: impl FnOnce() + Send + 'static) { + *self.before_database_commit.lock() = Some(Box::new(observer)); + } } impl Committer { @@ -273,6 +284,14 @@ impl Committer { // Step 4.: Check final invariants, perform the DB commit self.verify_post_commit_invariants(&end_state, &proof); + #[cfg(test)] + { + let observer = self.before_database_commit.lock().take(); + if let Some(observer) = observer { + observer(); + } + } + database.commit(commit_bundle_builder.build(proof, vertex_store)); drop(database); @@ -506,3 +525,6 @@ pub struct CommittedUserTransactionIdentifiers { pub transaction_intent_hash: TransactionIntentHash, pub notarized_transaction_hash: NotarizedTransactionHash, } + +#[cfg(test)] +type BeforeDatabaseCommit = Box; diff --git a/core-rust/state-manager/src/jni/node_rust_environment.rs b/core-rust/state-manager/src/jni/node_rust_environment.rs index 3336993118..09e1a3bef1 100644 --- a/core-rust/state-manager/src/jni/node_rust_environment.rs +++ b/core-rust/state-manager/src/jni/node_rust_environment.rs @@ -187,7 +187,7 @@ impl JNINodeRustEnvironment { (base_path, config) } - fn combine(base: &String, ext: &str) -> PathBuf { + fn combine(base: &str, ext: &str) -> PathBuf { [base, ext].iter().collect() } diff --git a/core-rust/state-manager/src/jni/state_computer.rs b/core-rust/state-manager/src/jni/state_computer.rs index 0d9fe34616..ae48d81f1e 100644 --- a/core-rust/state-manager/src/jni/state_computer.rs +++ b/core-rust/state-manager/src/jni/state_computer.rs @@ -121,4 +121,25 @@ extern "system" fn Java_com_radixdlt_statecomputer_RustStateComputer_protocolSta }) } +#[no_mangle] +extern "system" fn Java_com_radixdlt_statecomputer_RustStateComputer_ensureUserTransactionsAllowed( + env: JNIEnv, + _class: JClass, + j_node_rust_env: JObject, + request_payload: jbyteArray, +) -> jbyteArray { + jni_sbor_coded_call( + &env, + request_payload, + |epoch: Option| -> Result<(), UserTransactionMoratorium> { + let mempool_manager = + JNINodeRustEnvironment::get_mempool_manager(&env, j_node_rust_env); + match epoch { + Some(epoch) => mempool_manager.ensure_user_transactions_allowed_at_epoch(epoch), + None => mempool_manager.ensure_user_transactions_allowed(), + } + }, + ) +} + pub fn export_extern_functions() {} diff --git a/core-rust/state-manager/src/lib.rs b/core-rust/state-manager/src/lib.rs index 506b8b9a2a..8a7fe454e1 100644 --- a/core-rust/state-manager/src/lib.rs +++ b/core-rust/state-manager/src/lib.rs @@ -72,6 +72,7 @@ pub mod jni; mod limits; mod mempool; mod metrics; +mod moratorium_manager; mod protocol; pub mod query; mod receipt; @@ -94,6 +95,7 @@ pub mod prelude { // Public prelude pub use crate::formatter::*; pub use crate::mempool::*; + pub use crate::moratorium_manager::*; pub use crate::protocol::*; pub use crate::query::*; pub use crate::receipt::*; diff --git a/core-rust/state-manager/src/mempool/mempool_manager.rs b/core-rust/state-manager/src/mempool/mempool_manager.rs index d939622449..57735c5894 100644 --- a/core-rust/state-manager/src/mempool/mempool_manager.rs +++ b/core-rust/state-manager/src/mempool/mempool_manager.rs @@ -97,6 +97,7 @@ pub struct MempoolManager { pending_transaction_result_cache: RwLock, /// WARNING: Be sure to take out this lock in the correct order, as per the [`MempoolManager`] doc. committability_validator: Arc, + moratorium_manager: Arc, metrics: MempoolManagerMetrics, } @@ -107,6 +108,7 @@ impl MempoolManager { relay_dispatcher: MempoolRelayDispatcher, pending_transaction_result_cache: RwLock, committability_validator: Arc, + moratorium_manager: Arc, metric_registry: &MetricRegistry, ) -> Self { Self { @@ -114,6 +116,7 @@ impl MempoolManager { relay_dispatcher: Some(relay_dispatcher), pending_transaction_result_cache, committability_validator, + moratorium_manager, metrics: MempoolManagerMetrics::new(metric_registry), } } @@ -123,6 +126,7 @@ impl MempoolManager { mempool: RwLock, pending_transaction_result_cache: RwLock, committability_validator: Arc, + moratorium_manager: Arc, metric_registry: &MetricRegistry, ) -> Self { Self { @@ -130,16 +134,36 @@ impl MempoolManager { relay_dispatcher: None, pending_transaction_result_cache, committability_validator, + moratorium_manager, metrics: MempoolManagerMetrics::new(metric_registry), } } + /// Checks the policy using a fresh read of the committed epoch. + pub fn ensure_user_transactions_allowed(&self) -> Result<(), UserTransactionMoratorium> { + self.ensure_user_transactions_allowed_at_epoch( + self.committability_validator.current_epoch(), + ) + } + + pub(crate) fn ensure_user_transactions_allowed_at_epoch( + &self, + epoch: Epoch, + ) -> Result<(), UserTransactionMoratorium> { + self.moratorium_manager + .ensure_user_transactions_allowed(epoch) + } + + /// Suppresses all mempool entries from proposals during a moratorium. pub fn get_proposal_transactions( &self, max_count: usize, max_payload_size_bytes: u64, user_payload_hashes_to_exclude: &HashSet, ) -> Vec> { + if self.ensure_user_transactions_allowed().is_err() { + return Vec::new(); + } self.mempool.read().get_proposal_transactions( max_count, max_payload_size_bytes, @@ -152,12 +176,15 @@ impl MempoolManager { } /// Picks a random subset of transactions to be relayed via a mempool sync. - /// Obeys the given count/size limits. + /// Obeys the given count/size limits. Returns none during a moratorium. pub fn get_relay_transactions( &self, max_count: usize, max_payload_size_bytes: u64, ) -> Vec> { + if self.ensure_user_transactions_allowed().is_err() { + return Vec::new(); + } // TODO: Definitely a better algorithm could be used here, especially with extra information like: // which peer/peers are we sending this to? or what do we know about said peer to have in it's mempool? // However (NOTE/WARN): changing transactions selection without careful consideration of the peer selection, @@ -290,6 +317,17 @@ impl MempoolManager { raw_transaction: RawNotarizedTransaction, force_recalculate: bool, ) -> Result, MempoolAddError> { + // STEP 0 - Reject before validation and caching so the payload can be + // retried when the moratorium ends. + if let Err(moratorium) = self.ensure_user_transactions_allowed() { + return Err(MempoolAddError::Rejected( + Box::new(MempoolAddRejection::for_user_transaction_moratorium( + moratorium, + )), + None, + )); + } + // STEP 1 - We prepare the transaction to check it's in the right structure and so we have hashes to work with let prepared = match self .committability_validator @@ -300,7 +338,9 @@ impl MempoolManager { // If the transaction fails to prepare at this point then we don't even have a hash to assign against it, // so we can't cache anything - just return an error return Err(MempoolAddError::Rejected( - MempoolAddRejection::for_static_rejection(prepare_error.into()), + Box::new(MempoolAddRejection::for_static_rejection( + prepare_error.into(), + )), None, )); } @@ -381,7 +421,7 @@ impl MempoolManager { force_recalculate, ) { - return (record, CheckMetadata::Cached); + return (*record, CheckMetadata::Cached); } let metadata = TransactionMetadata::read_from_prepared(&prepared); @@ -451,7 +491,7 @@ impl MempoolManager { { return ShouldRecalculate::Yes; } - return ShouldRecalculate::No(record); + return ShouldRecalculate::No(Box::new(record)); } } @@ -549,9 +589,10 @@ enum ForceRecalculation { enum ShouldRecalculate { Yes, - No(PendingTransactionRecord), + No(Box), } +#[allow(clippy::large_enum_variant)] pub enum CheckMetadata { Cached, Fresh(StaticValidation), @@ -566,6 +607,7 @@ impl CheckMetadata { } } +#[allow(clippy::large_enum_variant)] pub enum StaticValidation { Valid { executable: ExecutableTransaction, diff --git a/core-rust/state-manager/src/mempool/metrics.rs b/core-rust/state-manager/src/mempool/metrics.rs index 1d1a050a22..b23229cf4c 100644 --- a/core-rust/state-manager/src/mempool/metrics.rs +++ b/core-rust/state-manager/src/mempool/metrics.rs @@ -156,6 +156,7 @@ impl MetricLabel for MempoolAddResult { MempoolRejectionReason::SubintentAlreadyFinalized(_) => "SubintentAlreadyFinalized", MempoolRejectionReason::FromExecution(_) => "ExecutionError", MempoolRejectionReason::ValidationError(_) => "ValidationError", + MempoolRejectionReason::UserTransactionMoratorium(_) => "UserTransactionMoratorium", }, Some(MempoolAddError::Duplicate(_)) => "Duplicate", } diff --git a/core-rust/state-manager/src/mempool/mod.rs b/core-rust/state-manager/src/mempool/mod.rs index d0be55506c..52f8008360 100644 --- a/core-rust/state-manager/src/mempool/mod.rs +++ b/core-rust/state-manager/src/mempool/mod.rs @@ -89,7 +89,7 @@ pub enum MempoolAddError { tip_basis_points: u32, }, Duplicate(NotarizedTransactionHash), - Rejected(MempoolAddRejection, Option), + Rejected(Box, Option), } #[derive(Debug, Clone)] @@ -114,6 +114,17 @@ impl MempoolAddRejection { } } + /// Rejects temporarily, with retry allowed from the moratorium's end epoch. + pub fn for_user_transaction_moratorium(moratorium: UserTransactionMoratorium) -> Self { + Self { + retry_from: RetryFrom::FromEpoch(moratorium.to_exclusive), + reason: MempoolRejectionReason::UserTransactionMoratorium(moratorium), + against_state: AtState::Static, + was_cached: false, + invalid_from_epoch: None, + } + } + pub fn is_permanent_for_payload(&self) -> bool { self.reason.is_permanent_for_payload(&self.against_state) } @@ -131,9 +142,9 @@ impl MempoolAddRejection { impl<'a> ContextualDisplay> for MempoolAddError { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter<'_>, context: &ScryptoValueDisplayContext<'a>, ) -> Result<(), Self::Error> { match self { diff --git a/core-rust/state-manager/src/mempool/pending_transaction_result_cache.rs b/core-rust/state-manager/src/mempool/pending_transaction_result_cache.rs index e1a3413542..cc7b0b3adb 100644 --- a/core-rust/state-manager/src/mempool/pending_transaction_result_cache.rs +++ b/core-rust/state-manager/src/mempool/pending_transaction_result_cache.rs @@ -16,6 +16,8 @@ pub enum MempoolRejectionReason { SubintentAlreadyFinalized(SubintentAlreadyFinalizedError), FromExecution(Box), ValidationError(TransactionValidationError), + /// A temporary policy refusal which must never enter the transaction cache. + UserTransactionMoratorium(UserTransactionMoratorium), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -45,6 +47,7 @@ impl MempoolRejectionReason { MempoolRejectionReason::SubintentAlreadyFinalized(_) => false, MempoolRejectionReason::FromExecution(_) => true, MempoolRejectionReason::ValidationError(_) => false, + MempoolRejectionReason::UserTransactionMoratorium(_) => false, } } @@ -77,6 +80,7 @@ impl MempoolRejectionReason { MempoolRejectionReason::SubintentAlreadyFinalized(_) => None, MempoolRejectionReason::FromExecution(_) => None, MempoolRejectionReason::ValidationError(_) => None, + MempoolRejectionReason::UserTransactionMoratorium(_) => None, } } @@ -155,6 +159,13 @@ impl MempoolRejectionReason { RejectionPermanence::PermanentForAnyPayloadWithThisTransactionIntent } }, + MempoolRejectionReason::UserTransactionMoratorium(moratorium) => { + RejectionPermanence::Temporary { + retry: RetrySettings::FromEpoch { + epoch: moratorium.to_exclusive, + }, + } + } } } } @@ -212,9 +223,9 @@ pub enum RetrySettings { impl<'a> ContextualDisplay> for MempoolRejectionReason { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter<'_>, context: &ScryptoValueDisplayContext<'a>, ) -> Result<(), Self::Error> { match self { @@ -230,6 +241,13 @@ impl<'a> ContextualDisplay> for MempoolRejectionR MempoolRejectionReason::ValidationError(validation_error) => { write!(f, "Validation Error: {validation_error:?}") } + MempoolRejectionReason::UserTransactionMoratorium(moratorium) => { + write!( + f, + "User transactions are temporarily not accepted; retry from epoch {}", + moratorium.to_exclusive.number(), + ) + } } } } @@ -467,26 +485,26 @@ impl PendingTransactionRecord { pub fn should_accept_into_mempool( self, check: CheckMetadata, - ) -> Result { + ) -> Result> { if let Some(permanent_rejection) = self.earliest_permanent_rejection { - return Err(MempoolAddRejection { + return Err(Box::new(MempoolAddRejection { reason: permanent_rejection.rejection.unwrap(), against_state: permanent_rejection.against_state, retry_from: self.retry_from, was_cached: check.was_cached(), invalid_from_epoch: self.intent_invalid_from_epoch, - }); + })); } if let Some(rejection_reason) = self.latest_attempt.rejection { // Regardless of whether it was a rejection against committed or prepared state, // let's block it from coming into our mempool for a while - return Err(MempoolAddRejection { + return Err(Box::new(MempoolAddRejection { reason: rejection_reason, against_state: self.latest_attempt.against_state, retry_from: self.retry_from, was_cached: check.was_cached(), invalid_from_epoch: self.intent_invalid_from_epoch, - }); + })); } match check { CheckMetadata::Cached => { diff --git a/core-rust/state-manager/src/mempool/priority_mempool.rs b/core-rust/state-manager/src/mempool/priority_mempool.rs index 9926cf1d49..04a423b350 100644 --- a/core-rust/state-manager/src/mempool/priority_mempool.rs +++ b/core-rust/state-manager/src/mempool/priority_mempool.rs @@ -648,7 +648,7 @@ mod tests { tip_percentage, }) .manifest(ManifestBuilder::new_v1().build()) - .sign(&Ed25519PrivateKey::from_u64(signer_discriminator).unwrap()) + .sign(Ed25519PrivateKey::from_u64(signer_discriminator).unwrap()) .notarize(¬ary) .build() .to_raw() diff --git a/core-rust/state-manager/src/moratorium_manager.rs b/core-rust/state-manager/src/moratorium_manager.rs new file mode 100644 index 0000000000..9d6721f81e --- /dev/null +++ b/core-rust/state-manager/src/moratorium_manager.rs @@ -0,0 +1,106 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +use crate::prelude::*; + +/// Evaluates moratoriums using the caller's epoch. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UserTransactionMoratoriumManager { + moratoriums: Vec, +} + +impl UserTransactionMoratoriumManager { + pub fn new(moratoriums: Vec) -> Self { + Self { moratoriums } + } + + /// Returns the active moratorium as an error, or `Ok(())` if none applies. + pub fn ensure_user_transactions_allowed( + &self, + epoch: Epoch, + ) -> Result<(), UserTransactionMoratorium> { + match self.moratoriums.iter().find(|range| range.matches(epoch)) { + Some(moratorium) => Err(*moratorium), + None => Ok(()), + } + } +} + +/// An epoch range during which user transactions are refused. +#[derive(Debug, Clone, Copy, PartialEq, Eq, ScryptoSbor)] +pub struct UserTransactionMoratorium { + /// The first committed epoch during which user transactions are refused. + pub from_inclusive: Epoch, + /// The first committed epoch from which clients may retry submissions. + pub to_exclusive: Epoch, +} + +impl UserTransactionMoratorium { + pub fn matches(&self, epoch: Epoch) -> bool { + self.from_inclusive <= epoch && epoch < self.to_exclusive + } +} + +#[cfg(test)] +mod test; diff --git a/core-rust/state-manager/src/moratorium_manager/test.rs b/core-rust/state-manager/src/moratorium_manager/test.rs new file mode 100644 index 0000000000..46e385fead --- /dev/null +++ b/core-rust/state-manager/src/moratorium_manager/test.rs @@ -0,0 +1,475 @@ +use super::*; +use crate::test::*; + +/// A no-op update used to isolate the moratorium policy from engine changes. +const MORATORIUM_TEST_PROTOCOL_VERSION: &str = "test-moratorium"; + +fn moratorium_test_protocol_version() -> ProtocolVersionName { + ProtocolVersionName::of(MORATORIUM_TEST_PROTOCOL_VERSION).unwrap() +} + +/// Schedules a no-op update at the moratorium's end. +fn state_manager_config_with_moratorium_trigger( + moratorium_from_epoch: Epoch, + enactment_epoch: Epoch, +) -> StateManagerConfig { + let mut state_manager_config = + StateManagerConfig::new_for_testing(tempfile::tempdir().unwrap().path().to_str().unwrap()); + state_manager_config.protocol_config = ProtocolConfig { + user_transaction_moratoriums: vec![UserTransactionMoratorium { + from_inclusive: moratorium_from_epoch, + to_exclusive: enactment_epoch, + }], + ..ProtocolConfig::new_with_triggers(hashmap! { + moratorium_test_protocol_version() => + ProtocolUpdateEnactmentCondition::EnactAtStartOfEpochUnconditionally(enactment_epoch) + }) + }; + state_manager_config +} + +/// Creates a transaction valid throughout the test epochs. +fn create_raw_user_transaction() -> RawNotarizedTransaction { + create_raw_user_transaction_with_nonce(1) +} + +/// Gives concurrent submissions distinct hashes. +fn create_raw_user_transaction_with_nonce(nonce: u32) -> RawNotarizedTransaction { + let notary = Ed25519PrivateKey::from_u64(7).unwrap(); + TransactionV1Builder::new() + .header(TransactionHeaderV1 { + network_id: NetworkDefinition::simulator().id, + start_epoch_inclusive: Epoch::of(1), + end_epoch_exclusive: Epoch::of(100), + nonce, + notary_public_key: notary.public_key().into(), + notary_is_signatory: true, + tip_percentage: 0, + }) + .manifest(ManifestBuilder::new_v1().lock_fee_from_faucet().build()) + .notarize(¬ary) + .build() + .to_raw() + .unwrap() +} + +#[test] +fn moratorium_trigger_enacts_update_at_start_of_enactment_epoch_and_lifts_moratorium() { + // Arrange + let state_manager_config = + state_manager_config_with_moratorium_trigger(Epoch::of(2), Epoch::of(3)); + let state_manager = + create_bootstrapped_state_manager_with_rounds_per_epoch(state_manager_config, 1); + let moratorium_before_enactment = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + + // Act + let (prepare_result, _commit_summary) = prepare_and_commit_round_update(&state_manager); + let moratorium_after_enactment_commit = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + state_manager.apply_known_pending_protocol_updates(); + + // Assert + assert_eq!( + moratorium_before_enactment, + Err(UserTransactionMoratorium { + from_inclusive: Epoch::of(2), + to_exclusive: Epoch::of(3), + }) + ); + assert_eq!(prepare_result.next_epoch.unwrap().epoch, Epoch::of(3)); + assert_eq!( + prepare_result.next_protocol_version, + Some(moratorium_test_protocol_version()) + ); + assert_eq!(moratorium_after_enactment_commit, Ok(())); + assert_eq!( + state_manager.protocol_manager.current_protocol_version(), + moratorium_test_protocol_version() + ); +} + +#[test] +fn moratorium_is_absent_before_its_starting_epoch_and_present_from_it() { + // Arrange + let state_manager_config = + state_manager_config_with_moratorium_trigger(Epoch::of(3), Epoch::of(4)); + let state_manager = + create_bootstrapped_state_manager_with_rounds_per_epoch(state_manager_config, 1); + let moratorium_at_epoch_two = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + + // Act + let (prepare_result, _commit_summary) = prepare_and_commit_round_update(&state_manager); + let moratorium_at_epoch_three = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + + // Assert + assert_eq!(moratorium_at_epoch_two, Ok(())); + assert_eq!(prepare_result.next_epoch.unwrap().epoch, Epoch::of(3)); + assert_eq!(prepare_result.next_protocol_version, None); + assert_eq!( + moratorium_at_epoch_three, + Err(UserTransactionMoratorium { + from_inclusive: Epoch::of(3), + to_exclusive: Epoch::of(4), + }) + ); +} + +#[test] +fn mempool_rejects_user_transactions_and_proposes_none_while_moratorium_is_in_force() { + // Arrange + let state_manager_config = + state_manager_config_with_moratorium_trigger(Epoch::of(2), Epoch::of(3)); + let state_manager = + create_bootstrapped_state_manager_with_rounds_per_epoch(state_manager_config, 1); + let raw_transaction = create_raw_user_transaction(); + + // Act + let add_result = state_manager.mempool_manager.add_if_committable( + MempoolAddSource::CoreApi, + raw_transaction, + false, + ); + let proposal_transactions = + state_manager + .mempool_manager + .get_proposal_transactions(10, 1_000_000, &HashSet::new()); + + // Assert + let (rejection, notarized_transaction_hash) = match add_result { + Err(MempoolAddError::Rejected(rejection, notarized_transaction_hash)) => { + (rejection, notarized_transaction_hash) + } + Err(other_error) => panic!("unexpected mempool error: {other_error:?}"), + Ok(_) => panic!("the transaction was unexpectedly admitted to the mempool"), + }; + assert_eq!( + rejection.reason, + MempoolRejectionReason::UserTransactionMoratorium(UserTransactionMoratorium { + from_inclusive: Epoch::of(2), + to_exclusive: Epoch::of(3), + }) + ); + assert!(matches!( + rejection.retry_from, + RetryFrom::FromEpoch(epoch) if epoch == Epoch::of(3) + )); + assert!(!rejection.is_permanent_for_payload()); + assert!(!rejection.is_permanent_for_intent()); + assert_eq!(notarized_transaction_hash, None); + assert!(proposal_transactions.is_empty()); + assert_eq!(state_manager.mempool_manager.get_mempool_count(), 0); +} + +#[test] +fn multi_epoch_moratorium_stays_in_force_across_epoch_changes_until_enactment() { + // Arrange + let state_manager_config = + state_manager_config_with_moratorium_trigger(Epoch::of(2), Epoch::of(4)); + let state_manager = + create_bootstrapped_state_manager_with_rounds_per_epoch(state_manager_config, 1); + let moratorium_at_epoch_two = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + + // Act + let (first_prepare_result, _) = prepare_and_commit_round_update(&state_manager); + let moratorium_at_epoch_three = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + let (second_prepare_result, _) = prepare_and_commit_round_update(&state_manager); + let moratorium_at_epoch_four = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + + // Assert + let expected_moratorium = Err(UserTransactionMoratorium { + from_inclusive: Epoch::of(2), + to_exclusive: Epoch::of(4), + }); + assert_eq!(moratorium_at_epoch_two, expected_moratorium); + assert_eq!(first_prepare_result.next_epoch.unwrap().epoch, Epoch::of(3)); + assert_eq!(first_prepare_result.next_protocol_version, None); + assert_eq!(moratorium_at_epoch_three, expected_moratorium); + assert_eq!( + second_prepare_result.next_epoch.unwrap().epoch, + Epoch::of(4) + ); + assert_eq!( + second_prepare_result.next_protocol_version, + Some(moratorium_test_protocol_version()) + ); + assert_eq!(moratorium_at_epoch_four, Ok(())); +} + +#[test] +fn mempool_relays_and_proposes_nothing_during_moratorium_but_keeps_earlier_transactions() { + // Arrange + let state_manager_config = + state_manager_config_with_moratorium_trigger(Epoch::of(3), Epoch::of(4)); + let state_manager = + create_bootstrapped_state_manager_with_rounds_per_epoch(state_manager_config, 1); + state_manager + .mempool_manager + .add_if_committable( + MempoolAddSource::CoreApi, + create_raw_user_transaction(), + false, + ) + .expect("the transaction should be admitted before the moratorium starts"); + let relayed_before_moratorium = state_manager + .mempool_manager + .get_relay_transactions(10, 1_000_000) + .len(); + + // Act + prepare_and_commit_round_update(&state_manager); + let relayed_during_moratorium = state_manager + .mempool_manager + .get_relay_transactions(10, 1_000_000) + .len(); + let proposed_during_moratorium = state_manager + .mempool_manager + .get_proposal_transactions(10, 1_000_000, &HashSet::new()) + .len(); + let held_during_moratorium = state_manager.mempool_manager.get_mempool_count(); + prepare_and_commit_round_update(&state_manager); + state_manager.apply_known_pending_protocol_updates(); + let relayed_after_enactment = state_manager + .mempool_manager + .get_relay_transactions(10, 1_000_000) + .len(); + let proposed_after_enactment = state_manager + .mempool_manager + .get_proposal_transactions(10, 1_000_000, &HashSet::new()) + .len(); + + // Assert + assert_eq!(relayed_before_moratorium, 1); + assert_eq!(relayed_during_moratorium, 0); + assert_eq!(proposed_during_moratorium, 0); + assert_eq!(held_during_moratorium, 1); + assert_eq!(relayed_after_enactment, 1); + assert_eq!(proposed_after_enactment, 1); +} + +#[test] +#[should_panic(expected = "protocol misconfiguration")] +fn boot_fails_when_moratorium_trigger_enactment_epoch_was_already_passed_without_enacting() { + // Arrange + let state_manager_config = + StateManagerConfig::new_for_testing(tempfile::tempdir().unwrap().path().to_str().unwrap()); + let state_manager = + create_bootstrapped_state_manager_with_rounds_per_epoch(state_manager_config, 1); + commit_round_updates_until_epoch(&state_manager, Epoch::of(4)); + let network_definition = NetworkDefinition::simulator(); + let genesis_data_resolver: Arc = + Arc::new(FixedGenesisDataResolver::new(JavaGenesisData::new_from( + BabylonSettings::test_default(), + vec![], + ))); + let scenarios_execution_config = ScenariosExecutionConfig::default(); + let triggers = vec![ProtocolUpdateTrigger::of( + moratorium_test_protocol_version(), + ProtocolUpdateEnactmentCondition::EnactAtStartOfEpochUnconditionally(Epoch::of(3)), + )]; + + // Act + let _ = ProtocolState::compute_initial( + &ProtocolUpdateContentOverrides::empty().into(), + ProtocolUpdateContext { + network: &network_definition, + database: &state_manager.database, + genesis_data_resolver: &genesis_data_resolver, + scenario_config: &scenarios_execution_config, + }, + &triggers, + ); + + // Assert + unreachable!("compute_initial must panic when an enactment epoch has been passed unenacted"); +} + +#[test] +fn epoch_changing_round_update_drops_every_proposed_user_transaction() { + // Arrange + let state_manager_config = + StateManagerConfig::new_for_testing(tempfile::tempdir().unwrap().path().to_str().unwrap()); + let state_manager = + create_bootstrapped_state_manager_with_rounds_per_epoch(state_manager_config, 1); + let database = state_manager.database.access_direct(); + let latest_proof: LedgerProof = database.get_latest_proof().unwrap(); + let latest_epoch_proof: LedgerProof = database.get_latest_epoch_proof().unwrap(); + let (_, top_identifiers) = database.get_top_transaction_identifiers().unwrap(); + let next_epoch = latest_epoch_proof + .ledger_header + .next_epoch + .as_ref() + .unwrap(); + let proposer_address = next_epoch.validator_set.first().unwrap().address; + drop(database); + + // Act + let prepare_result = state_manager.preparator.prepare(PrepareRequest { + committed_ledger_hashes: top_identifiers.resultant_ledger_hashes, + ancestor_transactions: vec![], + ancestor_ledger_hashes: top_identifiers.resultant_ledger_hashes, + proposed_transactions: vec![create_raw_user_transaction()], + round_history: RoundHistory { + is_fallback: false, + epoch: next_epoch.epoch, + round: Round::of(1), + gap_round_leader_addresses: vec![], + proposer_address, + proposer_timestamp_ms: latest_proof.ledger_header.proposer_timestamp_ms, + }, + }); + + // Assert + assert!(prepare_result.next_epoch.is_some()); + assert_eq!(prepare_result.committed.len(), 1); + assert_eq!(prepare_result.committed[0].index, None); + assert!(prepare_result.rejected.is_empty()); +} + +#[test] +fn transaction_rejected_during_moratorium_is_admitted_and_proposed_once_it_is_lifted() { + // Arrange + let state_manager_config = + state_manager_config_with_moratorium_trigger(Epoch::of(2), Epoch::of(3)); + let state_manager = + create_bootstrapped_state_manager_with_rounds_per_epoch(state_manager_config, 1); + let raw_transaction = create_raw_user_transaction(); + let rejected_during_moratorium = state_manager + .mempool_manager + .add_if_committable(MempoolAddSource::CoreApi, raw_transaction.clone(), false) + .is_err(); + + // Act + prepare_and_commit_round_update(&state_manager); + state_manager.apply_known_pending_protocol_updates(); + let admitted = state_manager + .mempool_manager + .add_if_committable(MempoolAddSource::CoreApi, raw_transaction.clone(), false) + .expect("the transaction should be admitted once the moratorium is lifted"); + let proposed = state_manager + .mempool_manager + .get_proposal_transactions(10, 1_000_000, &HashSet::new()) + .iter() + .map(|transaction| transaction.raw.clone()) + .collect::>(); + + // Assert + assert!(rejected_during_moratorium); + assert_eq!( + state_manager + .mempool_manager + .ensure_user_transactions_allowed(), + Ok(()) + ); + assert_eq!(admitted.raw, raw_transaction); + assert_eq!(proposed, vec![raw_transaction]); +} + +#[test] +fn moratorium_stays_active_until_the_epoch_transition_is_committed() { + // Arrange + let config = state_manager_config_with_moratorium_trigger(Epoch::of(3), Epoch::of(4)); + let state_manager = create_bootstrapped_state_manager_with_rounds_per_epoch(config, 1); + let transaction = create_raw_user_transaction(); + state_manager + .mempool_manager + .add_if_committable(MempoolAddSource::CoreApi, transaction.clone(), false) + .unwrap(); + prepare_and_commit_round_update(&state_manager); + let committed_version = state_manager.database.snapshot().max_state_version(); + let (paused_sender, paused_receiver) = std::sync::mpsc::channel(); + let (resume_sender, resume_receiver) = std::sync::mpsc::channel(); + state_manager + .committer + .before_next_database_commit(move || { + paused_sender.send(()).unwrap(); + resume_receiver + .recv_timeout(Duration::from_secs(30)) + .unwrap(); + }); + let committing_state_manager = state_manager.clone(); + let submitted_while_paused = create_raw_user_transaction_with_nonce(2); + + // Act + let worker = + std::thread::spawn(move || prepare_and_commit_round_update(&committing_state_manager)); + paused_receiver + .recv_timeout(Duration::from_secs(30)) + .unwrap(); + let moratorium_while_paused = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + let relay_while_paused = state_manager + .mempool_manager + .get_relay_transactions(10, 1_000_000); + let proposals_while_paused = + state_manager + .mempool_manager + .get_proposal_transactions(10, 1_000_000, &HashSet::new()); + let admission_while_paused = state_manager.mempool_manager.add_if_committable( + MempoolAddSource::CoreApi, + submitted_while_paused.clone(), + false, + ); + let snapshot_while_paused = state_manager.database.snapshot(); + let epoch_while_paused = snapshot_while_paused.get_epoch_and_round().0; + let version_while_paused = snapshot_while_paused.max_state_version(); + drop(snapshot_while_paused); + resume_sender.send(()).unwrap(); + let (prepare_result, _) = worker.join().unwrap(); + state_manager.apply_known_pending_protocol_updates(); + let moratorium_after_commit = state_manager + .mempool_manager + .ensure_user_transactions_allowed(); + let relayed_after_commit = state_manager + .mempool_manager + .get_relay_transactions(10, 1_000_000) + .iter() + .map(|entry| entry.raw.clone()) + .collect::>(); + + // Assert + assert_eq!(epoch_while_paused, Epoch::of(3)); + assert_eq!(version_while_paused, committed_version); + assert_eq!( + moratorium_while_paused, + Err(UserTransactionMoratorium { + from_inclusive: Epoch::of(3), + to_exclusive: Epoch::of(4), + }) + ); + assert!(relay_while_paused.is_empty()); + assert!(proposals_while_paused.is_empty()); + assert_eq!(prepare_result.next_epoch.unwrap().epoch, Epoch::of(4)); + assert_eq!(moratorium_after_commit, Ok(())); + assert_eq!(relayed_after_commit, vec![transaction]); + let Err(MempoolAddError::Rejected(rejection, payload_hash)) = admission_while_paused else { + panic!("the paused commit must not admit a new user transaction"); + }; + assert_eq!( + rejection.reason, + MempoolRejectionReason::UserTransactionMoratorium(UserTransactionMoratorium { + from_inclusive: Epoch::of(3), + to_exclusive: Epoch::of(4) + }) + ); + assert_eq!(payload_hash, None); + assert_eq!(rejection.retry_from, RetryFrom::FromEpoch(Epoch::of(4))); + assert!(!rejection.is_permanent_for_payload()); + assert!(!rejection.is_permanent_for_intent()); +} + +mod policy; +mod schedule; diff --git a/core-rust/state-manager/src/moratorium_manager/test/policy.rs b/core-rust/state-manager/src/moratorium_manager/test/policy.rs new file mode 100644 index 0000000000..e2c6fcb06c --- /dev/null +++ b/core-rust/state-manager/src/moratorium_manager/test/policy.rs @@ -0,0 +1,51 @@ +use super::*; + +#[test] +fn rejects_only_epochs_inside_the_inclusive_exclusive_range() { + // Arrange + let config = state_manager_config_with_moratorium_trigger(Epoch::of(3), Epoch::of(5)); + let manager = + UserTransactionMoratoriumManager::new(config.protocol_config.user_transaction_moratoriums); + let moratorium = UserTransactionMoratorium { + from_inclusive: Epoch::of(3), + to_exclusive: Epoch::of(5), + }; + + // Act + let results = + [2, 3, 4, 5, 6].map(|epoch| manager.ensure_user_transactions_allowed(Epoch::of(epoch))); + + // Assert + assert_eq!( + results, + [Ok(()), Err(moratorium), Err(moratorium), Ok(()), Ok(())] + ); +} + +#[test] +fn mainnet_has_the_incident_range_and_stokenet_has_no_moratorium() { + // Arrange + let mainnet = resolve_protocol_config(&NetworkDefinition::mainnet()); + let stokenet = resolve_protocol_config(&NetworkDefinition::stokenet()); + + // Act + let mainnet_manager = + UserTransactionMoratoriumManager::new(mainnet.user_transaction_moratoriums); + let stokenet_manager = + UserTransactionMoratoriumManager::new(stokenet.user_transaction_moratoriums); + let active = mainnet_manager.ensure_user_transactions_allowed(Epoch::of(339897)); + let enacted = mainnet_manager.ensure_user_transactions_allowed(Epoch::of(339898)); + let stokenet_check = stokenet_manager.ensure_user_transactions_allowed(Epoch::of(339897)); + + // Assert + assert_eq!( + active, + Err(UserTransactionMoratorium { + from_inclusive: Epoch::of(339897), + to_exclusive: Epoch::of(339898), + }) + ); + assert_eq!(enacted, Ok(())); + assert_eq!(stokenet_check, Ok(())); + assert_eq!(stokenet_manager.moratoriums, Vec::new()); +} diff --git a/core-rust/state-manager/src/moratorium_manager/test/schedule.rs b/core-rust/state-manager/src/moratorium_manager/test/schedule.rs new file mode 100644 index 0000000000..fb26b94d47 --- /dev/null +++ b/core-rust/state-manager/src/moratorium_manager/test/schedule.rs @@ -0,0 +1,180 @@ +use super::*; + +#[test] +fn fresh_node_syncs_historical_and_upcoming_moratoriums() { + // Arrange + let historical = UserTransactionMoratorium { + from_inclusive: Epoch::of(2), + to_exclusive: Epoch::of(3), + }; + let upcoming = UserTransactionMoratorium { + from_inclusive: Epoch::of(4), + to_exclusive: Epoch::of(5), + }; + let directory = tempfile::tempdir().unwrap(); + let protocol_config = protocol_config_with_schedule([historical, upcoming]); + let source_config = StateManagerConfig { + protocol_config: ProtocolConfig { + user_transaction_moratoriums: Vec::new(), + ..protocol_config.clone() + }, + ..StateManagerConfig::new_for_testing(directory.path().join("source").to_str().unwrap()) + }; + let source = create_bootstrapped_state_manager_with_rounds_per_epoch(source_config, 1); + commit_round_updates_until_epoch(&source, Epoch::of(4)); + let target_config = StateManagerConfig { + protocol_config, + ..StateManagerConfig::new_for_testing(directory.path().join("target").to_str().unwrap()) + }; + + // Act + let target = create_bootstrapped_state_manager_with_rounds_per_epoch(target_config, 1); + let policy_before_catch_up = target.mempool_manager.ensure_user_transactions_allowed(); + sync_available_history(&source, &target); + let policy_during_upcoming_moratorium = + target.mempool_manager.ensure_user_transactions_allowed(); + let caught_up_header = target + .database + .snapshot() + .get_latest_proof() + .unwrap() + .ledger_header; + let source_header = source + .database + .snapshot() + .get_latest_proof() + .unwrap() + .ledger_header; + commit_round_updates_until_epoch(&source, Epoch::of(5)); + sync_available_history(&source, &target); + let policy_after_enactment = target.mempool_manager.ensure_user_transactions_allowed(); + let final_header = target + .database + .snapshot() + .get_latest_proof() + .unwrap() + .ledger_header; + let final_source_header = source + .database + .snapshot() + .get_latest_proof() + .unwrap() + .ledger_header; + + // Assert + assert_eq!(policy_before_catch_up, Err(historical)); + assert_eq!(policy_during_upcoming_moratorium, Err(upcoming)); + assert_eq!(caught_up_header, source_header); + assert_eq!(policy_after_enactment, Ok(())); + assert_eq!(final_header, final_source_header); + assert_eq!( + target.committability_validator.current_epoch(), + Epoch::of(5) + ); + assert_eq!( + target.protocol_manager.current_protocol_version(), + ProtocolVersionName::of("test-range-1").unwrap() + ); +} + +#[test] +fn accepts_disjoint_ranges_in_reverse_epoch_order() { + // Arrange + let first = UserTransactionMoratorium { + from_inclusive: Epoch::of(2), + to_exclusive: Epoch::of(3), + }; + let second = UserTransactionMoratorium { + from_inclusive: Epoch::of(4), + to_exclusive: Epoch::of(5), + }; + let config = protocol_config_with_schedule([second, first]); + + // Act + let manager = UserTransactionMoratoriumManager::new(config.user_transaction_moratoriums); + let results = + [1, 2, 3, 4, 5, 6].map(|epoch| manager.ensure_user_transactions_allowed(Epoch::of(epoch))); + + // Assert + assert_eq!( + results, + [Ok(()), Err(first), Ok(()), Err(second), Ok(()), Ok(())] + ); +} + +#[test] +fn touching_ranges_switch_policy_at_the_shared_epoch() { + // Arrange + let first = UserTransactionMoratorium { + from_inclusive: Epoch::of(2), + to_exclusive: Epoch::of(3), + }; + let second = UserTransactionMoratorium { + from_inclusive: Epoch::of(3), + to_exclusive: Epoch::of(4), + }; + let config = protocol_config_with_schedule([first, second]); + + // Act + let manager = UserTransactionMoratoriumManager::new(config.user_transaction_moratoriums); + let results = [2, 3, 4].map(|epoch| manager.ensure_user_transactions_allowed(Epoch::of(epoch))); + + // Assert + assert_eq!(results, [Err(first), Err(second), Ok(())]); +} + +/// Associates each configured range with a distinct no-op protocol update. +fn protocol_config_with_schedule( + moratoriums: impl IntoIterator, +) -> ProtocolConfig { + let moratoriums = moratoriums.into_iter().collect::>(); + let config = ProtocolConfig::new_with_triggers(moratoriums.iter().enumerate().map( + |(index, moratorium)| { + ( + ProtocolVersionName::of(format!("test-range-{index}")).unwrap(), + ProtocolUpdateEnactmentCondition::EnactAtStartOfEpochUnconditionally( + moratorium.to_exclusive, + ), + ) + }, + )); + ProtocolConfig { + user_transaction_moratoriums: moratoriums, + ..config + } +} + +/// Uses served ledger proofs and normal commits, resuming each protocol update. +fn sync_available_history(source: &StateManager, target: &StateManager) { + let source_state_version = source.database.snapshot().max_state_version(); + while target.database.snapshot().max_state_version() < source_state_version { + let next_state_version = target + .database + .snapshot() + .max_state_version() + .next() + .unwrap(); + let TxnsAndProof { txns, proof } = source + .database + .snapshot() + .get_syncable_txns_and_proof(next_state_version, 100, 1_000_000) + .unwrap(); + let expected_protocol_version = proof.ledger_header.next_protocol_version.clone(); + target + .committer + .commit(CommitRequest { + transactions: txns, + proof, + vertex_store: None, + self_validator_id: None, + }) + .unwrap(); + if let Some(expected_protocol_version) = expected_protocol_version { + target.apply_known_pending_protocol_updates(); + assert_eq!( + target.protocol_manager.current_protocol_version(), + expected_protocol_version + ); + } + } +} diff --git a/core-rust/state-manager/src/protocol/protocol_config.rs b/core-rust/state-manager/src/protocol/protocol_config.rs index c0b820c8e0..b8f2c0e2f7 100644 --- a/core-rust/state-manager/src/protocol/protocol_config.rs +++ b/core-rust/state-manager/src/protocol/protocol_config.rs @@ -1,4 +1,5 @@ use crate::engine_prelude::*; +use crate::moratorium_manager::UserTransactionMoratorium; use crate::protocol::*; @@ -12,6 +13,8 @@ const ANEMONE_PROTOCOL_VERSION: &str = "anemone"; const BOTTLENOSE_PROTOCOL_VERSION: &str = "bottlenose"; const CUTTLEFISH_PART1_PROTOCOL_VERSION: &str = "cuttlefish"; const CUTTLEFISH_PART2_PROTOCOL_VERSION: &str = "cuttlefish-part2"; +const DUGONG_PROTOCOL_VERSION: &str = "dugong"; +const EAGLE_RAY_PROTOCOL_VERSION: &str = "eagle-ray"; pub enum ResolvedProtocolVersion { Babylon, @@ -19,6 +22,8 @@ pub enum ResolvedProtocolVersion { Bottlenose, CuttlefishPart1, CuttlefishPart2, + Dugong, + EagleRay, Custom(ProtocolVersionName), Test(ProtocolVersionName), } @@ -31,6 +36,8 @@ impl ResolvedProtocolVersion { BOTTLENOSE_PROTOCOL_VERSION => Some(ResolvedProtocolVersion::Bottlenose), CUTTLEFISH_PART1_PROTOCOL_VERSION => Some(ResolvedProtocolVersion::CuttlefishPart1), CUTTLEFISH_PART2_PROTOCOL_VERSION => Some(ResolvedProtocolVersion::CuttlefishPart2), + DUGONG_PROTOCOL_VERSION => Some(ResolvedProtocolVersion::Dugong), + EAGLE_RAY_PROTOCOL_VERSION => Some(ResolvedProtocolVersion::EagleRay), // Updates starting "custom-" are intended for use with tests, where the thresholds and config are injected on all nodes name_string if CustomProtocolUpdateDefinition::matches(name_string) => Some( ResolvedProtocolVersion::Custom(protocol_version_name.clone()), @@ -50,6 +57,8 @@ impl ResolvedProtocolVersion { ResolvedProtocolVersion::Bottlenose => Some(ProtocolVersion::Bottlenose), ResolvedProtocolVersion::CuttlefishPart1 => Some(ProtocolVersion::CuttlefishPart1), ResolvedProtocolVersion::CuttlefishPart2 => Some(ProtocolVersion::CuttlefishPart2), + ResolvedProtocolVersion::Dugong => Some(ProtocolVersion::Dugong), + ResolvedProtocolVersion::EagleRay => Some(ProtocolVersion::EagleRay), ResolvedProtocolVersion::Custom { .. } => None, ResolvedProtocolVersion::Test { .. } => None, } @@ -66,6 +75,8 @@ impl ResolvedProtocolVersion { ResolvedProtocolVersion::CuttlefishPart2 => { Box::new(CuttlefishPart2ProtocolUpdateDefinition) } + ResolvedProtocolVersion::Dugong => Box::new(DugongProtocolUpdateDefinition), + ResolvedProtocolVersion::EagleRay => Box::new(EagleRayProtocolUpdateDefinition), ResolvedProtocolVersion::Custom(..) => Box::new(CustomProtocolUpdateDefinition), ResolvedProtocolVersion::Test(name) => { Box::new(TestProtocolUpdateDefinition::new(name.clone())) @@ -95,6 +106,8 @@ pub struct ProtocolConfig { /// the definition of the protocol update, and if nodes use different overrides, they will execute /// different updates and need manual recovery. pub protocol_update_content_overrides: RawProtocolUpdateContentOverrides, + /// Moratorium ranges, independent of protocol enactment triggers. + pub user_transaction_moratoriums: Vec, } impl ProtocolConfig { @@ -102,8 +115,16 @@ impl ProtocolConfig { Self::new_with_triggers([]) } + /// Leaves the moratorium schedule empty. pub fn new_with_triggers( triggers: impl IntoIterator, + ) -> Self { + Self::new_with_triggers_and_moratoriums(triggers, []) + } + + pub fn new_with_triggers_and_moratoriums( + triggers: impl IntoIterator, + moratoriums: impl IntoIterator, ) -> Self { Self { protocol_update_triggers: triggers @@ -113,6 +134,7 @@ impl ProtocolConfig { }) .collect(), protocol_update_content_overrides: ProtocolUpdateContentOverrides::empty().into(), + user_transaction_moratoriums: moratoriums.into_iter().collect(), } } @@ -196,6 +218,14 @@ impl ProtocolVersionName { Self::of(CUTTLEFISH_PART2_PROTOCOL_VERSION).unwrap() } + pub fn dugong() -> Self { + Self::of(DUGONG_PROTOCOL_VERSION).unwrap() + } + + pub fn eagle_ray() -> Self { + Self::of(EAGLE_RAY_PROTOCOL_VERSION).unwrap() + } + pub fn for_engine(version: ProtocolVersion) -> Self { match version { ProtocolVersion::Unbootstrapped => panic!("Unbootstrapped is not supported!"), @@ -204,6 +234,8 @@ impl ProtocolVersionName { ProtocolVersion::Bottlenose => Self::bottlenose(), ProtocolVersion::CuttlefishPart1 => Self::cuttlefish_part1(), ProtocolVersion::CuttlefishPart2 => Self::cuttlefish_part2(), + ProtocolVersion::Dugong => Self::dugong(), + ProtocolVersion::EagleRay => Self::eagle_ray(), } } @@ -394,5 +426,6 @@ pub struct SignalledReadinessThreshold { /// "enact immediately at the beginning of an epoch on or above the threshold" /// - a value of 1 means: /// "enact at the beginning of the _next_ epoch (if it still has enough support)" + #[allow(clippy::doc_overindented_list_items)] pub required_consecutive_completed_epochs_of_support: u64, } diff --git a/core-rust/state-manager/src/protocol/protocol_configs/config_printer.rs b/core-rust/state-manager/src/protocol/protocol_configs/config_printer.rs index 4a674eba69..f7d135893d 100644 --- a/core-rust/state-manager/src/protocol/protocol_configs/config_printer.rs +++ b/core-rust/state-manager/src/protocol/protocol_configs/config_printer.rs @@ -19,7 +19,7 @@ use super::*; #[test] fn print_fixed_config_code() { // This is used for stokenet and dumunet - let version = ProtocolVersionName::cuttlefish_part2(); + let version = ProtocolVersionName::eagle_ray(); let start_epoch = Epoch::of(1); let end_epoch = Epoch::of(10000000); let thresholds = vec![SignalledReadinessThreshold { diff --git a/core-rust/state-manager/src/protocol/protocol_configs/dumunet_protocol_config.rs b/core-rust/state-manager/src/protocol/protocol_configs/dumunet_protocol_config.rs index d38a16689e..0c04196ecb 100644 --- a/core-rust/state-manager/src/protocol/protocol_configs/dumunet_protocol_config.rs +++ b/core-rust/state-manager/src/protocol/protocol_configs/dumunet_protocol_config.rs @@ -47,5 +47,19 @@ pub fn dumunet_protocol_config() -> ProtocolConfig { ProtocolVersionName::cuttlefish_part2() => EnactImmediatelyAfterEndOfProtocolUpdate { trigger_after: ProtocolVersionName::cuttlefish_part1(), }, + ProtocolVersionName::eagle_ray() => EnactAtStartOfEpochIfValidatorsReady { + // ================================================================= + // PROTOCOL_VERSION: "eagle-ray" + // READINESS_SIGNAL: "8ed71bbdf45861cb0000000eagle-ray" + // ================================================================= + lower_bound_inclusive: Epoch::of(1), + upper_bound_exclusive: Epoch::of(10000000), + readiness_thresholds: vec![ + SignalledReadinessThreshold { + required_ratio_of_stake_supported: dec!(0.8), + required_consecutive_completed_epochs_of_support: 10, + }, + ], + }, }) } diff --git a/core-rust/state-manager/src/protocol/protocol_configs/mainnet_protocol_config.rs b/core-rust/state-manager/src/protocol/protocol_configs/mainnet_protocol_config.rs index 8d65cc902e..a21e034010 100644 --- a/core-rust/state-manager/src/protocol/protocol_configs/mainnet_protocol_config.rs +++ b/core-rust/state-manager/src/protocol/protocol_configs/mainnet_protocol_config.rs @@ -1,70 +1,85 @@ use crate::engine_prelude::*; +use crate::moratorium_manager::UserTransactionMoratorium; use crate::protocol::*; use ProtocolUpdateEnactmentCondition::*; pub fn mainnet_protocol_config() -> ProtocolConfig { + const EAGLE_RAY_ENACTMENT_EPOCH: u64 = 339898; + // See config_printer.rs > print_calculated_protocol_config_code() - ProtocolConfig::new_with_triggers(hashmap! { - ProtocolVersionName::anemone() => EnactAtStartOfEpochIfValidatorsReady { - // ================================================================= - // PROTOCOL_VERSION: "anemone" - // READINESS_SIGNAL: "220e2a4a4e86e3e6000000000anemone" - // ================================================================= - // The below estimates are based off: - // - Calculating relative to epoch 66516 - // - Using that epoch 66516 started at 2024-01-24T14:05:57.229Z - // - Assuming epoch length will be 5 mins - // ================================================================= - lower_bound_inclusive: Epoch::of(70019), // estimated: 2024-02-05T18:00:57.229Z - upper_bound_exclusive: Epoch::of(74051), // estimated: 2024-02-19T18:00:57.229Z - readiness_thresholds: vec![ - SignalledReadinessThreshold { - required_ratio_of_stake_supported: dec!(0.75), - required_consecutive_completed_epochs_of_support: 1152, // estimated: 4 days - }, - ], - }, - ProtocolVersionName::bottlenose() => EnactAtStartOfEpochIfValidatorsReady { - // ================================================================= - // PROTOCOL_VERSION: "bottlenose" - // READINESS_SIGNAL: "86894b9104afb73a000000bottlenose" - // ================================================================= - // The below estimates are based off: - // - Calculating relative to epoch 97091 - // - Using that epoch 97091 started at 2024-05-09T18:01:00.000Z - // - Assuming epoch length will be 5 mins - // ================================================================= - lower_bound_inclusive: Epoch::of(104291), // estimated: 2024-06-03T18:01:00.000Z - upper_bound_exclusive: Epoch::of(112355), // estimated: 2024-07-01T18:01:00.000Z - readiness_thresholds: vec![ - SignalledReadinessThreshold { - required_ratio_of_stake_supported: dec!(0.75), - required_consecutive_completed_epochs_of_support: 4032, // estimated: 2 weeks - }, - ], - }, - ProtocolVersionName::cuttlefish_part1() => EnactAtStartOfEpochIfValidatorsReady { - // ================================================================= - // PROTOCOL_VERSION: "cuttlefish" - // READINESS_SIGNAL: "96e00440adafe5e2000000cuttlefish" - // ================================================================= - // The below estimates are based off: - // - Calculating relative to epoch 150729 - // - Using that epoch 150729 started at 2024-11-13T01:18:58.703Z - // - Assuming epoch length will be 5 mins - // ================================================================= - lower_bound_inclusive: Epoch::of(158682), // estimated: 2024-12-10T16:03:58.703Z - upper_bound_exclusive: Epoch::of(161562), // estimated: 2024-12-20T16:03:58.703Z - readiness_thresholds: vec![ - SignalledReadinessThreshold { - required_ratio_of_stake_supported: dec!(0.75), - required_consecutive_completed_epochs_of_support: 4032, // estimated: 2 weeks - }, - ], - }, - ProtocolVersionName::cuttlefish_part2() => EnactImmediatelyAfterEndOfProtocolUpdate { - trigger_after: ProtocolVersionName::cuttlefish_part1(), + ProtocolConfig::new_with_triggers_and_moratoriums( + hashmap! { + ProtocolVersionName::anemone() => EnactAtStartOfEpochIfValidatorsReady { + // ================================================================= + // PROTOCOL_VERSION: "anemone" + // READINESS_SIGNAL: "220e2a4a4e86e3e6000000000anemone" + // ================================================================= + // The below estimates are based off: + // - Calculating relative to epoch 66516 + // - Using that epoch 66516 started at 2024-01-24T14:05:57.229Z + // - Assuming epoch length will be 5 mins + // ================================================================= + lower_bound_inclusive: Epoch::of(70019), // estimated: 2024-02-05T18:00:57.229Z + upper_bound_exclusive: Epoch::of(74051), // estimated: 2024-02-19T18:00:57.229Z + readiness_thresholds: vec![ + SignalledReadinessThreshold { + required_ratio_of_stake_supported: dec!(0.75), + required_consecutive_completed_epochs_of_support: 1152, // estimated: 4 days + }, + ], + }, + ProtocolVersionName::bottlenose() => EnactAtStartOfEpochIfValidatorsReady { + // ================================================================= + // PROTOCOL_VERSION: "bottlenose" + // READINESS_SIGNAL: "86894b9104afb73a000000bottlenose" + // ================================================================= + // The below estimates are based off: + // - Calculating relative to epoch 97091 + // - Using that epoch 97091 started at 2024-05-09T18:01:00.000Z + // - Assuming epoch length will be 5 mins + // ================================================================= + lower_bound_inclusive: Epoch::of(104291), // estimated: 2024-06-03T18:01:00.000Z + upper_bound_exclusive: Epoch::of(112355), // estimated: 2024-07-01T18:01:00.000Z + readiness_thresholds: vec![ + SignalledReadinessThreshold { + required_ratio_of_stake_supported: dec!(0.75), + required_consecutive_completed_epochs_of_support: 4032, // estimated: 2 weeks + }, + ], + }, + ProtocolVersionName::cuttlefish_part1() => EnactAtStartOfEpochIfValidatorsReady { + // ================================================================= + // PROTOCOL_VERSION: "cuttlefish" + // READINESS_SIGNAL: "96e00440adafe5e2000000cuttlefish" + // ================================================================= + // The below estimates are based off: + // - Calculating relative to epoch 150729 + // - Using that epoch 150729 started at 2024-11-13T01:18:58.703Z + // - Assuming epoch length will be 5 mins + // ================================================================= + lower_bound_inclusive: Epoch::of(158682), // estimated: 2024-12-10T16:03:58.703Z + upper_bound_exclusive: Epoch::of(161562), // estimated: 2024-12-20T16:03:58.703Z + readiness_thresholds: vec![ + SignalledReadinessThreshold { + required_ratio_of_stake_supported: dec!(0.75), + required_consecutive_completed_epochs_of_support: 4032, // estimated: 2 weeks + }, + ], + }, + ProtocolVersionName::cuttlefish_part2() => EnactImmediatelyAfterEndOfProtocolUpdate { + trigger_after: ProtocolVersionName::cuttlefish_part1(), + }, + ProtocolVersionName::eagle_ray() => EnactAtStartOfEpochUnconditionally( + Epoch::of(EAGLE_RAY_ENACTMENT_EPOCH), + ), }, - }) + [ + // Mainnet incident of 1 September 2026. + UserTransactionMoratorium { + from_inclusive: Epoch::of(339897), + to_exclusive: Epoch::of(EAGLE_RAY_ENACTMENT_EPOCH), + }, + ], + ) } diff --git a/core-rust/state-manager/src/protocol/protocol_configs/stokenet_protocol_config.rs b/core-rust/state-manager/src/protocol/protocol_configs/stokenet_protocol_config.rs index d399d8c3c3..c841c48354 100644 --- a/core-rust/state-manager/src/protocol/protocol_configs/stokenet_protocol_config.rs +++ b/core-rust/state-manager/src/protocol/protocol_configs/stokenet_protocol_config.rs @@ -58,5 +58,19 @@ pub fn stokenet_protocol_config() -> ProtocolConfig { }, ], }, + ProtocolVersionName::eagle_ray() => EnactAtStartOfEpochIfValidatorsReady { + // ================================================================= + // PROTOCOL_VERSION: "eagle-ray" + // READINESS_SIGNAL: "8ed71bbdf45861cb0000000eagle-ray" + // ================================================================= + lower_bound_inclusive: Epoch::of(1), + upper_bound_exclusive: Epoch::of(10000000), + readiness_thresholds: vec![ + SignalledReadinessThreshold { + required_ratio_of_stake_supported: dec!(0.8), + required_consecutive_completed_epochs_of_support: 10, + }, + ], + }, }) } diff --git a/core-rust/state-manager/src/protocol/protocol_configs/testnet_protocol_config.rs b/core-rust/state-manager/src/protocol/protocol_configs/testnet_protocol_config.rs index a5078e7d48..9bfe053e2b 100644 --- a/core-rust/state-manager/src/protocol/protocol_configs/testnet_protocol_config.rs +++ b/core-rust/state-manager/src/protocol/protocol_configs/testnet_protocol_config.rs @@ -19,5 +19,8 @@ pub fn testnet_protocol_config() -> ProtocolConfig { ProtocolVersionName::cuttlefish_part2() => EnactImmediatelyAfterEndOfProtocolUpdate { trigger_after: ProtocolVersionName::cuttlefish_part1(), }, + ProtocolVersionName::eagle_ray() => EnactImmediatelyAfterEndOfProtocolUpdate { + trigger_after: ProtocolVersionName::cuttlefish_part2(), + }, }) } diff --git a/core-rust/state-manager/src/protocol/protocol_updates/definitions/dugong_definition.rs b/core-rust/state-manager/src/protocol/protocol_updates/definitions/dugong_definition.rs new file mode 100644 index 0000000000..a28f2e66ad --- /dev/null +++ b/core-rust/state-manager/src/protocol/protocol_updates/definitions/dugong_definition.rs @@ -0,0 +1,20 @@ +use crate::prelude::*; + +pub struct DugongProtocolUpdateDefinition; + +impl ProtocolUpdateDefinition for DugongProtocolUpdateDefinition { + type Overrides = DugongSettings; + + fn create_batch_generator( + &self, + context: ProtocolUpdateContext, + overrides_hash: Option, + overrides: Option, + ) -> Box { + Box::new(create_default_generator_with_scenarios( + context, + overrides_hash, + overrides, + )) + } +} diff --git a/core-rust/state-manager/src/protocol/protocol_updates/definitions/eagle_ray_definition.rs b/core-rust/state-manager/src/protocol/protocol_updates/definitions/eagle_ray_definition.rs new file mode 100644 index 0000000000..8046cab078 --- /dev/null +++ b/core-rust/state-manager/src/protocol/protocol_updates/definitions/eagle_ray_definition.rs @@ -0,0 +1,20 @@ +use crate::prelude::*; + +pub struct EagleRayProtocolUpdateDefinition; + +impl ProtocolUpdateDefinition for EagleRayProtocolUpdateDefinition { + type Overrides = EagleRaySettings; + + fn create_batch_generator( + &self, + context: ProtocolUpdateContext, + overrides_hash: Option, + overrides: Option, + ) -> Box { + Box::new(create_default_generator_with_scenarios( + context, + overrides_hash, + overrides, + )) + } +} diff --git a/core-rust/state-manager/src/protocol/protocol_updates/definitions/mod.rs b/core-rust/state-manager/src/protocol/protocol_updates/definitions/mod.rs index c518a4f7e3..5b2c2c8f03 100644 --- a/core-rust/state-manager/src/protocol/protocol_updates/definitions/mod.rs +++ b/core-rust/state-manager/src/protocol/protocol_updates/definitions/mod.rs @@ -4,6 +4,8 @@ mod bottlenose_definition; mod custom_definition; mod cuttlefish_part1_definition; mod cuttlefish_part2_definition; +mod dugong_definition; +mod eagle_ray_definition; mod test_definition; pub use anemone_definition::*; @@ -12,4 +14,6 @@ pub use bottlenose_definition::*; pub use custom_definition::*; pub use cuttlefish_part1_definition::*; pub use cuttlefish_part2_definition::*; +pub use dugong_definition::*; +pub use eagle_ray_definition::*; pub use test_definition::*; diff --git a/core-rust/state-manager/src/protocol/protocol_updates/protocol_content_overrides.rs b/core-rust/state-manager/src/protocol/protocol_updates/protocol_content_overrides.rs index f23830666c..87e701033b 100644 --- a/core-rust/state-manager/src/protocol/protocol_updates/protocol_content_overrides.rs +++ b/core-rust/state-manager/src/protocol/protocol_updates/protocol_content_overrides.rs @@ -12,6 +12,7 @@ pub struct ProtocolUpdateContentOverrides { bottlenose: Option>, cuttlefish_part1: Option>, cuttlefish_part2: Option>, + eagle_ray: Option>, custom: HashMap>, } @@ -54,6 +55,11 @@ impl ProtocolUpdateContentOverrides { self } + pub fn with_eagle_ray(mut self, config: Overrides) -> Self { + self.eagle_ray = Some(config); + self + } + pub fn with_custom( mut self, custom_name: ProtocolVersionName, @@ -104,6 +110,12 @@ impl From for RawProtocolUpdateContentOverrides scrypto_encode(&config).unwrap(), ); } + if let Some(config) = value.eagle_ray { + map.insert( + ProtocolVersionName::eagle_ray(), + scrypto_encode(&config).unwrap(), + ); + } for (update_name, config) in value.custom { if CustomProtocolUpdateDefinition::matches(update_name.as_str()) { diff --git a/core-rust/state-manager/src/staging/result.rs b/core-rust/state-manager/src/staging/result.rs index 66576604b8..e7ee3434e6 100644 --- a/core-rust/state-manager/src/staging/result.rs +++ b/core-rust/state-manager/src/staging/result.rs @@ -65,8 +65,8 @@ use crate::prelude::*; pub enum ProcessedTransactionReceipt { - Commit(ProcessedCommitResult), - Reject(ProcessedRejectResult), + Commit(Box), + Reject(Box), Abort(AbortResult), } @@ -99,7 +99,7 @@ impl ProcessedTransactionReceipt { ) -> Self { match receipt.result { TransactionResult::Commit(commit) => { - ProcessedTransactionReceipt::Commit(ProcessedCommitResult::process( + ProcessedTransactionReceipt::Commit(Box::new(ProcessedCommitResult::process( hash_update_context, commit, ExecutionFeeData { @@ -107,13 +107,13 @@ impl ProcessedTransactionReceipt { engine_costing_parameters: receipt.costing_parameters, transaction_costing_parameters: receipt.transaction_costing_parameters, }, - )) + ))) } TransactionResult::Reject(reject) => { - ProcessedTransactionReceipt::Reject(ProcessedRejectResult { + ProcessedTransactionReceipt::Reject(Box::new(ProcessedRejectResult { result: reject, fee_summary: receipt.fee_summary, - }) + })) } TransactionResult::Abort(abort) => ProcessedTransactionReceipt::Abort(abort), } @@ -134,7 +134,7 @@ impl ProcessedTransactionReceipt { pub fn expect_commit_or_reject( &self, description: &impl Display, - ) -> Result<&ProcessedCommitResult, ProcessedRejectResult> { + ) -> Result<&ProcessedCommitResult, Box> { match self { ProcessedTransactionReceipt::Commit(commit) => Ok(commit), ProcessedTransactionReceipt::Reject(reject) => Err(reject.clone()), diff --git a/core-rust/state-manager/src/state_manager.rs b/core-rust/state-manager/src/state_manager.rs index 8ea176878b..d1ba59840e 100644 --- a/core-rust/state-manager/src/state_manager.rs +++ b/core-rust/state-manager/src/state_manager.rs @@ -223,6 +223,9 @@ impl StateManager { let database = Arc::new(lock_factory.named("database").new_db_lock(raw_db)); + let moratorium_manager = Arc::new(UserTransactionMoratoriumManager::new( + protocol_config.user_transaction_moratoriums, + )); let formatter = Arc::new(Formatter::new(&network_definition)); let transaction_validator = Arc::new(lock_factory.named("validator").new_rwlock( @@ -274,6 +277,7 @@ impl StateManager { mempool, pending_transaction_result_cache, committability_validator.clone(), + moratorium_manager, metrics_registry, ), Some(mempool_relay_dispatcher) => MempoolManager::new( @@ -281,6 +285,7 @@ impl StateManager { mempool_relay_dispatcher, pending_transaction_result_cache, committability_validator.clone(), + moratorium_manager, metrics_registry, ), }); diff --git a/core-rust/state-manager/src/store/historical_state.rs b/core-rust/state-manager/src/store/historical_state.rs index eec95a1bbe..259aa429b8 100644 --- a/core-rust/state-manager/src/store/historical_state.rs +++ b/core-rust/state-manager/src/store/historical_state.rs @@ -777,6 +777,7 @@ mod tests { }) } + #[allow(mismatched_lifetime_syntaxes)] pub fn create_subject( &self, at_state_version: StateVersion, diff --git a/core-rust/state-manager/src/store/jmt_gc.rs b/core-rust/state-manager/src/store/jmt_gc.rs index aa2094dd18..0f23dc686a 100644 --- a/core-rust/state-manager/src/store/jmt_gc.rs +++ b/core-rust/state-manager/src/store/jmt_gc.rs @@ -465,6 +465,7 @@ mod tests { hex::decode(string.replace(' ', "")).unwrap() } + #[allow(clippy::manual_is_multiple_of)] fn nibbles(string: &str) -> NibblePath { let mut string = string.replace(' ', ""); if string.len() % 2 == 0 { diff --git a/core-rust/state-manager/src/transaction/preparation.rs b/core-rust/state-manager/src/transaction/preparation.rs index 31f180296e..e21386dfd6 100644 --- a/core-rust/state-manager/src/transaction/preparation.rs +++ b/core-rust/state-manager/src/transaction/preparation.rs @@ -141,7 +141,7 @@ impl Preparator { loop { let next = scenario .next(previous_engine_receipt.as_ref()) - .map_err(|err| err.into_full(&scenario)) + .map_err(|err| err.into_full(scenario.as_ref())) .unwrap(); match next { NextAction::Transaction(next) => { @@ -525,10 +525,11 @@ impl Preparator { } } } - Err(ProcessedRejectResult { - result, - fee_summary, - }) => { + Err(reject) => { + let ProcessedRejectResult { + result, + fee_summary, + } = *reject; let error_message = format!("{:?}", &result.reason); pending_transaction_results.push(PendingTransactionResult { user_transaction_hashes: user_hashes.clone(), diff --git a/core-rust/state-manager/src/transaction/series_execution.rs b/core-rust/state-manager/src/transaction/series_execution.rs index dbba023dfe..34f6857d3f 100644 --- a/core-rust/state-manager/src/transaction/series_execution.rs +++ b/core-rust/state-manager/src/transaction/series_execution.rs @@ -137,7 +137,7 @@ where executable: &LedgerExecutable, hashes: &LedgerTransactionHashes, description: &str, - ) -> Result { + ) -> Result> { let result = self.execute_no_state_update(executable, hashes, description); if let Ok(commit) = &result { self.update_state(commit); @@ -156,7 +156,7 @@ where executable: &LedgerExecutable, hashes: &LedgerTransactionHashes, description: &str, - ) -> Result { + ) -> Result> { let described_ledger_transaction_hash = DescribedTransactionHash { ledger_hash: hashes.ledger_transaction_hash, description, @@ -175,7 +175,7 @@ where &mut self, described_ledger_transaction_hash: &DescribedTransactionHash, wrapped_executable: T, - ) -> Result { + ) -> Result> { let mut execution_cache = self.execution_cache_manager.access_exclusively(); let processed = execution_cache.execute_transaction( self.store, diff --git a/core/src/main/java/com/radixdlt/consensus/bft/BFTBuilder.java b/core/src/main/java/com/radixdlt/consensus/bft/BFTBuilder.java index 1f06396014..7303a73bbb 100644 --- a/core/src/main/java/com/radixdlt/consensus/bft/BFTBuilder.java +++ b/core/src/main/java/com/radixdlt/consensus/bft/BFTBuilder.java @@ -70,6 +70,7 @@ import com.radixdlt.consensus.bft.processor.BFTQuorumAssembler.TimeoutQuorumDelayedResolution; import com.radixdlt.consensus.liveness.Pacemaker; import com.radixdlt.consensus.liveness.ProposerElection; +import com.radixdlt.consensus.liveness.UserTransactionMoratoriumProvider; import com.radixdlt.consensus.safety.SafetyRules; import com.radixdlt.crypto.Hasher; import com.radixdlt.environment.EventDispatcher; @@ -104,6 +105,7 @@ public final class BFTBuilder { private TimeSupplier timeSupplier; private Metrics metrics; private Addressing addressing; + private UserTransactionMoratoriumProvider userTransactionMoratoriumProvider; private BFTBuilder() { // Just making this inaccessible @@ -203,6 +205,12 @@ public BFTBuilder addressing(Addressing addressing) { return this; } + public BFTBuilder userTransactionMoratoriumProvider( + UserTransactionMoratoriumProvider userTransactionMoratoriumProvider) { + this.userTransactionMoratoriumProvider = userTransactionMoratoriumProvider; + return this; + } + public BFTEventProcessor build() { if (!validatorSet.containsValidator(self)) { return EmptyBFTEventProcessor.INSTANCE; @@ -215,6 +223,7 @@ public BFTEventProcessor build() { -> OneProposalPerRoundVerifier (verify that max 1 genuine proposal is received for each round) -> SyncUpPreprocessor (if needed, sync up to match BFT event's round) -> BFTEventPostSyncUpVerifier (verifies that we've synced up to a correct round) + -> UserTransactionMoratoriumVerifier (rejects proposals with user transactions during a moratorium) -> ProposalTimestampVerifier (verify proposal timestamp) -> BFTQuorumAssembler (processes votes and forms a quorum) -> Pacemaker (manages sending proposals, votes and timeouts) */ @@ -235,8 +244,15 @@ public BFTEventProcessor build() { new ProposalTimestampVerifier( quorumAssembler, timeSupplier, metrics, addressing, proposalRejectedDispatcher); + final var userTransactionMoratoriumVerifier = + new UserTransactionMoratoriumVerifier( + proposalTimestampVerifier, + userTransactionMoratoriumProvider, + addressing, + proposalRejectedDispatcher); + final var postSyncUpVerifier = - new BFTEventPostSyncUpVerifier(proposalTimestampVerifier, metrics, roundUpdate); + new BFTEventPostSyncUpVerifier(userTransactionMoratoriumVerifier, metrics, roundUpdate); final var syncUpPreprocessor = new SyncUpPreprocessor(postSyncUpVerifier, bftSyncer, metrics, roundUpdate); diff --git a/core/src/main/java/com/radixdlt/consensus/bft/processor/UserTransactionMoratoriumVerifier.java b/core/src/main/java/com/radixdlt/consensus/bft/processor/UserTransactionMoratoriumVerifier.java new file mode 100644 index 0000000000..1f3bb03cde --- /dev/null +++ b/core/src/main/java/com/radixdlt/consensus/bft/processor/UserTransactionMoratoriumVerifier.java @@ -0,0 +1,130 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.consensus.bft.processor; + +import com.radixdlt.addressing.Addressing; +import com.radixdlt.consensus.Proposal; +import com.radixdlt.consensus.bft.ProposalRejected; +import com.radixdlt.consensus.liveness.UserTransactionMoratoriumProvider; +import com.radixdlt.environment.EventDispatcher; +import java.util.Objects; +import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * During a moratorium, rejects proposals carrying user transactions after BFT sync processes their + * certificates. Dispatches {@link ProposalRejected} to allow an empty fallback. + */ +public final class UserTransactionMoratoriumVerifier implements BFTEventProcessorAtCurrentRound { + private static final Logger log = LogManager.getLogger(); + + private final BFTEventProcessorAtCurrentRound forwardTo; + private final UserTransactionMoratoriumProvider userTransactionMoratoriumProvider; + private final Addressing addressing; + private final EventDispatcher proposalRejectedDispatcher; + + public UserTransactionMoratoriumVerifier( + BFTEventProcessorAtCurrentRound forwardTo, + UserTransactionMoratoriumProvider userTransactionMoratoriumProvider, + Addressing addressing, + EventDispatcher proposalRejectedDispatcher) { + this.forwardTo = Objects.requireNonNull(forwardTo); + this.userTransactionMoratoriumProvider = + Objects.requireNonNull(userTransactionMoratoriumProvider); + this.addressing = Objects.requireNonNull(addressing); + this.proposalRejectedDispatcher = Objects.requireNonNull(proposalRejectedDispatcher); + } + + @Override + public void processProposal(Proposal proposal) { + final var transactionCount = proposal.getVertex().getTransactions().size(); + if (transactionCount == 0) { + forwardTo.processProposal(proposal); + return; + } + + final var moratorium = + userTransactionMoratoriumProvider.ensureUserTransactionsAllowed(proposal.getEpoch()); + if (moratorium.isSuccess()) { + forwardTo.processProposal(proposal); + return; + } + + log.warn( + "Rejecting a proposal from {} at round {}: it carries {} user transaction(s) while a user" + + " transaction moratorium is in force until epoch {}", + addressing.encode(proposal.getAuthor().getValidatorAddress()), + proposal.getRound(), + transactionCount, + moratorium.unwrapError().toExclusive()); + proposalRejectedDispatcher.dispatch(new ProposalRejected(proposal.getRound())); + } + + @Override + public Optional forwardTo() { + return Optional.of(forwardTo); + } +} diff --git a/core/src/main/java/com/radixdlt/consensus/epoch/EpochsConsensusModule.java b/core/src/main/java/com/radixdlt/consensus/epoch/EpochsConsensusModule.java index 663f663c44..42dff2601a 100644 --- a/core/src/main/java/com/radixdlt/consensus/epoch/EpochsConsensusModule.java +++ b/core/src/main/java/com/radixdlt/consensus/epoch/EpochsConsensusModule.java @@ -99,6 +99,9 @@ protected void configure() { OptionalBinder.newOptionalBinder( binder(), EpochManager.class); // So that this is consistent with tests bind(EpochManager.class).in(Scopes.SINGLETON); + OptionalBinder.newOptionalBinder(binder(), UserTransactionMoratoriumProvider.class) + .setDefault() + .toInstance(UserTransactionMoratoriumProvider.NONE); var eventBinder = Multibinder.newSetBinder(binder(), new TypeLiteral>() {}, LocalEvents.class) .permitDuplicates(); @@ -314,6 +317,7 @@ EventProcessor initialEpochsTimeoutProcessor( private PacemakerFactory pacemakerFactory( Metrics metrics, ProposalGenerator proposalGenerator, + UserTransactionMoratoriumProvider userTransactionMoratoriumProvider, Hasher hasher, EventDispatcher timeoutEventDispatcher, ScheduledEventDispatcher> localTimeoutSender, @@ -339,6 +343,7 @@ private PacemakerFactory pacemakerFactory( localTimeoutSender.dispatch(new Epoched(epoch, scheduledTimeout), ms), timeoutCalculator, proposalGenerator, + userTransactionMoratoriumProvider, (n, m) -> { var nodeId = NodeId.fromPublicKey(n.getKey()); proposalDispatcher.dispatch(nodeId, m); @@ -366,7 +371,8 @@ private BFTFactory bftFactory( timeoutQuorumDelayedResolutionDispatcher, EventDispatcher doubleVoteEventDispatcher, EventDispatcher proposalRejectedDispatcher, - @TimeoutQuorumResolutionDelayMs long timeoutQuorumResolutionDelayMs) { + @TimeoutQuorumResolutionDelayMs long timeoutQuorumResolutionDelayMs, + UserTransactionMoratoriumProvider userTransactionMoratoriumProvider) { return (self, pacemaker, bftSyncer, @@ -378,6 +384,7 @@ private BFTFactory bftFactory( proposerElection) -> BFTBuilder.create() .self(self) + .userTransactionMoratoriumProvider(userTransactionMoratoriumProvider) .hasher(hasher) .verifier(verifier) .proposalRejectedDispatcher( diff --git a/core/src/main/java/com/radixdlt/consensus/liveness/Pacemaker.java b/core/src/main/java/com/radixdlt/consensus/liveness/Pacemaker.java index 7a4f15d5d6..0b991eaef8 100644 --- a/core/src/main/java/com/radixdlt/consensus/liveness/Pacemaker.java +++ b/core/src/main/java/com/radixdlt/consensus/liveness/Pacemaker.java @@ -104,6 +104,7 @@ private enum RoundStatus { private final ScheduledEventDispatcher scheduledLocalTimeoutDispatcher; private final PacemakerTimeoutCalculator timeoutCalculator; private final ProposalGenerator proposalGenerator; + private final UserTransactionMoratoriumProvider userTransactionMoratoriumProvider; private final Hasher hasher; private final RemoteEventDispatcher proposalDispatcher; private final RemoteEventDispatcher voteDispatcher; @@ -137,6 +138,7 @@ public Pacemaker( ScheduledEventDispatcher scheduledLocalTimeoutDispatcher, PacemakerTimeoutCalculator timeoutCalculator, ProposalGenerator proposalGenerator, + UserTransactionMoratoriumProvider userTransactionMoratoriumProvider, RemoteEventDispatcher proposalDispatcher, RemoteEventDispatcher voteDispatcher, EventDispatcher noVoteDispatcher, @@ -152,6 +154,8 @@ public Pacemaker( this.timeoutDispatcher = Objects.requireNonNull(timeoutDispatcher); this.timeoutCalculator = Objects.requireNonNull(timeoutCalculator); this.proposalGenerator = Objects.requireNonNull(proposalGenerator); + this.userTransactionMoratoriumProvider = + Objects.requireNonNull(userTransactionMoratoriumProvider); this.proposalDispatcher = Objects.requireNonNull(proposalDispatcher); this.voteDispatcher = Objects.requireNonNull(voteDispatcher); this.noVoteDispatcher = Objects.requireNonNull(noVoteDispatcher); @@ -270,6 +274,11 @@ In this case we might still want to send our (obsolete) vote } private void attemptVoteOnVertex(ExecutedVertex executedVertex) { + if (isVoteWithheldByUserTransactionMoratorium(executedVertex)) { + this.noVoteDispatcher.dispatch(new NoVote(executedVertex.getVertexWithHash())); + return; + } + final var bftHeader = new BFTHeader( executedVertex.getRound(), @@ -296,6 +305,31 @@ private void attemptVoteOnVertex(ExecutedVertex executedVertex) { () -> this.noVoteDispatcher.dispatch(new NoVote(executedVertex.getVertexWithHash()))); } + /** + * Blocks new votes for vertices carrying user transactions during a moratorium, including those + * received through BFT sync. Existing votes may still be resent with a timeout. + */ + private boolean isVoteWithheldByUserTransactionMoratorium(ExecutedVertex executedVertex) { + final var transactionCount = executedVertex.vertex().getTransactions().size(); + if (transactionCount == 0) { + return false; + } + final var moratorium = + this.userTransactionMoratoriumProvider.ensureUserTransactionsAllowed( + executedVertex.vertex().getEpoch()); + if (moratorium.isSuccess()) { + return false; + } + log.warn( + "Not voting for vertex {} at round {}: it carries {} user transaction(s) while a user" + + " transaction moratorium is in force until epoch {}", + executedVertex.getVertexHash(), + executedVertex.getRound(), + transactionCount, + moratorium.unwrapError().toExclusive()); + return true; + } + private void dispatchVote(Vote vote) { // The vote is sent to all if any timeout has occurred (even if the round was prolonged). // Note that a vote might include a timeout flag (f.e. in case of handling proposalRejected diff --git a/core/src/main/java/com/radixdlt/consensus/liveness/UserTransactionMoratoriumProvider.java b/core/src/main/java/com/radixdlt/consensus/liveness/UserTransactionMoratoriumProvider.java new file mode 100644 index 0000000000..43635e7c22 --- /dev/null +++ b/core/src/main/java/com/radixdlt/consensus/liveness/UserTransactionMoratoriumProvider.java @@ -0,0 +1,79 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.consensus.liveness; + +import com.radixdlt.lang.Result; +import com.radixdlt.lang.Tuple; +import com.radixdlt.protocol.UserTransactionMoratorium; + +/** Checks user transaction permission at the consensus event's epoch. */ +@FunctionalInterface +public interface UserTransactionMoratoriumProvider { + /** Default for setups without a state manager. */ + UserTransactionMoratoriumProvider NONE = epoch -> Result.success(Tuple.tuple()); + + /** Returns the active moratorium as an error, or success if none applies. */ + Result ensureUserTransactionsAllowed(long epoch); +} diff --git a/core/src/main/java/com/radixdlt/p2p/P2PModule.java b/core/src/main/java/com/radixdlt/p2p/P2PModule.java index 1fa9a80761..76fb7fa224 100644 --- a/core/src/main/java/com/radixdlt/p2p/P2PModule.java +++ b/core/src/main/java/com/radixdlt/p2p/P2PModule.java @@ -164,33 +164,34 @@ private StartProcessorOnRunner clearNearProtocolUpdateBans( return new StartProcessorOnRunner( Runners.P2P_NETWORK, () -> { - final var initialEpoch = latestProof.primaryProof().ledgerHeader().epoch().toLong(); - final var shouldClearAllBans = - initialProtocolState.pendingProtocolUpdates().values().stream() - .anyMatch( - pendingProtocolUpdate -> - switch (pendingProtocolUpdate - .protocolUpdateTrigger() - .enactmentCondition()) { - case EnactAtStartOfEpochIfValidatorsReady cond -> { - // Clear if we're within enactment bounds or one epoch before the - // lower bound - final var lower = cond.lowerBoundInclusive().toLong(); - final var upper = cond.upperBoundExclusive().toLong(); - yield initialEpoch >= (lower - 1) && initialEpoch < upper; - } - case EnactAtStartOfEpochUnconditionally cond -> { - final var updateEpoch = cond.epoch().toLong(); - // Clear if we're right before the update - yield initialEpoch == updateEpoch - 1; - } - case EnactImmediatelyAfterEndOfProtocolUpdate ignored -> - // Bans already cleared due to the preceding protocol update - false; - }); - if (shouldClearAllBans) { + // An epoch-change proof's header names the epoch that ended, not the current epoch. + final var initialEpoch = latestProof.resultantEpoch(); + if (shouldClearNearProtocolUpdateBans(initialProtocolState, initialEpoch)) { addressBook.clearAllBans(); } }); } + + static boolean shouldClearNearProtocolUpdateBans(ProtocolState protocolState, long currentEpoch) { + return protocolState.pendingProtocolUpdates().values().stream() + .anyMatch( + pendingProtocolUpdate -> + switch (pendingProtocolUpdate.protocolUpdateTrigger().enactmentCondition()) { + case EnactAtStartOfEpochIfValidatorsReady cond -> { + // Clear if we're within enactment bounds or one epoch before the lower bound + final var lower = cond.lowerBoundInclusive().toLong(); + final var upper = cond.upperBoundExclusive().toLong(); + yield currentEpoch >= (lower - 1) && currentEpoch < upper; + } + case EnactAtStartOfEpochUnconditionally cond -> { + final var updateEpoch = cond.epoch().toLong(); + // Clear if we're right before the update + yield currentEpoch == updateEpoch - 1; + } + + case EnactImmediatelyAfterEndOfProtocolUpdate ignored -> + // Bans already cleared due to the preceding protocol update + false; + }); + } } diff --git a/core/src/main/java/com/radixdlt/rev2/REv2StateComputer.java b/core/src/main/java/com/radixdlt/rev2/REv2StateComputer.java index de329ca119..00b5e9fac0 100644 --- a/core/src/main/java/com/radixdlt/rev2/REv2StateComputer.java +++ b/core/src/main/java/com/radixdlt/rev2/REv2StateComputer.java @@ -174,6 +174,10 @@ public void addToMempool(MempoolAdd mempoolAdd, NodeId origin) { @Override public List getTransactionsForProposal( List previousExecutedTransactions) { + final var moratorium = this.stateComputer.ensureUserTransactionsAllowed(); + if (moratorium.isError()) { + return List.of(); + } final var previousTransactionHashes = previousExecutedTransactions.stream() diff --git a/core/src/main/java/com/radixdlt/rev2/RustUserTransactionMoratoriumProvider.java b/core/src/main/java/com/radixdlt/rev2/RustUserTransactionMoratoriumProvider.java new file mode 100644 index 0000000000..f3dc1c1d22 --- /dev/null +++ b/core/src/main/java/com/radixdlt/rev2/RustUserTransactionMoratoriumProvider.java @@ -0,0 +1,88 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.rev2; + +import com.google.inject.Inject; +import com.radixdlt.consensus.liveness.UserTransactionMoratoriumProvider; +import com.radixdlt.lang.Result; +import com.radixdlt.lang.Tuple; +import com.radixdlt.protocol.UserTransactionMoratorium; +import com.radixdlt.statecomputer.RustStateComputer; +import java.util.Objects; + +public final class RustUserTransactionMoratoriumProvider + implements UserTransactionMoratoriumProvider { + private final RustStateComputer rustStateComputer; + + @Inject + public RustUserTransactionMoratoriumProvider(RustStateComputer rustStateComputer) { + this.rustStateComputer = Objects.requireNonNull(rustStateComputer); + } + + @Override + public Result ensureUserTransactionsAllowed(long epoch) { + return this.rustStateComputer.ensureUserTransactionsAllowed(epoch); + } +} diff --git a/core/src/main/java/com/radixdlt/rev2/modules/REv2StateManagerModule.java b/core/src/main/java/com/radixdlt/rev2/modules/REv2StateManagerModule.java index e5be73b17f..1171b4a6bb 100644 --- a/core/src/main/java/com/radixdlt/rev2/modules/REv2StateManagerModule.java +++ b/core/src/main/java/com/radixdlt/rev2/modules/REv2StateManagerModule.java @@ -65,10 +65,12 @@ package com.radixdlt.rev2.modules; import com.google.inject.*; +import com.google.inject.multibindings.OptionalBinder; import com.google.inject.multibindings.ProvidesIntoSet; import com.radixdlt.consensus.BFTConfiguration; import com.radixdlt.consensus.ProposalLimitsConfig; import com.radixdlt.consensus.bft.*; +import com.radixdlt.consensus.liveness.UserTransactionMoratoriumProvider; import com.radixdlt.consensus.vertexstore.PersistentVertexStore; import com.radixdlt.crypto.Hasher; import com.radixdlt.db.checkpoint.RustDbCheckpoints; @@ -205,6 +207,9 @@ public void configure() { bind(DatabaseConfig.class).toInstance(databaseConfig); bind(LedgerSyncLimitsConfig.class).toInstance(ledgerSyncLimitsConfig); bind(ProtocolConfig.class).toInstance(protocolConfig); + OptionalBinder.newOptionalBinder(binder(), UserTransactionMoratoriumProvider.class) + .setBinding() + .to(RustUserTransactionMoratoriumProvider.class); install(proposalLimitsConfig.asModule()); install( @@ -224,7 +229,9 @@ private NodeRustEnvironment stateManager( FatalPanicHandler fatalPanicHandler, Network network, @NodeStorageLocation DatabaseBackendConfig nodeDatabaseBackendConfig, - DatabaseConfig databaseConfig) { + DatabaseConfig databaseConfig, + // Injection lets restart tests replace the protocol configuration. + ProtocolConfig protocolConfig) { return new NodeRustEnvironment( genesisProvider, mempoolRelayDispatcher, diff --git a/core/src/test-core/java/com/radixdlt/api/core/generated/models/SystemVersion.java b/core/src/test-core/java/com/radixdlt/api/core/generated/models/SystemVersion.java index 2467188a9f..e12f3e146e 100644 --- a/core/src/test-core/java/com/radixdlt/api/core/generated/models/SystemVersion.java +++ b/core/src/test-core/java/com/radixdlt/api/core/generated/models/SystemVersion.java @@ -33,7 +33,11 @@ public enum SystemVersion { V2("V2"), - V3("V3"); + V3("V3"), + + V4("V4"), + + V5("V5"); private String value; @@ -61,4 +65,3 @@ public static SystemVersion fromValue(String value) { throw new IllegalArgumentException("Unexpected value '" + value + "'"); } } - diff --git a/core/src/test-core/java/com/radixdlt/environment/NoEpochsConsensusModule.java b/core/src/test-core/java/com/radixdlt/environment/NoEpochsConsensusModule.java index 55ed7447fc..fa3bae02e7 100644 --- a/core/src/test-core/java/com/radixdlt/environment/NoEpochsConsensusModule.java +++ b/core/src/test-core/java/com/radixdlt/environment/NoEpochsConsensusModule.java @@ -106,6 +106,9 @@ public void configure() { OptionalBinder.newOptionalBinder( binder(), EpochManager.class); // So that this is consistent with tests + OptionalBinder.newOptionalBinder(binder(), UserTransactionMoratoriumProvider.class) + .setDefault() + .toInstance(UserTransactionMoratoriumProvider.NONE); var eventBinder = Multibinder.newSetBinder(binder(), new TypeLiteral>() {}, LocalEvents.class) .permitDuplicates(); @@ -161,7 +164,8 @@ public BFTEventProcessor bftEventProcessor( EventDispatcher doubleVoteEventDispatcher, EventDispatcher proposalRejectedDispatcher, RoundUpdate roundUpdate, - @TimeoutQuorumResolutionDelayMs long timeoutQuorumResolutionDelayMs) { + @TimeoutQuorumResolutionDelayMs long timeoutQuorumResolutionDelayMs, + UserTransactionMoratoriumProvider userTransactionMoratoriumProvider) { /* TODO: consider cleaning this up (but most probably it's not worth it :)) This is a little hacky. @@ -198,6 +202,7 @@ present in the current validator set (so it won't be processing any events). .metrics(metrics) .addressing(addressing) .proposerElection(proposerElection) + .userTransactionMoratoriumProvider(userTransactionMoratoriumProvider) .build(); } @@ -232,6 +237,7 @@ private Pacemaker pacemaker( ScheduledEventDispatcher timeoutSender, PacemakerTimeoutCalculator timeoutCalculator, ProposalGenerator proposalGenerator, + UserTransactionMoratoriumProvider userTransactionMoratoriumProvider, Hasher hasher, RemoteEventDispatcher proposalDispatcher, RemoteEventDispatcher voteDispatcher, @@ -249,6 +255,7 @@ private Pacemaker pacemaker( timeoutSender, timeoutCalculator, proposalGenerator, + userTransactionMoratoriumProvider, (n, m) -> { var nodeId = NodeId.fromPublicKey(n.getKey()); proposalDispatcher.dispatch(nodeId, m); diff --git a/core/src/test-core/java/com/radixdlt/environment/deterministic/network/DeterministicNetwork.java b/core/src/test-core/java/com/radixdlt/environment/deterministic/network/DeterministicNetwork.java index c39ca3991c..e0abe5c386 100644 --- a/core/src/test-core/java/com/radixdlt/environment/deterministic/network/DeterministicNetwork.java +++ b/core/src/test-core/java/com/radixdlt/environment/deterministic/network/DeterministicNetwork.java @@ -179,6 +179,14 @@ public long currentTime() { return this.currentTime; } + /** Advances time without delivering queued messages. New messages use the advanced time. */ + public void advanceTime(long millis) { + if (millis < 0) { + throw new IllegalArgumentException("Simulated time can only move forward"); + } + this.currentTime += millis; + } + long delayForChannel(ChannelId channelId) { if (channelId.isLocal()) { return DEFAULT_LOCAL_LATENCY; diff --git a/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicNodes.java b/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicNodes.java index 13ab70d87f..eabe907780 100644 --- a/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicNodes.java +++ b/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicNodes.java @@ -107,7 +107,7 @@ public final class DeterministicNodes implements AutoCloseable { private final ControlledAddressBook addressBook; private final Map nodeConfigs; private final Module baseModule; - private final Module overrideModule; + private Module overrideModule; // Network private final DeterministicNetwork network; @@ -166,6 +166,14 @@ public Integer apply(NodeId nodeId) { } } + void setOverrideModule(Module overrideModule) { + this.overrideModule = overrideModule; + } + + Module getOverrideModule() { + return this.overrideModule; + } + private Injector createBFTInstance( int nodeIndex, Module baseModule, Module overrideModule, long time) { var config = this.nodeConfigs.get(nodeIndex); diff --git a/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicTest.java b/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicTest.java index 2d79cb7882..2b93a876a7 100644 --- a/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicTest.java +++ b/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicTest.java @@ -90,6 +90,7 @@ import com.radixdlt.rev2.ComponentAddress; import com.radixdlt.rev2.ScryptoConstants; import io.reactivex.rxjava3.schedulers.Timed; +import java.time.Duration; import java.util.*; import java.util.function.Predicate; import java.util.stream.Stream; @@ -313,11 +314,27 @@ public void startNode(int nodeIndex) { this.nodes.startNode(nodeIndex, this.network.currentTime()); } + /** Advances the clock observed by subsequently restarted nodes. */ + public void advanceTime(Duration duration) { + this.network.advanceTime(duration.toMillis()); + } + public void restartNode(int nodeIndex) { this.shutdownNode(nodeIndex); this.startNode(nodeIndex); } + public void restartNodeWithOverrideModule(int nodeIndex, Module overrideModule) { + this.shutdownNode(nodeIndex); + final var originalOverrideModule = this.nodes.getOverrideModule(); + this.nodes.setOverrideModule(overrideModule); + try { + this.startNode(nodeIndex); + } finally { + this.nodes.setOverrideModule(originalOverrideModule); + } + } + public void restartNodeWithConfig(int nodeIndex, PhysicalNodeConfig config) { this.shutdownNode(nodeIndex); this.nodes.setNodeConfig(nodeIndex, config); diff --git a/core/src/test/java/com/radixdlt/consensus/PacemakerGenerateProposalTest.java b/core/src/test/java/com/radixdlt/consensus/PacemakerGenerateProposalTest.java index 0870a16543..0df14b84f6 100644 --- a/core/src/test/java/com/radixdlt/consensus/PacemakerGenerateProposalTest.java +++ b/core/src/test/java/com/radixdlt/consensus/PacemakerGenerateProposalTest.java @@ -147,6 +147,7 @@ public void setup() { timeoutSender, timeoutCalculator, proposalGenerator, + UserTransactionMoratoriumProvider.NONE, proposalDispatcher, voteDispatcher, noVoteDispatcher, diff --git a/core/src/test/java/com/radixdlt/consensus/bft/processor/UserTransactionMoratoriumVerifierTest.java b/core/src/test/java/com/radixdlt/consensus/bft/processor/UserTransactionMoratoriumVerifierTest.java new file mode 100644 index 0000000000..42632a9906 --- /dev/null +++ b/core/src/test/java/com/radixdlt/consensus/bft/processor/UserTransactionMoratoriumVerifierTest.java @@ -0,0 +1,186 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.consensus.bft.processor; + +import static com.radixdlt.utils.TypedMocks.rmock; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +import com.google.common.primitives.Bytes; +import com.radixdlt.addressing.Addressing; +import com.radixdlt.consensus.Proposal; +import com.radixdlt.consensus.Vertex; +import com.radixdlt.consensus.bft.BFTValidatorId; +import com.radixdlt.consensus.bft.ProposalRejected; +import com.radixdlt.consensus.bft.Round; +import com.radixdlt.consensus.liveness.UserTransactionMoratoriumProvider; +import com.radixdlt.environment.EventDispatcher; +import com.radixdlt.lang.Result; +import com.radixdlt.lang.Tuple; +import com.radixdlt.networks.Network; +import com.radixdlt.protocol.UserTransactionMoratorium; +import com.radixdlt.rev2.ComponentAddress; +import com.radixdlt.transactions.RawNotarizedTransaction; +import com.radixdlt.utils.PrivateKeys; +import com.radixdlt.utils.UInt64; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +public final class UserTransactionMoratoriumVerifierTest { + private static final UserTransactionMoratorium MORATORIUM = + new UserTransactionMoratorium(UInt64.fromNonNegativeLong(3), UInt64.fromNonNegativeLong(4)); + private static final Round ROUND = Round.of(5); + + private BFTEventProcessorAtCurrentRound forwardTo; + private EventDispatcher proposalRejectedDispatcher; + private UserTransactionMoratoriumProvider provider; + + @Before + public void setup() { + this.forwardTo = mock(BFTEventProcessorAtCurrentRound.class); + this.proposalRejectedDispatcher = rmock(EventDispatcher.class); + this.provider = mock(UserTransactionMoratoriumProvider.class); + } + + @Test + public void rejects_proposal_carrying_user_transactions_while_moratorium_is_in_force() { + // Arrange + final var verifier = createVerifier(Result.error(MORATORIUM)); + final var proposal = + createProposal(List.of(RawNotarizedTransaction.create(new byte[] {1, 2, 3}))); + + // Act + verifier.processProposal(proposal); + + // Assert + verify(provider).ensureUserTransactionsAllowed(3); + verify(forwardTo, never()).processProposal(any()); + verify(proposalRejectedDispatcher, times(1)).dispatch(new ProposalRejected(ROUND)); + } + + @Test + public void forwards_proposal_without_user_transactions_while_moratorium_is_in_force() { + // Arrange + final var verifier = createVerifier(Result.error(MORATORIUM)); + final var proposal = createProposal(List.of()); + + // Act + verifier.processProposal(proposal); + + // Assert + verifyNoInteractions(provider); + verify(forwardTo, times(1)).processProposal(proposal); + verify(proposalRejectedDispatcher, never()).dispatch(any()); + } + + @Test + public void forwards_proposal_carrying_user_transactions_when_no_moratorium_is_in_force() { + // Arrange + final var verifier = createVerifier(Result.success(Tuple.tuple())); + final var proposal = + createProposal(List.of(RawNotarizedTransaction.create(new byte[] {1, 2, 3}))); + when(proposal.getEpoch()).thenReturn(4L); + + // Act + verifier.processProposal(proposal); + + // Assert + verify(provider).ensureUserTransactionsAllowed(4); + verify(forwardTo, times(1)).processProposal(proposal); + verify(proposalRejectedDispatcher, never()).dispatch(any()); + } + + private UserTransactionMoratoriumVerifier createVerifier( + Result moratorium) { + when(provider.ensureUserTransactionsAllowed(anyLong())).thenReturn(moratorium); + return new UserTransactionMoratoriumVerifier( + forwardTo, + provider, + Addressing.ofNetwork(Network.INTEGRATIONTESTNET), + proposalRejectedDispatcher); + } + + private Proposal createProposal(List transactions) { + // Logging must be able to render the fake validator ID. + final var address = + new ComponentAddress( + Bytes.toArray( + Collections.nCopies( + ComponentAddress.BYTE_LENGTH, + ComponentAddress.VALIDATOR_COMPONENT_ADDRESS_ENTITY_ID))); + final var author = BFTValidatorId.create(address, PrivateKeys.ofNumeric(1).getPublicKey()); + final var vertex = mock(Vertex.class); + when(vertex.getTransactions()).thenReturn(transactions); + final var proposal = mock(Proposal.class); + when(proposal.getEpoch()).thenReturn(3L); + when(proposal.getAuthor()).thenReturn(author); + when(proposal.getRound()).thenReturn(ROUND); + when(proposal.getVertex()).thenReturn(vertex); + return proposal; + } +} diff --git a/core/src/test/java/com/radixdlt/consensus/liveness/PacemakerTest.java b/core/src/test/java/com/radixdlt/consensus/liveness/PacemakerTest.java index a7b9ada6ef..1c0d50117e 100644 --- a/core/src/test/java/com/radixdlt/consensus/liveness/PacemakerTest.java +++ b/core/src/test/java/com/radixdlt/consensus/liveness/PacemakerTest.java @@ -134,6 +134,7 @@ public void setUp() { this.timeoutSender, this.timeoutCalculator, this.proposalGenerator, + UserTransactionMoratoriumProvider.NONE, this.proposalDispatcher, this.voteDispatcher, this.noVoteDispatcher, diff --git a/core/src/test/java/com/radixdlt/consensus/liveness/PacemakerUserTransactionMoratoriumTest.java b/core/src/test/java/com/radixdlt/consensus/liveness/PacemakerUserTransactionMoratoriumTest.java new file mode 100644 index 0000000000..2a53dfbd60 --- /dev/null +++ b/core/src/test/java/com/radixdlt/consensus/liveness/PacemakerUserTransactionMoratoriumTest.java @@ -0,0 +1,240 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.consensus.liveness; + +import static com.radixdlt.utils.TypedMocks.rmock; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +import com.google.common.hash.HashCode; +import com.radixdlt.consensus.*; +import com.radixdlt.consensus.bft.*; +import com.radixdlt.consensus.safety.SafetyRules; +import com.radixdlt.consensus.vertexstore.ExecutedVertex; +import com.radixdlt.consensus.vertexstore.VertexStoreAdapter; +import com.radixdlt.crypto.Blake2b256Hasher; +import com.radixdlt.crypto.Hasher; +import com.radixdlt.environment.EventDispatcher; +import com.radixdlt.environment.RemoteEventDispatcher; +import com.radixdlt.environment.ScheduledEventDispatcher; +import com.radixdlt.lang.Result; +import com.radixdlt.lang.Tuple; +import com.radixdlt.monitoring.Metrics; +import com.radixdlt.monitoring.MetricsInitializer; +import com.radixdlt.protocol.UserTransactionMoratorium; +import com.radixdlt.serialization.DefaultSerialization; +import com.radixdlt.transactions.RawNotarizedTransaction; +import com.radixdlt.utils.TimeSupplier; +import com.radixdlt.utils.UInt64; +import java.util.List; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; + +public final class PacemakerUserTransactionMoratoriumTest { + private static final Hasher hasher = new Blake2b256Hasher(DefaultSerialization.getInstance()); + private static final UserTransactionMoratorium MORATORIUM = + new UserTransactionMoratorium(UInt64.fromNonNegativeLong(3), UInt64.fromNonNegativeLong(4)); + private static final Round CURRENT_ROUND = Round.of(1); + + private final BFTValidatorId self = mock(BFTValidatorId.class); + private final BFTValidatorSet validatorSet = mock(BFTValidatorSet.class); + private final VertexStoreAdapter vertexStore = mock(VertexStoreAdapter.class); + private final SafetyRules safetyRules = mock(SafetyRules.class); + private final PacemakerTimeoutCalculator timeoutCalculator = + mock(PacemakerTimeoutCalculator.class); + private final ProposalGenerator proposalGenerator = mock(ProposalGenerator.class); + private final RemoteEventDispatcher voteDispatcher = + rmock(RemoteEventDispatcher.class); + private final RemoteEventDispatcher proposalDispatcher = + rmock(RemoteEventDispatcher.class); + private final EventDispatcher timeoutDispatcher = + rmock(EventDispatcher.class); + private final EventDispatcher noVoteDispatcher = rmock(EventDispatcher.class); + private final ScheduledEventDispatcher timeoutSender = + rmock(ScheduledEventDispatcher.class); + private final TimeSupplier timeSupplier = mock(TimeSupplier.class); + private final UserTransactionMoratoriumProvider provider = + mock(UserTransactionMoratoriumProvider.class); + private final Metrics metrics = new MetricsInitializer().initialize(); + + private HighQC highQC; + + @Before + public void setUp() { + this.highQC = mock(HighQC.class); + final var committedQc = mock(QuorumCertificate.class); + when(committedQc.getRound()).thenReturn(Round.of(0)); + when(this.highQC.highestCommittedQC()).thenReturn(committedQc); + when(this.highQC.getHighestRound()).thenReturn(Round.of(0)); + when(this.safetyRules.getLastVote(any())).thenReturn(Optional.empty()); + } + + @Test + public void withholds_vote_for_vertex_carrying_user_transactions_while_moratorium_is_in_force() { + // Arrange + final var pacemaker = createPacemaker(Result.error(MORATORIUM)); + final var insertUpdate = + insertUpdateOfVertexWith(List.of(RawNotarizedTransaction.create(new byte[] {1, 2, 3}))); + + // Act + pacemaker.processBFTUpdate(insertUpdate); + + // Assert + verify(this.provider).ensureUserTransactionsAllowed(3); + verify(this.noVoteDispatcher, times(1)).dispatch(any(NoVote.class)); + verify(this.safetyRules, never()).createVote(any(), any(), anyLong(), any()); + verifyNoInteractions(this.voteDispatcher); + } + + @Test + public void votes_for_vertex_without_user_transactions_while_moratorium_is_in_force() { + // Arrange + final var pacemaker = createPacemaker(Result.error(MORATORIUM)); + final var insertUpdate = insertUpdateOfVertexWith(List.of()); + final var vote = mockVote(); + when(this.safetyRules.createVote(any(), any(), anyLong(), any())).thenReturn(Optional.of(vote)); + + // Act + pacemaker.processBFTUpdate(insertUpdate); + + // Assert + verifyNoInteractions(this.provider); + verify(this.safetyRules, times(1)).createVote(any(), any(), anyLong(), any()); + verify(this.noVoteDispatcher, never()).dispatch(any()); + } + + @Test + public void votes_for_vertex_carrying_user_transactions_when_no_moratorium_is_in_force() { + // Arrange + final var pacemaker = createPacemaker(Result.success(Tuple.tuple())); + final var insertUpdate = + insertUpdateOfVertexWith(List.of(RawNotarizedTransaction.create(new byte[] {1, 2, 3}))); + when(insertUpdate.insertedVertex().vertex().getEpoch()).thenReturn(4L); + final var vote = mockVote(); + when(this.safetyRules.createVote(any(), any(), anyLong(), any())).thenReturn(Optional.of(vote)); + + // Act + pacemaker.processBFTUpdate(insertUpdate); + + // Assert + verify(this.provider).ensureUserTransactionsAllowed(4); + verify(this.safetyRules, times(1)).createVote(any(), any(), anyLong(), any()); + verify(this.noVoteDispatcher, never()).dispatch(any()); + } + + private Pacemaker createPacemaker(Result moratorium) { + when(this.provider.ensureUserTransactionsAllowed(anyLong())).thenReturn(moratorium); + final var initialRoundUpdate = + new RoundUpdate( + CURRENT_ROUND, this.highQC, mock(BFTValidatorId.class), mock(BFTValidatorId.class)); + return new Pacemaker( + this.self, + this.validatorSet, + this.vertexStore, + this.safetyRules, + this.timeoutDispatcher, + this.timeoutSender, + this.timeoutCalculator, + this.proposalGenerator, + provider, + this.proposalDispatcher, + this.voteDispatcher, + this.noVoteDispatcher, + hasher, + this.timeSupplier, + initialRoundUpdate, + this.metrics); + } + + private static Vote mockVote() { + final var proposed = mock(BFTHeader.class); + when(proposed.getVertexId()).thenReturn(HashCode.fromInt(99)); + final var voteData = mock(VoteData.class); + when(voteData.getProposed()).thenReturn(proposed); + final var vote = mock(Vote.class); + when(vote.getVoteData()).thenReturn(voteData); + return vote; + } + + private BFTInsertUpdate insertUpdateOfVertexWith(List transactions) { + final var vertex = mock(Vertex.class); + when(vertex.getEpoch()).thenReturn(3L); + when(vertex.getTransactions()).thenReturn(transactions); + final var executedVertex = mock(ExecutedVertex.class); + when(executedVertex.getRound()).thenReturn(CURRENT_ROUND); + when(executedVertex.vertex()).thenReturn(vertex); + when(executedVertex.getVertexWithHash()).thenReturn(mock(VertexWithHash.class)); + when(executedVertex.getVertexHash()).thenReturn(HashCode.fromInt(99)); + when(executedVertex.getLedgerHeader()).thenReturn(mock(LedgerHeader.class)); + final var insertUpdate = mock(BFTInsertUpdate.class); + when(insertUpdate.insertedVertex()).thenReturn(executedVertex); + final var header = + new BFTHeader(CURRENT_ROUND, HashCode.fromInt(99), executedVertex.getLedgerHeader()); + when(insertUpdate.getHeader()).thenReturn(header); + return insertUpdate; + } +} diff --git a/core/src/test/java/com/radixdlt/p2p/P2PModuleBanClearingTest.java b/core/src/test/java/com/radixdlt/p2p/P2PModuleBanClearingTest.java new file mode 100644 index 0000000000..c8f15372d7 --- /dev/null +++ b/core/src/test/java/com/radixdlt/p2p/P2PModuleBanClearingTest.java @@ -0,0 +1,159 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.p2p; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableMap; +import com.radixdlt.protocol.ProtocolUpdateEnactmentCondition; +import com.radixdlt.protocol.ProtocolUpdateTrigger; +import com.radixdlt.rev2.Decimal; +import com.radixdlt.statecomputer.ProtocolState; +import org.junit.Test; + +public final class P2PModuleBanClearingTest { + private static final long ENACTMENT_EPOCH = 339898; + + @Test + public void clears_bans_in_the_epoch_right_before_an_unconditional_enactment() { + // Arrange + final var protocolState = + pending(ProtocolUpdateEnactmentCondition.unconditionallyAtEpoch(ENACTMENT_EPOCH)); + + // Act + final var oneEpochBefore = + P2PModule.shouldClearNearProtocolUpdateBans(protocolState, ENACTMENT_EPOCH - 1); + final var twoEpochsBefore = + P2PModule.shouldClearNearProtocolUpdateBans(protocolState, ENACTMENT_EPOCH - 2); + final var atEnactmentEpoch = + P2PModule.shouldClearNearProtocolUpdateBans(protocolState, ENACTMENT_EPOCH); + + // Assert + assertTrue(oneEpochBefore); + assertFalse(twoEpochsBefore); + assertFalse(atEnactmentEpoch); + } + + @Test + public void clears_bans_in_the_epoch_right_before_a_moratorium_enactment() { + // Arrange + final var protocolState = + pending(ProtocolUpdateEnactmentCondition.unconditionallyAtEpoch(ENACTMENT_EPOCH)); + + // Act + final var oneEpochBefore = + P2PModule.shouldClearNearProtocolUpdateBans(protocolState, ENACTMENT_EPOCH - 1); + final var twoEpochsBefore = + P2PModule.shouldClearNearProtocolUpdateBans(protocolState, ENACTMENT_EPOCH - 2); + + // Assert + assertTrue(oneEpochBefore); + assertFalse(twoEpochsBefore); + } + + @Test + public void clears_bans_from_one_epoch_before_the_lower_bound_of_a_readiness_enactment() { + // Arrange + final var protocolState = + pending( + ProtocolUpdateEnactmentCondition.singleReadinessThresholdBetweenEpochs( + 100, 200, Decimal.ofNonNegativeFraction(3, 4), 1)); + + // Act + final var beforeWindow = P2PModule.shouldClearNearProtocolUpdateBans(protocolState, 98); + final var oneBeforeLowerBound = P2PModule.shouldClearNearProtocolUpdateBans(protocolState, 99); + final var insideWindow = P2PModule.shouldClearNearProtocolUpdateBans(protocolState, 150); + final var atUpperBound = P2PModule.shouldClearNearProtocolUpdateBans(protocolState, 200); + + // Assert + assertFalse(beforeWindow); + assertTrue(oneBeforeLowerBound); + assertTrue(insideWindow); + assertFalse(atUpperBound); + } + + @Test + public void never_clears_bans_for_an_update_chained_after_another() { + // Arrange + final var protocolState = pending(ProtocolUpdateEnactmentCondition.immediatelyAfter("test-v1")); + + // Act + final var result = P2PModule.shouldClearNearProtocolUpdateBans(protocolState, 5); + + // Assert + assertFalse(result); + } + + private static ProtocolState pending(ProtocolUpdateEnactmentCondition condition) { + final var trigger = new ProtocolUpdateTrigger("test-v2", condition); + return new ProtocolState( + ImmutableMap.of(), + ImmutableMap.of( + "test-v2", + new ProtocolState.PendingProtocolUpdate( + trigger, new ProtocolState.PendingProtocolUpdateState.Empty()))); + } +} diff --git a/core/src/test/java/com/radixdlt/rev2/protocol/EagleRayProtocolUpdateTest.java b/core/src/test/java/com/radixdlt/rev2/protocol/EagleRayProtocolUpdateTest.java new file mode 100644 index 0000000000..d15e77a0f8 --- /dev/null +++ b/core/src/test/java/com/radixdlt/rev2/protocol/EagleRayProtocolUpdateTest.java @@ -0,0 +1,220 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.rev2.protocol; + +import static com.radixdlt.environment.deterministic.network.MessageSelector.firstSelector; +import static com.radixdlt.harness.predicates.NodesPredicate.allAtOrOverEpoch; +import static org.junit.Assert.assertEquals; + +import com.google.inject.Module; +import com.radixdlt.api.CoreApiHelper; +import com.radixdlt.api.core.generated.api.StreamApi; +import com.radixdlt.api.core.generated.models.BootLoaderModuleFieldSystemBootSubstate; +import com.radixdlt.api.core.generated.models.FlashLedgerTransaction; +import com.radixdlt.api.core.generated.models.ProtocolUpdateStatusModuleFieldSummarySubstate; +import com.radixdlt.api.core.generated.models.ProtocolUpdateStatusType; +import com.radixdlt.api.core.generated.models.StreamTransactionsRequest; +import com.radixdlt.api.core.generated.models.SystemVersion; +import com.radixdlt.genesis.GenesisBuilder; +import com.radixdlt.genesis.GenesisConsensusManagerConfig; +import com.radixdlt.harness.deterministic.DeterministicTest; +import com.radixdlt.harness.deterministic.PhysicalNodeConfig; +import com.radixdlt.modules.FunctionalRadixNodeModule; +import com.radixdlt.modules.StateComputerConfig; +import com.radixdlt.networks.Network; +import com.radixdlt.protocol.ProtocolConfig; +import com.radixdlt.rev2.Decimal; +import com.radixdlt.statecomputer.RustStateComputer; +import com.radixdlt.sync.TransactionsAndProofReader; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public final class EagleRayProtocolUpdateTest { + /** Keeps the update late enough to observe Cuttlefish Part 2 first. */ + private static final long EAGLE_RAY_EPOCH = 8; + + private static final ProtocolConfig EAGLE_RAY_AT_EPOCH = + ProtocolConfig.enactAtEpoch(ProtocolConfig.EAGLE_RAY_PROTOCOL_VERSION_NAME, EAGLE_RAY_EPOCH); + + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + private DeterministicTest createTest(Module... extraModules) { + final var genesis = + GenesisBuilder.createTestGenesisWithNumValidators( + 1, Decimal.ONE, GenesisConsensusManagerConfig.Builder.testWithRoundsPerEpoch(5)); + return DeterministicTest.builder() + .addPhysicalNodes(PhysicalNodeConfig.createBatch(1, true)) + .messageSelector(firstSelector()) + .addModules(extraModules) + .functionalNodeModule( + new FunctionalRadixNodeModule( + FunctionalRadixNodeModule.NodeStorageConfig.tempFolder(folder), + true, + FunctionalRadixNodeModule.SafetyRecoveryConfig.REAL, + FunctionalRadixNodeModule.ConsensusConfig.testDefault(), + FunctionalRadixNodeModule.LedgerConfig.stateComputerNoSync( + StateComputerConfig.rev2() + .withGenesis(genesis) + .withProtocolConfig(EAGLE_RAY_AT_EPOCH)))); + } + + @Test + public void eagle_ray_enacts_after_cuttlefish_part2_with_expected_flashes() throws Exception { + // Arrange + final var coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); + try (var test = createTest(coreApiHelper.module())) { + test.startAllNodes(); + final var stateComputer = test.getInstance(0, RustStateComputer.class); + test.runUntilState(allAtOrOverEpoch(EAGLE_RAY_EPOCH - 1)); + assertEquals( + ProtocolConfig.CUTTLEFISH_PART2_PROTOCOL_VERSION_NAME, + stateComputer.protocolState().currentProtocolVersion()); + assertEquals( + ProtocolConfig.CUTTLEFISH_PART2_PROTOCOL_VERSION_NAME, + coreApiHelper.getNetworkStatus().getCurrentProtocolVersion()); + final var preEagleRayStateVersion = + test.getInstance(0, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .resultantStateVersion(); + + // Act + test.runUntilState(allAtOrOverEpoch(EAGLE_RAY_EPOCH)); + final var committedFlashTransactions = + new StreamApi(coreApiHelper.client()) + .streamTransactionsPost( + new StreamTransactionsRequest() + .network(Network.INTEGRATIONTESTNET.getLogicalName()) + .limit(1000) + .fromStateVersion(preEagleRayStateVersion)) + .getTransactions() + .stream() + .filter( + transaction -> + transaction.getLedgerTransaction() instanceof FlashLedgerTransaction) + .toList(); + + // Assert + assertEquals( + ProtocolConfig.EAGLE_RAY_PROTOCOL_VERSION_NAME, + stateComputer.protocolState().currentProtocolVersion()); + assertEquals( + ProtocolConfig.EAGLE_RAY_PROTOCOL_VERSION_NAME, + coreApiHelper.getNetworkStatus().getCurrentProtocolVersion()); + final var postProtocolUpdateProof = + test.getInstance(0, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow(); + assertEquals( + ProtocolConfig.EAGLE_RAY_PROTOCOL_VERSION_NAME, + postProtocolUpdateProof + .latestProofWhichInitiatedOneOrMoreProtocolUpdates() + .unwrap() + .ledgerHeader() + .nextProtocolVersion() + .unwrap()); + assertEquals( + List.of("eagle-ray-system-version-update", "status-summary", "status-summary"), + committedFlashTransactions.stream() + .map( + transaction -> + ((FlashLedgerTransaction) transaction.getLedgerTransaction()).getName()) + .toList()); + ProtocolUpdateTestUtils.verifyFlashTransactionReceipts(committedFlashTransactions); + + final var systemBoot = + committedFlashTransactions + .get(0) + .getReceipt() + .getStateUpdates() + .getUpdatedSubstates() + .stream() + .map(updatedSubstate -> updatedSubstate.getNewValue().getSubstateData()) + .filter(BootLoaderModuleFieldSystemBootSubstate.class::isInstance) + .map(BootLoaderModuleFieldSystemBootSubstate.class::cast) + .findFirst() + .orElseThrow(); + assertEquals(SystemVersion.V5, systemBoot.getValue().getSystemVersion()); + + final var latestStatus = + committedFlashTransactions + .get(2) + .getReceipt() + .getStateUpdates() + .getUpdatedSubstates() + .stream() + .map(updatedSubstate -> updatedSubstate.getNewValue().getSubstateData()) + .filter(ProtocolUpdateStatusModuleFieldSummarySubstate.class::isInstance) + .map(ProtocolUpdateStatusModuleFieldSummarySubstate.class::cast) + .findFirst() + .orElseThrow(); + assertEquals( + ProtocolConfig.EAGLE_RAY_PROTOCOL_VERSION_NAME, latestStatus.getProtocolVersion()); + assertEquals(ProtocolUpdateStatusType.COMPLETE, latestStatus.getUpdateStatus().getType()); + } + } +} diff --git a/core/src/test/java/com/radixdlt/rev2/protocol/UserTransactionMoratoriumRestartTest.java b/core/src/test/java/com/radixdlt/rev2/protocol/UserTransactionMoratoriumRestartTest.java new file mode 100644 index 0000000000..67fa16795d --- /dev/null +++ b/core/src/test/java/com/radixdlt/rev2/protocol/UserTransactionMoratoriumRestartTest.java @@ -0,0 +1,566 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.rev2.protocol; + +import static com.radixdlt.environment.deterministic.network.MessageSelector.firstSelector; +import static com.radixdlt.harness.predicates.EventPredicate.onlyConsensusEventsAndSelfLedgerUpdates; +import static com.radixdlt.harness.predicates.EventPredicate.onlyLocalMempoolAddEvents; +import static com.radixdlt.harness.predicates.NodesPredicate.allAtOrOverEpoch; +import static com.radixdlt.harness.predicates.NodesPredicate.allAtOrOverProtocolVersion; +import static com.radixdlt.harness.predicates.NodesPredicate.allAtOrOverStateVersion; +import static com.radixdlt.harness.predicates.NodesPredicate.allCommittedTransactionSuccess; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.inject.AbstractModule; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.ProvidesIntoSet; +import com.radixdlt.consensus.Proposal; +import com.radixdlt.consensus.Vote; +import com.radixdlt.consensus.bft.BFTHighQCUpdate; +import com.radixdlt.consensus.bft.Round; +import com.radixdlt.environment.EventDispatcher; +import com.radixdlt.environment.deterministic.network.ControlledMessage; +import com.radixdlt.genesis.GenesisBuilder; +import com.radixdlt.genesis.GenesisConsensusManagerConfig; +import com.radixdlt.harness.deterministic.DeterministicTest; +import com.radixdlt.harness.deterministic.PhysicalNodeConfig; +import com.radixdlt.harness.deterministic.invariants.DeterministicMonitors; +import com.radixdlt.harness.deterministic.invariants.MessageMonitor; +import com.radixdlt.harness.predicates.NodePredicate; +import com.radixdlt.lang.Option; +import com.radixdlt.mempool.MempoolAdd; +import com.radixdlt.mempool.MempoolRejectedException; +import com.radixdlt.mempool.RustMempool; +import com.radixdlt.modules.FunctionalRadixNodeModule; +import com.radixdlt.modules.StateComputerConfig; +import com.radixdlt.monitoring.Metrics; +import com.radixdlt.protocol.ProtocolConfig; +import com.radixdlt.protocol.UserTransactionMoratorium; +import com.radixdlt.rev2.Decimal; +import com.radixdlt.rev2.TransactionBuilder; +import com.radixdlt.statecomputer.RustStateComputer; +import com.radixdlt.sync.SyncRelayConfig; +import com.radixdlt.sync.TransactionsAndProofReader; +import com.radixdlt.transactions.RawNotarizedTransaction; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests restart with persisted votes, certificates and unequal ledger tips. */ +public final class UserTransactionMoratoriumRestartTest { + private static final String EAGLE_RAY = ProtocolConfig.EAGLE_RAY_PROTOCOL_VERSION_NAME; + private static final int NUM_VALIDATORS = 4; + private static final long ROUNDS_PER_EPOCH = 10; + private static final long WARM_UP_EPOCH = 3; + private static final int MAX_MESSAGES_PER_STEP = 200_000; + private static final int MAX_LOCAL_EVENTS_PER_NODE = 500; + private static final int MAX_VOTES_PER_ROUND = 20; + private static final long MIN_ROUNDS_PER_EPOCH = 10; + private static final long MAX_ROUNDS_PER_EPOCH = 100; + private static final Duration EPOCH_TARGET_DURATION = Duration.ofMinutes(10); + private static final Duration HALT_DURATION = Duration.ofDays(4); + + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + private final List highQcRounds = new CopyOnWriteArrayList<>(); + + @Test + public void split_in_progress_round_resolves_by_timeout_without_certifying_the_transaction() { + try (var test = createTest()) { + // Arrange + final var halted = haltWithProposalDeliveredTo(test, List.of(1, 2)); + + // Act + restartAllWithMoratorium(test, halted.haltEpoch()); + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + + // Assert + assertFalse(isCommittedOnAnyNode(test, halted.inFlight())); + assertTrue(timeoutQuorumResolutions(test) > 0.0); + assertEquals(NUM_VALIDATORS, test.numNodesLive()); + } + } + + @Test + public void replayed_votes_of_a_quorum_certify_the_in_flight_vertex_and_nothing_else_commits() { + try (var test = createTest()) { + // Arrange + final var halted = haltWithProposalDeliveredTo(test, List.of(0, 1, 2)); + + // Act + restartAllWithMoratorium(test, halted.haltEpoch()); + final var submittedDuring = TransactionBuilder.forTests().prepare().raw(); + final var rejection = + assertThrows( + MempoolRejectedException.class, + () -> test.getInstance(3, RustMempool.class).addTransaction(submittedDuring)); + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + final var inFlightCommittedPerNode = commitStatusPerNode(test, halted.inFlight()); + final var submittedDuringCommitted = isCommittedOnAnyNode(test, submittedDuring); + submit(test, submittedDuring); + test.runUntilState(allCommittedTransactionSuccess(submittedDuring), MAX_MESSAGES_PER_STEP); + + // Assert + assertEquals(List.of(true, true, true, true), inFlightCommittedPerNode); + assertTrue( + rejection.getMessage(), + rejection + .getMessage() + .contains("temporarily not accepted; retry from epoch " + (halted.haltEpoch() + 1))); + assertFalse(submittedDuringCommitted); + } + } + + @Test + public void validators_with_different_committed_tips_converge_after_restart() { + try (var test = createTest()) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(WARM_UP_EPOCH), MAX_MESSAGES_PER_STEP); + test.runUntilState(allAtOrOverStateVersion(stateVersion(test, 0) + 3), MAX_MESSAGES_PER_STEP); + test.runUntilState( + ignored -> test.getNetwork().allMessages().stream().anyMatch(this::isProposal), + MAX_MESSAGES_PER_STEP, + message -> !isProposal(message)); + test.runNext(message -> isProposal(message) && message.channelId().receiverIndex() == 1); + drainLocalEvents(test, 1); + final var aheadVersion = stateVersion(test, 1); + final var behindVersion = stateVersion(test, 0); + final var haltEpoch = highestEpoch(test); + haltAll(test); + + // Act + restartAllWithMoratorium(test, haltEpoch); + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + + // Assert + assertTrue(aheadVersion > behindVersion); + assertTrue(allAtOrOverStateVersion(aheadVersion).test(test.getNodeInjectors())); + assertEquals(NUM_VALIDATORS, test.numNodesLive()); + } + } + + @Test + public void node_refuses_to_start_when_the_enactment_epoch_has_already_passed() { + try (var test = createTest()) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(WARM_UP_EPOCH), MAX_MESSAGES_PER_STEP); + final var currentEpoch = currentEpoch(test, 0); + final var staleConfig = withMoratorium(currentEpoch - 2, currentEpoch - 1); + final var stateVersionBefore = stateVersion(test, 1); + + // Act + final var failure = + assertThrows( + RuntimeException.class, + () -> test.restartNodeWithOverrideModule(0, protocolConfigOverride(staleConfig))); + test.runUntilState( + nodesMatch(List.of(1, 2, 3), NodePredicate.atOrOverStateVersion(stateVersionBefore + 5)), + MAX_MESSAGES_PER_STEP); + + // Assert + assertTrue( + messageChain(failure), messageChain(failure).contains("protocol misconfiguration")); + assertEquals(NUM_VALIDATORS - 1, test.numNodesLive()); + } + } + + /** Resumes the next leader's persisted QC without relying on vote replay. */ + @Test + public void pre_halt_certified_vertex_in_persisted_stores_commits_after_restart() { + try (var test = createTest()) { + // Arrange + final var halted = haltWithProposalDeliveredTo(test, List.of(0, 1, 2), true); + final var certifiedBeforeHalt = highQcRounds.contains(halted.round()); + + // Act + restartAllWithMoratorium(test, halted.haltEpoch()); + final var submittedDuring = TransactionBuilder.forTests().prepare().raw(); + final var rejection = + assertThrows( + MempoolRejectedException.class, + () -> test.getInstance(3, RustMempool.class).addTransaction(submittedDuring)); + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + final var inFlightCommittedPerNode = commitStatusPerNode(test, halted.inFlight()); + + // Assert + assertTrue(certifiedBeforeHalt); + assertEquals(List.of(true, true, true, true), inFlightCommittedPerNode); + assertTrue( + rejection.getMessage(), + rejection + .getMessage() + .contains("temporarily not accepted; retry from epoch " + (halted.haltEpoch() + 1))); + } + } + + /** + * The halt exceeds the epoch target duration, leaving the minimum round count as the remaining + * constraint on epoch completion. + */ + @Test + public void + restart_after_a_long_halt_accepts_proposals_and_ends_the_epoch_at_the_minimum_round() { + try (var test = + createTest( + GenesisConsensusManagerConfig.Builder.testDefaults() + .epochMinRoundCount(MIN_ROUNDS_PER_EPOCH) + .epochMaxRoundCount(MAX_ROUNDS_PER_EPOCH) + .epochTargetDurationMillis(EPOCH_TARGET_DURATION.toMillis()))) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(WARM_UP_EPOCH), MAX_MESSAGES_PER_STEP); + test.runUntilState(allAtOrOverStateVersion(stateVersion(test, 0) + 3), MAX_MESSAGES_PER_STEP); + final var warmUpEpochChangeRound = epochChangeRound(test, 0); + final var haltEpoch = highestEpoch(test); + final var haltRound = highestRound(test); + haltAll(test); + test.advanceTime(HALT_DURATION); + + // Act + restartAllWithMoratorium(test, haltEpoch); + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + final var enactingEpochChangeRound = epochChangeRound(test, 0); + + // Assert + assertEquals(MAX_ROUNDS_PER_EPOCH, warmUpEpochChangeRound); + assertTrue(haltRound < MIN_ROUNDS_PER_EPOCH); + assertTrue(enactingEpochChangeRound >= MIN_ROUNDS_PER_EPOCH); + assertTrue(enactingEpochChangeRound <= MIN_ROUNDS_PER_EPOCH + 5); + assertEquals(0.0, rejectedProposalsForTimestamp(test), 0.0); + assertEquals(NUM_VALIDATORS, test.numNodesLive()); + } + } + + private record HaltedNetwork(RawNotarizedTransaction inFlight, Round round, long haltEpoch) {} + + /** Halts after the selected validators persist their votes, without delivering those votes. */ + private HaltedNetwork haltWithProposalDeliveredTo(DeterministicTest test, List voters) { + return haltWithProposalDeliveredTo(test, voters, false); + } + + /** + * Persists the selected validators' votes for a user proposal. With {@code deliverVotes}, + * delivers them so a quorum can form a QC before the halt. Drops queued messages when halting. + */ + private HaltedNetwork haltWithProposalDeliveredTo( + DeterministicTest test, List voters, boolean deliverVotes) { + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(WARM_UP_EPOCH), MAX_MESSAGES_PER_STEP); + final var inFlight = TransactionBuilder.forTests().prepare().raw(); + submit(test, inFlight); + test.runUntilState( + ignored -> + test.getNetwork().allMessages().stream() + .anyMatch(message -> carriesProposalOf(message, inFlight)), + MAX_MESSAGES_PER_STEP, + message -> !carriesProposalOf(message, inFlight)); + final var round = + test.getNetwork().allMessages().stream() + .filter(message -> carriesProposalOf(message, inFlight)) + .map(message -> ((Proposal) message.message()).getRound()) + .findFirst() + .orElseThrow(); + for (final var voter : voters) { + test.runNext( + message -> + carriesProposalOf(message, inFlight) && message.channelId().receiverIndex() == voter); + drainLocalEvents(test, voter); + } + if (deliverVotes) { + test.runUntilOutOfMessagesOfType( + MAX_VOTES_PER_ROUND, + message -> message.message() instanceof Vote vote && vote.getRound().equals(round)); + test.getNodeIndices().forEach(nodeIndex -> drainLocalEvents(test, nodeIndex)); + } + final var haltEpoch = highestEpoch(test); + haltAll(test); + return new HaltedNetwork(inFlight, round, haltEpoch); + } + + /** + * Drains consensus events needed to persist votes. Excludes periodic triggers because they + * reschedule themselves indefinitely. + */ + private static void drainLocalEvents(DeterministicTest test, int nodeIndex) { + test.runUntilOutOfMessagesOfType( + MAX_LOCAL_EVENTS_PER_NODE, + onlyConsensusEventsAndSelfLedgerUpdates() + .and(message -> message.channelId().isLocal(nodeIndex))); + } + + private static void haltAll(DeterministicTest test) { + test.getNodeIndices().forEach(test::shutdownNode); + test.getNetwork().dropAllMessages(); + } + + private static void restartAllWithMoratorium(DeterministicTest test, long haltEpoch) { + final var upgradedBinary = protocolConfigOverride(withMoratorium(haltEpoch, haltEpoch + 1)); + test.getNodeIndices() + .forEach(nodeIndex -> test.restartNodeWithOverrideModule(nodeIndex, upgradedBinary)); + } + + private static Module protocolConfigOverride(ProtocolConfig protocolConfig) { + return new AbstractModule() { + @Override + protected void configure() { + bind(ProtocolConfig.class).toInstance(protocolConfig); + } + }; + } + + private DeterministicTest createTest() { + return createTest( + GenesisConsensusManagerConfig.Builder.testWithRoundsPerEpoch(ROUNDS_PER_EPOCH)); + } + + private DeterministicTest createTest(GenesisConsensusManagerConfig.Builder consensusConfig) { + return DeterministicTest.builder() + .addPhysicalNodes(PhysicalNodeConfig.createBatch(NUM_VALIDATORS, true)) + .addMonitors(DeterministicMonitors.byzantineBehaviorNotDetected(), highQcRoundMonitor()) + .messageSelector(firstSelector()) + .functionalNodeModule( + new FunctionalRadixNodeModule( + FunctionalRadixNodeModule.NodeStorageConfig.tempFolder(folder), + true, + FunctionalRadixNodeModule.SafetyRecoveryConfig.REAL, + FunctionalRadixNodeModule.ConsensusConfig.testDefault(), + FunctionalRadixNodeModule.LedgerConfig.stateComputerWithSyncRelay( + StateComputerConfig.rev2() + .withGenesis( + GenesisBuilder.createTestGenesisWithNumValidators( + NUM_VALIDATORS, Decimal.ONE, consensusConfig)) + .withProtocolConfig(withoutMoratorium()) + .withProposerConfig( + StateComputerConfig.REV2ProposerConfig.Mempool.defaults()), + SyncRelayConfig.of(5000, 10, 3000L)))); + } + + private Module highQcRoundMonitor() { + return new AbstractModule() { + @ProvidesIntoSet + MessageMonitor recordHighQcRounds() { + return (message, time) -> { + if (message.message() instanceof BFTHighQCUpdate update) { + highQcRounds.add(update.getHighQC().highestQC().getRound()); + } + }; + } + }; + } + + private static ProtocolConfig withMoratorium(long moratoriumFromEpoch, long enactmentEpoch) { + return ProtocolConfig.enactAtEpochWithUserTransactionMoratorium( + EAGLE_RAY, moratoriumFromEpoch, enactmentEpoch); + } + + private static ProtocolConfig withoutMoratorium() { + return ProtocolConfig.enactAtEpoch(EAGLE_RAY, 1000); + } + + private static boolean carriesProposalOf( + ControlledMessage message, RawNotarizedTransaction transaction) { + return message.message() instanceof Proposal proposal + && proposal.getVertex().getTransactions().stream() + .anyMatch(carried -> Arrays.equals(carried.getPayload(), transaction.getPayload())); + } + + private boolean isProposal(ControlledMessage message) { + return message.message() instanceof Proposal; + } + + private static void submit(DeterministicTest test, RawNotarizedTransaction transaction) { + final var mempoolDispatcher = + test.getInstance(0, Key.get(new TypeLiteral>() {})); + mempoolDispatcher.dispatch(new MempoolAdd(List.of(transaction))); + test.runUntilOutOfMessagesOfType(100, onlyLocalMempoolAddEvents()); + } + + private static boolean isCommittedOnAnyNode( + DeterministicTest test, RawNotarizedTransaction transaction) { + return commitStatusPerNode(test, transaction).contains(true); + } + + private static List commitStatusPerNode( + DeterministicTest test, RawNotarizedTransaction transaction) { + return test.getNodeInjectors().stream() + .map( + injector -> + NodePredicate.committedUserTransaction(transaction, false, false).test(injector)) + .toList(); + } + + private static double timeoutQuorumResolutions(DeterministicTest test) { + return test.getNodeInjectors().stream() + .mapToDouble( + injector -> + injector + .getInstance(Metrics.class) + .bft() + .quorumResolutions() + .label(new Metrics.Bft.QuorumResolution(true)) + .get()) + .sum(); + } + + private static long currentEpoch(DeterministicTest test, int nodeIndex) { + return test.getInstance(nodeIndex, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .resultantEpoch(); + } + + private static long epochChangeRound(DeterministicTest test, int nodeIndex) { + return test.getInstance(nodeIndex, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .latestProofWhichInitiatedAnEpochChange() + .ledgerHeader() + .round() + .toLong(); + } + + private static long highestRound(DeterministicTest test) { + return test.getNodeIndices().stream() + .mapToLong( + nodeIndex -> + test.getInstance(nodeIndex, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .resultantRound() + .number()) + .max() + .orElseThrow(); + } + + private static double rejectedProposalsForTimestamp(DeterministicTest test) { + return test.getNodeInjectors().stream() + .flatMap( + injector -> + Stream.of(Metrics.RejectedConsensusEvent.TimestampIssue.values()) + .map( + issue -> + injector + .getInstance(Metrics.class) + .bft() + .rejectedConsensusEvents() + .label( + new Metrics.RejectedConsensusEvent( + Metrics.RejectedConsensusEvent.Type.PROPOSAL, issue)) + .get())) + .mapToDouble(Double::doubleValue) + .sum(); + } + + private static long highestEpoch(DeterministicTest test) { + return test.getNodeIndices().stream() + .mapToLong(nodeIndex -> currentEpoch(test, nodeIndex)) + .max() + .orElseThrow(); + } + + private static long stateVersion(DeterministicTest test, int nodeIndex) { + return test.getInstance(nodeIndex, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .resultantStateVersion(); + } + + private static Predicate> nodesMatch( + List nodeIndices, Predicate nodePredicate) { + return injectors -> + nodeIndices.stream().allMatch(index -> nodePredicate.test(injectors.get(index))); + } + + private static String messageChain(Throwable throwable) { + return Stream.iterate(throwable, Objects::nonNull, Throwable::getCause) + .map(Throwable::getMessage) + .filter(Objects::nonNull) + .collect(Collectors.joining(" | ")); + } + + @SuppressWarnings("unused") + private static Option moratoriumOf( + DeterministicTest test, int nodeIndex) { + return test.getInstance(nodeIndex, RustStateComputer.class) + .ensureUserTransactionsAllowed() + .toOptionOfError(); + } +} diff --git a/core/src/test/java/com/radixdlt/rev2/protocol/UserTransactionMoratoriumTest.java b/core/src/test/java/com/radixdlt/rev2/protocol/UserTransactionMoratoriumTest.java new file mode 100644 index 0000000000..52f0bee8d4 --- /dev/null +++ b/core/src/test/java/com/radixdlt/rev2/protocol/UserTransactionMoratoriumTest.java @@ -0,0 +1,863 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.rev2.protocol; + +import static com.radixdlt.environment.deterministic.network.MessageSelector.firstSelector; +import static com.radixdlt.harness.predicates.EventPredicate.onlyLedgerSyncEvents; +import static com.radixdlt.harness.predicates.EventPredicate.onlyLocalMempoolAddEvents; +import static com.radixdlt.harness.predicates.NodesPredicate.allAtOrOverEpoch; +import static com.radixdlt.harness.predicates.NodesPredicate.allAtOrOverProtocolVersion; +import static com.radixdlt.harness.predicates.NodesPredicate.allCommittedTransactionSuccess; +import static com.radixdlt.harness.predicates.NodesPredicate.anyCommittedTransactionSuccess; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.inject.AbstractModule; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.OptionalBinder; +import com.google.inject.multibindings.ProvidesIntoSet; +import com.radixdlt.api.CoreApiHelper; +import com.radixdlt.api.core.generated.models.TransactionSubmitErrorResponse; +import com.radixdlt.api.core.generated.models.TransactionSubmitRejectedErrorDetails; +import com.radixdlt.api.core.generated.models.TransactionSubmitRequest; +import com.radixdlt.consensus.ConsensusEvent; +import com.radixdlt.consensus.Proposal; +import com.radixdlt.consensus.Vote; +import com.radixdlt.consensus.bft.BFTInsertUpdate; +import com.radixdlt.consensus.bft.NoVote; +import com.radixdlt.consensus.bft.Round; +import com.radixdlt.consensus.bft.Self; +import com.radixdlt.consensus.epoch.EpochProposalRejected; +import com.radixdlt.consensus.epoch.Epoched; +import com.radixdlt.consensus.liveness.ProposalGenerator; +import com.radixdlt.consensus.liveness.ProposerElections; +import com.radixdlt.consensus.liveness.UserTransactionMoratoriumProvider; +import com.radixdlt.crypto.ECDSASecp256k1PublicKey; +import com.radixdlt.crypto.Hasher; +import com.radixdlt.environment.EventDispatcher; +import com.radixdlt.environment.RemoteEventDispatcher; +import com.radixdlt.environment.deterministic.network.ControlledMessage; +import com.radixdlt.genesis.GenesisBuilder; +import com.radixdlt.genesis.GenesisConsensusManagerConfig; +import com.radixdlt.harness.deterministic.DeterministicTest; +import com.radixdlt.harness.deterministic.PhysicalNodeConfig; +import com.radixdlt.harness.deterministic.invariants.MessageMonitor; +import com.radixdlt.harness.predicates.NodePredicate; +import com.radixdlt.lang.Option; +import com.radixdlt.ledger.LedgerUpdate; +import com.radixdlt.mempool.MempoolAdd; +import com.radixdlt.mempool.MempoolRejectedException; +import com.radixdlt.mempool.RustMempool; +import com.radixdlt.modules.FunctionalRadixNodeModule; +import com.radixdlt.modules.StateComputerConfig; +import com.radixdlt.monitoring.Metrics; +import com.radixdlt.networks.Network; +import com.radixdlt.p2p.NodeId; +import com.radixdlt.protocol.ProtocolConfig; +import com.radixdlt.protocol.UserTransactionMoratorium; +import com.radixdlt.rev2.Decimal; +import com.radixdlt.rev2.REv2ToConsensus; +import com.radixdlt.rev2.TransactionBuilder; +import com.radixdlt.statecomputer.RustStateComputer; +import com.radixdlt.sync.SyncRelayConfig; +import com.radixdlt.sync.TransactionsAndProofReader; +import com.radixdlt.transactions.RawNotarizedTransaction; +import com.radixdlt.utils.UInt64; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; +import java.util.stream.Stream; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +@RunWith(JUnitParamsRunner.class) +public final class UserTransactionMoratoriumTest { + private static final String EAGLE_RAY = ProtocolConfig.EAGLE_RAY_PROTOCOL_VERSION_NAME; + private static final int NUM_VALIDATORS = 4; + private static final long ROUNDS_PER_EPOCH = 10; + private static final long MORATORIUM_FROM_EPOCH = 4; + private static final long ENACTMENT_EPOCH = 5; + private static final int MAX_MESSAGES_PER_STEP = 200_000; + + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + private final List rejectedProposals = new CopyOnWriteArrayList<>(); + + @Test + @Parameters({"4", "5"}) + public void future_user_proposal_waits_for_epoch_change_before_moratorium_check( + long proposalEpoch) { + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(proposalEpoch - 1), MAX_MESSAGES_PER_STEP); + test.runUntilState( + nodesMatch(List.of(1, 2, 3), NodePredicate.atOrOverEpoch(proposalEpoch)), + MAX_MESSAGES_PER_STEP, + message -> message.channelId().receiverIndex() != 0); + final var transaction = TransactionBuilder.forTests().prepare().raw(); + test.restartNodeWithOverrideModule(1, oldBinaryEmulation(transaction)); + final Predicate isFutureUserProposal = + message -> + message.channelId().receiverIndex() == 0 + && message.message() instanceof Proposal proposal + && proposal.getEpoch() == proposalEpoch + && proposal.getVertex().getTransactions().contains(transaction); + test.runUntilState( + ignored -> test.getNetwork().allMessages().stream().anyMatch(isFutureUserProposal), + MAX_MESSAGES_PER_STEP, + message -> message.channelId().receiverIndex() != 0); + final var queuedProposal = + test.getNetwork().allMessages().stream() + .filter(isFutureUserProposal) + .findFirst() + .orElseThrow(); + final var proposal = (Proposal) queuedProposal.message(); + final var vertexHash = proposal.getVertex().withId(test.getInstance(0, Hasher.class)).hash(); + final var metrics = test.getInstance(0, Metrics.class); + final var queuedBefore = metrics.epochManager().enqueuedConsensusEvents().get(); + final var checkedBefore = metrics.bft().proposalsReceived().get(); + final var committedBefore = currentEpoch(test); + final Predicate isDecision = + message -> + message.channelId().senderIndex() == 0 + && (message.message() instanceof Vote vote + && vote.getVoteData().getProposed().getVertexId().equals(vertexHash) + || message.message() instanceof EpochProposalRejected rejected + && rejected.epoch() == proposalEpoch + && rejected.proposalRejected().round().equals(proposal.getRound())); + + // Act + test.runNext(queuedProposal::equals); + final var queuedAfter = metrics.epochManager().enqueuedConsensusEvents().get(); + final var checkedWhileBehind = metrics.bft().proposalsReceived().get(); + final var committedWhileQueued = currentEpoch(test); + final var decidedWhileBehind = test.getNetwork().allMessages().stream().anyMatch(isDecision); + test.runUntilState( + ignored -> test.getNetwork().allMessages().stream().anyMatch(isDecision), + MAX_MESSAGES_PER_STEP, + message -> + !(message.message() instanceof ConsensusEvent + || message.message() instanceof Epoched)); + final var decision = + test.getNetwork().allMessages().stream() + .filter(isDecision) + .findFirst() + .orElseThrow() + .message(); + final var committedAtDecision = currentEpoch(test); + final var protocolAtDecision = + test.getInstance(0, RustStateComputer.class).protocolState().currentProtocolVersion(); + test.runUntilState(allCommittedTransactionSuccess(transaction), MAX_MESSAGES_PER_STEP); + + // Assert + assertEquals(proposalEpoch - 1, committedBefore); + assertEquals(committedBefore, committedWhileQueued); + assertEquals(queuedBefore + 1, queuedAfter, 0.0); + assertEquals(checkedBefore, checkedWhileBehind, 0.0); + assertFalse(decidedWhileBehind); + assertEquals(proposalEpoch, committedAtDecision); + assertEquals(proposalEpoch < ENACTMENT_EPOCH, decision instanceof EpochProposalRejected); + assertEquals(proposalEpoch >= ENACTMENT_EPOCH, decision instanceof Vote); + assertEquals(proposalEpoch >= ENACTMENT_EPOCH, EAGLE_RAY.equals(protocolAtDecision)); + } + } + + @Test + @Parameters({"3", "4"}) + public void pending_vertex_vote_uses_vertex_epoch_after_committed_epoch_crosses_boundary( + long vertexEpoch) { + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(vertexEpoch), MAX_MESSAGES_PER_STEP); + final var transaction = TransactionBuilder.forTests().prepare().raw(); + List.of(1, 2, 3) + .forEach( + index -> test.restartNodeWithOverrideModule(index, oldBinaryEmulation(transaction))); + final var metrics = test.getInstance(0, Metrics.class); + final Predicate isUserInsert = + message -> + message.channelId().isLocal(0) + && message.message() instanceof BFTInsertUpdate update + && update.insertedVertex().vertex().getEpoch() == vertexEpoch + && update.insertedVertex().vertex().getTransactions().contains(transaction); + final Predicate isCurrentUserInsert = + isUserInsert.and( + message -> + ((BFTInsertUpdate) message.message()).insertedVertex().getRound().number() + == metrics.bft().pacemaker().round().get()); + test.runUntilState( + ignored -> test.getNetwork().allMessages().stream().anyMatch(isCurrentUserInsert), + MAX_MESSAGES_PER_STEP, + message -> + !isUserInsert.test(message) + && !(message.channelId().receiverIndex() == 0 + && (message.message() instanceof Epoched + || message.message() instanceof EpochProposalRejected))); + final var pendingInsert = + test.getNetwork().allMessages().stream() + .filter(isCurrentUserInsert) + .findFirst() + .orElseThrow(); + final var vertex = ((BFTInsertUpdate) pendingInsert.message()).insertedVertex(); + final var committedBefore = currentEpoch(test); + final var roundBefore = metrics.bft().pacemaker().round().get(); + final Predicate isDecision = + message -> + message.channelId().senderIndex() == 0 + && (message.message() instanceof Vote vote + && vote.getVoteData() + .getProposed() + .getVertexId() + .equals(vertex.getVertexHash()) + || message.message() instanceof NoVote noVote + && noVote.vertex().hash().equals(vertex.getVertexHash())); + + // Act + test.runUntilState( + ignored -> currentEpoch(test) >= vertexEpoch + 1, + MAX_MESSAGES_PER_STEP, + message -> + (message.channelId().receiverIndex() != 0 + || onlyLedgerSyncEvents().test(message) + && !(message.message() instanceof LedgerUpdate)) + && !(message.message() instanceof ConsensusEvent event + && event.getEpoch() > vertexEpoch)); + final var committedBeforeCallback = currentEpoch(test); + final var roundBeforeCallback = metrics.bft().pacemaker().round().get(); + final var decidedBeforeCallback = + test.getNetwork().allMessages().stream().anyMatch(isDecision); + test.runNext(pendingInsert::equals); + final var decisions = + test.getNetwork().allMessages().stream() + .filter(isDecision) + .map(ControlledMessage::message) + .toList(); + final var afterEnactment = TransactionBuilder.forTests().prepare().raw(); + test.runUntilState(allAtOrOverEpoch(ENACTMENT_EPOCH), MAX_MESSAGES_PER_STEP); + submit(test, afterEnactment); + test.runUntilState(allCommittedTransactionSuccess(afterEnactment), MAX_MESSAGES_PER_STEP); + + // Assert + assertEquals(vertexEpoch, committedBefore); + assertEquals(vertexEpoch + 1, committedBeforeCallback); + assertEquals(roundBefore, roundBeforeCallback, 0.0); + assertFalse(decidedBeforeCallback); + assertFalse(decisions.isEmpty()); + assertTrue( + decisions.stream() + .allMatch( + vertexEpoch < MORATORIUM_FROM_EPOCH + ? Vote.class::isInstance + : NoVote.class::isInstance)); + } + } + + @Test + public void consensus_checks_use_event_epoch_even_when_committed_epoch_is_across_a_boundary() { + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + final var stateComputer = test.getInstance(0, RustStateComputer.class); + final var provider = test.getInstance(0, UserTransactionMoratoriumProvider.class); + final var epochs = List.of(MORATORIUM_FROM_EPOCH - 1, MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH); + final var committedEpochs = new ArrayList(); + final var allowed = Option.none(); + final var refused = + Option.some( + new UserTransactionMoratorium( + UInt64.fromNonNegativeLong(MORATORIUM_FROM_EPOCH), + UInt64.fromNonNegativeLong(ENACTMENT_EPOCH))); + + // Act + final var results = + epochs.stream() + .map( + committedEpoch -> { + test.runUntilState(allAtOrOverEpoch(committedEpoch), MAX_MESSAGES_PER_STEP); + committedEpochs.add(currentEpoch(test)); + return List.of( + stateComputer.ensureUserTransactionsAllowed().toOptionOfError(), + provider + .ensureUserTransactionsAllowed(MORATORIUM_FROM_EPOCH - 1) + .toOptionOfError(), + provider + .ensureUserTransactionsAllowed(MORATORIUM_FROM_EPOCH) + .toOptionOfError(), + provider.ensureUserTransactionsAllowed(ENACTMENT_EPOCH).toOptionOfError()); + }) + .toList(); + + // Assert + assertEquals(epochs, committedEpochs); + assertEquals( + List.of( + List.of(allowed, allowed, refused, allowed), + List.of(refused, allowed, refused, allowed), + List.of(allowed, allowed, refused, allowed)), + results); + } + } + + @Test + public void submissions_are_rejected_and_nothing_commits_until_the_update_is_enacted() { + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + final var beforeMoratorium = TransactionBuilder.forTests().prepare().raw(); + submit(test, beforeMoratorium); + test.runUntilState(allCommittedTransactionSuccess(beforeMoratorium), MAX_MESSAGES_PER_STEP); + final var moratoriaBeforeStart = moratoria(test); + test.runUntilState(allAtOrOverEpoch(MORATORIUM_FROM_EPOCH), MAX_MESSAGES_PER_STEP); + final var moratoriaDuring = moratoria(test); + final var duringMoratorium = TransactionBuilder.forTests().prepare().raw(); + final var mempool = test.getInstance(0, RustMempool.class); + + // Act + final var rejection = + assertThrows( + MempoolRejectedException.class, () -> mempool.addTransaction(duringMoratorium)); + submit(test, duringMoratorium); + test.runUntilState(allAtOrOverEpoch(ENACTMENT_EPOCH), MAX_MESSAGES_PER_STEP); + final var committedBeforeEnactment = isCommittedOnAnyNode(test, duringMoratorium); + final var moratoriaAfterEnactment = moratoria(test); + submit(test, duringMoratorium); + test.runUntilState(allCommittedTransactionSuccess(duringMoratorium), MAX_MESSAGES_PER_STEP); + final var epochOfCommitAfterEnactment = currentEpoch(test); + + // Assert + assertEquals(noMoratoriumOnEveryNode(), moratoriaBeforeStart); + assertEquals(moratoriumOnEveryNode(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH), moratoriaDuring); + assertTrue( + rejection.getMessage(), + rejection + .getMessage() + .contains("temporarily not accepted; retry from epoch " + ENACTMENT_EPOCH)); + assertFalse(committedBeforeEnactment); + assertTrue(allAtOrOverProtocolVersion(EAGLE_RAY).test(test.getNodeInjectors())); + assertEquals(noMoratoriumOnEveryNode(), moratoriaAfterEnactment); + assertEquals(ENACTMENT_EPOCH, epochOfCommitAfterEnactment); + } + } + + @Test + public void proposals_carrying_user_transactions_are_rejected_during_the_moratorium() { + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(MORATORIUM_FROM_EPOCH), MAX_MESSAGES_PER_STEP); + final var forcedTransaction = TransactionBuilder.forTests().prepare().raw(); + test.restartNodeWithOverrideModule( + 0, + new AbstractModule() { + @Override + protected void configure() { + bind(ProposalGenerator.class) + .toInstance((round, prepared) -> List.of(forcedTransaction)); + } + }); + + // Act + test.runUntilState(allAtOrOverEpoch(ENACTMENT_EPOCH), MAX_MESSAGES_PER_STEP); + final var committedBeforeEnactment = isCommittedOnAnyNode(test, forcedTransaction); + final var rejectedDuringMoratorium = + rejectedProposals.stream() + .anyMatch(rejected -> rejected.epoch() == MORATORIUM_FROM_EPOCH); + test.runUntilState(anyCommittedTransactionSuccess(forcedTransaction), MAX_MESSAGES_PER_STEP); + + // Assert + assertFalse(committedBeforeEnactment); + assertTrue(rejectedDuringMoratorium); + assertTrue(allAtOrOverProtocolVersion(EAGLE_RAY).test(test.getNodeInjectors())); + } + } + + @Test + public void coordinated_restart_into_a_moratorium_with_in_flight_transactions_converges() { + try (var test = createTest(withoutMoratorium())) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(3), MAX_MESSAGES_PER_STEP); + final var inFlight = TransactionBuilder.forTests().prepare().raw(); + submit(test, inFlight); + test.runUntilMessage( + timedMessage -> + timedMessage.value().message() instanceof Proposal proposal + && proposal.getVertex().getTransactions().stream() + .anyMatch( + transaction -> + Arrays.equals(transaction.getPayload(), inFlight.getPayload())), + true, + MAX_MESSAGES_PER_STEP); + test.runForCount(8); + final var haltEpoch = currentEpoch(test); + test.getNodeIndices().forEach(test::shutdownNode); + test.getNetwork().dropMessages(message -> true); + final var restartConfig = withMoratorium(haltEpoch, haltEpoch + 1); + final var upgradedBinary = + new AbstractModule() { + @Override + protected void configure() { + bind(ProtocolConfig.class).toInstance(restartConfig); + } + }; + + // Act + test.getNodeIndices() + .forEach(nodeIndex -> test.restartNodeWithOverrideModule(nodeIndex, upgradedBinary)); + final var moratoriaAfterRestart = moratoria(test); + final var submittedDuring = TransactionBuilder.forTests().prepare().raw(); + final var rejection = + assertThrows( + MempoolRejectedException.class, + () -> test.getInstance(0, RustMempool.class).addTransaction(submittedDuring)); + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + final var inFlightCommittedPerNode = + test.getNodeInjectors().stream() + .map( + injector -> + NodePredicate.committedUserTransaction(inFlight, false, false).test(injector)) + .toList(); + final var submittedDuringCommitted = isCommittedOnAnyNode(test, submittedDuring); + + // Assert + assertEquals(moratoriumOnEveryNode(haltEpoch, haltEpoch + 1), moratoriaAfterRestart); + assertTrue( + rejection.getMessage(), + rejection + .getMessage() + .contains("temporarily not accepted; retry from epoch " + (haltEpoch + 1))); + assertTrue(allAtOrOverEpoch(haltEpoch + 1).test(test.getNodeInjectors())); + assertEquals(noMoratoriumOnEveryNode(), moratoria(test)); + assertEquals(1, inFlightCommittedPerNode.stream().distinct().count()); + assertFalse(submittedDuringCommitted); + } + } + + @Test + public void validator_without_moratorium_cannot_get_user_transactions_certified() { + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(MORATORIUM_FROM_EPOCH), MAX_MESSAGES_PER_STEP); + final var forcedTransaction = TransactionBuilder.forTests().prepare().raw(); + test.restartNodeWithOverrideModule( + 0, + new AbstractModule() { + @Override + protected void configure() { + // Emulate an old validator that still proposes and votes for user transactions. + bind(ProposalGenerator.class) + .toInstance((round, prepared) -> List.of(forcedTransaction)); + OptionalBinder.newOptionalBinder(binder(), UserTransactionMoratoriumProvider.class) + .setBinding() + .toInstance(UserTransactionMoratoriumProvider.NONE); + } + }); + + // Act + test.runUntilState(allAtOrOverEpoch(ENACTMENT_EPOCH), MAX_MESSAGES_PER_STEP); + final var committedBeforeEnactment = isCommittedOnAnyNode(test, forcedTransaction); + final var rejectedDuringMoratorium = + rejectedProposals.stream() + .anyMatch(rejected -> rejected.epoch() == MORATORIUM_FROM_EPOCH); + test.runUntilState(anyCommittedTransactionSuccess(forcedTransaction), MAX_MESSAGES_PER_STEP); + + // Assert + assertFalse(committedBeforeEnactment); + assertTrue(rejectedDuringMoratorium); + assertTrue(allAtOrOverProtocolVersion(EAGLE_RAY).test(test.getNodeInjectors())); + } + } + + @Test + public void late_joining_node_syncs_through_the_moratorium_and_restarted_validator_rejoins() { + final var validatorIndices = List.of(0, 1, 2, 3); + final var fullNodeIndex = NUM_VALIDATORS; + final var nodes = + Stream.concat( + PhysicalNodeConfig.createBatch(NUM_VALIDATORS, true).stream(), + PhysicalNodeConfig.createBatchStream(false).skip(NUM_VALIDATORS).limit(1)) + .toList(); + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH), nodes)) { + // Arrange + validatorIndices.forEach(test::startNode); + test.runUntilState( + nodesMatch(validatorIndices, NodePredicate.atOrOverProtocolVersion(EAGLE_RAY)), + MAX_MESSAGES_PER_STEP); + final var stateVersionAtEnactment = stateVersion(test, 0); + + // Act + test.startNode(fullNodeIndex); + test.runUntilState( + nodesMatch( + List.of(fullNodeIndex), + NodePredicate.atOrOverProtocolVersion(EAGLE_RAY) + .and(NodePredicate.atOrOverStateVersion(stateVersionAtEnactment))), + MAX_MESSAGES_PER_STEP); + final var fullNodeMoratorium = moratoriumOf(test, fullNodeIndex); + test.restartNode(1); + final var restartedValidatorMoratorium = moratoriumOf(test, 1); + final var stateVersionBeforeProgress = stateVersion(test, 0); + test.runUntilState( + nodesMatch( + List.of(0, 1), NodePredicate.atOrOverStateVersion(stateVersionBeforeProgress + 5)), + MAX_MESSAGES_PER_STEP); + + // Assert + assertEquals(Option.empty(), fullNodeMoratorium); + assertEquals(Option.empty(), restartedValidatorMoratorium); + assertEquals(NUM_VALIDATORS + 1, test.numNodesLive()); + } + } + + @Test + public void core_api_refusal_is_temporary_and_the_same_payload_commits_after_enactment() + throws Exception { + final var coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(MORATORIUM_FROM_EPOCH), MAX_MESSAGES_PER_STEP); + test.restartNodeWithOverrideModule(0, coreApiHelper.module()); + final var transaction = TransactionBuilder.forTests().prepare(); + final var request = + new TransactionSubmitRequest() + .network(Network.INTEGRATIONTESTNET.getLogicalName()) + .notarizedTransactionHex(transaction.hexPayloadBytes()); + + // Act + final var errorResponse = + coreApiHelper.assertErrorResponseOfType( + () -> coreApiHelper.transactionApi().transactionSubmitPost(request), + TransactionSubmitErrorResponse.class); + final var details = (TransactionSubmitRejectedErrorDetails) errorResponse.getDetails(); + final var mempoolCountDuringMoratorium = test.getInstance(0, RustMempool.class).getCount(); + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + final var accepted = coreApiHelper.transactionApi().transactionSubmitPost(request); + test.runUntilState(allCommittedTransactionSuccess(transaction.raw()), MAX_MESSAGES_PER_STEP); + + // Assert + assertEquals(Integer.valueOf(400), errorResponse.getCode()); + assertEquals(Boolean.FALSE, details.getIsIntentRejectionPermanent()); + assertEquals(Boolean.FALSE, details.getIsPayloadRejectionPermanent()); + assertEquals(Long.valueOf(ENACTMENT_EPOCH), details.getRetryFromEpoch()); + assertEquals(0, mempoolCountDuringMoratorium); + assertEquals(Boolean.FALSE, accepted.getDuplicate()); + assertTrue(isCommittedOnAnyNode(test, transaction.raw())); + assertTrue( + details.getErrorMessage(), + details + .getErrorMessage() + .contains("temporarily not accepted; retry from epoch " + ENACTMENT_EPOCH)); + } + } + + /** The empty fallback still produces the round update needed to end the epoch. */ + @Test + public void old_binary_leader_of_the_epoch_ending_round_cannot_delay_or_pollute_the_enactment() { + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(MORATORIUM_FROM_EPOCH), MAX_MESSAGES_PER_STEP); + final var epochEndingRound = Round.of(ROUNDS_PER_EPOCH); + final var oldLeader = leaderOf(test, MORATORIUM_FROM_EPOCH, epochEndingRound); + final var upgradedNodes = + test.getNodeIndices().stream().filter(nodeIndex -> nodeIndex != oldLeader).toList(); + final var forcedTransaction = TransactionBuilder.forTests().prepare().raw(); + test.restartNodeWithOverrideModule(oldLeader, oldBinaryEmulation(forcedTransaction)); + + // Act + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + final var committedByEnactment = isCommittedOnAnyNode(test, forcedTransaction); + final var enactingEpochChangeRound = epochChangeRound(test, upgradedNodes.get(0)); + final var rejectedInEpochEndingRound = + rejectedProposals.stream() + .anyMatch( + rejected -> + rejected.epoch() == MORATORIUM_FROM_EPOCH + && rejected.proposalRejected().round().equals(epochEndingRound)); + test.runUntilState(anyCommittedTransactionSuccess(forcedTransaction), MAX_MESSAGES_PER_STEP); + + // Assert + assertFalse(committedByEnactment); + assertTrue(rejectedProposals.toString(), rejectedInEpochEndingRound); + assertEquals(epochEndingRound.number(), enactingEpochChangeRound); + } + } + + @Test + public void relayed_user_transactions_are_discarded_during_the_moratorium_and_accepted_after() { + try (var test = createTest(withMoratorium(MORATORIUM_FROM_EPOCH, ENACTMENT_EPOCH))) { + // Arrange + test.startAllNodes(); + test.runUntilState(allAtOrOverEpoch(MORATORIUM_FROM_EPOCH), MAX_MESSAGES_PER_STEP); + final var gossiped = TransactionBuilder.forTests().prepare().raw(); + + // Act + relayFromTo(test, 0, 1, gossiped); + final var heldByReceiver = test.getInstance(1, RustMempool.class).getCount(); + test.runUntilState(allAtOrOverProtocolVersion(EAGLE_RAY), MAX_MESSAGES_PER_STEP); + final var committedByEnactment = isCommittedOnAnyNode(test, gossiped); + final var heldByAnyNodeAtEnactment = + test.getNodeIndices().stream() + .mapToInt(nodeIndex -> test.getInstance(nodeIndex, RustMempool.class).getCount()) + .sum(); + relayFromTo(test, 0, 1, gossiped); + test.runUntilState(allCommittedTransactionSuccess(gossiped), MAX_MESSAGES_PER_STEP); + + // Assert + assertEquals(0, heldByReceiver); + assertFalse(committedByEnactment); + assertEquals(0, heldByAnyNodeAtEnactment); + } + } + + private DeterministicTest createTest(ProtocolConfig protocolConfig) { + return createTest(protocolConfig, PhysicalNodeConfig.createBatch(NUM_VALIDATORS, true)); + } + + private DeterministicTest createTest( + ProtocolConfig protocolConfig, List nodes) { + return DeterministicTest.builder() + .addPhysicalNodes(nodes) + .addMonitors(rejectedProposalMonitor()) + .messageSelector(firstSelector()) + .functionalNodeModule(functionalNodeModule(protocolConfig)); + } + + private FunctionalRadixNodeModule functionalNodeModule(ProtocolConfig protocolConfig) { + return new FunctionalRadixNodeModule( + FunctionalRadixNodeModule.NodeStorageConfig.tempFolder(folder), + true, + FunctionalRadixNodeModule.SafetyRecoveryConfig.REAL, + FunctionalRadixNodeModule.ConsensusConfig.testDefault(), + FunctionalRadixNodeModule.LedgerConfig.stateComputerWithSyncRelay( + StateComputerConfig.rev2() + .withGenesis( + GenesisBuilder.createTestGenesisWithNumValidators( + NUM_VALIDATORS, + Decimal.ONE, + GenesisConsensusManagerConfig.Builder.testWithRoundsPerEpoch( + ROUNDS_PER_EPOCH))) + .withProtocolConfig(protocolConfig) + .withProposerConfig(StateComputerConfig.REV2ProposerConfig.Mempool.defaults()), + SyncRelayConfig.of(5000, 10, 3000L))); + } + + private Module rejectedProposalMonitor() { + return new AbstractModule() { + @ProvidesIntoSet + MessageMonitor recordRejectedProposals() { + return (message, time) -> { + if (message.message() instanceof EpochProposalRejected rejected) { + rejectedProposals.add(rejected); + } + }; + } + }; + } + + /** Keeps proposing and voting for user transactions, as an old binary would. */ + private static Module oldBinaryEmulation(RawNotarizedTransaction proposedTransaction) { + return new AbstractModule() { + @Override + protected void configure() { + bind(ProposalGenerator.class).toInstance((round, prepared) -> List.of(proposedTransaction)); + OptionalBinder.newOptionalBinder(binder(), UserTransactionMoratoriumProvider.class) + .setBinding() + .toInstance(UserTransactionMoratoriumProvider.NONE); + } + }; + } + + /** Returns the node index of the round's leader. */ + private static int leaderOf(DeterministicTest test, long epoch, Round round) { + final var epochProof = + test.getInstance(0, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .latestProofWhichInitiatedAnEpochChange(); + final var nextEpoch = epochProof.ledgerHeader().nextEpoch().orElseThrow(); + if (nextEpoch.epoch().toLong() != epoch) { + throw new IllegalStateException("The nodes are not at the start of epoch " + epoch); + } + final var validatorSet = REv2ToConsensus.validatorSet(nextEpoch.validators()); + final var leaderKey = + ProposerElections.defaultRotation(epoch, validatorSet).getProposer(round).getKey(); + return test.getNodeIndices().stream() + .filter(nodeIndex -> selfKey(test, nodeIndex).equals(leaderKey)) + .findFirst() + .orElseThrow(); + } + + private static ECDSASecp256k1PublicKey selfKey(DeterministicTest test, int nodeIndex) { + return test.getInstance(nodeIndex, Key.get(ECDSASecp256k1PublicKey.class, Self.class)); + } + + private static void relayFromTo( + DeterministicTest test, int fromNode, int toNode, RawNotarizedTransaction transaction) { + final var receiver = test.getInstance(toNode, Key.get(NodeId.class, Self.class)); + test.getInstance( + fromNode, Key.get(new TypeLiteral>() {})) + .dispatch(receiver, new MempoolAdd(List.of(transaction))); + test.runUntilOutOfMessagesOfType( + 100, message -> message.message() instanceof MempoolAdd && !message.channelId().isLocal()); + } + + private static long epochChangeRound(DeterministicTest test, int nodeIndex) { + return test.getInstance(nodeIndex, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .latestProofWhichInitiatedAnEpochChange() + .ledgerHeader() + .round() + .toLong(); + } + + private static Predicate> nodesMatch( + List nodeIndices, Predicate nodePredicate) { + return injectors -> + nodeIndices.stream().allMatch(index -> nodePredicate.test(injectors.get(index))); + } + + private static long stateVersion(DeterministicTest test, int nodeIndex) { + return test.getInstance(nodeIndex, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .resultantStateVersion(); + } + + private static Option moratoriumOf( + DeterministicTest test, int nodeIndex) { + return test.getInstance(nodeIndex, RustStateComputer.class) + .ensureUserTransactionsAllowed() + .toOptionOfError(); + } + + private static ProtocolConfig withMoratorium(long moratoriumFromEpoch, long enactmentEpoch) { + return ProtocolConfig.enactAtEpochWithUserTransactionMoratorium( + EAGLE_RAY, moratoriumFromEpoch, enactmentEpoch); + } + + private static ProtocolConfig withoutMoratorium() { + return ProtocolConfig.enactAtEpoch(EAGLE_RAY, 1000); + } + + private static void submit(DeterministicTest test, RawNotarizedTransaction transaction) { + final var mempoolDispatcher = + test.getInstance(0, Key.get(new TypeLiteral>() {})); + mempoolDispatcher.dispatch(new MempoolAdd(List.of(transaction))); + test.runUntilOutOfMessagesOfType(100, onlyLocalMempoolAddEvents()); + } + + private static boolean isCommittedOnAnyNode( + DeterministicTest test, RawNotarizedTransaction transaction) { + return test.getNodeInjectors().stream() + .anyMatch(NodePredicate.committedUserTransaction(transaction, false, false)); + } + + private static long currentEpoch(DeterministicTest test) { + return test.getInstance(0, TransactionsAndProofReader.class) + .getLatestProofBundle() + .orElseThrow() + .resultantEpoch(); + } + + private static List> moratoria(DeterministicTest test) { + return test.getNodeInjectors().stream() + .map( + injector -> + injector + .getInstance(RustStateComputer.class) + .ensureUserTransactionsAllowed() + .toOptionOfError()) + .toList(); + } + + private static List> moratoriumOnEveryNode( + long fromEpoch, long enactmentEpoch) { + final var moratorium = + new UserTransactionMoratorium( + UInt64.fromNonNegativeLong(fromEpoch), UInt64.fromNonNegativeLong(enactmentEpoch)); + return java.util.Collections.nCopies(NUM_VALIDATORS, Option.some(moratorium)); + } + + private static List> noMoratoriumOnEveryNode() { + return java.util.Collections.nCopies(NUM_VALIDATORS, Option.empty()); + } +} diff --git a/sdk/typescript/lib/generated/models/SystemVersion.ts b/sdk/typescript/lib/generated/models/SystemVersion.ts index 66c995630d..3675ca1c4c 100644 --- a/sdk/typescript/lib/generated/models/SystemVersion.ts +++ b/sdk/typescript/lib/generated/models/SystemVersion.ts @@ -20,7 +20,9 @@ export const SystemVersion = { V1: 'V1', V2: 'V2', - V3: 'V3' + V3: 'V3', + V4: 'V4', + V5: 'V5' } as const; export type SystemVersion = typeof SystemVersion[keyof typeof SystemVersion];