#include #include #include #include #include #include #include #include #include #include #include namespace xrpl { 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; }; STTx createFeeTx(Rules const& rules, std::uint32_t seq, FeeSettingsFields const& fields) { 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); } }; 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 missingRequiredFields is true, we don't add the required fields // (default behavior) }; return STTx(ttFEE, fill); } bool applyFeeAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx) { auto const res = apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal); return res.ter == tesSUCCESS; } 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; } 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() { 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); } { Section config; config.append( {"reference_fee = 50", "account_reserve = 1234567", "owner_reserve = 1234"}); auto setup = setup_FeeVote(config); BEAST_EXPECT(setup.reference_fee == 50); BEAST_EXPECT(setup.account_reserve == 1234567); BEAST_EXPECT(setup.owner_reserve == 1234); } { Section config; config.append( {"reference_fee = blah", "account_reserve = yada", "owner_reserve = foo"}); // 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); } { Section config; config.append( {"reference_fee = -50", "account_reserve = -1234567", "owner_reserve = -1234"}); // 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)); } { 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}); // 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); } } void testBasic() { testcase("Basic SetFee transaction"); // Test with XRPFees disabled (legacy format) { jtx::Env env(*this, jtx::testable_amendments() - featureXRPFees); 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(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() | featureXRPFees); 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(applyFeeAndTestResult(env, accum, feeTx)); accum.apply(*ledger); // Verify fee object was created/updated correctly BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); } } void testTransactionValidation() { testcase("Fee Transaction Validation"); { jtx::Env env(*this, jtx::testable_amendments() - featureXRPFees); 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(!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(!applyFeeAndTestResult(env, accum, disallowedTx)); } { jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); 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(!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(!applyFeeAndTestResult(env, accum, disallowedTx)); } } void testPseudoTransactionProperties() { testcase("Pseudo Transaction Properties"); 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()); 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(applyFeeAndTestResult(env, closedAccum, feeTx)); } } void testMultipleFeeUpdates() { testcase("Multiple Fee Updates"); jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); 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(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(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() | featureXRPFees); 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(applyFeeAndTestResult(env, accum, feeTx)); } void testPartialFieldUpdates() { testcase("Partial Field Updates"); jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); 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(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(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() | featureXRPFees); 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(!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() | featureXRPFees); 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); 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() | featureXRPFees); // 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 run() override { testSetup(); testBasic(); testTransactionValidation(); testPseudoTransactionProperties(); testMultipleFeeUpdates(); testWrongLedgerSequence(); testPartialFieldUpdates(); testSingleInvalidTransaction(); testDoValidation(); testDoVoting(); } }; BEAST_DEFINE_TESTSUITE(FeeVote, app, xrpl); } // namespace test } // namespace xrpl