Compare commits

..
Author SHA1 Message Date
tequ 1ba2941cc6 Add fix20261005 Amendment placeholder 2026-10-05 11:11:06 +09:00
23 changed files with 448 additions and 2192 deletions

No files matched your search

+150 -1
View File
@@ -8,6 +8,12 @@ on:
types: [opened, synchronize, reopened, labeled, unlabeled]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
full_matrix:
description: "Force full matrix (6 configs)"
required: false
default: "false"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -24,14 +30,37 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: escape double quotes
id: escape
shell: bash
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
ESCAPED_PR_TITLE="${PR_TITLE//\"/\\\"}"
echo "title=${ESCAPED_PR_TITLE}" >> "$GITHUB_OUTPUT"
- name: Generate build matrix
id: set-matrix
shell: python
env:
GH_TOKEN: ${{ github.token }}
run: |
import json
import os
import urllib.request
matrix = [
# Full matrix with all 6 compiler configurations
# Each configuration includes all parameters needed by the build job
full_matrix = [
{
"compiler_id": "gcc-11-libstdcxx",
"compiler": "gcc",
"cc": "gcc-11",
"cxx": "g++-11",
"compiler_version": 11,
"stdlib": "default",
"configuration": "Debug",
"job_type": "build"
},
{
"compiler_id": "gcc-13-libstdcxx",
"compiler": "gcc",
@@ -64,6 +93,17 @@ jobs:
"grpc/*": ["-Wno-missing-template-arg-list-after-template-kw"]
}
},
{
"compiler_id": "clang-14-libstdcxx-gcc11",
"compiler": "clang",
"cc": "clang-14",
"cxx": "clang++-14",
"compiler_version": 14,
"stdlib": "libstdcxx",
"clang_gcc_toolchain": 11,
"configuration": "Debug",
"job_type": "build"
},
{
"compiler_id": "clang-16-libstdcxx-gcc13",
"compiler": "clang",
@@ -74,8 +114,117 @@ jobs:
"clang_gcc_toolchain": 13,
"configuration": "Debug",
"job_type": "build"
},
{
"compiler_id": "clang-17-libcxx",
"compiler": "clang",
"cc": "clang-17",
"cxx": "clang++-17",
"compiler_version": 17,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "build"
},
{
# Clang 18 - testing if it's faster than Clang 17 with libc++
# Requires patching Conan v1 settings.yml to add version 18
"compiler_id": "clang-18-libcxx",
"compiler": "clang",
"cc": "clang-18",
"cxx": "clang++-18",
"compiler_version": 18,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "build"
}
]
# Minimal matrix for PRs and feature branches
minimal_matrix = [
full_matrix[1], # gcc-13 (middle-ground gcc)
full_matrix[2], # clang-20 llvm-cov coverage
full_matrix[3] # clang-14 (mature, stable clang)
]
# Determine which matrix to use based on the target branch
ref = "${{ github.ref }}"
base_ref = "${{ github.base_ref }}" # For PRs, this is the target branch
event_name = "${{ github.event_name }}"
pr_title = """${{ steps.escape.outputs.title }}"""
pr_labels = """${{ join(github.event.pull_request.labels.*.name, ',') }}"""
pr_head_sha = "${{ github.event.pull_request.head.sha }}"
# Get commit message - for PRs, fetch via API since head_commit.message is empty
if event_name == "pull_request" and pr_head_sha:
try:
url = f"https://api.github.com/repos/${{ github.repository }}/commits/{pr_head_sha}"
req = urllib.request.Request(url, headers={
"Accept": "application/vnd.github.v3+json",
"Authorization": f"Bearer {os.environ.get('GH_TOKEN', '')}"
})
with urllib.request.urlopen(req) as response:
data = json.load(response)
commit_message = data["commit"]["message"]
except Exception as e:
print(f"Failed to fetch commit message: {e}")
commit_message = ""
else:
commit_message = """${{ github.event.head_commit.message }}"""
# Debug logging
print(f"Event: {event_name}")
print(f"Ref: {ref}")
print(f"Base ref: {base_ref}")
print(f"PR head SHA: {pr_head_sha}")
print(f"PR title: {pr_title}")
print(f"PR labels: {pr_labels}")
print(f"Commit message: {commit_message}")
# Manual trigger input to force full matrix.
manual_full = "${{ github.event.inputs.full_matrix || 'false' }}" == "true"
# Label/manual overrides, while preserving existing title/commit behavior.
force_full = (
manual_full
or "[ci-nix-full-matrix]" in commit_message
or "[ci-nix-full-matrix]" in pr_title
or ("ci-full-build" in pr_labels and "ci-nix-full-matrix" in pr_labels)
)
force_min = (
"ci-full-build" in pr_labels
)
print(f"Force full matrix: {force_full}")
print(f"Force min matrix: {force_min}")
# Check if this is targeting a main branch
# For PRs: check base_ref (target branch)
# For pushes: check ref (current branch)
main_branches = ["refs/heads/dev", "refs/heads/release", "refs/heads/candidate"]
if force_full:
# Override: always use full matrix if forced by manual input or label.
use_full = True
elif force_min:
# Override: always use minimal matrix if ci-full-build label is present.
use_full = False
elif event_name == "pull_request":
# For PRs, base_ref is just the branch name (e.g., "dev", not "refs/heads/dev")
# Check if the PR targets release or candidate (more critical branches)
use_full = base_ref in ["release", "candidate"]
else:
# For pushes, ref is the full reference (e.g., "refs/heads/dev")
use_full = ref in main_branches
# Select the appropriate matrix
if use_full:
if force_full:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - forced by [ci-nix-full-matrix] tag")
else:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - targeting main branch")
matrix = full_matrix
else:
print(f"Using MINIMAL matrix (3 configs) - feature branch/PR")
matrix = minimal_matrix
# Add runs_on based on job_type
for entry in matrix:
-1
View File
@@ -69,7 +69,6 @@ enum TELcodes : TERUnderlyingType {
telIMPORT_VL_KEY_NOT_RECOGNISED,
telCAN_NOT_QUEUE_IMPORT,
telENV_RPC_FAILED,
telMANIFEST_FEE_MISMATCH,
};
//------------------------------------------------------------------------------
+1 -1
View File
@@ -38,7 +38,7 @@ XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYe
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
+2 -2
View File
@@ -56,9 +56,9 @@ getVersionString()
(versionString == std::string("0.") + std::string("0.0") ||
versionString == std::string("0.0.0+DEBUG")))
{
std::string y = std::string(&__DATE__[7]);
std::string y = std::string(__DATE__ + 7);
std::string d = std::string(
&__DATE__[4 + (__DATE__[4] == ' ' ? 1 : 0)],
__DATE__ + 4 + (__DATE__[4] == ' ' ? 1 : 0),
__DATE__[4] == ' ' ? 1 : 2);
std::string m;
switch (__DATE__[0])
-1
View File
@@ -174,7 +174,6 @@ transResults()
MAKE_ERROR(telIMPORT_VL_KEY_NOT_RECOGNISED, "Import vl key was not recognized."),
MAKE_ERROR(telCAN_NOT_QUEUE_IMPORT, "Import transaction was not able to be directly applied and cannot be queued."),
MAKE_ERROR(telENV_RPC_FAILED, "Unit test RPC failure."),
MAKE_ERROR(telMANIFEST_FEE_MISMATCH, "Unsigned SetManifest fee is not, or cannot be confirmed as, canonical for this server's ledger fee settings."),
MAKE_ERROR(temMALFORMED, "Malformed transaction."),
MAKE_ERROR(temBAD_AMM_TOKENS, "Malformed: Invalid LPTokens."),
+14 -23
View File
@@ -3281,30 +3281,21 @@ public:
using namespace hook_api;
auto const alice = Account{"alice"};
for (bool const withFix : {true, false})
{
Env env{*this, withFix ? features : features - fix20261005};
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
OpenView ov{*env.current()};
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
Env env{*this, features};
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
OpenView ov{*env.current()};
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
// Invalid argument (wrong size)
BEAST_EXPECT(
api.slot_set(Bytes{1, 2, 3}, 0).error() == INVALID_ARGUMENT);
// Invalid argument (slot_no beyond max)
BEAST_EXPECT(
api.slot_set(Bytes(32, 0), hook_api::max_slots + 1).error() ==
INVALID_ARGUMENT);
// 32-byte txn hash: rejected with fix20261005, looked up (and
// not found here) without it
BEAST_EXPECT(
api.slot_set(Bytes(32, 0), 1).error() ==
(withFix ? INVALID_ARGUMENT : DOESNT_EXIST));
}
// Invalid argument (wrong size)
BEAST_EXPECT(
api.slot_set(Bytes{1, 2, 3}, 0).error() == INVALID_ARGUMENT);
// Invalid argument (slot_no beyond max)
BEAST_EXPECT(
api.slot_set(Bytes(32, 0), hook_api::max_slots + 1).error() ==
INVALID_ARGUMENT);
}
void
+5 -2
View File
@@ -8439,8 +8439,11 @@ private:
using namespace std::literals;
testcase("set manifest TSH");
// OnChainManifests is not yet Supported::yes, so enable it explicitly.
features = features | featureOnChainManifests;
if (!features[featureOnChainManifests])
{
pass();
return;
}
// otxn: master
// tsh: ephemeral
+23 -619
View File
@@ -9493,9 +9493,12 @@ public:
{
testcase("Test slot_set");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice);
env.fund(XRP(10000), bob);
TestHook hook_wasm = wasm[R"[test.hook](
#include <stdint.h>
@@ -9569,49 +9572,32 @@ public:
for (uint32_t i = 2; GUARD(257), i < 256; ++i)
ASSERT(s == slot_size(i));
// slot a txn by hash. With fix20261005 a 32 byte argument is
// rejected outright, without it the otxn is slotted.
// The result is reported as the accept code.
// slot a txn
uint8_t txn[32];
ASSERT(otxn_id(SBUF(txn), 0) == 32);
int64_t r = slot_set(SBUF(txn), 1);
ASSERT(slot_set(SBUF(txn), 1) == 1);
// ensure it's not the same object that was there before
if (r == 1)
ASSERT(s != slot_size(1) && slot_size(1) > 0);
uint32_t s2 = slot_size(1);
// ensure it's not the same object that was there before
ASSERT(s != s2 && s2 > 0);
// done!
accept(0,0,r);
accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(hook);
for (bool const withFix : {true, false})
{
Env env{*this, withFix ? features : features - fix20261005};
env.fund(XRP(10000), alice, bob);
// install the hook on alice
env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0),
M("set slot_set"),
HSFEE);
env.close();
EXPECT_HOOK_FEE(hook, 11653);
// install the hook on alice
env(ripple::test::jtx::hook(alice, {{hso(hook_wasm)}}, 0),
M("set slot_set"),
HSFEE);
env.close();
EXPECT_HOOK_FEE(hook, 11645);
// invoke the hook
env(pay(bob, alice, XRP(1)), M("test slot_set"), fee(XRP(1)));
env.close();
auto const meta = env.meta();
BEAST_REQUIRE(meta && meta->isFieldPresent(sfHookExecutions));
auto const& execs = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(execs.size() == 1);
BEAST_EXPECT(
execs[0].getFieldU64(sfHookReturnCode) ==
(withFix ? 0x8000000000000007ULL /* INVALID_ARGUMENT */
: 1ULL));
}
// invoke the hook
env(pay(bob, alice, XRP(1)), M("test slot_set"), fee(XRP(1)));
}
void
@@ -10971,587 +10957,6 @@ public:
}
}
void
test_state_foreign_set_touch(FeatureBitset features)
{
testcase("Test hook state writes touch the state owner");
using namespace jtx;
if (!features[featureTouch])
return;
for (bool const withFix : {false, true})
{
Env env{
*this,
withFix ? features | fix20261005 : features - fix20261005};
auto const alice = Account{"alice"}; // grantor
auto const bob = Account{"bob"}; // strong grantee
auto const cho = Account{"cho"}; // invoker
auto const dan = Account{"dan"}; // weak grantee
auto const eve = Account{"eve"}; // callback grantee
env.fund(XRP(10000), alice, bob, cho, dan, eve);
// Same hooks as test_state_foreign_set: bob's hook writes the
// current txn id under key 1, namespace 0, of the account named
// in the InvoiceID; alice's hook deletes that key whenever alice
// sends a txn.
TestHook grantee_wasm = wasm[R"[test.hook](
#include <stdint.h>
#define sfInvoiceID ((5U << 16U) + 17U)
extern int32_t _g (uint32_t id, uint32_t maxiter);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t state_foreign (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
extern int64_t state_foreign_set (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
extern int64_t otxn_id(uint32_t, uint32_t, uint32_t);
extern int64_t otxn_field(uint32_t, uint32_t, uint32_t);
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define TOO_BIG (-3)
#define TOO_SMALL (-4)
#define OUT_OF_BOUNDS (-1)
#define DOESNT_EXIST (-5)
#define INVALID_ARGUMENT (-7)
#define SBUF(x) (uint32_t)(x), sizeof(x)
int64_t hook(uint32_t reserved )
{
_g(1,1);
// bounds tests
int64_t y;
ASSERT((y=state_foreign_set(1111111, 32, 1, 32, 1, 32, 1, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 1111111, 1, 32, 1, 32, 1, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 32, 1111111, 32, 1, 32, 1, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 32, 1, 1111111, 1, 32, 1, 20)) == TOO_BIG);
ASSERT((y=state_foreign_set(1, 32, 1, 32, 1111111, 32, 1, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 32, 1, 32, 1, 1111111, 1, 20)) == INVALID_ARGUMENT);
ASSERT((y=state_foreign_set(1, 32, 1, 32, 1, 32, 1111111, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 32, 1, 32, 1, 32, 1, 1111111)) == INVALID_ARGUMENT);
// get this transaction id
uint8_t txn[32];
ASSERT(otxn_id(SBUF(txn), 0) == 32);
// get the invoice id, which contains the grantor account
uint8_t grantor[32];
ASSERT(otxn_field(SBUF(grantor), sfInvoiceID) == 32);
// set the current txn id on the grantor's state under key 1, namespace 0
uint8_t one[32]; one[31] = 1U;
uint8_t zero[32];
ASSERT(state_foreign_set(SBUF(txn), SBUF(one), SBUF(zero), grantor + 12, 20) == 32);
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(grantee);
TestHook grantor_wasm = wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t otxn_field (uint32_t, uint32_t, uint32_t);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t state_foreign_set (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
#define DOESNT_EXIST (-5)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define BUFFER_EQUAL_20(buf1, buf2)\
(\
*(((uint64_t*)(buf1)) + 0) == *(((uint64_t*)(buf2)) + 0) &&\
*(((uint64_t*)(buf1)) + 1) == *(((uint64_t*)(buf2)) + 1) &&\
*(((uint32_t*)(buf1)) + 4) == *(((uint32_t*)(buf2)) + 4))
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define sfAccount ((8U << 16U) + 1U)
int64_t hook(uint32_t reserved )
{
_g(1,1);
uint8_t otxnacc[20];
ASSERT(otxn_field(SBUF(otxnacc), sfAccount) == 20);
uint8_t hookacc[20];
ASSERT(hook_account(SBUF(hookacc)) == 20);
if (BUFFER_EQUAL_20(otxnacc, hookacc))
{
// outgoin txn, delete the state
uint8_t one[32]; one[31] = 1U;
uint8_t zero[32];
// we can use state_foreign_set to do the deletion, a concise way to test this functionality too
int64_t y = state_foreign_set(0,0, SBUF(one), SBUF(zero), SBUF(hookacc));
ASSERT(y == 0 || y == DOESNT_EXIST);
}
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(grantor);
// dan's hook writes nothing when executed strongly, it only asks
// to be executed again weakly. Then it writes the txn id under key
// 2, namespace 0, of the account named in the InvoiceID or, if the
// txn carries the parameter D, deletes key 4 there instead.
TestHook weak_grantee_wasm = wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t hook_again (void);
extern int64_t otxn_id (uint32_t, uint32_t, uint32_t);
extern int64_t otxn_field (uint32_t, uint32_t, uint32_t);
extern int64_t otxn_param (uint32_t, uint32_t, uint32_t, uint32_t);
extern int64_t state_foreign_set (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
#define sfInvoiceID ((5U << 16U) + 17U)
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
uint8_t ns[32];
uint8_t write_key[32] = { [31] = 2U };
uint8_t delete_key[32] = { [31] = 4U };
int64_t hook(uint32_t r)
{
_g(1,1);
// strong execution: write nothing, ask to be executed again weakly
if (r == 0)
{
ASSERT(hook_again() == 1);
return accept(0,0,0);
}
// the grantor is the account in the low 20 bytes of the InvoiceID
uint8_t grantor[32];
ASSERT(otxn_field(SBUF(grantor), sfInvoiceID) == 32);
// with otxn parameter D, delete a key the grantor does not have
uint8_t d[1];
if (otxn_param(SBUF(d), (uint32_t)"D", 1) == 1)
{
ASSERT(state_foreign_set(0, 0, SBUF(delete_key), SBUF(ns), (uint32_t)(grantor + 12), 20) == 0);
return accept(0,0,0);
}
// otherwise write the otxn id under key 2
uint8_t txn[32];
ASSERT(otxn_id(SBUF(txn), 0) == 32);
ASSERT(state_foreign_set(SBUF(txn), SBUF(write_key), SBUF(ns), (uint32_t)(grantor + 12), 20) == 32);
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(weak_grantee);
// eve's hook emits an Invoke. Its callback, which runs when that
// Invoke is applied, writes the Invoke's id under key 3, namespace
// 0, of the account in the hook parameter G.
TestHook cbak_grantee_wasm = wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t emit (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t hook_account (uint32_t write_ptr, uint32_t write_len);
extern int64_t hook_param (uint32_t, uint32_t, uint32_t, uint32_t);
extern int64_t etxn_reserve (uint32_t);
extern int64_t etxn_fee_base (uint32_t read_ptr, uint32_t read_len);
extern int64_t etxn_details (uint32_t write_ptr, uint32_t write_len);
extern int64_t ledger_seq (void);
extern int64_t otxn_id (uint32_t, uint32_t, uint32_t);
extern int64_t state_foreign_set (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
uint8_t ns[32];
uint8_t write_key[32] = { [31] = 3U };
// an Invoke from the hook account, emitted so its callback runs
uint8_t txn[229] =
{
/* size, upto, field name */
/* 3, 0, tt = Invoke */ 0x12U, 0x00U, 0x63U,
/* 5, 3, flags */ 0x22U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 5, 8, sequence */ 0x24U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 6, 13, firstledgersequence */ 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U,
/* 6, 19, lastledgersequence */ 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U,
/* 9, 25, fee */ 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 35, 34, signingpubkey */ 0x73U, 0x21U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* 22, 69, account */ 0x81U, 0x14U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* 138, 91, emit details */
/* 0, 229, */
};
#define FLS_OUT (txn + 15U)
#define LLS_OUT (txn + 21U)
#define FEE_OUT (txn + 26U)
#define ACCOUNT_OUT (txn + 71U)
#define EMIT_OUT (txn + 91U)
#define SET_UINT32(ptr, value)\
{\
uint32_t v = (value);\
(ptr)[0] = v >> 24U; (ptr)[1] = v >> 16U; (ptr)[2] = v >> 8U; (ptr)[3] = v;\
}
#define SET_NATIVE_AMOUNT(ptr, amount)\
{\
uint64_t a = (amount);\
(ptr)[0] = 0x40U + ((a >> 56U) & 0x3FU);\
(ptr)[1] = a >> 48U; (ptr)[2] = a >> 40U; (ptr)[3] = a >> 32U;\
(ptr)[4] = a >> 24U; (ptr)[5] = a >> 16U; (ptr)[6] = a >> 8U; (ptr)[7] = a;\
}
int64_t cbak(uint32_t r)
{
_g(1,1);
// the grantor is the hook parameter G
uint8_t grantor[20];
ASSERT(hook_param(SBUF(grantor), (uint32_t)"G", 1) == 20);
// in a callback the otxn is the emitted txn: write its id under key 3
uint8_t id[32];
ASSERT(otxn_id(SBUF(id), 0) == 32);
ASSERT(state_foreign_set(SBUF(id), SBUF(write_key), SBUF(ns), SBUF(grantor)) == 32);
return accept(0,0,0);
}
int64_t hook(uint32_t r)
{
_g(1,1);
ASSERT(etxn_reserve(1) == 1);
uint32_t fls = (uint32_t)ledger_seq() + 1;
SET_UINT32(FLS_OUT, fls);
SET_UINT32(LLS_OUT, fls + 4);
ASSERT(hook_account(ACCOUNT_OUT, 20) == 20);
ASSERT(etxn_details(EMIT_OUT, 138U) == 138);
int64_t fee = etxn_fee_base(SBUF(txn));
ASSERT(fee > 0);
SET_NATIVE_AMOUNT(FEE_OUT, fee);
uint8_t emithash[32];
ASSERT(emit(SBUF(emithash), SBUF(txn)) == 32);
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(cbak_grantee);
// alice grants each grantee hook the right to write her state
{
Json::Value grants{Json::arrayValue};
auto const grant = [&](std::string const& hash,
Account const& grantee) {
Json::Value g;
g[jss::HookGrant][jss::HookHash] = hash;
g[jss::HookGrant][jss::Authorize] = grantee.human();
grants.append(g);
};
grant(grantee_hash_str, bob);
grant(weak_grantee_hash_str, dan);
grant(cbak_grantee_hash_str, eve);
Json::Value json = ripple::test::jtx::hook(
alice, {{hso(grantor_wasm, overrideFlag)}}, 0);
json[jss::Hooks][0U][jss::Hook][jss::HookGrants] = grants;
env(json, M("touch: set grantor"), HSFEE);
}
env(ripple::test::jtx::hook(
bob, {{hso(grantee_wasm, overrideFlag)}}, 0),
M("touch: set grantee"),
HSFEE);
env(ripple::test::jtx::hook(
dan, {{hso(weak_grantee_wasm, overrideFlag)}}, 0),
M("touch: set weak grantee"),
HSFEE);
{
Json::Value params{Json::arrayValue};
params[0U][jss::HookParameter][jss::HookParameterName] =
strHex(std::string("G"));
params[0U][jss::HookParameter][jss::HookParameterValue] =
strHex(alice.id());
Json::Value json = ripple::test::jtx::hook(
eve, {{hso(cbak_grantee_wasm, overrideFlag)}}, 0);
json[jss::Hooks][0U][jss::Hook][jss::HookParameters] = params;
env(json, M("touch: set callback grantee"), HSFEE);
}
env.close();
uint256 const ns{beast::zero};
auto const aliceState = [&](std::uint8_t n) {
uint256 key;
key.data()[31] = n;
return env.le(keylet::hookState(alice.id(), key, ns));
};
auto const stateIs = [&](std::uint8_t n, uint256 const& txid) {
auto const sle = aliceState(n);
return sle &&
sle->getFieldVL(sfHookStateData) ==
Blob(txid.begin(), txid.end());
};
auto const touchCount = [&]() -> std::uint64_t {
auto const sle = env.le(alice);
return sle->isFieldPresent(sfTouchCount)
? sle->getFieldU64(sfTouchCount)
: 0;
};
auto const prevTxnID = [&]() {
return env.le(alice)->getFieldH256(sfPreviousTxnID);
};
auto const aliceKey = keylet::account(alice.id()).key;
auto const inMeta =
[&](std::shared_ptr<STObject const> const& meta) -> bool {
if (!meta)
return false;
for (auto const& node : meta->getFieldArray(sfAffectedNodes))
if (node.getFieldH256(sfLedgerIndex) == aliceKey)
return true;
return false;
};
// env.meta() closes the ledger, so call it before reading state
auto const aliceInMeta = [&]() -> bool {
return inMeta(env.meta());
};
std::string const invid = std::string(24, '0') + strHex(alice.id());
auto const payTo = [&](Account const& from,
Account const& to,
PrettyAmount const& amount) {
Json::Value json = pay(from, to, amount);
json[jss::InvoiceID] = invid;
return json;
};
// 1. strong pass, first write creates key 1: HookStateCount
// changes, so alice's root is modified either way; only the
// fix also touches it
{
auto const before = touchCount();
env(payTo(cho, bob, XRP(1)),
fee(XRP(1)),
M("touch: create"),
ter(tesSUCCESS));
auto const txid = env.tx()->getTransactionID();
BEAST_EXPECT(aliceInMeta());
BEAST_EXPECT(touchCount() == before + (withFix ? 1 : 0));
BEAST_EXPECT(stateIs(1, txid));
}
// 2. overwrite of the existing key: alice is not a stakeholder,
// so without the fix her root is untouched and unthreaded
{
auto const before = touchCount();
auto const prev = prevTxnID();
env(payTo(cho, bob, XRP(1)),
fee(XRP(1)),
M("touch: overwrite"),
ter(tesSUCCESS));
auto const txid = env.tx()->getTransactionID();
BEAST_EXPECT(aliceInMeta() == withFix);
BEAST_EXPECT(touchCount() == before + (withFix ? 1 : 0));
BEAST_EXPECT(prevTxnID() == (withFix ? txid : prev));
BEAST_EXPECT(stateIs(1, txid));
}
// 3. alice is also a strong stakeholder (payment destination) and
// bob's outgoing hook writes her state: touched exactly once
{
auto const before = touchCount();
env(payTo(bob, alice, XRP(1)),
fee(XRP(1)),
M("touch: tsh"),
ter(tesSUCCESS));
BEAST_EXPECT(aliceInMeta());
BEAST_EXPECT(touchCount() == before + 1);
}
// 4. a rejected invocation writes nothing and touches nothing
{
auto const before = touchCount();
Json::Value json = pay(cho, bob, XRP(1));
json[jss::InvoiceID] =
"0000000000000000000000000000000000000000000000000000000000"
"000001";
env(json,
fee(XRP(1)),
M("touch: rejected"),
ter(tecHOOK_REJECTED));
BEAST_EXPECT(!aliceInMeta());
BEAST_EXPECT(touchCount() == before);
}
// the otxn account is a weak stakeholder under fixHookAPI20251128
bool const senderTouched =
env.current()->rules().enabled(fixHookAPI20251128);
// 5. alice sends to bob: bob's strong hook overwrites her key 1
// before the weak pass touches her as the sender. Under the fix
// that write has already touched her, so the weak pass must
// not touch her again.
{
auto const before = touchCount();
env(payTo(alice, bob, XRP(1)),
fee(XRP(1)),
M("touch: foreign write, then weak tsh"),
ter(tesSUCCESS));
auto const txid = env.tx()->getTransactionID();
BEAST_EXPECT(aliceInMeta());
BEAST_EXPECT(
touchCount() ==
before + ((withFix || senderTouched) ? 1 : 0));
BEAST_EXPECT(stateIs(1, txid));
}
// 6. as 5, but the payment fails after the strong pass. The reset
// discards the strong pass' state write and touch, so the weak
// pass must touch alice afresh.
{
auto const before = touchCount();
auto const sle = aliceState(1);
BEAST_REQUIRE(sle);
auto const value = sle->getFieldVL(sfHookStateData);
env(payTo(alice, bob, XRP(100000)),
fee(XRP(1)),
M("touch: reset"),
ter(tecUNFUNDED_PAYMENT));
BEAST_EXPECT(aliceInMeta());
BEAST_EXPECT(touchCount() == before + (senderTouched ? 1 : 0));
auto const after = aliceState(1);
BEAST_EXPECT(
after && after->getFieldVL(sfHookStateData) == value);
}
// every execution of the last txn's hooks accepted
auto const allAccepted =
[&](std::shared_ptr<STObject const> const& meta,
std::size_t count) -> bool {
if (!meta || !meta->isFieldPresent(sfHookExecutions))
return false;
auto const& executions = meta->getFieldArray(sfHookExecutions);
if (executions.size() != count)
return false;
for (auto const& execution : executions)
if (execution.getFieldU8(sfHookResult) !=
static_cast<std::uint8_t>(hook_api::ExitType::ACCEPT))
return false;
return true;
};
// 7. weak pass: dan's hook writes key 2 only when it is executed
// again as weak, after the payment has applied. The first
// write creates the key and so modifies alice's root either
// way; the second only overwrites it.
for (bool const create : {true, false})
{
auto const before = touchCount();
auto const prev = prevTxnID();
env(payTo(cho, dan, XRP(1)),
fee(XRP(1)),
M(create ? "touch: weak create" : "touch: weak overwrite"),
ter(tesSUCCESS));
auto const txid = env.tx()->getTransactionID();
auto const meta = env.meta();
BEAST_EXPECT(allAccepted(meta, 2));
BEAST_EXPECT(inMeta(meta) == (create || withFix));
BEAST_EXPECT(touchCount() == before + (withFix ? 1 : 0));
BEAST_EXPECT(
prevTxnID() == ((create || withFix) ? txid : prev));
BEAST_EXPECT(stateIs(2, txid));
}
// 8. dan's weak execution deletes key 4, which alice never had.
// Nothing of hers changes, so nothing touches her.
{
auto const before = touchCount();
auto const prev = prevTxnID();
Json::Value params{Json::arrayValue};
params[0U][jss::HookParameter][jss::HookParameterName] =
strHex(std::string("D"));
params[0U][jss::HookParameter][jss::HookParameterValue] = "01";
Json::Value json = payTo(cho, dan, XRP(1));
json[jss::HookParameters] = params;
env(json,
fee(XRP(1)),
M("touch: weak delete of a missing key"),
ter(tesSUCCESS));
auto const meta = env.meta();
BEAST_EXPECT(allAccepted(meta, 2));
BEAST_EXPECT(!inMeta(meta));
BEAST_EXPECT(touchCount() == before);
BEAST_EXPECT(prevTxnID() == prev);
BEAST_EXPECT(!aliceState(4));
}
// 9. callback: eve's hook emits an Invoke, whose callback writes
// key 3 when the Invoke is applied in the following ledger.
// Again the first write creates the key, the second overwrites.
for (bool const create : {true, false})
{
auto const before = touchCount();
auto const prev = prevTxnID();
env(invoke::invoke(eve),
fee(XRP(1)),
M(create ? "touch: callback create"
: "touch: callback overwrite"),
ter(tesSUCCESS));
env.close();
auto const meta = env.meta();
BEAST_REQUIRE(meta && meta->isFieldPresent(sfHookEmissions));
auto const etxn =
meta->getFieldArray(sfHookEmissions)[0u].getFieldH256(
sfEmittedTxnID);
// the emitted txn, and with it the callback, applies here
env.close();
auto const emitted = env.closed()->txRead(etxn);
BEAST_REQUIRE(emitted.first && emitted.second);
BEAST_EXPECT(allAccepted(emitted.second, 1));
BEAST_EXPECT(inMeta(emitted.second) == (create || withFix));
BEAST_EXPECT(touchCount() == before + (withFix ? 1 : 0));
BEAST_EXPECT(
prevTxnID() == ((create || withFix) ? etxn : prev));
BEAST_EXPECT(stateIs(3, etxn));
}
}
}
void
test_state_set(FeatureBitset features)
{
@@ -16211,12 +15616,11 @@ public:
test_slot_subfield(features); //
test_slot_type(features); //
test_state(features); //
test_state_foreign(features); //
test_state_foreign_set(features); //
test_state_foreign_set_max(features); //
test_state_foreign_set_touch(features); //
test_state_set(features); //
test_state(features); //
test_state_foreign(features); //
test_state_foreign_set(features); //
test_state_foreign_set_max(features); //
test_state_set(features); //
test_sto_emplace(features); //
test_sto_erase(features); //
File diff suppressed because it is too large. Load diff
+41 -194
View File
@@ -20,9 +20,7 @@
#include <test/jtx.h>
#include <test/jtx/network.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpld/core/Config.h>
@@ -56,10 +54,10 @@ struct SetManifest_test : public beast::unit_test::suite
// A manifest transaction is unsigned, so the network id is mandatory and
// the network must be one that requires it (id > 1024).
static std::unique_ptr<Config>
makeConfig(std::string fee = "10")
makeConfig()
{
return jtx::network::makeNetworkConfig(
21337, std::move(fee), "1000000", "200000");
21337, "10", "1000000", "200000");
}
/** Builds a manifest signed by `master` nominating `ephemeral`.
@@ -113,15 +111,6 @@ struct SetManifest_test : public beast::unit_test::suite
return env.rpc("json", "submit", to_string(params))[jss::result];
}
/** Submits an already formed SetManifest transaction. */
static Json::Value
submit(jtx::Env& env, std::shared_ptr<STTx const> const& tx)
{
Serializer s;
tx->add(s);
return env.rpc("submit", strHex(s.slice()))[jss::result];
}
static std::string
engineResult(Json::Value const& result)
{
@@ -177,13 +166,13 @@ struct SetManifest_test : public beast::unit_test::suite
});
};
// The canonical Fee makeSetManifestTx() uses for the open ledger.
SerialIter mit{makeSlice(manifest)};
STObject const manifestObject{mit, sfManifest};
return build(
canonicalSetManifestFee(env.current()->fees().base, manifestObject)
.value_or(XRPAmount{0}),
true);
// The same two-pass pricing makeSetManifestTx() does: encode once with
// a placeholder purely to have something to measure, then encode at the
// ceiling checkFee() will accept.
auto const base = SetManifest::calculateBaseFee(
*env.current(), *build(XRPAmount{0}, false));
return build(mulRatio(base, 12, 10, /*roundUp*/ true), true);
}
/** A mutable copy of a ledger object, suitable for the RawView interface.
@@ -608,9 +597,7 @@ struct SetManifest_test : public beast::unit_test::suite
BEAST_EXPECT(
applyDirect(env, envelope(env, good, other.id())) == temMALFORMED);
// Every envelope field a relayer could otherwise choose is pinned: the
// envelope is one exact transaction, with Account and NetworkID derived
// rather than mirrored.
// Every envelope field a relayer could otherwise choose is pinned.
for (auto const& [name, tweak] : std::vector<
std::pair<char const*, std::function<void(STObject&)>>>{
{"Sequence",
@@ -621,175 +608,33 @@ struct SetManifest_test : public beast::unit_test::suite
}},
{"TicketSequence",
[](STObject& obj) { obj.setFieldU32(sfTicketSequence, 1); }},
{"SigningPubKey",
[&](STObject& obj) {
{"SigningPubKey", [&](STObject& obj) {
obj.setFieldVL(sfSigningPubKey, master.pk().slice());
}},
{"Memos",
[](STObject& obj) {
obj.setFieldArray(sfMemos, STArray(sfMemos, 1));
STObject memo{sfMemo};
memo.setFieldVL(sfMemoData, Blob{0x01});
obj.peekFieldArray(sfMemos).emplace_back(std::move(memo));
}},
{"SourceTag",
[](STObject& obj) { obj.setFieldU32(sfSourceTag, 1); }},
{"LastLedgerSequence",
[](STObject& obj) {
obj.setFieldU32(sfLastLedgerSequence, 1'000'000);
}},
{"NetworkID",
[](STObject& obj) { obj.setFieldU32(sfNetworkID, 21338); }}})
}}})
{
auto const variant = envelope(env, good, master.id(), tweak);
// The generic network check runs first for a foreign NetworkID.
auto const expected = std::string(name) == "NetworkID"
? TER{telWRONG_NETWORK}
: TER{temMALFORMED};
BEAST_EXPECTS(applyDirect(env, variant) == expected, name);
// Ingress rejects the variant as a bad envelope before any
// manifest signature work, and never as a soft fee miss.
auto const [validity, reason] = checkValidity(
env.app().getHashRouter(),
*variant,
env.current()->rules(),
env.app().config());
BEAST_EXPECTS(validity == Validity::SigBad, name);
BEAST_EXPECTS(
checkManifestIngressFee(env.app(), *variant).first ==
ManifestIngressFee::NotApplicable,
applyDirect(env, envelope(env, good, master.id(), tweak)) ==
temMALFORMED,
name);
}
// The Fee is pinned to the one canonical value for the applying view's
// base fee. A different value is a local miss, not a malformed
// transaction, and ingress drops it before manifest crypto.
// sfFee is the one envelope field preflight cannot bound, because the
// base fee is not in scope until preclaim. checkFee() caps it instead.
auto const priced = envelope(env, good, master.id());
auto const canonicalFee = priced->getFieldAmount(sfFee).xrp();
BEAST_EXPECT(
checkManifestIngressFee(env.app(), *priced).first ==
ManifestIngressFee::Canonical);
for (auto const fee :
{canonicalFee + XRPAmount{1}, canonicalFee - XRPAmount{1}})
{
auto const variant =
envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, fee);
});
BEAST_EXPECT(
checkManifestIngressFee(env.app(), *variant).first ==
ManifestIngressFee::Refused);
BEAST_EXPECT(applyDirect(env, variant) == telMANIFEST_FEE_MISMATCH);
}
// At the canonical value exactly, which is what Submit sends.
BEAST_EXPECT(applyDirect(env, priced) == tesSUCCESS);
}
void
testCanonicalFee(FeatureBitset features)
{
testcase("canonical fee");
using namespace jtx;
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const units = [](STObject const& manifestObject) {
return 100 +
10 *
static_cast<std::int64_t>(
manifestObject.getSerializer().getDataLength());
};
auto const manifest = makeManifest(master, eph1, 1);
// The builder prices against the voted base fee of the open ledger.
auto const build = [&](Env& env) -> std::shared_ptr<STTx const> {
auto const hex = makeSetManifestTx(
makeSlice(manifest),
env.app().config().NETWORK_ID,
*env.current(),
env.app().journal("SetManifest_test"));
if (!BEAST_EXPECT(hex))
return nullptr;
auto const bytes = strUnHex(*hex);
if (!BEAST_EXPECT(bytes))
return nullptr;
SerialIter txIter{makeSlice(*bytes)};
return std::make_shared<STTx const>(std::ref(txIter));
};
auto const manifestOf = [](STTx const& tx) -> STObject const& {
return const_cast<STTx&>(tx)
.getField(sfManifest)
.downcast<STObject>();
};
std::shared_ptr<STTx const> ordinaryTx;
{
Env ordinary{*this, makeConfig("10"), features};
ordinaryTx = build(ordinary);
if (!ordinaryTx)
return;
BEAST_EXPECT(
(*ordinaryTx)[sfFee].xrp().drops() ==
10 * units(manifestOf(*ordinaryTx)));
// A zero base would make the transaction free, so there is no
// canonical Fee for it at all.
BEAST_EXPECT(!canonicalSetManifestFee(
XRPAmount{0}, manifestOf(*ordinaryTx)));
}
// A different fee setting maps the same manifest to exactly one
// different txid.
Env expensive{*this, makeConfig("100000"), features};
auto const expensiveTx = build(expensive);
if (!expensiveTx)
return;
BEAST_EXPECT(
(*expensiveTx)[sfFee].xrp().drops() ==
100'000 * units(manifestOf(*expensiveTx)));
BEAST_EXPECT(
expensiveTx->getTransactionID() != ordinaryTx->getTransactionID());
expensive.fund(XRP(10000), master);
expensive.close();
// A wrapper priced for another fee setting is a local miss, not a bad
// signature: ingress drops it before manifest crypto without marking
// it bad, and preclaim refuses it with a tel result.
BEAST_EXPECT(
checkManifestIngressFee(expensive.app(), *ordinaryTx).first ==
ManifestIngressFee::Refused);
BEAST_EXPECT(
submit(expensive, ordinaryTx)[jss::error] == "invalidTransaction");
BEAST_EXPECT(
(expensive.app().getHashRouter().getFlags(
ordinaryTx->getTransactionID()) &
SF_BAD) == 0);
BEAST_EXPECT(
applyDirect(expensive, ordinaryTx) == telMANIFEST_FEE_MISMATCH);
auto const ceiling = priced->getFieldAmount(sfFee).xrp();
BEAST_EXPECT(
checkManifestIngressFee(expensive.app(), *expensiveTx).first ==
ManifestIngressFee::Canonical);
BEAST_EXPECT(engineResult(submit(expensive, manifest)) == "tesSUCCESS");
expensive.close();
BEAST_EXPECT(expensive.le(keylet::manifest(master.pk())));
applyDirect(
env, envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, ceiling + XRPAmount{1});
})) == temBAD_FEE);
// An amendment-blocked server has no base fee it can trust, so ingress
// refuses even the canonical transaction rather than verify it.
Env blocked{*this, makeConfig("10"), features};
auto const blockedTx = build(blocked);
if (!blockedTx)
return;
// At the ceiling exactly, which is what Submit sends.
BEAST_EXPECT(
checkManifestIngressFee(blocked.app(), *blockedTx).first ==
ManifestIngressFee::Canonical);
blocked.app().getOPs().setAmendmentBlocked();
BEAST_EXPECT(
checkManifestIngressFee(blocked.app(), *blockedTx).first ==
ManifestIngressFee::Refused);
applyDirect(
env, envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, ceiling);
})) == tesSUCCESS);
}
void
@@ -953,19 +798,21 @@ public:
void
run() override
{
using namespace test::jtx;
auto const sa = supported_amendments();
testSubmission(sa);
testUpdate(sa);
testRevocation(sa);
testRetrieval(sa);
testSigningKeyRetrieval(sa);
testMalformed(sa);
testEnvelopeRejections(sa);
testCanonicalFee(sa);
testCorruptLedger(sa);
testGossipSelection(sa);
testDisabled(sa);
// RH TODO: re-add these when the amendment is ready for supported::yes
/*
using namespace test::jtx;
auto const sa = supported_amendments();
testSubmission(sa);
testUpdate(sa);
testRevocation(sa);
testRetrieval(sa);
testSigningKeyRetrieval(sa);
testMalformed(sa);
testEnvelopeRejections(sa);
testCorruptLedger(sa);
testGossipSelection(sa);
testDisabled(sa);
*/
}
};
-40
View File
@@ -17,7 +17,6 @@
*/
//==============================================================================
#include <xrpl/beast/core/SemanticVersion.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/BuildInfo.h>
@@ -26,44 +25,6 @@ namespace ripple {
class BuildInfo_test : public beast::unit_test::suite
{
public:
void
testGetVersionString()
{
testcase("GetVersionString");
auto const& s = BuildInfo::getVersionString();
BEAST_EXPECT(!s.empty());
// Same reference on every call (static cache).
BEAST_EXPECT(&BuildInfo::getVersionString() == &s);
// Must be strict semver and round-trip unchanged (no leading
// zeros, no stray whitespace).
beast::SemanticVersion v;
BEAST_EXPECT(v.parse(s));
BEAST_EXPECT(v.print() == s);
BEAST_EXPECT(BuildInfo::getFullVersionString() == "xahaud-" + s);
if (s.find("-CustomBuild") == std::string::npos)
return;
// Version generated from __DATE__ ("Mmm dd yyyy"):
// <year>.<month>.<day>-CustomBuild[+DEBUG]
BEAST_EXPECT(v.majorVersion >= 2023 && v.majorVersion <= 9999);
BEAST_EXPECT(v.minorVersion >= 1 && v.minorVersion <= 12);
BEAST_EXPECT(v.patchVersion >= 1 && v.patchVersion <= 31);
BEAST_EXPECT(
v.preReleaseIdentifiers ==
beast::SemanticVersion::identifier_list{"CustomBuild"});
#ifdef DEBUG
BEAST_EXPECT(
v.metaData == beast::SemanticVersion::identifier_list{"DEBUG"});
#else
BEAST_EXPECT(v.metaData.empty());
#endif
}
void
testEncodeSoftwareVersion()
{
@@ -158,7 +119,6 @@ public:
void
run() override
{
testGetVersionString();
testEncodeSoftwareVersion();
testIsRippledVersion();
testIsNewerVersion();
+1 -6
View File
@@ -15,7 +15,6 @@
#include <memory>
#include <optional>
#include <queue>
#include <set>
#include <utility>
#include <vector>
#include <wasmedge/wasmedge.h>
@@ -258,15 +257,11 @@ finalizeHookResult(
bool doEmit);
// write state map to ledger
// if `written` is supplied, every account whose hook state was successfully
// written is inserted into it (own and foreign namespaces alike); deleting an
// entry that does not exist is not a write
ripple::TER
finalizeHookState(
HookStateMap const&,
ripple::ApplyContext&,
ripple::uint256 const&,
std::set<ripple::AccountID>* written = nullptr);
ripple::uint256 const&);
// if the txn being executed was an emitted txn then this removes it from the
// emission directory
-3
View File
@@ -2145,9 +2145,6 @@ HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const
}
else if (data.size() == 32)
{
if (hookCtx.applyCtx.view().rules().enabled(fix20261005))
return Unexpected(INVALID_ARGUMENT);
uint256 hash = uint256::fromVoid(data.data());
ripple::error_code_i ec{ripple::error_code_i::rpcUNKNOWN};
+1 -11
View File
@@ -1368,8 +1368,7 @@ ripple::TER
hook::finalizeHookState(
HookStateMap const& stateMap,
ripple::ApplyContext& applyCtx,
ripple::uint256 const& txnID,
std::set<ripple::AccountID>* written)
ripple::uint256 const& txnID)
{
auto const& j = applyCtx.app.journal("View");
uint16_t changeCount = 0;
@@ -1401,12 +1400,6 @@ hook::finalizeHookState(
// this entry isn't just cached, it was actually modified
auto slice = Slice(blob.data(), blob.size());
// deleting an entry that does not exist leaves the
// ledger unchanged, so it does not count as a write
bool const changesLedger = !written || !blob.empty() ||
applyCtx.view().exists(
ripple::keylet::hookState(acc, key, ns));
TER result = setHookState(applyCtx, acc, ns, key, slice);
if (!isTesSuccess(result))
@@ -1418,9 +1411,6 @@ hook::finalizeHookState(
return result;
}
// ^ should not fail... checks were done before map insert
if (written && changesLedger)
written->insert(acc);
}
}
}
-23
View File
@@ -1136,16 +1136,6 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
try
{
// Not a bad signature: this server may not have reached the fee
// setting the transaction was built for. Drop without marking it bad.
if (auto const [fee, feeReason] = checkManifestIngressFee(app_, *trans);
fee == ManifestIngressFee::Refused)
{
JLOG(m_journal.debug())
<< "Submitted manifest-authorized tx dropped: " << feeReason;
return;
}
auto const [validity, reason] = checkValidity(
app_.getHashRouter(),
*trans,
@@ -1283,19 +1273,6 @@ NetworkOPsImp::processTransaction(
// NOTE eahennis - I think this check is redundant,
// but I'm not 100% sure yet.
// If so, only cost is looking up HashRouter flags.
// Before checkValidity(), which verifies the manifest: a fee miss is local
// and must not mark the transaction bad.
if (auto const [fee, feeReason] =
checkManifestIngressFee(app_, *transaction->getSTransaction());
fee == ManifestIngressFee::Refused)
{
JLOG(m_journal.debug())
<< "Manifest-authorized tx dropped: " << feeReason;
transaction->setStatus(INVALID);
transaction->setResult(telMANIFEST_FEE_MISMATCH);
return;
}
auto const [validity, reason] = checkValidity(
app_.getHashRouter(),
*transaction->getSTransaction(),
-36
View File
@@ -68,42 +68,6 @@ checkValidity(
Config const& config,
ApplyFlags const flags = tapNONE);
/** Ingress fee outcome for a manifest-authorized SetManifest.
@see checkManifestIngressFee
*/
enum class ManifestIngressFee {
/// Not a manifest-authorized SetManifest, or the amendment is not
/// enabled; checkValidity applies as usual.
NotApplicable,
/// The Fee is canonical for the ledger this server would apply it to.
Canonical,
/// A canonically shaped envelope whose Fee cannot be confirmed here. Drop
/// without verifying the manifest. Not a bad signature: the same bytes
/// may be canonical on another node, or here once this node reaches the
/// fee setting they were built for.
Refused
};
/** Checks an unsigned SetManifest's Fee before any manifest signature work.
The canonical Fee follows the voted base fee of the parent of the ledger
a transaction is applied to. Ingress accepts exactly the value for this
server's open ledger, priced from its parent: the same value preclaim
enforces, so nothing that passes here is refused there for its Fee. A
server with no validated ledger, or one that is amendment blocked, has no
trustworthy base and refuses.
Every ingress caller runs this before checkValidity(), which no longer
checks the Fee because the transactor engine calls it without a ledger.
A Refused result must be dropped without marking the transaction bad or
charging for an invalid signature.
@return the outcome and, if Refused, a reason
*/
std::pair<ManifestIngressFee, std::string>
checkManifestIngressFee(Application& app, STTx const& tx);
/** Sets the validity of a given transaction in the cache.
@warning Use with extreme care.
+76 -155
View File
@@ -26,126 +26,14 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/XRPAmount.h> // mulRatio
#include <xrpl/protocol/serialize.h>
#include <xrpl/protocol/st.h>
#include <limits>
namespace ripple {
bool
hasManifestAuthorityMarkers(STTx const& tx) noexcept
{
try
{
return tx.getTxnType() == ttMANIFEST_SET &&
tx.isFieldPresent(sfSigningPubKey) &&
tx.getSigningPubKey().empty() &&
tx.isFieldPresent(sfTxnSignature) && tx.getSignature().empty();
}
catch (std::exception const&)
{
return false;
}
}
/** Build the only permitted outer transaction for `manifest`. */
static STTx
canonicalSetManifest(
STObject const& manifest,
AccountID const& account,
std::optional<std::uint32_t> networkID,
XRPAmount fee)
{
return STTx(ttMANIFEST_SET, [&](STObject& obj) {
obj.setAccountID(sfAccount, account);
obj.setFieldU32(sfSequence, 0);
if (networkID)
obj.setFieldU32(sfNetworkID, *networkID);
obj.setFieldAmount(sfFee, fee);
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setFieldVL(sfTxnSignature, Blob{});
obj.peekFieldObject(sfManifest) = manifest;
});
}
/** The NetworkID a canonical envelope carries for this server's network.
Networks above 1024 require the field and legacy networks forbid it
(preflight0), so the canonical envelope follows the same rule.
*/
static std::optional<std::uint32_t>
canonicalNetworkID(std::uint32_t networkID)
{
if (networkID > 1024)
return networkID;
return std::nullopt;
}
bool
hasCanonicalSetManifestShape(STTx const& tx, std::uint32_t networkID) noexcept
{
try
{
auto const& manifest =
const_cast<STTx&>(tx).getField(sfManifest).downcast<STObject>();
auto const masterKey = manifest.getFieldVL(sfPublicKey);
if (!publicKeyType(makeSlice(masterKey)))
return false;
auto const canonical = canonicalSetManifest(
manifest,
calcAccountID(PublicKey(makeSlice(masterKey))),
canonicalNetworkID(networkID),
tx[sfFee].xrp());
auto const actualBytes = tx.getSerializer();
auto const canonicalBytes = canonical.getSerializer();
return actualBytes.slice() == canonicalBytes.slice();
}
catch (std::exception const&)
{
return false;
}
}
std::optional<XRPAmount>
canonicalSetManifestFee(XRPAmount base, STObject const& manifest)
{
// Every relayer must compute the same Fee, so it depends only on the voted
// reference fee (ledger state, never local load) and the manifest's size.
// Only the multipliers are fixed; a fee vote changes the canonical txid
// once.
constexpr std::int64_t baseUnits = 100;
constexpr std::int64_t unitsPerByte = 10;
constexpr auto maxDrops = std::numeric_limits<std::int64_t>::max();
// Largest size for which baseUnits + bytes * unitsPerByte cannot overflow.
constexpr std::uint64_t maxBytes = (maxDrops - baseUnits) / unitsPerByte;
auto const bytes = manifest.getSerializer().getDataLength();
// A non-positive base would make the transaction free.
if (base <= beast::zero || bytes == 0 || bytes > maxBytes)
return std::nullopt;
auto const units =
baseUnits + static_cast<std::int64_t>(bytes) * unitsPerByte;
// base is positive, so this is an exact test for units * base overflowing.
if (units > maxDrops / base.drops())
return std::nullopt;
XRPAmount const fee{base.drops() * units};
if (!isLegalAmount(fee) || fee <= beast::zero)
return std::nullopt;
return fee;
}
TxConsequences
SetManifest::makeTxConsequences(PreflightContext const& ctx)
{
@@ -170,19 +58,6 @@ SetManifest::preflight(PreflightContext const& ctx)
return temINVALID_FLAG;
}
// The envelope carries no account signature: authority comes solely from
// the manifest's own master and ephemeral signatures, which do not cover
// it. So the whole envelope is one exact transaction per manifest, with
// Account derived from the manifest's master key and NetworkID from this
// server, and only the Fee mirrored here (checkFee() pins it). Checked
// before any signature work, so a relayer cannot vary a field to mint
// fresh txids that each cost manifest signature checks.
if (!hasCanonicalSetManifestShape(tx, ctx.app.config().NETWORK_ID))
{
JLOG(j.warn()) << "SetManifest: non-canonical envelope.";
return temMALFORMED;
}
// rules:
// 1. sfManifest must match the manifest template and be validly signed
// 2. the signingpubkey must match the master r-address
@@ -220,6 +95,23 @@ SetManifest::preflight(PreflightContext const& ctx)
// 3. not already revoked will be checked in preclaim because it depends on
// lgr state
// 4. the envelope carries no account signature: authority comes solely
// from the manifest's own master/ephemeral signatures, which do not cover
// the envelope. Pin every envelope field a relayer could otherwise choose.
// The shape below must match the one checkValidity() recognises, or the
// txn falls through to the ordinary signature path and is rejected there.
// sfFee cannot be bounded here because the computed base fee is not in
// scope until preclaim; checkFee() bounds it instead.
if (!tx.isFieldPresent(sfSigningPubKey) || !tx.getSigningPubKey().empty() ||
!tx.isFieldPresent(sfTxnSignature) || !tx.getSignature().empty() ||
tx.isFieldPresent(sfSigners) || tx.isFieldPresent(sfAccountTxnID) ||
tx.isFieldPresent(sfTicketSequence) || tx.getFieldU32(sfSequence) != 0)
{
JLOG(j.warn())
<< "SetManifest: envelope must be unsigned with Sequence 0.";
return temMALFORMED;
}
return preflight2(ctx);
}
@@ -245,9 +137,9 @@ SetManifest::preclaim(PreclaimContext const& ctx)
return tefINTERNAL; // preflight already parsed this successfully
// Replay protection. A byte-identical resubmission is rejected as
// tefALREADY by checkPriorTxAndLastLedger, but the canonical Fee follows
// the voted base fee, so the same manifest can arrive under a different
// txid after a fee change. The strictly-increasing sequence test below
// tefALREADY by checkPriorTxAndLastLedger, but sfFee may vary within the
// band checkFee() allows, so the same manifest can also arrive under a
// different txid. The strictly-increasing sequence test below is what
// covers that, both within this ledger and in every later one. Either
// result is tef, so a replay is never included and never claims a fee.
if (sle->isFieldPresent(sfManifestID))
@@ -420,26 +312,33 @@ SetManifest::calculateBaseFee(ReadView const& view, STTx const& tx)
return Transactor::calculateBaseFee(view, tx) + manifestFee;
}
/** The most sfFee may be: the same 1.2x headroom Submit applies.
Kept in one place so the value Submit writes and the value preclaim will
accept cannot drift apart.
*/
static XRPAmount
manifestFeeCeiling(XRPAmount baseFee)
{
return mulRatio(baseFee, 12, 10, /*roundUp*/ true);
}
TER
SetManifest::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
{
// The envelope carries no account signature, so a relayer-chosen Fee would
// be both a malleable field and a way to charge the master account. The
// canonical Fee is priced from this view's voted base fee, which is the
// parent ledger's: one exact value per fee setting. A mismatch is local,
// not final: the same bytes are canonical on a server that has reached
// the fee setting they were built for. The check also runs on a closed
// ledger, so a proposer cannot charge the master account an arbitrary Fee
// by skipping it.
STObject const& manifest =
const_cast<STTx&>(ctx.tx).getField(sfManifest).downcast<STObject>();
auto const canonical =
canonicalSetManifestFee(ctx.view.fees().base, manifest);
if (!canonical || ctx.tx[sfFee].xrp() != *canonical)
// A ceiling is required because the envelope carries no account signature,
// so sfFee is chosen by whoever relays the txn -- and manifests are public:
// they are gossiped over the peer protocol and embedded in published UNLs,
// so the relayer need not be the master key holder. Uncapped, any observer
// of a not-yet-recorded manifest could wrap it with sfFee set to that
// validator's entire balance. The 20% band is headroom against a fee floor
// that has risen since the txn was built, and bounds what an attacker can
// burn to the same 20%.
if (ctx.tx[sfFee].xrp() > manifestFeeCeiling(baseFee))
{
JLOG(ctx.j.trace()) << "SetManifest: non-canonical fee: "
JLOG(ctx.j.trace()) << "SetManifest: fee above ceiling: "
<< to_string(ctx.tx[sfFee].xrp());
return telMANIFEST_FEE_MISMATCH;
return temBAD_FEE;
}
// Floor and balance are the ordinary rules.
@@ -459,19 +358,41 @@ makeSetManifestTx(
if (!man || !man->verify())
return std::nullopt;
SerialIter manifestIter{manifest};
STObject const manifestObject{manifestIter, sfManifest};
auto const encode = [&](XRPAmount fee) {
return serializeHex(STTx(ttMANIFEST_SET, [&](STObject& obj) {
obj.setAccountID(sfAccount, calcAccountID(man->masterKey));
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfNetworkID, networkID);
obj.setFieldAmount(sfFee, fee);
obj.setFieldVL(sfSigningPubKey, std::vector<std::uint8_t>{});
obj.setFieldVL(sfTxnSignature, std::vector<std::uint8_t>{});
auto const fee =
canonicalSetManifestFee(openView.fees().base, manifestObject);
if (!fee)
// sfManifest is soeREQUIRED, so STObject::set(SOTemplate) has
// already materialised it as a present, empty object. Fill
// that one in: emitting a second is a duplicate field, which
// STObject::set(SerialIter&) rejects on the way back in.
SerialIter mit{manifest};
obj.peekFieldObject(sfManifest).set(mit);
}));
};
// calculateBaseFee() takes a parsed transaction, so encode once with a
// placeholder fee purely to have something to price. The resulting fee
// does not depend on the placeholder: it is derived from the length of
// the manifest object and the ledger's base fee.
auto const priced = strUnHex(encode(XRPAmount{0}));
if (!priced || priced->empty())
return std::nullopt;
return serializeHex(canonicalSetManifest(
manifestObject,
calcAccountID(man->masterKey),
canonicalNetworkID(networkID),
*fee));
SerialIter sit{makeSlice(*priced)};
STTx const probe{std::ref(sit)};
// Submit the ceiling exactly. preclaim rejects anything above it, and
// the floor rises with network load, so the ceiling is both always
// acceptable and the value most likely to still clear the floor by the
// time the transaction is applied.
return encode(
manifestFeeCeiling(SetManifest::calculateBaseFee(openView, probe)));
}
catch (std::exception const& e)
{
+13 -38
View File
@@ -25,54 +25,29 @@
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Indexes.h>
#include <optional>
namespace ripple {
/** Return whether empty outer signatures nominate manifest-only authority. */
bool
hasManifestAuthorityMarkers(STTx const& tx) noexcept;
/** Return whether every outer byte has the canonical SetManifest shape.
Account is derived from the manifest's master key and NetworkID from
`networkID`, this server's network. Only the Fee is mirrored from the
candidate; checkFee pins it against the applying view.
*/
bool
hasCanonicalSetManifestShape(STTx const& tx, std::uint32_t networkID) noexcept;
/** Return the one canonical Fee for a SetManifest carrying `manifest`.
The Fee is `base * (100 + 10 * manifestBytes)`, where `base` is the voted
reference fee of the ledger the transaction is checked against, which is
its parent's. Every node reading the same ledger computes the same value,
so one manifest maps to one transaction ID per fee setting; local load
never enters it.
Returns nullopt when there is no canonical Fee: a zero base, or a product
that overflows or is not a legal amount.
*/
std::optional<XRPAmount>
canonicalSetManifestFee(XRPAmount base, STObject const& manifest);
/** Encode the transaction that publishes `manifest` on-ledger.
A manifest transaction carries no account signature, so the whole
envelope is one exact transaction: Account is the manifest master key's
account, Sequence is 0, SigningPubKey and TxnSignature are empty, NetworkID
is this network's, and Fee is the canonical Fee for `openView`'s base fee.
No other field is allowed. SetManifest::preflight and SetManifest::checkFee
reject anything else, and every caller that submits a manifest builds it
here.
A manifest transaction carries no account signature, so the protocol pins
the whole envelope: Sequence must be 0, SigningPubKey and TxnSignature must
be empty, and Fee must fall between the computed base fee and a ceiling
above it. SetManifest::preflight and SetManifest::checkFee reject anything
else. Every caller that submits a manifest builds it here so those rules
cannot drift apart from the ones the transactor enforces.
Returns hex rather than an STTx because the manifest is appended to the
encoded transaction verbatim, behind its object marker, instead of being
parsed and re-emitted: the bytes the master key signed survive untouched,
and a future change to the manifest format needs no change here.
@param manifest Serialized manifest
@param networkID Network the transaction is for
@param openView Open ledger the Fee is priced against
@param openView Ledger the fee is priced against
@param j Journal
@return the hex-encoded transaction, or nullopt if the manifest does not
parse or does not verify, or there is no canonical Fee
parse or does not verify
*/
std::optional<std::string>
makeSetManifestTx(
+6 -63
View File
@@ -1198,46 +1198,6 @@ touchAccount(ApplyView& view, AccountID const& id)
view.update(sle);
}
void
Transactor::touchOnce(AccountID const& id)
{
auto& view = ctx_.view();
// before fix20261005 every request touches, and touched_ stays empty
if (!view.rules().enabled(fix20261005))
{
touchAccount(view, id);
return;
}
if (touched_.insert(id).second)
touchAccount(view, id);
}
void
Transactor::touchHookStateOwners(std::set<AccountID> const& written)
{
if (!view().rules().enabled(fix20261005))
return;
// A hook may write into another account's namespace (state_foreign_set
// under a HookGrant) without that account being a transactional
// stakeholder. Touch it so the transaction threads to its AccountRoot.
for (auto const& acc : written)
touchOnce(acc);
}
void
Transactor::discardView()
{
ctx_.discard();
// The touches touched_ records were discarded with the view. touched_ is
// only populated under fix20261005, so before the fix this is a no-op and
// needs no amendment guard.
touched_.clear();
}
static void
removeUnfundedOffers(
ApplyView& view,
@@ -1324,7 +1284,7 @@ Transactor::reset(XRPAmount fee)
std::vector<STObject> executions;
std::vector<STObject> emissions;
avi.copyHookMetaData(executions, emissions);
discardView();
ctx_.discard();
ApplyViewImpl& avi2 = dynamic_cast<ApplyViewImpl&>(ctx_.view());
avi2.setHookMetaData(std::move(executions), std::move(emissions));
@@ -1635,12 +1595,8 @@ Transactor::doHookCallback(
// write any state changes if cbak resulted in accept()
if (success)
{
std::set<AccountID> written;
hook::finalizeHookState(
stateMap, ctx_, ctx_.tx.getTransactionID(), &written);
touchHookStateOwners(written);
}
stateMap, ctx_, ctx_.tx.getTransactionID());
// write the final result
ripple::TER result =
@@ -1751,10 +1707,7 @@ Transactor::doTSH(
if ((!canRollback && strong) || (canRollback && !strong))
continue;
// under fix20261005 an account is touched at most once per
// transaction (a foreign state write in the strong pass or a callback
// may already have touched an account that is a weak TSH)
touchOnce(tshAccountID);
touchAccount(view, tshAccountID);
if (view.rules().enabled(fixHookAPI20251128))
{
@@ -2064,12 +2017,7 @@ Transactor::operator()()
// write state if all chains executed successfully
if (isTesSuccess(result))
{
std::set<AccountID> written;
hook::finalizeHookState(
stateMap, ctx_, ctx_.tx.getTransactionID(), &written);
touchHookStateOwners(written);
}
hook::finalizeHookState(stateMap, ctx_, ctx_.tx.getTransactionID());
// write hook results
// this happens irrespective of whether final result was a tesSUCCESS
@@ -2123,7 +2071,7 @@ Transactor::finishApply(
{
// If the tapFAIL_HARD flag is set, a tec result
// must not do anything
discardView();
ctx_.discard();
applied = false;
}
else if (
@@ -2459,12 +2407,7 @@ Transactor::finishApply(
doAgainAsWeak(accID, hookHashes, stateMap, weakResults, proMeta);
// write hook results
{
std::set<AccountID> written;
hook::finalizeHookState(
stateMap, ctx_, ctx_.tx.getTransactionID(), &written);
touchHookStateOwners(written);
}
hook::finalizeHookState(stateMap, ctx_, ctx_.tx.getTransactionID());
for (auto& weakResult : weakResults)
hook::finalizeHookResult(weakResult, ctx_, isTesSuccess(result));
-19
View File
@@ -218,21 +218,6 @@ protected:
void
addWeakTSHFromBalanceChanges(detail::ApplyViewBase const& pv);
// Touch an account (featureTouch). Under fix20261005 each account is
// touched at most once per transaction.
void
touchOnce(AccountID const& id);
// fix20261005: touch every account whose hook state finalizeHookState
// wrote, so the transaction threads to the AccountRoot of each
void
touchHookStateOwners(std::set<AccountID> const& written);
// Discard every change made to the view, together with the record of
// which accounts were touched in it.
void
discardView();
// hooks amendment fields, these are unpopulated and unused unless
// featureHooks is enabled
int executedHookCount_ =
@@ -242,10 +227,6 @@ protected:
// end of the transactor, who isn't able to be
// deduced until after apply i.e. pathing
// participants, crossed offers
std::set<AccountID>
touched_; // fix20261005: accounts touched (featureTouch) in the
// current view. Never populated before the fix, emptied
// whenever the view is discarded.
// The post-apply pipeline of operator(): tec handling (reset),
// invariants, balance rewards, weak hooks and commit.
+5 -67
View File
@@ -17,15 +17,10 @@
*/
//==============================================================================
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/applySteps.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
@@ -80,17 +75,12 @@ checkValidity(
}
if (rules.enabled(featureOnChainManifests) &&
hasManifestAuthorityMarkers(tx) && tx.isFieldPresent(sfManifest))
tx.getTxnType() == ttMANIFEST_SET &&
tx.isFieldPresent(sfTxnSignature) &&
tx.getFieldVL(sfTxnSignature).empty() &&
tx.isFieldPresent(sfSigningPubKey) &&
tx.getFieldVL(sfSigningPubKey).empty() && tx.isFieldPresent(sfManifest))
{
// Structural nonsense must not buy manifest signature checks. The Fee
// depends on ledger fee state, which this function does not have;
// ingress callers pin it first with checkManifestIngressFee(), and the
// transactor pins it against the applying view in checkFee().
if (!hasCanonicalSetManifestShape(tx, config.NETWORK_ID))
return {
Validity::SigBad,
"Manifest-authorized envelope is not canonical"};
// perform alternative signature check over manifest
STObject const& manObj = const_cast<ripple::STTx&>(tx)
.getField(sfManifest)
@@ -231,56 +221,4 @@ applyTransaction(
}
}
std::pair<ManifestIngressFee, std::string>
checkManifestIngressFee(Application& app, STTx const& tx)
{
auto const open = app.openLedger().current();
if (!open->rules().enabled(featureOnChainManifests) ||
!hasManifestAuthorityMarkers(tx))
return {ManifestIngressFee::NotApplicable, {}};
// A non-canonical shape can never become valid, so it is not a fee miss:
// leave it to checkValidity(), which rejects it as a bad envelope.
if (!hasCanonicalSetManifestShape(tx, app.config().NETWORK_ID))
return {ManifestIngressFee::NotApplicable, {}};
// An amendment-blocked node's open ledger may sit on history the network
// is not validating, and with no validated ledger there is no agreed base
// at all. Neither is a reason to spend manifest crypto finding out.
if (app.getOPs().isAmendmentBlocked())
return {ManifestIngressFee::Refused, "server is amendment blocked"};
auto const validated = app.getLedgerMaster().getValidatedLedger();
if (!validated)
return {ManifestIngressFee::Refused, "no validated ledger"};
try
{
auto const fee = tx[sfFee].xrp();
auto const& manifest =
const_cast<STTx&>(tx).getField(sfManifest).downcast<STObject>();
// A transaction is priced from the parent of the ledger it is applied
// to. Here that is the open ledger, whose fees() are its parent's:
// exactly what preclaim enforces. A wrapper priced for any other fee
// setting cannot apply on this server, so it is not worth verifying,
// and no remembered setting widens the set of txids that reach
// manifest signature checks.
if (auto const canonical =
canonicalSetManifestFee(open->fees().base, manifest);
canonical && *canonical == fee)
return {ManifestIngressFee::Canonical, {}};
}
catch (std::exception const&)
{
// Malformed Fee or manifest: leave it to checkValidity()'s shape
// check, which rejects it as a bad envelope.
return {ManifestIngressFee::NotApplicable, {}};
}
return {
ManifestIngressFee::Refused,
"Fee is not the canonical SetManifest fee for this server's open "
"ledger"};
}
} // namespace ripple
-14
View File
@@ -2878,20 +2878,6 @@ PeerImp::checkTransaction(
}
}
// A manifest-authorized SetManifest's Fee follows the voted base fee,
// so a miss may be a fee setting this server has not reached yet.
// Drop it before any manifest signature work, but do not mark it bad:
// the same bytes can become canonical here.
if (auto const [fee, feeReason] = checkManifestIngressFee(app_, *stx);
fee == ManifestIngressFee::Refused)
{
JLOG(p_journal_.trace())
<< "Dropping manifest-authorized tx " << stx->getTransactionID()
<< ": " << feeReason;
charge(Resource::feeUselessData, "manifest fee miss");
return;
}
if (checkSignature)
{
// Check the signature before handing off to the job queue.
-12
View File
@@ -197,18 +197,6 @@ doSubmit(RPC::JsonContext& context)
}
{
// Before any manifest signature work. Not a bad signature, so the
// transaction is not marked bad.
if (auto const [fee, feeReason] =
checkManifestIngressFee(context.app, *stTx);
fee == ManifestIngressFee::Refused)
{
jvResult[jss::error] = "invalidTransaction";
jvResult[jss::error_exception] = "fails local checks: " + feeReason;
return jvResult;
}
if (!context.app.checkSigs())
forceValidity(
context.app.getHashRouter(),