//------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled Copyright (c) 2019 Ripple Labs Inc. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ //============================================================================== #include #include #include #include #include #include #include #include #include #include #include namespace ripple { namespace test { struct FeeSettingsFields { std::optional baseFee = std::nullopt; std::optional reserveBase = std::nullopt; std::optional reserveIncrement = std::nullopt; std::optional referenceFeeUnits = std::nullopt; std::optional baseFeeDrops = std::nullopt; std::optional reserveBaseDrops = std::nullopt; std::optional reserveIncrementDrops = std::nullopt; std::optional extensionComputeLimit = std::nullopt; std::optional extensionSizeLimit = std::nullopt; std::optional gasPrice = std::nullopt; }; STTx createFeeTx( Rules const& rules, std::uint32_t seq, FeeSettingsFields const& fields, bool forceAllFields = false) { auto fill = [&](auto& obj) { obj.setAccountID(sfAccount, AccountID()); obj.setFieldU32(sfLedgerSequence, seq); if (rules.enabled(featureXRPFees)) { // New XRPFees format - all three fields are REQUIRED obj.setFieldAmount( sfBaseFeeDrops, fields.baseFeeDrops ? *fields.baseFeeDrops : XRPAmount{0}); obj.setFieldAmount( sfReserveBaseDrops, fields.reserveBaseDrops ? *fields.reserveBaseDrops : XRPAmount{0}); obj.setFieldAmount( sfReserveIncrementDrops, fields.reserveIncrementDrops ? *fields.reserveIncrementDrops : XRPAmount{0}); } else { // Legacy format - all four fields are REQUIRED obj.setFieldU64(sfBaseFee, fields.baseFee ? *fields.baseFee : 0); obj.setFieldU32( sfReserveBase, fields.reserveBase ? *fields.reserveBase : 0); obj.setFieldU32( sfReserveIncrement, fields.reserveIncrement ? *fields.reserveIncrement : 0); obj.setFieldU32( sfReferenceFeeUnits, fields.referenceFeeUnits ? *fields.referenceFeeUnits : 0); } if (rules.enabled(featureSmartEscrow) || forceAllFields) { obj.setFieldU32( sfExtensionComputeLimit, fields.extensionComputeLimit ? *fields.extensionComputeLimit : 0); obj.setFieldU32( sfExtensionSizeLimit, fields.extensionSizeLimit ? *fields.extensionSizeLimit : 0); obj.setFieldU32(sfGasPrice, fields.gasPrice ? *fields.gasPrice : 0); } }; return STTx(ttFEE, fill); } STTx createInvalidFeeTx( Rules const& rules, std::uint32_t seq, bool missingRequiredFields = true, bool wrongFeatureFields = false, std::uint32_t uniqueValue = 42) { auto fill = [&](auto& obj) { obj.setAccountID(sfAccount, AccountID()); obj.setFieldU32(sfLedgerSequence, seq); if (wrongFeatureFields) { if (rules.enabled(featureXRPFees)) { obj.setFieldU64(sfBaseFee, 10 + uniqueValue); obj.setFieldU32(sfReserveBase, 200000); obj.setFieldU32(sfReserveIncrement, 50000); obj.setFieldU32(sfReferenceFeeUnits, 10); } else { obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10 + uniqueValue}); obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); } } else if (!missingRequiredFields) { // Create valid transaction (all required fields present) if (rules.enabled(featureXRPFees)) { obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10 + uniqueValue}); obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); } else { obj.setFieldU64(sfBaseFee, 10 + uniqueValue); obj.setFieldU32(sfReserveBase, 200000); obj.setFieldU32(sfReserveIncrement, 50000); obj.setFieldU32(sfReferenceFeeUnits, 10); } if (rules.enabled(featureSmartEscrow)) { obj.setFieldU32(sfExtensionComputeLimit, 100 + uniqueValue); obj.setFieldU32(sfExtensionSizeLimit, 200 + uniqueValue); obj.setFieldU32(sfGasPrice, 300 + uniqueValue); } } // If missingRequiredFields is true, we don't add the required fields // (default behavior) }; return STTx(ttFEE, fill); } TER applyFeeAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx) { auto const res = apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal); return res.ter; } bool verifyFeeObject( std::shared_ptr const& ledger, Rules const& rules, FeeSettingsFields const& expected) { auto const feeObject = ledger->read(keylet::fees()); if (!feeObject) return false; auto checkEquality = [&](auto const& field, auto const& expected) { if (!feeObject->isFieldPresent(field)) return false; return feeObject->at(field) == expected; }; if (rules.enabled(featureXRPFees)) { if (feeObject->isFieldPresent(sfBaseFee) || feeObject->isFieldPresent(sfReserveBase) || feeObject->isFieldPresent(sfReserveIncrement) || feeObject->isFieldPresent(sfReferenceFeeUnits)) return false; if (!checkEquality( sfBaseFeeDrops, expected.baseFeeDrops.value_or(XRPAmount{0}))) return false; if (!checkEquality( sfReserveBaseDrops, expected.reserveBaseDrops.value_or(XRPAmount{0}))) return false; if (!checkEquality( sfReserveIncrementDrops, expected.reserveIncrementDrops.value_or(XRPAmount{0}))) return false; } else { if (feeObject->isFieldPresent(sfBaseFeeDrops) || feeObject->isFieldPresent(sfReserveBaseDrops) || feeObject->isFieldPresent(sfReserveIncrementDrops)) return false; // Read sfBaseFee as a hex string and compare to expected.baseFee if (!checkEquality(sfBaseFee, expected.baseFee)) return false; if (!checkEquality(sfReserveBase, expected.reserveBase)) return false; if (!checkEquality(sfReserveIncrement, expected.reserveIncrement)) return false; if (!checkEquality(sfReferenceFeeUnits, expected.referenceFeeUnits)) return false; } if (rules.enabled(featureSmartEscrow)) { if (!checkEquality( sfExtensionComputeLimit, expected.extensionComputeLimit.value_or(0))) return false; if (!checkEquality( sfExtensionSizeLimit, expected.extensionSizeLimit.value_or(0))) return false; if (!checkEquality(sfGasPrice, expected.gasPrice.value_or(0))) return false; } else { if (feeObject->isFieldPresent(sfExtensionComputeLimit) || feeObject->isFieldPresent(sfExtensionSizeLimit) || feeObject->isFieldPresent(sfGasPrice)) return false; } return true; } std::vector getTxs(std::shared_ptr const& txSet) { std::vector txs; for (auto i = txSet->begin(); i != txSet->end(); ++i) { auto const data = i->slice(); auto serialIter = SerialIter(data); txs.push_back(STTx(serialIter)); } return txs; }; class FeeVote_test : public beast::unit_test::suite { void testSetup() { testcase("FeeVote setup"); FeeSetup const defaultSetup; { // defaults Section config; auto setup = setup_FeeVote(config); BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee); BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve); BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve); BEAST_EXPECT( setup.extension_compute_limit == defaultSetup.extension_compute_limit); BEAST_EXPECT( setup.extension_size_limit == defaultSetup.extension_size_limit); BEAST_EXPECT(setup.gas_price == defaultSetup.gas_price); } { Section config; config.append( {"reference_fee = 50", "account_reserve = 1234567", "owner_reserve = 1234", "extension_compute_limit = 100", "extension_size_limit = 200", " gas_price = 300"}); auto setup = setup_FeeVote(config); BEAST_EXPECT(setup.reference_fee == 50); BEAST_EXPECT(setup.account_reserve == 1234567); BEAST_EXPECT(setup.owner_reserve == 1234); BEAST_EXPECT(setup.extension_compute_limit == 100); BEAST_EXPECT(setup.extension_size_limit == 200); BEAST_EXPECT(setup.gas_price == 300); } { Section config; config.append( {"reference_fee = blah", "account_reserve = yada", "owner_reserve = foo", "extension_compute_limit = bar", "extension_size_limit = baz", "gas_price = qux"}); // Illegal values are ignored, and the defaults left unchanged auto setup = setup_FeeVote(config); BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee); BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve); BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve); BEAST_EXPECT( setup.extension_compute_limit == defaultSetup.extension_compute_limit); BEAST_EXPECT( setup.extension_size_limit == defaultSetup.extension_size_limit); BEAST_EXPECT(setup.gas_price == defaultSetup.gas_price); } { Section config; config.append( {"reference_fee = -50", "account_reserve = -1234567", "owner_reserve = -1234", "extension_compute_limit = -100", "extension_size_limit = -200", "gas_price = -300"}); // Illegal values are ignored, and the defaults left unchanged auto setup = setup_FeeVote(config); BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee); BEAST_EXPECT( setup.account_reserve == static_cast(-1234567)); BEAST_EXPECT( setup.owner_reserve == static_cast(-1234)); BEAST_EXPECT( setup.extension_compute_limit == static_cast(-100)); BEAST_EXPECT( setup.extension_size_limit == static_cast(-200)); BEAST_EXPECT(setup.gas_price == static_cast(-300)); } { auto const big64 = std::to_string( static_cast( std::numeric_limits::max()) + 1); Section config; config.append( {"reference_fee = " + big64, "account_reserve = " + big64, "owner_reserve = " + big64, "extension_compute_limit = " + big64, "extension_size_limit = " + big64, "gas_price = " + big64}); // Illegal values are ignored, and the defaults left unchanged auto setup = setup_FeeVote(config); BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee); BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve); BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve); BEAST_EXPECT( setup.extension_compute_limit == defaultSetup.extension_compute_limit); BEAST_EXPECT( setup.extension_size_limit == defaultSetup.extension_size_limit); BEAST_EXPECT(setup.gas_price == defaultSetup.gas_price); } } void testBasic() { testcase("Basic SetFee transaction"); // Test with XRPFees disabled (legacy format) { jtx::Env env( *this, jtx::testable_amendments() - featureXRPFees - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // Create the next ledger to apply transaction to ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); // Test successful fee transaction with legacy fields FeeSettingsFields fields{ .baseFee = 10, .reserveBase = 200000, .reserveIncrement = 50000, .referenceFeeUnits = 10}; auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); OpenView accum(ledger.get()); BEAST_EXPECT( isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); accum.apply(*ledger); // Verify fee object was created/updated correctly BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); } // Test with XRPFees enabled (new format) { jtx::Env env( *this, jtx::testable_amendments() - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // Create the next ledger to apply transaction to ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); FeeSettingsFields fields{ .baseFeeDrops = XRPAmount{10}, .reserveBaseDrops = XRPAmount{200000}, .reserveIncrementDrops = XRPAmount{50000}}; // Test successful fee transaction with new fields auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); OpenView accum(ledger.get()); BEAST_EXPECT( isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); accum.apply(*ledger); // Verify fee object was created/updated correctly BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); } // Test with both XRPFees and SmartEscrow enabled { jtx::Env env(*this, jtx::testable_amendments()); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // Create the next ledger to apply transaction to ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); FeeSettingsFields fields{ .baseFeeDrops = XRPAmount{10}, .reserveBaseDrops = XRPAmount{200000}, .reserveIncrementDrops = XRPAmount{50000}, .extensionComputeLimit = 100, .extensionSizeLimit = 200, .gasPrice = 300}; // Test successful fee transaction with new fields auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); OpenView accum(ledger.get()); BEAST_EXPECT( isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); accum.apply(*ledger); // Verify fee object was created/updated correctly BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); } // Test that the Smart Escrow fields are rejected if the // feature is disabled { jtx::Env env( *this, jtx::testable_amendments() - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // Create the next ledger to apply transaction to ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); FeeSettingsFields fields{ .baseFeeDrops = XRPAmount{10}, .reserveBaseDrops = XRPAmount{200000}, .reserveIncrementDrops = XRPAmount{50000}, .extensionComputeLimit = 100, .extensionSizeLimit = 200, .gasPrice = 300}; // Test successful fee transaction with new fields auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields, true); OpenView accum(ledger.get()); BEAST_EXPECT( !isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); } } void testTransactionValidation() { testcase("Fee Transaction Validation"); { jtx::Env env( *this, jtx::testable_amendments() - featureXRPFees - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // Create the next ledger to apply transaction to ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); // Test transaction with missing required legacy fields auto invalidTx = createInvalidFeeTx( ledger->rules(), ledger->seq(), true, false, 1); OpenView accum(ledger.get()); BEAST_EXPECT( !isTesSuccess(applyFeeAndTestResult(env, accum, invalidTx))); // Test transaction with new format fields when XRPFees is disabled auto disallowedTx = createInvalidFeeTx( ledger->rules(), ledger->seq(), false, true, 2); BEAST_EXPECT( !isTesSuccess(applyFeeAndTestResult(env, accum, disallowedTx))); } { jtx::Env env( *this, jtx::testable_amendments() - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // Create the next ledger to apply transaction to ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); // Test transaction with missing required new fields auto invalidTx = createInvalidFeeTx( ledger->rules(), ledger->seq(), true, false, 3); OpenView accum(ledger.get()); BEAST_EXPECT( !isTesSuccess(applyFeeAndTestResult(env, accum, invalidTx))); // Test transaction with legacy fields when XRPFees is enabled auto disallowedTx = createInvalidFeeTx( ledger->rules(), ledger->seq(), false, true, 4); BEAST_EXPECT( !isTesSuccess(applyFeeAndTestResult(env, accum, disallowedTx))); } { jtx::Env env( *this, jtx::testable_amendments() | featureXRPFees | featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // Create the next ledger to apply transaction to ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); // Test transaction with missing required new fields auto invalidTx = createInvalidFeeTx( ledger->rules(), ledger->seq(), true, false, 5); OpenView accum(ledger.get()); BEAST_EXPECT( !isTesSuccess(applyFeeAndTestResult(env, accum, invalidTx))); // Test transaction with legacy fields when XRPFees is enabled auto disallowedTx = createInvalidFeeTx( ledger->rules(), ledger->seq(), false, true, 6); BEAST_EXPECT( !isTesSuccess(applyFeeAndTestResult(env, accum, disallowedTx))); } } void testPseudoTransactionProperties() { testcase("Pseudo Transaction Properties"); jtx::Env env(*this, jtx::testable_amendments() - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // Create the next ledger to apply transaction to ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); auto feeTx = createFeeTx( ledger->rules(), ledger->seq(), {.baseFeeDrops = XRPAmount{10}, .reserveBaseDrops = XRPAmount{200000}, .reserveIncrementDrops = XRPAmount{50000}}); // Verify pseudo-transaction properties BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID()); BEAST_EXPECT(feeTx.getFieldAmount(sfFee) == XRPAmount{0}); BEAST_EXPECT(feeTx.getSigningPubKey().empty()); BEAST_EXPECT(feeTx.getSignature().empty()); BEAST_EXPECT(!feeTx.isFieldPresent(sfSigners)); BEAST_EXPECT(feeTx.getFieldU32(sfSequence) == 0); BEAST_EXPECT(!feeTx.isFieldPresent(sfPreviousTxnID)); // But can be applied to a closed ledger { OpenView closedAccum(ledger.get()); BEAST_EXPECT( isTesSuccess(applyFeeAndTestResult(env, closedAccum, feeTx))); } } void testMultipleFeeUpdates() { testcase("Multiple Fee Updates"); jtx::Env env(*this, jtx::testable_amendments() - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); FeeSettingsFields fields1{ .baseFeeDrops = XRPAmount{10}, .reserveBaseDrops = XRPAmount{200000}, .reserveIncrementDrops = XRPAmount{50000}}; auto feeTx1 = createFeeTx(ledger->rules(), ledger->seq(), fields1); { OpenView accum(ledger.get()); BEAST_EXPECT( isTesSuccess(applyFeeAndTestResult(env, accum, feeTx1))); accum.apply(*ledger); } BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields1)); // Apply second fee transaction with different values ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); FeeSettingsFields fields2{ .baseFeeDrops = XRPAmount{20}, .reserveBaseDrops = XRPAmount{300000}, .reserveIncrementDrops = XRPAmount{75000}}; auto feeTx2 = createFeeTx(ledger->rules(), ledger->seq(), fields2); { OpenView accum(ledger.get()); BEAST_EXPECT( isTesSuccess(applyFeeAndTestResult(env, accum, feeTx2))); accum.apply(*ledger); } // Verify second update overwrote the first BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields2)); } void testWrongLedgerSequence() { testcase("Wrong Ledger Sequence"); jtx::Env env(*this, jtx::testable_amendments() - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); // Test transaction with wrong ledger sequence auto feeTx = createFeeTx( ledger->rules(), ledger->seq() + 5, // Wrong sequence (should be ledger->seq()) {.baseFeeDrops = XRPAmount{10}, .reserveBaseDrops = XRPAmount{200000}, .reserveIncrementDrops = XRPAmount{50000}}); OpenView accum(ledger.get()); // The transaction should still succeed as long as other fields are // valid // The ledger sequence field is only used for informational purposes BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); } void testPartialFieldUpdates() { testcase("Partial Field Updates"); jtx::Env env(*this, jtx::testable_amendments() - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); FeeSettingsFields fields1{ .baseFeeDrops = XRPAmount{10}, .reserveBaseDrops = XRPAmount{200000}, .reserveIncrementDrops = XRPAmount{50000}}; auto feeTx1 = createFeeTx(ledger->rules(), ledger->seq(), fields1); { OpenView accum(ledger.get()); BEAST_EXPECT( isTesSuccess(applyFeeAndTestResult(env, accum, feeTx1))); accum.apply(*ledger); } BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields1)); ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); // Apply partial update (only some fields) FeeSettingsFields fields2{ .baseFeeDrops = XRPAmount{20}, .reserveBaseDrops = XRPAmount{200000}}; auto feeTx2 = createFeeTx(ledger->rules(), ledger->seq(), fields2); { OpenView accum(ledger.get()); BEAST_EXPECT( isTesSuccess(applyFeeAndTestResult(env, accum, feeTx2))); accum.apply(*ledger); } // Verify the partial update worked BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields2)); } void testSingleInvalidTransaction() { testcase("Single Invalid Transaction"); jtx::Env env(*this, jtx::testable_amendments() - featureSmartEscrow); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); // Test invalid transaction with non-zero account - this should fail // validation auto invalidTx = STTx(ttFEE, [&](auto& obj) { obj.setAccountID( sfAccount, AccountID(1)); // Should be zero (this makes it invalid) obj.setFieldU32(sfLedgerSequence, ledger->seq()); obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10}); obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); }); OpenView accum(ledger.get()); BEAST_EXPECT( !isTesSuccess(applyFeeAndTestResult(env, accum, invalidTx))); } void testDoValidation() { testcase("doValidation"); using namespace jtx; FeeSetup setup; setup.reference_fee = 42; setup.account_reserve = 1234567; setup.owner_reserve = 7654321; // Test with XRPFees enabled { Env env(*this, testable_amendments() - featureSmartEscrow); auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); auto sec = randomSecretKey(); auto pub = derivePublicKey(KeyType::secp256k1, sec); auto val = std::make_shared( env.app().timeKeeper().now(), pub, sec, calcNodeID(pub), [](STValidation& v) { v.setFieldU32(sfLedgerSequence, 12345); }); // Use the current ledger's fees as the "current" fees for // doValidation auto const& currentFees = ledger->fees(); feeVote->doValidation(currentFees, ledger->rules(), *val); BEAST_EXPECT(val->isFieldPresent(sfBaseFeeDrops)); BEAST_EXPECT( val->getFieldAmount(sfBaseFeeDrops) == XRPAmount(setup.reference_fee)); } // Test with XRPFees disabled (legacy format) { Env env( *this, testable_amendments() - featureXRPFees - featureSmartEscrow); auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); auto sec = randomSecretKey(); auto pub = derivePublicKey(KeyType::secp256k1, sec); auto val = std::make_shared( env.app().timeKeeper().now(), pub, sec, calcNodeID(pub), [](STValidation& v) { v.setFieldU32(sfLedgerSequence, 12345); }); auto const& currentFees = ledger->fees(); feeVote->doValidation(currentFees, ledger->rules(), *val); // In legacy mode, should vote using legacy fields BEAST_EXPECT(val->isFieldPresent(sfBaseFee)); BEAST_EXPECT(val->getFieldU64(sfBaseFee) == setup.reference_fee); } } void testDoVoting() { testcase("doVoting"); using namespace jtx; FeeSetup setup; setup.reference_fee = 42; setup.account_reserve = 1234567; setup.owner_reserve = 7654321; Env env(*this, testable_amendments() - featureSmartEscrow); // establish what the current fees are BEAST_EXPECT( env.current()->fees().base == XRPAmount{UNIT_TEST_REFERENCE_FEE}); BEAST_EXPECT(env.current()->fees().reserve == XRPAmount{200'000'000}); BEAST_EXPECT(env.current()->fees().increment == XRPAmount{50'000'000}); auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // doVoting requires a flag ledger (every 256th ledger) // We need to create a ledger at sequence 256 to make it a flag ledger for (int i = 0; i < 256 - 1; ++i) { ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(ledger->isFlagLedger()); // Create some mock validations with fee votes std::vector> validations; for (int i = 0; i < 5; i++) { auto sec = randomSecretKey(); auto pub = derivePublicKey(KeyType::secp256k1, sec); auto val = std::make_shared( env.app().timeKeeper().now(), pub, sec, calcNodeID(pub), [&](STValidation& v) { v.setFieldU32(sfLedgerSequence, ledger->seq()); // Vote for different fees than current v.setFieldAmount( sfBaseFeeDrops, XRPAmount{setup.reference_fee}); v.setFieldAmount( sfReserveBaseDrops, XRPAmount{setup.account_reserve}); v.setFieldAmount( sfReserveIncrementDrops, XRPAmount{setup.owner_reserve}); }); if (i % 2) val->setTrusted(); validations.push_back(val); } auto txSet = std::make_shared( SHAMapType::TRANSACTION, env.app().getNodeFamily()); // This should not throw since we have a flag ledger feeVote->doVoting(ledger, validations, txSet); auto const txs = getTxs(txSet); BEAST_EXPECT(txs.size() == 1); auto const& feeTx = txs[0]; BEAST_EXPECT(feeTx.getTxnType() == ttFEE); BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID()); BEAST_EXPECT(feeTx.getFieldU32(sfLedgerSequence) == ledger->seq() + 1); BEAST_EXPECT(feeTx.isFieldPresent(sfBaseFeeDrops)); BEAST_EXPECT(feeTx.isFieldPresent(sfReserveBaseDrops)); BEAST_EXPECT(feeTx.isFieldPresent(sfReserveIncrementDrops)); // The legacy fields should NOT be present BEAST_EXPECT(!feeTx.isFieldPresent(sfBaseFee)); BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveBase)); BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveIncrement)); BEAST_EXPECT(!feeTx.isFieldPresent(sfReferenceFeeUnits)); // Check the values BEAST_EXPECT( feeTx.getFieldAmount(sfBaseFeeDrops) == XRPAmount{setup.reference_fee}); BEAST_EXPECT( feeTx.getFieldAmount(sfReserveBaseDrops) == XRPAmount{setup.account_reserve}); BEAST_EXPECT( feeTx.getFieldAmount(sfReserveIncrementDrops) == XRPAmount{setup.owner_reserve}); } void testDoVotingSmartEscrow() { testcase("doVoting with Smart Escrow"); using namespace jtx; FeeSetup setup; setup.reference_fee = 42; setup.account_reserve = 1234567; setup.owner_reserve = 7654321; setup.extension_compute_limit = 100; setup.extension_size_limit = 200; setup.gas_price = 300; Env env( *this, testable_amendments() | featureXRPFees | featureSmartEscrow); // establish what the current fees are BEAST_EXPECT( env.current()->fees().base == XRPAmount{UNIT_TEST_REFERENCE_FEE}); BEAST_EXPECT(env.current()->fees().reserve == XRPAmount{200'000'000}); BEAST_EXPECT(env.current()->fees().increment == XRPAmount{50'000'000}); BEAST_EXPECT(env.current()->fees().extensionComputeLimit == 0); BEAST_EXPECT(env.current()->fees().extensionSizeLimit == 0); BEAST_EXPECT(env.current()->fees().gasPrice == 0); auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); auto ledger = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // doVoting requires a flag ledger (every 256th ledger) // We need to create a ledger at sequence 256 to make it a flag // ledger for (int i = 0; i < 256 - 1; ++i) { ledger = std::make_shared( *ledger, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(ledger->isFlagLedger()); // Create some mock validations with fee votes std::vector> validations; for (int i = 0; i < 5; i++) { auto sec = randomSecretKey(); auto pub = derivePublicKey(KeyType::secp256k1, sec); auto val = std::make_shared( env.app().timeKeeper().now(), pub, sec, calcNodeID(pub), [&](STValidation& v) { v.setFieldU32(sfLedgerSequence, ledger->seq()); // Vote for different fees than current v.setFieldAmount( sfBaseFeeDrops, XRPAmount{setup.reference_fee}); v.setFieldAmount( sfReserveBaseDrops, XRPAmount{setup.account_reserve}); v.setFieldAmount( sfReserveIncrementDrops, XRPAmount{setup.owner_reserve}); v.setFieldU32( sfExtensionComputeLimit, setup.extension_compute_limit); v.setFieldU32( sfExtensionSizeLimit, setup.extension_size_limit); v.setFieldU32(sfGasPrice, setup.gas_price); }); if (i % 2) val->setTrusted(); validations.push_back(val); } auto txSet = std::make_shared( SHAMapType::TRANSACTION, env.app().getNodeFamily()); // This should not throw since we have a flag ledger feeVote->doVoting(ledger, validations, txSet); auto const txs = getTxs(txSet); BEAST_EXPECT(txs.size() == 1); auto const& feeTx = txs[0]; BEAST_EXPECT(feeTx.getTxnType() == ttFEE); BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID()); BEAST_EXPECT(feeTx.getFieldU32(sfLedgerSequence) == ledger->seq() + 1); BEAST_EXPECT(feeTx.isFieldPresent(sfBaseFeeDrops)); BEAST_EXPECT(feeTx.isFieldPresent(sfReserveBaseDrops)); BEAST_EXPECT(feeTx.isFieldPresent(sfReserveIncrementDrops)); // The legacy fields should NOT be present BEAST_EXPECT(!feeTx.isFieldPresent(sfBaseFee)); BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveBase)); BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveIncrement)); BEAST_EXPECT(!feeTx.isFieldPresent(sfReferenceFeeUnits)); // Check the values BEAST_EXPECT( feeTx.getFieldAmount(sfBaseFeeDrops) == XRPAmount{setup.reference_fee}); BEAST_EXPECT( feeTx.getFieldAmount(sfReserveBaseDrops) == XRPAmount{setup.account_reserve}); BEAST_EXPECT( feeTx.getFieldAmount(sfReserveIncrementDrops) == XRPAmount{setup.owner_reserve}); BEAST_EXPECT( feeTx.getFieldU32(sfExtensionComputeLimit) == setup.extension_compute_limit); BEAST_EXPECT( feeTx.getFieldU32(sfExtensionSizeLimit) == setup.extension_size_limit); BEAST_EXPECT(feeTx.getFieldU32(sfGasPrice) == setup.gas_price); } void run() override { testSetup(); testBasic(); testTransactionValidation(); testPseudoTransactionProperties(); testMultipleFeeUpdates(); testWrongLedgerSequence(); testPartialFieldUpdates(); testSingleInvalidTransaction(); testDoValidation(); testDoVoting(); testDoVotingSmartEscrow(); } }; BEAST_DEFINE_TESTSUITE(FeeVote, app, ripple); } // namespace test } // namespace ripple