mirror of
https://github.com/Xahau/xahaud.git
synced 2026-08-25 09:20:51 +00:00
Compare commits
2 Commits
external-e
...
jsontx
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e904290e4e | ||
|
|
723103150c |
@@ -124,6 +124,9 @@ find_package(date REQUIRED)
|
||||
find_package(xxHash REQUIRED)
|
||||
find_package(magic_enum REQUIRED)
|
||||
|
||||
find_package(fmt REQUIRED)
|
||||
target_link_libraries(ripple_libs INTERFACE fmt::fmt)
|
||||
|
||||
include(deps/WasmEdge)
|
||||
if(TARGET nudb::core)
|
||||
set(nudb nudb::core)
|
||||
|
||||
@@ -179,121 +179,7 @@ if(xrpld)
|
||||
file(GLOB_RECURSE sources CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test/*.cpp"
|
||||
)
|
||||
if(HOOKS_TEST_ONLY OR DEFINED ENV{HOOKS_TEST_ONLY})
|
||||
# Keep test infra but drop the individual *_test.cpp files
|
||||
list(FILTER sources EXCLUDE REGEX "_test\\.cpp$")
|
||||
message(STATUS "HOOKS_TEST_ONLY: excluded *_test.cpp from src/test/")
|
||||
endif()
|
||||
target_sources(rippled PRIVATE ${sources})
|
||||
|
||||
# Optional: include external hook test sources from another directory.
|
||||
# Set via -DHOOKS_TEST_DIR=/path/to/tests or env HOOKS_TEST_DIR.
|
||||
# Optionally set HOOKS_C_DIR to pass --hooks-c-dir args to the compiler
|
||||
# (e.g. "tipbot=/path/to/hooks" — multiple values separated by ";").
|
||||
#
|
||||
# hookz build-test-hooks must be on PATH. It auto-compiles hooks referenced
|
||||
# in each *_test.cpp and generates *_test_hooks.h next to the test file.
|
||||
if(NOT HOOKS_TEST_DIR AND DEFINED ENV{HOOKS_TEST_DIR})
|
||||
set(HOOKS_TEST_DIR $ENV{HOOKS_TEST_DIR})
|
||||
endif()
|
||||
if(NOT HOOKS_C_DIR AND DEFINED ENV{HOOKS_C_DIR})
|
||||
set(HOOKS_C_DIR $ENV{HOOKS_C_DIR})
|
||||
endif()
|
||||
if(HOOKS_TEST_DIR AND EXISTS "${HOOKS_TEST_DIR}")
|
||||
file(GLOB EXTERNAL_HOOK_TESTS CONFIGURE_DEPENDS
|
||||
"${HOOKS_TEST_DIR}/*_test.cpp"
|
||||
)
|
||||
if(EXTERNAL_HOOK_TESTS)
|
||||
# Build extra args for hookz build-test-hooks
|
||||
set(_hooks_extra_args "")
|
||||
set(_hooks_source_deps "")
|
||||
if(HOOKS_C_DIR)
|
||||
foreach(_dir ${HOOKS_C_DIR})
|
||||
list(APPEND _hooks_extra_args "--hooks-c-dir" "${_dir}")
|
||||
|
||||
string(REGEX REPLACE "^[^=]+=" "" _hook_dir "${_dir}")
|
||||
if(EXISTS "${_hook_dir}")
|
||||
file(GLOB_RECURSE _hook_dir_deps CONFIGURE_DEPENDS
|
||||
"${_hook_dir}/*.c"
|
||||
"${_hook_dir}/*.h"
|
||||
)
|
||||
if(HOOKS_TEST_DIR)
|
||||
list(FILTER _hook_dir_deps EXCLUDE REGEX "^${HOOKS_TEST_DIR}/")
|
||||
endif()
|
||||
list(APPEND _hooks_source_deps ${_hook_dir_deps})
|
||||
endif()
|
||||
endforeach()
|
||||
list(REMOVE_DUPLICATES _hooks_source_deps)
|
||||
endif()
|
||||
if(HOOKS_COVERAGE OR DEFINED ENV{HOOKS_COVERAGE})
|
||||
list(APPEND _hooks_extra_args "--hook-coverage")
|
||||
message(STATUS "Hook coverage enabled: compiling hooks with hookz")
|
||||
endif()
|
||||
set(_hooks_always_run OFF)
|
||||
if(HOOKS_FORCE_RECOMPILE OR DEFINED ENV{HOOKS_FORCE_RECOMPILE})
|
||||
list(APPEND _hooks_extra_args "--force-write" "--no-cache")
|
||||
set(_hooks_always_run ON)
|
||||
message(STATUS "Hook force recompile enabled (cache bypassed)")
|
||||
endif()
|
||||
if(HOOKZ_BUILDBOX OR "$ENV{HOOKZ_BUILDBOX}" STREQUAL "1")
|
||||
# Remote evidence must observe the service on every build. An
|
||||
# OUTPUT-cached header may otherwise preserve locally built WASM
|
||||
# after the compiler mode changes.
|
||||
list(APPEND _hooks_extra_args "--buildbox")
|
||||
if(NOT _hooks_always_run)
|
||||
list(APPEND _hooks_extra_args "--force-write")
|
||||
endif()
|
||||
set(_hooks_always_run ON)
|
||||
message(STATUS "Canonical buildbox enabled (always recompiled)")
|
||||
endif()
|
||||
|
||||
# Run hookz build-test-hooks on each test file before compilation
|
||||
foreach(_test_file ${EXTERNAL_HOOK_TESTS})
|
||||
get_filename_component(_stem ${_test_file} NAME_WE)
|
||||
set(_hooks_header "${HOOKS_TEST_DIR}/${_stem}_hooks.h")
|
||||
if(_hooks_always_run)
|
||||
# Always run — no DEPENDS, no OUTPUT caching
|
||||
add_custom_target(compile_hooks_${_stem} ALL
|
||||
COMMAND hookz build-test-hooks "${_test_file}" ${_hooks_extra_args}
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Compiling hooks for ${_stem} (forced)"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND EXTERNAL_HOOK_TARGETS compile_hooks_${_stem})
|
||||
else()
|
||||
add_custom_command(
|
||||
OUTPUT "${_hooks_header}"
|
||||
COMMAND hookz build-test-hooks "${_test_file}" ${_hooks_extra_args}
|
||||
DEPENDS "${_test_file}" ${_hooks_source_deps}
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Compiling hooks for ${_stem}"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND EXTERNAL_HOOK_HEADERS "${_hooks_header}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Ensure headers are generated before rippled compiles
|
||||
if(_hooks_always_run)
|
||||
foreach(_tgt ${EXTERNAL_HOOK_TARGETS})
|
||||
add_dependencies(rippled ${_tgt})
|
||||
endforeach()
|
||||
else()
|
||||
add_custom_target(compile_external_hooks DEPENDS ${EXTERNAL_HOOK_HEADERS})
|
||||
add_dependencies(rippled compile_external_hooks)
|
||||
endif()
|
||||
|
||||
target_sources(rippled PRIVATE ${EXTERNAL_HOOK_TESTS})
|
||||
# Keep the generated hook-header include path scoped to the external
|
||||
# test sources so changing HOOKS_TEST_DIR doesn't invalidate the
|
||||
# compile command for the rest of rippled.
|
||||
set_property(
|
||||
SOURCE ${EXTERNAL_HOOK_TESTS}
|
||||
APPEND PROPERTY INCLUDE_DIRECTORIES "${HOOKS_TEST_DIR}"
|
||||
)
|
||||
message(STATUS "Including external hook tests from: ${HOOKS_TEST_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(rippled
|
||||
|
||||
@@ -35,6 +35,7 @@ class Xrpl(ConanFile):
|
||||
'soci/4.0.3@xahaud/stable',
|
||||
'xxhash/0.8.2',
|
||||
'zlib/1.3.1',
|
||||
'fmt/12.1.0',
|
||||
]
|
||||
|
||||
tool_requires = [
|
||||
@@ -191,6 +192,7 @@ class Xrpl(ConanFile):
|
||||
'sqlite3::sqlite',
|
||||
'xxhash::xxhash',
|
||||
'zlib::zlib',
|
||||
'fmt::fmt',
|
||||
]
|
||||
if self.options.rocksdb:
|
||||
libxrpl.requires.append('rocksdb::librocksdb')
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include <boost/beast/core/string.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -166,7 +165,6 @@ private:
|
||||
beast::severities::Severity thresh_;
|
||||
File file_;
|
||||
bool silent_ = false;
|
||||
std::function<std::string(std::string const&)> transform_;
|
||||
|
||||
public:
|
||||
Logs(beast::severities::Severity level);
|
||||
@@ -205,33 +203,6 @@ public:
|
||||
std::string const& text,
|
||||
bool console);
|
||||
|
||||
/** Set a transform applied to every log message before output.
|
||||
* Useful in tests to replace raw account IDs with human-readable names.
|
||||
* Pass nullptr to clear.
|
||||
*
|
||||
* TODO: This is test-only infrastructure (used by TestEnv). Consider
|
||||
* moving to SuiteLogs or a test-specific subclass if the Logs interface
|
||||
* needs to stay clean for production.
|
||||
*/
|
||||
void
|
||||
setTransform(std::function<std::string(std::string const&)> fn)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
transform_ = std::move(fn);
|
||||
}
|
||||
|
||||
/** Apply the current transform to text (or return as-is if none set). */
|
||||
std::string const&
|
||||
applyTransform(std::string const& text) const
|
||||
{
|
||||
if (!transform_)
|
||||
return text;
|
||||
// Store in thread_local to return a const ref
|
||||
thread_local std::string buf;
|
||||
buf = transform_(text);
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string
|
||||
rotate();
|
||||
|
||||
|
||||
@@ -417,7 +417,6 @@ getImportWhitelist(Rules const& rules)
|
||||
#define int64_t 0x7EU
|
||||
#define int32_t 0x7FU
|
||||
#define uint32_t 0x7FU
|
||||
#define void_t 0x00U
|
||||
|
||||
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
|
||||
|
||||
@@ -429,15 +428,11 @@ getImportWhitelist(Rules const& rules)
|
||||
|
||||
#include "hook_api.macro"
|
||||
|
||||
// Coverage callback: void __on_source_line(uint32_t line, uint32_t col)
|
||||
whitelist["__on_source_line"] = {void_t, uint32_t, uint32_t};
|
||||
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef HOOK_WRAP_PARAMS
|
||||
#undef int64_t
|
||||
#undef int32_t
|
||||
#undef uint32_t
|
||||
#undef void_t
|
||||
#pragma pop_macro("HOOK_API_DEFINITION")
|
||||
|
||||
return whitelist;
|
||||
|
||||
@@ -1383,51 +1383,21 @@ validateGuards(
|
||||
int result_count = parseLeb128(wasm, i, &i);
|
||||
CHECK_SHORT_HOOK();
|
||||
|
||||
if (j == hook_type_idx)
|
||||
// this needs a reliable hook cleaner otherwise it will catch
|
||||
// most compilers out
|
||||
if (result_count != 1)
|
||||
{
|
||||
// hook/cbak must return exactly one value (i64)
|
||||
if (result_count != 1)
|
||||
{
|
||||
GUARDLOG(hook::log::FUNC_RETURN_COUNT)
|
||||
<< "Malformed transaction. "
|
||||
<< "hook/cbak function type must return exactly "
|
||||
"one value. "
|
||||
<< "\n";
|
||||
return {};
|
||||
}
|
||||
}
|
||||
else if (first_signature)
|
||||
{
|
||||
// For whitelisted imports, check expected return count.
|
||||
// void_t (0x00) means 0 return values.
|
||||
uint8_t expected_return = (*first_signature).get()[0];
|
||||
int expected_result_count =
|
||||
(expected_return == 0x00U) ? 0 : 1;
|
||||
if (result_count != expected_result_count)
|
||||
{
|
||||
GUARDLOG(hook::log::FUNC_RETURN_COUNT)
|
||||
<< "Malformed transaction. "
|
||||
<< "Hook API: " << *first_name
|
||||
<< " has wrong return count "
|
||||
<< "(expected " << expected_result_count << ", got "
|
||||
<< result_count << ")."
|
||||
<< "\n";
|
||||
return {};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result_count != 1)
|
||||
{
|
||||
GUARDLOG(hook::log::FUNC_RETURN_COUNT)
|
||||
<< "Malformed transaction. "
|
||||
<< "Hook declares a function type that returns "
|
||||
"fewer or more than one value. "
|
||||
<< "\n";
|
||||
return {};
|
||||
}
|
||||
GUARDLOG(hook::log::FUNC_RETURN_COUNT)
|
||||
<< "Malformed transaction. "
|
||||
<< "Hook declares a function type that returns fewer "
|
||||
"or more than one value. "
|
||||
<< "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
// this can only ever be 1 in production, but in testing it may
|
||||
// also be 0 or >1 so for completeness this loop is here but can
|
||||
// be taken out in prod
|
||||
for (int k = 0; k < result_count; ++k)
|
||||
{
|
||||
int result_type = parseLeb128(wasm, i, &i);
|
||||
|
||||
@@ -150,23 +150,22 @@
|
||||
WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \
|
||||
COMMA __VA_ARGS__))
|
||||
|
||||
#define HOOK_SETUP() \
|
||||
using enum hook_api::hook_return_code; \
|
||||
try \
|
||||
{ \
|
||||
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
|
||||
[[maybe_unused]] auto& view = applyCtx.view(); \
|
||||
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
|
||||
[[maybe_unused]] auto jh = applyCtx.app.journal("HooksTrace"); \
|
||||
[[maybe_unused]] WasmEdge_MemoryInstanceContext* memoryCtx = \
|
||||
WasmEdge_CallingFrameGetMemoryInstance(&frameCtx, 0); \
|
||||
[[maybe_unused]] unsigned char* memory = \
|
||||
WasmEdge_MemoryInstanceGetPointer(memoryCtx, 0, 0); \
|
||||
[[maybe_unused]] const uint64_t memory_length = \
|
||||
WasmEdge_MemoryInstanceGetPageSize(memoryCtx) * \
|
||||
WasmEdge_kPageSize; \
|
||||
[[maybe_unused]] auto& api = hookCtx.api(); \
|
||||
if (!memoryCtx || !memory || !memory_length) \
|
||||
#define HOOK_SETUP() \
|
||||
using enum hook_api::hook_return_code; \
|
||||
try \
|
||||
{ \
|
||||
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
|
||||
[[maybe_unused]] auto& view = applyCtx.view(); \
|
||||
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
|
||||
[[maybe_unused]] WasmEdge_MemoryInstanceContext* memoryCtx = \
|
||||
WasmEdge_CallingFrameGetMemoryInstance(&frameCtx, 0); \
|
||||
[[maybe_unused]] unsigned char* memory = \
|
||||
WasmEdge_MemoryInstanceGetPointer(memoryCtx, 0, 0); \
|
||||
[[maybe_unused]] const uint64_t memory_length = \
|
||||
WasmEdge_MemoryInstanceGetPageSize(memoryCtx) * \
|
||||
WasmEdge_kPageSize; \
|
||||
[[maybe_unused]] auto& api = hookCtx.api(); \
|
||||
if (!memoryCtx || !memory || !memory_length) \
|
||||
return INTERNAL_ERROR;
|
||||
|
||||
#define HOOK_TEARDOWN() \
|
||||
|
||||
557
include/xrpl/protocol/JSONTxSignatures.h
Normal file
557
include/xrpl/protocol/JSONTxSignatures.h
Normal file
@@ -0,0 +1,557 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012-2014 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_JSONTXSIGNATURES_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_JSONTXSIGNATURES_H_INCLUDED
|
||||
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <fmt/format.h>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// jsontx: plaintext-JSON signing support (featureJsonTx)
|
||||
//
|
||||
// The delta is attacker-controlled: it arrives over the wire beside a binary
|
||||
// transaction and is not covered by the signature it helps reconstruct. Every
|
||||
// bound below is therefore explicit, and unsanitize_jsontx accepts only the
|
||||
// exact encoding sanitize_jsontx would have produced.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
static constexpr std::size_t jsontx_max_text = 8192; // canonical and original
|
||||
static constexpr std::size_t jsontx_max_diff = 1024; // delta bytes
|
||||
static constexpr std::size_t jsontx_max_ops = 256; // delta instructions
|
||||
static constexpr std::size_t jsontx_min_copy = 4; // encoder match threshold
|
||||
static constexpr std::size_t jsontx_max_cand = 64; // encoder candidate cap
|
||||
|
||||
// Case-insensitive field-name -> canonical SField. Built once from
|
||||
// SField::knownCodeToField, the same table doServerDefinitions publishes, using
|
||||
// its serializability filter (useful, binary, non-pseudo). sfInvalid if
|
||||
// unknown.
|
||||
static SField const&
|
||||
jsontx_field(std::string const& name)
|
||||
{
|
||||
static auto const tbl = [] {
|
||||
std::unordered_map<std::string, SField const*> m;
|
||||
for (auto const& [code, f] : SField::knownCodeToField)
|
||||
if (f->isUseful() && f->isBinary() && f->fieldType < 10000 &&
|
||||
!f->fieldName.empty())
|
||||
m.emplace(boost::algorithm::to_lower_copy(f->fieldName), f);
|
||||
return m;
|
||||
}();
|
||||
|
||||
auto const i = tbl.find(boost::algorithm::to_lower_copy(name));
|
||||
return i == tbl.end() ? sfInvalid : *i->second;
|
||||
}
|
||||
|
||||
// Civil calendar arithmetic (Howard Hinnant's algorithm), used in both
|
||||
// directions. Pure integer maths - no strptime, no timegm, no locale, no
|
||||
// tzdata - because these conversions decide whether a signature verifies and
|
||||
// so must give the same answer on every node forever.
|
||||
static constexpr std::int64_t
|
||||
jsontx_days(int y, unsigned m, unsigned d) // days from 1970-01-01
|
||||
{
|
||||
y -= m <= 2;
|
||||
std::int64_t const era = (y >= 0 ? y : y - 399) / 400;
|
||||
unsigned const yoe = static_cast<unsigned>(y - era * 400);
|
||||
unsigned const doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
||||
unsigned const doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
return era * 146097 + doe - 719468;
|
||||
}
|
||||
|
||||
static constexpr std::int64_t jsontx_epoch_day = jsontx_days(2000, 1, 1);
|
||||
|
||||
// 9999-12-31T23:59:59.999Z: past this toISOString() switches to expanded years
|
||||
// (+275760-09-13T...) and the fixed 24 character shape no longer holds
|
||||
static constexpr std::uint64_t jsontx_max_time =
|
||||
(static_cast<std::uint64_t>(jsontx_days(9999, 12, 31) - jsontx_epoch_day) *
|
||||
86400 +
|
||||
86399) *
|
||||
1000 +
|
||||
999;
|
||||
|
||||
static_assert(jsontx_epoch_day == 10957); // matches chrono.h epoch_offset
|
||||
|
||||
// Strict Date().toISOString() -> milliseconds since the ripple epoch. Exactly
|
||||
// YYYY-MM-DDTHH:MM:SS.sssZ, always UTC, always three fractional digits.
|
||||
static std::uint64_t
|
||||
jsontx_iso(std::string const& s)
|
||||
{
|
||||
static constexpr char pat[] = "0000-00-00T00:00:00.000Z";
|
||||
if (s.size() != 24)
|
||||
throw std::runtime_error("jsontx: Time must be an ISO 8601 instant");
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
if (pat[i] == '0' ? !std::isdigit(static_cast<unsigned char>(s[i]))
|
||||
: s[i] != pat[i])
|
||||
throw std::runtime_error("jsontx: malformed Time");
|
||||
|
||||
auto const n = [&s](std::size_t i, std::size_t c) {
|
||||
int v = 0;
|
||||
while (c--)
|
||||
v = v * 10 + (s[i++] - '0');
|
||||
return v;
|
||||
};
|
||||
int const y = n(0, 4), mo = n(5, 2), d = n(8, 2), h = n(11, 2),
|
||||
mi = n(14, 2), se = n(17, 2), ms = n(20, 3);
|
||||
if (mo < 1 || mo > 12)
|
||||
throw std::runtime_error("jsontx: Time month out of range");
|
||||
bool const leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
|
||||
int const dim =
|
||||
mo == 2 ? (leap ? 29 : 28) : ((mo % 2 == 1) == (mo <= 7) ? 31 : 30);
|
||||
// 60 is rejected: JS cannot emit a leap second and the ledger cannot
|
||||
// represent one
|
||||
if (d < 1 || d > dim || h > 23 || mi > 59 || se > 59)
|
||||
throw std::runtime_error("jsontx: Time out of range");
|
||||
|
||||
std::int64_t const t = (jsontx_days(y, mo, d) - jsontx_epoch_day) * 86400 +
|
||||
h * 3600 + mi * 60 + se;
|
||||
if (t < 0)
|
||||
throw std::runtime_error("jsontx: Time precedes the ripple epoch");
|
||||
return static_cast<std::uint64_t>(t) * 1000 + ms;
|
||||
}
|
||||
|
||||
// The exact inverse. Total over [0, jsontx_max_time] and injective, so sfTime
|
||||
// and its ISO spelling are two views of one value and the delta carries
|
||||
// nothing for the field.
|
||||
static std::string
|
||||
jsontx_iso_str(std::uint64_t ms)
|
||||
{
|
||||
if (ms > jsontx_max_time)
|
||||
throw std::runtime_error("jsontx: Time out of range");
|
||||
std::int64_t const z =
|
||||
static_cast<std::int64_t>(ms / 86400000) + jsontx_epoch_day + 719468;
|
||||
unsigned const tod = static_cast<unsigned>(ms / 1000 % 86400);
|
||||
std::int64_t const era = (z >= 0 ? z : z - 146096) / 146097;
|
||||
unsigned const doe = static_cast<unsigned>(z - era * 146097);
|
||||
unsigned const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
unsigned const doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
unsigned const mp = (5 * doy + 2) / 153;
|
||||
unsigned const d = doy - (153 * mp + 2) / 5 + 1;
|
||||
unsigned const m = mp + (mp < 10 ? 3 : -9);
|
||||
return fmt::format(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
|
||||
static_cast<std::int64_t>(yoe) + era * 400 + (m <= 2),
|
||||
m,
|
||||
d,
|
||||
tod / 3600,
|
||||
tod / 60 % 60,
|
||||
tod % 60,
|
||||
static_cast<unsigned>(ms % 1000));
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// jsoncpp compatibility
|
||||
//
|
||||
// This is xrpl's vendored jsoncpp, which predates JSON_HAS_INT64: Json::Int is
|
||||
// int and Json::UInt is unsigned int, both 32 bit, and there is no isInt64 /
|
||||
// asInt64 / asUInt64. Reader::decodeNumber yields intValue or uintValue only
|
||||
// while the digits fit in 32 bits; on overflow, and for any token carrying a
|
||||
// '.' or an exponent, it falls through to decodeDouble and the number arrives
|
||||
// as a realValue.
|
||||
//
|
||||
// So a large integer is not lost, but it is no longer held as an integer, and
|
||||
// the digits the signer wrote are recoverable only while the double is an
|
||||
// exact integer view of them. That holds to 2^53; above it consecutive doubles
|
||||
// are more than 1 apart and distinct decimal integers collapse onto the same
|
||||
// double. Past that point the value is refused rather than guessed at.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
static constexpr double jsontx_exact_max = 9007199254740992.0; // 2^53
|
||||
|
||||
// Exact integer view of a json number. False if v is not a number at all, or
|
||||
// is one this build cannot reproduce digit for digit.
|
||||
static bool
|
||||
jsontx_exact(Json::Value const& v, std::int64_t& out)
|
||||
{
|
||||
switch (v.type())
|
||||
{
|
||||
case Json::intValue:
|
||||
out = v.asInt();
|
||||
return true;
|
||||
case Json::uintValue:
|
||||
out = static_cast<std::int64_t>(v.asUInt());
|
||||
return true;
|
||||
case Json::realValue:
|
||||
break;
|
||||
default: // including booleanValue, which isIntegral() would admit
|
||||
return false;
|
||||
}
|
||||
double const d = v.asDouble();
|
||||
if (!std::isfinite(d) || d != std::trunc(d) || d < -jsontx_exact_max ||
|
||||
d > jsontx_exact_max)
|
||||
return false;
|
||||
out = static_cast<std::int64_t>(d);
|
||||
return true;
|
||||
}
|
||||
|
||||
// STI_UINT64 renders as a fixed 16 digit hex string and so needs the value
|
||||
// unsigned. A negative is refused rather than wrapped: the old
|
||||
// static_cast<std::uint64_t>(v.asDouble()) was undefined for a negative or
|
||||
// oversized double, and a silent wrap would let -1 and 18446744073709551615
|
||||
// canonicalize to the same bytes.
|
||||
static std::uint64_t
|
||||
jsontx_u64(Json::Value const& v)
|
||||
{
|
||||
std::int64_t n = 0;
|
||||
if (!jsontx_exact(v, n) || n < 0)
|
||||
throw std::runtime_error(
|
||||
"jsontx: UInt64 must be an exact non-negative integer");
|
||||
return static_cast<std::uint64_t>(n);
|
||||
}
|
||||
|
||||
// Renders a javascript number as an exact integer. Anything this build cannot
|
||||
// reproduce digit for digit - a fraction, an infinity, a magnitude past 2^53 -
|
||||
// is rejected outright: shortest-round-trip rendering of a double is not
|
||||
// portable enough to sit in a consensus preimage, and nothing in a transaction
|
||||
// needs one. Fractional amounts arrive as strings, which is what the ledger
|
||||
// wants anyway.
|
||||
static std::string
|
||||
jsontx_num(Json::Value const& v)
|
||||
{
|
||||
std::int64_t n = 0;
|
||||
if (!jsontx_exact(v, n))
|
||||
throw std::runtime_error("jsontx: number must be an exact integer");
|
||||
return std::to_string(n);
|
||||
}
|
||||
|
||||
// Returns { sanitized, diff }. `sanitized` is the canonical form: whitespace
|
||||
// stripped, field names capitalized to their xahau spelling, members reordered
|
||||
// by field code, numbers reformatted per field type. `diff` is a binary delta
|
||||
// which, applied to `sanitized` by unsanitize_jsontx, reproduces `raw` byte for
|
||||
// byte. Throws on anything it cannot canonicalize.
|
||||
static std::pair<std::string, std::string>
|
||||
sanitize_jsontx(std::string_view raw)
|
||||
{
|
||||
if (raw.size() > jsontx_max_text)
|
||||
throw std::runtime_error("jsontx: document too large");
|
||||
|
||||
Json::Value jv;
|
||||
if (Json::Reader r; !r.parse(raw.data(), raw.data() + raw.size(), jv) ||
|
||||
!jv.isObject())
|
||||
throw std::runtime_error("jsontx: malformed json");
|
||||
|
||||
// (a plain recursive lambda; deducing-this would drop the std::function)
|
||||
std::function<
|
||||
void(Json::Value const&, SerializedTypeID, SField const*, std::string&)>
|
||||
emit = [&](Json::Value const& v,
|
||||
SerializedTypeID ty,
|
||||
SField const* fld,
|
||||
std::string& o) {
|
||||
if (v.isObject())
|
||||
{
|
||||
auto keys = v.getMemberNames();
|
||||
o += '{';
|
||||
if (ty == STI_OBJECT) // keys are xahau fields
|
||||
{
|
||||
std::vector<std::pair<SField const*, std::string>> ks;
|
||||
for (auto const& k : keys)
|
||||
{
|
||||
auto const& f = jsontx_field(k);
|
||||
if (f == sfInvalid)
|
||||
throw std::runtime_error(
|
||||
"jsontx: unknown field '" + k + "'");
|
||||
ks.emplace_back(&f, k);
|
||||
}
|
||||
std::sort(
|
||||
ks.begin(), ks.end(), [](auto const& a, auto const& b) {
|
||||
return a.first->fieldCode < b.first->fieldCode;
|
||||
});
|
||||
for (std::size_t n = 0; n < ks.size(); ++n)
|
||||
{
|
||||
auto const& [f, k] = ks[n];
|
||||
if (n && f == ks[n - 1].first) // e.g. "Fee" and "fee"
|
||||
throw std::runtime_error(
|
||||
"jsontx: duplicate field '" + f->fieldName +
|
||||
"'");
|
||||
if (o.back() != '{')
|
||||
o += ',';
|
||||
o += Json::valueToQuotedString(f->fieldName.c_str()) +
|
||||
':';
|
||||
emit(v[k], f->fieldType, f, o);
|
||||
}
|
||||
}
|
||||
else // amount / issue style subobject: lexicographic, quoted
|
||||
{
|
||||
std::sort(keys.begin(), keys.end());
|
||||
for (auto const& k : keys)
|
||||
{
|
||||
if (o.back() != '{')
|
||||
o += ',';
|
||||
o += Json::valueToQuotedString(k.c_str()) + ':';
|
||||
emit(v[k], STI_NOTPRESENT, nullptr, o);
|
||||
}
|
||||
}
|
||||
o += '}';
|
||||
}
|
||||
else if (v.isArray())
|
||||
{
|
||||
o += '[';
|
||||
for (auto const& e : v)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
emit(e, STI_OBJECT, nullptr, o);
|
||||
}
|
||||
o += ']';
|
||||
}
|
||||
else if (v.isString())
|
||||
{
|
||||
// Time is spelled Date().toISOString() in the preimage and
|
||||
// stored as an sfTime u64 of milliseconds. Re-emitting the
|
||||
// round-tripped spelling rather than the input is what makes
|
||||
// the canonical form a fixed point: any string that is not
|
||||
// exactly what jsontx_iso_str produces is rejected here.
|
||||
if (fld && *fld == sfTime)
|
||||
o += Json::valueToQuotedString(
|
||||
jsontx_iso_str(jsontx_iso(v.asString())).c_str());
|
||||
else
|
||||
o += Json::valueToQuotedString(v.asCString());
|
||||
}
|
||||
else if (v.isBool())
|
||||
o += v.asBool() ? "true" : "false";
|
||||
else if (v.isNull())
|
||||
throw std::runtime_error("jsontx: null value");
|
||||
else if (ty == STI_UINT8 || ty == STI_UINT16 || ty == STI_UINT32)
|
||||
o += jsontx_num(v); // small ints stay bare
|
||||
else if (ty == STI_UINT64)
|
||||
o += '"' + fmt::format("{:016X}", jsontx_u64(v)) + '"';
|
||||
else // amounts, u64, everything else the ledger wants as a string
|
||||
o += Json::valueToQuotedString(jsontx_num(v).c_str());
|
||||
};
|
||||
|
||||
std::string out;
|
||||
emit(jv, STI_OBJECT, nullptr, out);
|
||||
|
||||
// Greedy copy/insert delta over `raw`, sourcing from `out`.
|
||||
// op 0x00 <varint len> <bytes> literal
|
||||
// op 0x01 <varint off> <varint len> copy from sanitized
|
||||
std::string diff, lit;
|
||||
auto const gram = [](std::string_view s, std::size_t i) {
|
||||
return std::uint32_t(std::uint8_t(s[i])) << 24 |
|
||||
std::uint32_t(std::uint8_t(s[i + 1])) << 16 |
|
||||
std::uint32_t(std::uint8_t(s[i + 2])) << 8 |
|
||||
std::uint32_t(std::uint8_t(s[i + 3]));
|
||||
};
|
||||
auto const varint = [](std::string& o, std::uint64_t v) {
|
||||
do
|
||||
{
|
||||
std::uint8_t const c = v & 0x7F;
|
||||
v >>= 7;
|
||||
o += static_cast<char>(c | (v ? 0x80 : 0));
|
||||
} while (v);
|
||||
};
|
||||
auto const flush = [&] {
|
||||
if (lit.empty())
|
||||
return;
|
||||
diff += char(0);
|
||||
varint(diff, lit.size());
|
||||
diff += lit;
|
||||
lit.clear();
|
||||
};
|
||||
|
||||
// This encoder is normative - unsanitize_jsontx only accepts its exact
|
||||
// output - so it must emit identical bytes on every node and every stdlib.
|
||||
// An unordered container would not: bucket order for equal keys is
|
||||
// unspecified, and with a candidate cap that changes which match wins.
|
||||
std::map<std::uint32_t, std::vector<std::uint32_t>> idx;
|
||||
for (std::size_t i = 0; i + jsontx_min_copy <= out.size(); ++i)
|
||||
idx[gram(out, i)].push_back(i);
|
||||
|
||||
for (std::size_t i = 0; i < raw.size();)
|
||||
{
|
||||
std::size_t bo = 0, bl = 0;
|
||||
if (i + jsontx_min_copy <= raw.size())
|
||||
if (auto const it = idx.find(gram(raw, i)); it != idx.end())
|
||||
{
|
||||
std::size_t tried = 0;
|
||||
for (auto const off : it->second) // ascending, so ties keep
|
||||
{ // the lowest offset
|
||||
if (++tried > jsontx_max_cand)
|
||||
break;
|
||||
std::size_t l = 0;
|
||||
while (i + l < raw.size() && off + l < out.size() &&
|
||||
out[off + l] == raw[i + l])
|
||||
++l;
|
||||
if (l > bl)
|
||||
bl = l, bo = off;
|
||||
}
|
||||
}
|
||||
if (bl >= jsontx_min_copy)
|
||||
{
|
||||
flush();
|
||||
diff += char(1);
|
||||
varint(diff, bo);
|
||||
varint(diff, bl);
|
||||
i += bl;
|
||||
}
|
||||
else
|
||||
lit += raw[i++];
|
||||
}
|
||||
flush();
|
||||
|
||||
return {std::move(out), std::move(diff)};
|
||||
}
|
||||
|
||||
// Applies an UNTRUSTED delta to a canonical form the node derived itself.
|
||||
// Copies read only from `sanitized`, never from the output being built, so a
|
||||
// short delta cannot expand geometrically. Offsets and lengths are range
|
||||
// checked before use, varints are length- and minimality-bounded, and the two
|
||||
// encodings the encoder can never emit - an unmerged literal run, and a copy
|
||||
// abutting the previous copy in the source - are rejected. Throws on anything
|
||||
// else.
|
||||
static std::string
|
||||
unsanitize_jsontx(std::string_view sanitized, std::string_view diff)
|
||||
{
|
||||
if (sanitized.size() > jsontx_max_text || diff.size() > jsontx_max_diff)
|
||||
throw std::runtime_error("jsontx: oversize delta input");
|
||||
|
||||
std::string out;
|
||||
std::size_t p = 0, ops = 0, prevEnd = 0;
|
||||
int prev = -1;
|
||||
|
||||
auto const varint = [&](std::uint64_t max) -> std::uint64_t {
|
||||
std::uint64_t v = 0;
|
||||
for (int s = 0; s <= 21; s += 7) // four bytes; caps far under a shift
|
||||
{ // wide enough to be undefined
|
||||
if (p >= diff.size())
|
||||
throw std::runtime_error("jsontx: truncated delta");
|
||||
std::uint8_t const c = diff[p++];
|
||||
v |= std::uint64_t(c & 0x7F) << s;
|
||||
if (c & 0x80)
|
||||
continue;
|
||||
if (s && !(c & 0x7F))
|
||||
throw std::runtime_error("jsontx: non-minimal varint");
|
||||
if (v > max)
|
||||
throw std::runtime_error("jsontx: delta value out of range");
|
||||
return v;
|
||||
}
|
||||
throw std::runtime_error("jsontx: overlong varint");
|
||||
};
|
||||
|
||||
while (p < diff.size())
|
||||
{
|
||||
if (++ops > jsontx_max_ops)
|
||||
throw std::runtime_error("jsontx: too many delta ops");
|
||||
|
||||
std::uint8_t const op = diff[p++];
|
||||
if (op > 1)
|
||||
throw std::runtime_error("jsontx: unknown delta op");
|
||||
|
||||
std::size_t n = 0;
|
||||
if (op == 0) // literal
|
||||
{
|
||||
if (prev == 0)
|
||||
throw std::runtime_error("jsontx: unmerged literal run");
|
||||
n = varint(jsontx_max_text);
|
||||
if (n == 0 || n > diff.size() - p)
|
||||
throw std::runtime_error("jsontx: bad literal length");
|
||||
if (out.size() + n > jsontx_max_text)
|
||||
throw std::runtime_error("jsontx: delta expands too far");
|
||||
out += diff.substr(p, n);
|
||||
p += n;
|
||||
}
|
||||
else // copy from the canonical form
|
||||
{
|
||||
auto const off = varint(sanitized.size());
|
||||
n = varint(sanitized.size() - off);
|
||||
if (n < jsontx_min_copy)
|
||||
throw std::runtime_error("jsontx: undersize copy");
|
||||
if (prev == 1 && off == prevEnd)
|
||||
throw std::runtime_error("jsontx: unmerged copy run");
|
||||
if (out.size() + n > jsontx_max_text)
|
||||
throw std::runtime_error("jsontx: delta expands too far");
|
||||
out += sanitized.substr(off, n);
|
||||
prevEnd = off + n;
|
||||
}
|
||||
prev = op;
|
||||
}
|
||||
|
||||
if (out.empty())
|
||||
throw std::runtime_error("jsontx: empty delta");
|
||||
return out;
|
||||
}
|
||||
|
||||
// The complete untrusted-side check, in one place so the RPC path and the
|
||||
// relay/consensus path cannot drift. Takes the transaction exactly as it came
|
||||
// off the wire and returns the reconstructed preimage.
|
||||
static std::string
|
||||
jsontx_verify(STTx const& stx, std::string_view diff)
|
||||
{
|
||||
if (!stx.isFieldPresent(sfTxnSignature) || stx.isFieldPresent(sfSigners))
|
||||
throw std::runtime_error("jsontx: expects a lone TxnSignature");
|
||||
|
||||
// Out comes everything the signer did not have in front of them: the
|
||||
// signature, and (once the field exists) the delta carrier. SigningPubKey
|
||||
// stays. It being inside the preimage is what binds key to signature and
|
||||
// stops a third party re-signing a captured preimage under their own key.
|
||||
auto txj = stx.STObject::getJson(JsonOptions::none);
|
||||
txj.removeMember(sfTxnSignature.fieldName);
|
||||
|
||||
// sfTime is a u64 of milliseconds on the wire and an ISO 8601 instant in
|
||||
// the preimage. The two are a bijection over the representable range, so
|
||||
// this is a rewrite rather than a reconstruction and the delta carries
|
||||
// nothing for the field.
|
||||
if (stx.isFieldPresent(sfTime))
|
||||
txj[sfTime.fieldName] = jsontx_iso_str(stx.getFieldU64(sfTime));
|
||||
|
||||
auto const pkb = stx.getSigningPubKey();
|
||||
if (publicKeyType(makeSlice(pkb)) != KeyType::ed25519)
|
||||
throw std::runtime_error("jsontx: SigningPubKey must be ed25519");
|
||||
|
||||
// canonical form, derived only from data the node has already validated
|
||||
auto const san = sanitize_jsontx(Json::FastWriter{}.write(txj)).first;
|
||||
|
||||
// reconstruct the signed preimage under the caps above
|
||||
auto const raw = unsanitize_jsontx(san, diff);
|
||||
|
||||
// Bind the preimage to the transaction. This is the load-bearing check,
|
||||
// not a sanity check: a delta of pure literals can reconstruct ANY text,
|
||||
// so without it any ed25519 signature the key ever produced over anything
|
||||
// at all would authorise this transaction. Comparing the delta too - not
|
||||
// just the canonical form - makes the delta a pure function of the
|
||||
// preimage, which rules out a second delta reconstructing the same bytes
|
||||
// and yielding a second valid transaction id.
|
||||
auto const [san2, diff2] = sanitize_jsontx(raw);
|
||||
if (san2 != san || diff2 != diff)
|
||||
throw std::runtime_error("jsontx: preimage does not match transaction");
|
||||
|
||||
if (!verify(
|
||||
PublicKey(makeSlice(pkb)),
|
||||
makeSlice(raw),
|
||||
makeSlice(stx.getFieldVL(sfTxnSignature))))
|
||||
throw std::runtime_error("jsontx: signature does not verify");
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
#endif
|
||||
@@ -34,6 +34,7 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(JsonTx, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -153,6 +153,7 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie
|
||||
TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default)
|
||||
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
|
||||
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
||||
TYPED_SFIELD(sfTime, UINT64, 96)
|
||||
TYPED_SFIELD(sfTouchCount, UINT64, 97)
|
||||
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
|
||||
TYPED_SFIELD(sfAccountCount, UINT64, 99)
|
||||
@@ -293,6 +294,7 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
|
||||
TYPED_SFIELD(sfProvider, VL, 30)
|
||||
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
|
||||
TYPED_SFIELD(sfCredentialType, VL, 32)
|
||||
TYPED_SFIELD(sfJsonTxDelta, VL, 96)
|
||||
TYPED_SFIELD(sfHookName, VL, 97)
|
||||
TYPED_SFIELD(sfRemarkValue, VL, 98)
|
||||
TYPED_SFIELD(sfRemarkName, VL, 99)
|
||||
|
||||
@@ -630,6 +630,7 @@ JSS(server_status); // out: NetworkOPs
|
||||
JSS(server_version); // out: NetworkOPs
|
||||
JSS(settle_delay); // out: AccountChannels
|
||||
JSS(severity); // in: LogLevel
|
||||
JSS(sig);
|
||||
JSS(signature); // out: NetworkOPs, ChannelAuthorize
|
||||
JSS(signature_verified); // out: ChannelVerify
|
||||
JSS(signing_key); // out: NetworkOPs
|
||||
|
||||
@@ -196,10 +196,9 @@ Logs::write(
|
||||
std::string const& text,
|
||||
bool console)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::string const& transformed = transform_ ? transform_(text) : text;
|
||||
std::string s;
|
||||
format(s, transformed, level, partition);
|
||||
format(s, text, level, partition);
|
||||
std::lock_guard lock(mutex_);
|
||||
file_.writeln(s);
|
||||
if (!silent_)
|
||||
std::cerr << s << '\n';
|
||||
|
||||
@@ -49,6 +49,8 @@ TxFormats::TxFormats()
|
||||
{sfNetworkID, soeOPTIONAL},
|
||||
{sfHookParameters, soeOPTIONAL},
|
||||
{sfHookName, soeOPTIONAL},
|
||||
{sfTime, soeOPTIONAL},
|
||||
{sfJsonTxDelta, soeOPTIONAL},
|
||||
};
|
||||
|
||||
#pragma push_macro("UNWRAP")
|
||||
|
||||
@@ -106,8 +106,7 @@ public:
|
||||
std::string const& partition,
|
||||
beast::severities::Severity threshold) override
|
||||
{
|
||||
return std::make_unique<SuiteJournalSink>(
|
||||
partition, threshold, suite_, this);
|
||||
return std::make_unique<SuiteJournalSink>(partition, threshold, suite_);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
#ifndef TEST_JTX_TESTENV_H_INCLUDED
|
||||
#define TEST_JTX_TESTENV_H_INCLUDED
|
||||
|
||||
#include <test/jtx/Env.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
/**
|
||||
* TestEnv wraps Env with:
|
||||
* - Named account registry: env.account("alice")
|
||||
* - Auto log transform: replaces r-addresses with Account(name) in log output
|
||||
* - Env-var driven per-partition log levels via TESTENV_LOGGING
|
||||
*
|
||||
* Usage:
|
||||
* TestEnv env{suite, features};
|
||||
* auto const& alice = env.account("alice");
|
||||
* auto const& bob = env.account("bob");
|
||||
* env.fund(XRP(10000), alice, bob);
|
||||
* // Logs now show Account(alice), Account(bob) instead of r-addresses
|
||||
*
|
||||
* Log levels via env var:
|
||||
* TESTENV_LOGGING="HooksTrace=trace,View=debug"
|
||||
*
|
||||
* Valid levels: trace, debug, info, warning, error, fatal
|
||||
*/
|
||||
class TestEnv : public Env
|
||||
{
|
||||
std::map<std::string, Account> accounts_;
|
||||
std::string prefix_;
|
||||
|
||||
public:
|
||||
TestEnv(beast::unit_test::suite& suite, FeatureBitset features)
|
||||
: Env(suite, features)
|
||||
{
|
||||
installTransform();
|
||||
applyLoggingEnvVar();
|
||||
}
|
||||
|
||||
TestEnv(
|
||||
beast::unit_test::suite& suite,
|
||||
std::unique_ptr<Config> config,
|
||||
FeatureBitset features,
|
||||
std::unique_ptr<Logs> logs = nullptr,
|
||||
beast::severities::Severity thresh = beast::severities::kError)
|
||||
: Env(suite, std::move(config), features, std::move(logs), thresh)
|
||||
{
|
||||
installTransform();
|
||||
applyLoggingEnvVar();
|
||||
}
|
||||
|
||||
~TestEnv()
|
||||
{
|
||||
app().logs().setTransform(nullptr);
|
||||
}
|
||||
|
||||
/// Get or create a named account.
|
||||
/// First call creates the Account; subsequent calls return the same one.
|
||||
Account const&
|
||||
account(std::string const& name)
|
||||
{
|
||||
auto [it, inserted] = accounts_.try_emplace(name, name);
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/// Set a prefix that appears at the start of every log line.
|
||||
/// Useful for visually separating test phases in trace output.
|
||||
/// Pass empty string to clear.
|
||||
void
|
||||
setPrefix(std::string const& prefix)
|
||||
{
|
||||
prefix_ = prefix.empty() ? "" : "[" + prefix + "] ";
|
||||
}
|
||||
|
||||
private:
|
||||
static beast::severities::Severity
|
||||
parseSeverity(std::string const& s)
|
||||
{
|
||||
if (s == "trace")
|
||||
return beast::severities::kTrace;
|
||||
if (s == "debug")
|
||||
return beast::severities::kDebug;
|
||||
if (s == "info")
|
||||
return beast::severities::kInfo;
|
||||
if (s == "warning")
|
||||
return beast::severities::kWarning;
|
||||
if (s == "error")
|
||||
return beast::severities::kError;
|
||||
if (s == "fatal")
|
||||
return beast::severities::kFatal;
|
||||
return beast::severities::kError;
|
||||
}
|
||||
|
||||
void
|
||||
applyLoggingEnvVar()
|
||||
{
|
||||
// Parse TESTENV_LOGGING="Partition1=level,Partition2=level"
|
||||
auto const* envVal = std::getenv("TESTENV_LOGGING");
|
||||
if (!envVal || !envVal[0])
|
||||
return;
|
||||
|
||||
std::istringstream ss(envVal);
|
||||
std::string pair;
|
||||
while (std::getline(ss, pair, ','))
|
||||
{
|
||||
auto eq = pair.find('=');
|
||||
if (eq == std::string::npos)
|
||||
continue;
|
||||
auto partition = pair.substr(0, eq);
|
||||
auto level = pair.substr(eq + 1);
|
||||
app().logs().get(partition).threshold(parseSeverity(level));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
installTransform()
|
||||
{
|
||||
app().logs().setTransform([this](std::string const& text) {
|
||||
std::string out = prefix_ + text;
|
||||
for (auto const& [name, acc] : accounts_)
|
||||
{
|
||||
auto raddr = toBase58(acc.id());
|
||||
std::string::size_type pos = 0;
|
||||
std::string replacement = "Account(" + name + ")";
|
||||
while ((pos = out.find(raddr, pos)) != std::string::npos)
|
||||
{
|
||||
out.replace(pos, raddr.size(), replacement);
|
||||
pos += replacement.size();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#ifndef TEST_UNIT_TEST_SUITE_JOURNAL_H
|
||||
#define TEST_UNIT_TEST_SUITE_JOURNAL_H
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <mutex>
|
||||
@@ -32,18 +31,13 @@ class SuiteJournalSink : public beast::Journal::Sink
|
||||
{
|
||||
std::string partition_;
|
||||
beast::unit_test::suite& suite_;
|
||||
Logs* logs_ = nullptr;
|
||||
|
||||
public:
|
||||
SuiteJournalSink(
|
||||
std::string const& partition,
|
||||
beast::severities::Severity threshold,
|
||||
beast::unit_test::suite& suite,
|
||||
Logs* logs = nullptr)
|
||||
: Sink(threshold, false)
|
||||
, partition_(partition + " ")
|
||||
, suite_(suite)
|
||||
, logs_(logs)
|
||||
beast::unit_test::suite& suite)
|
||||
: Sink(threshold, false), partition_(partition + " "), suite_(suite)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -103,12 +97,11 @@ SuiteJournalSink::writeAlways(
|
||||
// Only write the string if the level at least equals the threshold.
|
||||
if (level >= threshold())
|
||||
{
|
||||
std::string const& output = logs_ ? logs_->applyTransform(text) : text;
|
||||
// std::endl flushes → sync() → str()/str("") race in shared buffer →
|
||||
// crashes
|
||||
static std::mutex log_mutex;
|
||||
std::lock_guard lock(log_mutex);
|
||||
suite_.log << s << partition_ << output << std::endl;
|
||||
suite_.log << s << partition_ << text << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <any>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wasmedge/wasmedge.h>
|
||||
@@ -307,130 +305,6 @@ static WasmEdge_String hookFunctionName =
|
||||
// see: lib/system/allocator.cpp
|
||||
#define WasmEdge_kPageSize 65536ULL
|
||||
|
||||
// --- Coverage infrastructure ---
|
||||
//
|
||||
// Global coverage accumulator keyed by hook hash. Persists across all hook
|
||||
// executions in the process. Each __on_source_line call records a (line, col)
|
||||
// pair under the executing hook's hash.
|
||||
//
|
||||
// Test API:
|
||||
// hook::coverageReset() — clear all accumulated data
|
||||
// hook::coverageHits(hookHash) — get hits for a specific hook
|
||||
// hook::coverageLabel(hash, label) — register a human-readable label
|
||||
// hook::coverageDump(path) — write all data to a file
|
||||
//
|
||||
// The dump file format is:
|
||||
// [label or hash]
|
||||
// hits=<line:col>,<line:col>,...
|
||||
|
||||
struct CoverageData
|
||||
{
|
||||
std::set<uint32_t> hits{};
|
||||
};
|
||||
|
||||
// Global accumulator — survives across HookContext lifetimes
|
||||
inline std::map<ripple::uint256, CoverageData>&
|
||||
coverageMap()
|
||||
{
|
||||
static std::map<ripple::uint256, CoverageData> map;
|
||||
return map;
|
||||
}
|
||||
|
||||
// Hash → label mapping (e.g. hash → "file:tipbot/tip.c")
|
||||
inline std::map<ripple::uint256, std::string>&
|
||||
coverageLabels()
|
||||
{
|
||||
static std::map<ripple::uint256, std::string> labels;
|
||||
return labels;
|
||||
}
|
||||
|
||||
inline void
|
||||
coverageReset()
|
||||
{
|
||||
coverageMap().clear();
|
||||
coverageLabels().clear();
|
||||
}
|
||||
|
||||
inline void
|
||||
coverageLabel(ripple::uint256 const& hookHash, std::string const& label)
|
||||
{
|
||||
coverageLabels()[hookHash] = label;
|
||||
}
|
||||
|
||||
inline std::set<uint32_t> const*
|
||||
coverageHits(ripple::uint256 const& hookHash)
|
||||
{
|
||||
auto& map = coverageMap();
|
||||
auto it = map.find(hookHash);
|
||||
if (it == map.end())
|
||||
return nullptr;
|
||||
return &it->second.hits;
|
||||
}
|
||||
|
||||
inline bool
|
||||
coverageDump(std::string const& path)
|
||||
{
|
||||
auto& map = coverageMap();
|
||||
if (map.empty())
|
||||
return false;
|
||||
|
||||
auto& labels = coverageLabels();
|
||||
|
||||
std::ofstream out(path);
|
||||
if (!out)
|
||||
return false;
|
||||
|
||||
for (auto const& [hash, data] : map)
|
||||
{
|
||||
auto it = labels.find(hash);
|
||||
if (it != labels.end())
|
||||
out << "[" << it->second << "]\n";
|
||||
else
|
||||
out << "[" << to_string(hash) << "]\n";
|
||||
|
||||
out << "hits=";
|
||||
bool first = true;
|
||||
for (auto key : data.hits)
|
||||
{
|
||||
if (!first)
|
||||
out << ",";
|
||||
out << (key >> 16) << ":" << (key & 0xFFFF);
|
||||
first = false;
|
||||
}
|
||||
out << "\n\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Coverage host callback ---
|
||||
|
||||
inline WasmEdge_Result
|
||||
onSourceLine(
|
||||
void* data_ptr,
|
||||
const WasmEdge_CallingFrameContext* frameCtx,
|
||||
const WasmEdge_Value* in,
|
||||
WasmEdge_Value* out)
|
||||
{
|
||||
// Called by hookz-instrumented WASM at each DWARF source location.
|
||||
// in[0] = line number, in[1] = column number.
|
||||
(void)out;
|
||||
(void)frameCtx;
|
||||
auto* hookCtx = reinterpret_cast<HookContext*>(data_ptr);
|
||||
if (!hookCtx)
|
||||
return WasmEdge_Result_Success;
|
||||
|
||||
uint32_t line = WasmEdge_ValueGetI32(in[0]);
|
||||
uint32_t col = WasmEdge_ValueGetI32(in[1]);
|
||||
|
||||
// Pack (line, col) into a single uint32_t key.
|
||||
// Limits: line < 65536, col < 65536 — more than sufficient for hooks.
|
||||
uint32_t key = (line << 16) | (col & 0xFFFF);
|
||||
coverageMap()[hookCtx->result.hookHash].hits.insert(key);
|
||||
|
||||
return WasmEdge_Result_Success;
|
||||
}
|
||||
|
||||
/**
|
||||
* HookExecutor is effectively a two-part function:
|
||||
* The first part sets up the Hook Api inside the wasm import, ready for use
|
||||
@@ -609,22 +483,6 @@ public:
|
||||
#undef HOOK_WRAP_PARAMS
|
||||
#pragma pop_macro("HOOK_API_DEFINITION")
|
||||
|
||||
// Coverage callback: void __on_source_line(i32 line, i32 col)
|
||||
// Registered unconditionally — production hooks don't import it,
|
||||
// so it's harmless. Instrumented hooks call it at each DWARF
|
||||
// source location to record line:col coverage hits.
|
||||
{
|
||||
static WasmEdge_ValType paramsOSL[] = {
|
||||
WasmEdge_ValType_I32, WasmEdge_ValType_I32};
|
||||
static auto* ftOSL =
|
||||
WasmEdge_FunctionTypeCreate(paramsOSL, 2, nullptr, 0);
|
||||
auto* hfOSL = WasmEdge_FunctionInstanceCreate(
|
||||
ftOSL, hook::onSourceLine, (void*)(&ctx), 0);
|
||||
static auto nameOSL =
|
||||
WasmEdge_StringCreateByCString("__on_source_line");
|
||||
WasmEdge_ModuleInstanceAddFunction(importObj, nameOSL, hfOSL);
|
||||
}
|
||||
|
||||
WasmEdge_TableInstanceContext* hostTable =
|
||||
WasmEdge_TableInstanceCreate(tableType);
|
||||
WasmEdge_ModuleInstanceAddTable(importObj, tableName, hostTable);
|
||||
|
||||
@@ -1111,7 +1111,7 @@ DEFINE_HOOK_FUNCTION(
|
||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
if (!jh.trace())
|
||||
if (!j.trace())
|
||||
return 0ULL;
|
||||
|
||||
if (read_len > 128)
|
||||
@@ -1125,16 +1125,16 @@ DEFINE_HOOK_FUNCTION(
|
||||
|
||||
if (read_len > 0)
|
||||
{
|
||||
JLOG(jh.trace())
|
||||
<< "HookTrace[" << HC_ACC() << "]: "
|
||||
<< std::string_view((const char*)memory + read_ptr, read_len)
|
||||
<< ": " << number;
|
||||
j.trace() << "HookTrace[" << HC_ACC() << "]: "
|
||||
<< std::string_view(
|
||||
(const char*)memory + read_ptr, read_len)
|
||||
<< ": " << number;
|
||||
|
||||
return 0ULL;
|
||||
}
|
||||
}
|
||||
|
||||
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]: " << number;
|
||||
j.trace() << "HookTrace[" << HC_ACC() << "]: " << number;
|
||||
return 0ULL;
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
@@ -1154,7 +1154,7 @@ DEFINE_HOOK_FUNCTION(
|
||||
NOT_IN_BOUNDS(dread_ptr, dread_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
if (!jh.trace())
|
||||
if (!j.trace())
|
||||
return 0ULL;
|
||||
|
||||
if (mread_len > 128)
|
||||
@@ -1214,9 +1214,8 @@ DEFINE_HOOK_FUNCTION(
|
||||
|
||||
if (out_len > 0)
|
||||
{
|
||||
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]: "
|
||||
<< std::string_view(
|
||||
(const char*)output_storage, out_len);
|
||||
j.trace() << "HookTrace[" << HC_ACC() << "]: "
|
||||
<< std::string_view((const char*)output_storage, out_len);
|
||||
}
|
||||
|
||||
return 0ULL;
|
||||
@@ -3404,7 +3403,7 @@ DEFINE_HOOK_FUNCTION(
|
||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
if (!jh.trace())
|
||||
if (!j.trace())
|
||||
return 0ULL;
|
||||
|
||||
if (read_len > 128)
|
||||
@@ -3421,8 +3420,8 @@ DEFINE_HOOK_FUNCTION(
|
||||
|
||||
if (float1 == 0)
|
||||
{
|
||||
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]: " << messageKey
|
||||
<< ": Float 0*10^(0) <ZERO>";
|
||||
j.trace() << "HookTrace[" << HC_ACC() << "]: " << messageKey
|
||||
<< ": Float 0*10^(0) <ZERO>";
|
||||
return 0ULL;
|
||||
}
|
||||
|
||||
@@ -3433,14 +3432,14 @@ DEFINE_HOOK_FUNCTION(
|
||||
man.value() > maxMantissa || exp.value() < minExponent ||
|
||||
exp.value() > maxExponent)
|
||||
{
|
||||
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]: " << messageKey
|
||||
<< ": Float <INVALID>";
|
||||
j.trace() << "HookTrace[" << HC_ACC() << "]: " << messageKey
|
||||
<< ": Float <INVALID>";
|
||||
return 0ULL;
|
||||
}
|
||||
|
||||
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]:" << messageKey
|
||||
<< ": Float " << (neg ? "-" : "") << man.value() << "*10^("
|
||||
<< exp.value() << ")";
|
||||
j.trace() << "HookTrace[" << HC_ACC() << "]:" << messageKey << ": Float "
|
||||
<< (neg ? "-" : "") << man.value() << "*10^(" << exp.value()
|
||||
<< ")";
|
||||
return 0ULL;
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
|
||||
@@ -546,7 +546,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
}
|
||||
|
||||
auto result = validateGuards(
|
||||
hook,
|
||||
hook, // wasm to verify
|
||||
logger,
|
||||
hsacc,
|
||||
hook_api::getImportWhitelist(ctx.rules),
|
||||
|
||||
@@ -26,10 +26,17 @@
|
||||
#include <xrpld/rpc/GRPCHandlers.h>
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
#include <xrpld/rpc/detail/TransactionSign.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
#include <xrpl/protocol/JSONTxSignatures.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static NetworkOPs::FailHard
|
||||
@@ -83,7 +90,8 @@ doInject(RPC::JsonContext& context)
|
||||
}
|
||||
|
||||
// {
|
||||
// tx_blob: <string> XOR tx_json: <object>,
|
||||
// tx_blob: <string> XOR tx_json: <object>
|
||||
// XOR { tx: <json text>, signature: <hex> },
|
||||
// secret: <secret>
|
||||
// }
|
||||
Json::Value
|
||||
@@ -91,7 +99,18 @@ doSubmit(RPC::JsonContext& context)
|
||||
{
|
||||
context.loadType = Resource::feeMediumBurdenRPC;
|
||||
|
||||
if (!context.params.isMember(jss::tx_blob))
|
||||
bool const hasJsonTx = context.ledgerMaster.getCurrentLedger()->rules().enabled(featureJsonTx);
|
||||
|
||||
bool const isJsonTx = !context.params.isMember(jss::tx_blob) &&
|
||||
context.params.isMember(jss::tx) &&
|
||||
context.params.isMember(jss::sig);
|
||||
|
||||
if (isJsonTx && !hasJsonTx)
|
||||
return RPC::make_error(
|
||||
rpcNOT_SUPPORTED, "JsonTx is not enabled yet.");
|
||||
|
||||
|
||||
if (!context.params.isMember(jss::tx_blob) && !isJsonTx)
|
||||
{
|
||||
auto const failType = getFailHard(context);
|
||||
|
||||
@@ -119,18 +138,73 @@ doSubmit(RPC::JsonContext& context)
|
||||
|
||||
Json::Value jvResult;
|
||||
|
||||
auto ret = strUnHex(context.params[jss::tx_blob].asString());
|
||||
std::optional<Blob> ret;
|
||||
if (!isJsonTx)
|
||||
{
|
||||
ret = strUnHex(context.params[jss::tx_blob].asString());
|
||||
|
||||
if (!ret || !ret->size())
|
||||
return rpcError(rpcINVALID_PARAMS);
|
||||
|
||||
SerialIter sitTrans(makeSlice(*ret));
|
||||
if (!ret || !ret->size())
|
||||
return rpcError(rpcINVALID_PARAMS);
|
||||
}
|
||||
|
||||
std::shared_ptr<STTx const> stTx;
|
||||
|
||||
try
|
||||
{
|
||||
stTx = std::make_shared<STTx const>(std::ref(sitTrans));
|
||||
if (!isJsonTx)
|
||||
{
|
||||
SerialIter sitTrans(makeSlice(*ret));
|
||||
stTx = std::make_shared<STTx const>(std::ref(sitTrans));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string const raw = context.params[jss::tx].asString();
|
||||
auto const [san, diff] = sanitize_jsontx(raw);
|
||||
auto const sig = strUnHex(context.params[jss::sig].asString());
|
||||
if (!sig || sig->empty())
|
||||
throw std::runtime_error("JsonTx: bad signature");
|
||||
|
||||
Json::Value jv;
|
||||
if (Json::Reader r; !r.parse(san, jv))
|
||||
throw std::runtime_error("JsonTx: unparsable canonical form");
|
||||
|
||||
// The preimage carries the key but not the signature over itself.
|
||||
for (auto const& n :
|
||||
{sfTxnSignature.fieldName, sfSigners.fieldName})
|
||||
if (jv.isMember(n))
|
||||
throw std::runtime_error(
|
||||
"JsonTx: " + n + " must not appear in tx");
|
||||
if (!jv.isMember(sfSigningPubKey.fieldName))
|
||||
throw std::runtime_error("JsonTx: tx must carry SigningPubKey");
|
||||
|
||||
// Hand the parser the u64 rather than teaching STUInt64 a second
|
||||
// spelling; the ISO form only ever exists in the preimage.
|
||||
std::optional<std::uint64_t> ms;
|
||||
if (jv.isMember(sfTime.fieldName))
|
||||
{
|
||||
ms = jsontx_iso(jv[sfTime.fieldName].asString());
|
||||
jv.removeMember(sfTime.fieldName);
|
||||
}
|
||||
|
||||
STParsedJSONObject parsed("tx_json", jv);
|
||||
if (!parsed.object)
|
||||
throw std::runtime_error(
|
||||
parsed.error[jss::error_message].asString());
|
||||
if (ms)
|
||||
parsed.object->setFieldU64(sfTime, *ms);
|
||||
parsed.object->setFieldVL(sfTxnSignature, *sig);
|
||||
stTx = std::make_shared<STTx const>(std::move(*parsed.object));
|
||||
|
||||
// Round-trip the binary codec, then run the exact check a relaying
|
||||
// node will run, so this path cannot accept anything the network
|
||||
// would later reject.
|
||||
Serializer s;
|
||||
stTx->add(s);
|
||||
SerialIter si(s.slice());
|
||||
STTx const rt{si};
|
||||
if (jsontx_verify(rt, diff) != raw)
|
||||
throw std::runtime_error("JsonTx: does not round-trip");
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
@@ -141,7 +215,9 @@ doSubmit(RPC::JsonContext& context)
|
||||
}
|
||||
|
||||
{
|
||||
if (!context.app.checkSigs())
|
||||
// JsonTx signs the plaintext preimage rather than the binary one, so
|
||||
// the binary TxnSignature check is satisfied out of band above.
|
||||
if (!context.app.checkSigs() || isJsonTx)
|
||||
forceValidity(
|
||||
context.app.getHashRouter(),
|
||||
stTx->getTransactionID(),
|
||||
|
||||
Reference in New Issue
Block a user