mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 03:37:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ba2941cc6 |
No files matched your search
@@ -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:
|
||||
|
||||
@@ -69,7 +69,6 @@ enum TELcodes : TERUnderlyingType {
|
||||
telIMPORT_VL_KEY_NOT_RECOGNISED,
|
||||
telCAN_NOT_QUEUE_IMPORT,
|
||||
telENV_RPC_FAILED,
|
||||
telMANIFEST_FEE_MISMATCH,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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); //
|
||||
|
||||
+110
-861
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user