Compare commits

..

20 Commits

Author SHA1 Message Date
Nicholas Dudfield
dd616d807f Merge remote-tracking branch 'origin/dev' into coverage-llm 2026-06-17 14:35:13 +07:00
Nicholas Dudfield
98eb3be7af Merge remote-tracking branch 'origin/dev' into coverage-llm
# Conflicts:
#	.codecov.yml
2026-06-04 10:31:58 +07:00
Nicholas Dudfield
273273d7a2 ci: relax patch coverage for PeerImp 2026-05-06 16:27:15 +07:00
Nicholas Dudfield
c149351ccf ci: bump apt retries to 5 to handle ppa.launchpadcontent.net flakes 2026-05-05 10:48:13 +07:00
Nicholas Dudfield
436a0d5540 fix(ci): scope conan_deps_cxxflags per Conan package pattern
prior shape (bare json list -> 'tools.build:cxxflags=[...]') leaked into
the consumer/rippled build via the conan-generated toolchain
(CMAKE_CXX_FLAGS_INIT). on macOS, where the build action doesn't
override CMAKE_CXX_FLAGS, this would silently apply the workaround flag
to rippled itself.

reshape the input to a json object keyed by Conan package pattern:
  {"grpc/*":["-Wno-..."]}

action emits package-pattern scoped lines:
  grpc/*:tools.build:cxxflags=["-Wno-..."]

flags only apply while building the matching dependency. consumer
toolchain stays clean. python validator rejects the consumer pattern
('&') and malformed shapes.
2026-05-05 10:13:59 +07:00
Nicholas Dudfield
3834ec5997 ci: undisable workflows, drop gcc-13 coverage row, keep clang-20 llvm-cov as sole codecov reporter
- restore the 8 .yml.disabled workflows back to .yml
- drop the gcc-13 gcov coverage matrix row; native clang-20 llvm-cov is
  now the only codecov reporter
- update minimal_matrix indices to match
- remove the TEMP narrowing block that filtered to just the llvm-cov row
- log line says '7 configs (build x6 + clang-20 llvm-cov coverage)' to
  reflect the new shape
2026-05-05 09:44:48 +07:00
Nicholas Dudfield
6b3fb5ea14 ci: dedupe conan_deps_cxxflags in macOS workflow row
prior turn-of-events created two identical conan_deps_cxxflags entries
in the same matrix row from overlapping restore commits. keep one.
2026-05-05 09:41:18 +07:00
Nicholas Dudfield
83a6d14f7a ci: restore matrix-row comments explaining clang-20 + grpc workaround 2026-05-05 09:40:16 +07:00
Nicholas Dudfield
10fbafe996 fix(ci): restore conan_deps_cxxflags on macOS workflow
reverts part of f8a30c528d - removing this line dropped the apple-clang
grpc workaround that the dependencies action used to apply
unconditionally before the matrix-driven refactor. the .disabled rename
is itself a TEMP measure being reverted before merge, so the field needs
to be in place when the macOS workflow comes back.
2026-05-05 09:36:29 +07:00
Nicholas Dudfield
0342badb5d Revert "chore(cov): drop ... .disabled file edit" (partial)
restore the conan_deps_cxxflags addition to xahau-ga-macos.yml.disabled.
prior commit framed it as dead code, but the .disabled rename is itself
a TEMP measure being reverted before merge - the field is needed for the
macOS workflow to keep building grpc 1.50.1 once it's re-enabled, since
the unconditional workaround was removed from the dependencies action.
2026-05-05 09:27:01 +07:00
Nicholas Dudfield
f8a30c528d chore(cov): drop dead BASE_DIRECTORY arg + revert .disabled file edit
- CodeCoverageLLVM.cmake: BASE_DIRECTORY was parsed but never used
  (no analog to gcovr's -r in the llvm-cov commands we emit). dead arg.
- xahau-ga-macos.yml.disabled: revert the conan_deps_cxxflags addition.
  the file is disabled so the edit was bit-rotting in unreachable code.
  whoever revives the macOS workflow can wire up the field then.
2026-05-05 09:25:30 +07:00
Nicholas Dudfield
fe162a99a9 fix(cov): tighten coverage_tool=llvm validation + macOS tool discovery
two bugs caught in review:

- RippledSettings.cmake: the clang-only guard for coverage_tool=llvm ran
  before coverage_test could auto-enable coverage. so
  '-Dcoverage_tool=llvm -Dcoverage_test=Foo' on a gcc build slipped past
  the guard and produced an instrumentation/tool mismatch. move the
  guard after the auto-enable block.

- CodeCoverageLLVM.cmake: _find_llvm_cov_tools unconditionally
  preferred xcrun's tools on APPLE, which on a homebrew clang-N build
  would pair the user's clang with xcode's llvm-cov - exactly the
  version mismatch the helper is trying to avoid. gate the xcrun branch
  on CMAKE_CXX_COMPILER_ID == AppleClang.
2026-05-05 09:24:20 +07:00
Nicholas Dudfield
132bcf6e57 Merge remote-tracking branch 'origin/dev' into coverage-llm 2026-05-05 09:05:34 +07:00
Nicholas Dudfield
5b4a6703ea ci(deps): replace hardcoded grpc workaround with matrix-driven conan_deps_cxxflags
new dependencies action input `conan_deps_cxxflags` (json list, default
'[]') drives `tools.build:cxxflags` in the conan profile. clearly named
to indicate the flags only apply to conan dependency builds, not the
rippled build itself.

removes the per-os/per-compiler hardcoded workaround (linux clang
conditional + unconditional macOS block) that used to set the same flag
in two places kept-in-sync by hand.

call sites:
- xahau-ga-nix.yml: clang-20 coverage row gets the
  -Wno-missing-template-arg-list-after-template-kw workaround for grpc 1.50.1
- xahau-ga-macos.yml.disabled: same flag plumbed through (preserves
  prior behaviour when re-enabled)

drop the workaround entries when grpc is bumped past the fix.
2026-05-05 08:57:30 +07:00
Nicholas Dudfield
51cd3ddf25 fix(cov): use absolute binary path in llvm-cov run command
bare 'rippled' wasn't on PATH and the build dir isn't '.', so the
profile-collection step failed with 'No such file or directory'.
splice the resolved absolute path back into Cov_EXECUTABLE before
the run command consumes it.
2026-04-30 15:53:45 +07:00
Nicholas Dudfield
cf244334d3 ci(temp): disable all workflows except Nix GA on this branch
renames every non-nix workflow to .yml.disabled so they stop firing on
PR pushes while we iterate on the llvm-cov coverage row. MUST be
reverted before merging to dev.
2026-04-30 15:25:07 +07:00
Nicholas Dudfield
43a37afb51 ci(deps): apply Wno-missing-template-arg-list workaround to Linux clang
grpc 1.50.1 hits -Werror=missing-template-arg-list-after-template-kw on
clang-19+. macOS clang already had this workaround in the conan profile;
mirror it to the Linux branch, gated on compiler==clang.
2026-04-30 15:20:03 +07:00
Nicholas Dudfield
8d609f9cf3 ci: actually narrow matrix to just the llvm-cov row
prior guard checked base_ref against ['dev','candidate','release'] which
excluded the very PR we're iterating on. drop the guard - this branch
is for iteration only and must be reverted before merging anyway.
2026-04-30 15:15:46 +07:00
Nicholas Dudfield
94a62f5572 ci: drop coverage-llm from push triggers, PR trigger covers it 2026-04-30 15:13:47 +07:00
Nicholas Dudfield
7b8d671f52 ci: add -Dcoverage_tool=gcov|llvm option, wire native llvm-cov in matrix
introduces native llvm source-based coverage as an alternative to the
existing gcov + gcovr pipeline. driven by a new -Dcoverage_tool cache
variable (default 'gcov' for backwards compat; 'llvm' enables
-fprofile-instr-generate / -fcoverage-mapping + llvm-profdata + llvm-cov).

cmake:
- RippledSettings.cmake: add coverage_tool with validation
- RippledInterface.cmake: split coverage compile/link flags by tool
- RippledCov.cmake: dispatch to the new helper when tool=llvm
- CodeCoverageLLVM.cmake (new): setup_target_for_coverage_llvm() driving
  profraw -> profdata -> export with format-aware output (lcov/json/txt/html)

ci:
- new clang-20 llvm-cov coverage matrix row (apt.llvm.org bootstrap for
  clang >= 19 since 24.04 default repos cap at clang-18)
- conditional install of llvm-N vs gcovr based on coverage_tool
- artifact + codecov upload generalised to coverage.lcov | coverage.xml
- coverage cmake-args switched to *_FLAGS_DEBUG so the build action's
  stdlib flag isn't clobbered

temp (revert before merging to dev):
- matrix narrowed to just the llvm-cov row on non-main refs so we can
  iterate without burning runners
- 'coverage-llm' added to push trigger for direct-push CI runs
2026-04-30 15:07:52 +07:00
17 changed files with 322 additions and 698 deletions

View File

@@ -19,6 +19,15 @@ coverage:
default:
target: auto
threshold: 2%
paths:
# PeerImp is historically hard to exercise in the current unit-test
# harness. Keep this list narrow; new testable code should remain
# covered by the default patch gate.
- "!src/xrpld/overlay/detail/PeerImp.cpp"
historically-untested:
target: 0%
paths:
- "src/xrpld/overlay/detail/PeerImp.cpp"
changes: false
github_checks:

View File

@@ -50,6 +50,10 @@ inputs:
options:
- libstdcxx
- libcxx
conan_deps_cxxflags:
description: 'Extra cxxflags applied to Conan dependency package builds only (NOT the rippled build). JSON object keyed by Conan package pattern, e.g. {"grpc/*":["-Wno-foo"]}. Maps to <pattern>:tools.build:cxxflags.'
required: false
default: '{}'
outputs:
cache-hit:
@@ -81,6 +85,8 @@ runs:
- name: Configure Conan
shell: bash
env:
CONAN_DEPS_CXXFLAGS: ${{ inputs.conan_deps_cxxflags }}
run: |
# Create the default profile directory if it doesn't exist
mkdir -p ~/.conan2/profiles
@@ -105,7 +111,14 @@ runs:
os=${{ inputs.os }}
EOF
# Add buildenv and conf sections for Linux (not needed for macOS)
# [buildenv] + [conf] sections.
# Linux pins compiler executables; macOS uses the system toolchain.
# conan_deps_cxxflags (matrix-driven) optionally adds package-pattern
# scoped tools.build:cxxflags for Conan dependency builds only - typically
# grpc workarounds for newer clang's stricter diagnostics. Because these
# are profile-pattern scoped (e.g. grpc/*:...), they do NOT affect the
# consumer/rippled toolchain generated for the main build.
NEED_CONF=0
if [ "${{ inputs.os }}" = "Linux" ] && [ -n "${{ inputs.cc }}" ]; then
cat >> ~/.conan2/profiles/default <<EOF
@@ -116,16 +129,38 @@ runs:
[conf]
tools.build:compiler_executables={"c": "/usr/bin/${{ inputs.cc }}", "cpp": "/usr/bin/${{ inputs.cxx }}"}
EOF
NEED_CONF=1
fi
# Add macOS-specific conf if needed
if [ "${{ inputs.os }}" = "Macos" ]; then
cat >> ~/.conan2/profiles/default <<EOF
if [ -n "${CONAN_DEPS_CXXFLAGS}" ] && [ "${CONAN_DEPS_CXXFLAGS}" != "{}" ]; then
CONAN_DEPS_CXXFLAGS_LINES="$(python3 - <<'PY'
import json
import os
import sys
[conf]
# Workaround for gRPC with newer Apple Clang
tools.build:cxxflags=["-Wno-missing-template-arg-list-after-template-kw"]
EOF
raw = os.environ["CONAN_DEPS_CXXFLAGS"]
data = json.loads(raw)
if not isinstance(data, dict):
sys.exit("conan_deps_cxxflags must be a JSON object like {\"grpc/*\": [\"-Wno-...\"]}")
for pattern, flags in data.items():
if not isinstance(pattern, str) or not pattern:
sys.exit("conan_deps_cxxflags keys must be non-empty Conan package patterns")
if pattern == "&":
sys.exit("conan_deps_cxxflags must target dependency package patterns, not the consumer (&)")
if not isinstance(flags, list) or not all(isinstance(flag, str) for flag in flags):
sys.exit(f"{pattern}: cxxflags must be a JSON string list")
if flags:
print(f"{pattern}:tools.build:cxxflags={json.dumps(flags, separators=(',', ':'))}")
PY
)"
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ] && [ "$NEED_CONF" = "0" ]; then
echo "" >> ~/.conan2/profiles/default
echo "[conf]" >> ~/.conan2/profiles/default
fi
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ]; then
printf '%s\n' "${CONAN_DEPS_CXXFLAGS_LINES}" >> ~/.conan2/profiles/default
fi
fi
# Display profile for verification

View File

@@ -107,6 +107,9 @@ jobs:
compiler: apple-clang
compiler_version: ${{ steps.detect-compiler.outputs.compiler_version }}
stdlib: libcxx
# grpc 1.50.1 trips clang-19+ -Werror=missing-template-arg-list-after-template-kw
# on Apple Clang. Drop when grpc is bumped past the fix.
conan_deps_cxxflags: '{"grpc/*":["-Wno-missing-template-arg-list-after-template-kw"]}'
- name: Build
uses: ./.github/actions/xahau-ga-build

View File

@@ -72,15 +72,26 @@ jobs:
"job_type": "build"
},
{
"compiler_id": "gcc-13-libstdcxx",
"compiler": "gcc",
"cc": "gcc-13",
"cxx": "g++-13",
"gcov": "gcov-13",
"compiler_version": 13,
"stdlib": "default",
# Latest stable Clang for the most accurate source-based
# coverage mapping (newer language features, fewer bugs in
# llvm-cov region inference). Pulled from apt.llvm.org since
# Ubuntu 24.04 default repos cap at clang-18.
"compiler_id": "clang-20-libcxx",
"compiler": "clang",
"cc": "clang-20",
"cxx": "clang++-20",
"compiler_version": 20,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "coverage"
"job_type": "coverage",
"coverage_tool": "llvm",
"coverage_format": "lcov",
# grpc 1.50.1 uses `Foo::template Bar(...)` without an
# angle-bracket arg list; clang-19+ promoted that to
# -Werror. Drop when grpc is bumped past the fix.
"conan_deps_cxxflags": {
"grpc/*": ["-Wno-missing-template-arg-list-after-template-kw"]
}
},
{
"compiler_id": "clang-14-libstdcxx-gcc11",
@@ -131,7 +142,7 @@ jobs:
# Minimal matrix for PRs and feature branches
minimal_matrix = [
full_matrix[1], # gcc-13 (middle-ground gcc)
full_matrix[2], # gcc-13 coverage
full_matrix[2], # clang-20 llvm-cov coverage
full_matrix[3] # clang-14 (mature, stable clang)
]
@@ -207,9 +218,9 @@ jobs:
# Select the appropriate matrix
if use_full:
if force_full:
print(f"Using FULL matrix (7 configs) - forced by [ci-nix-full-matrix] tag")
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) - targeting main branch")
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")
@@ -257,9 +268,25 @@ jobs:
- name: Install build dependencies
run: |
# Bump apt's default 3 retries; papers over short upstream blips
# like the recurring ppa.launchpadcontent.net outages.
echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries
apt-get update
apt-get install -y software-properties-common
add-apt-repository ppa:ubuntu-toolchain-r/test -y
# apt.llvm.org for Clang versions newer than what Ubuntu 24.04 ships
# (24.04 default repos cap at clang-18). The bootstrap script adds
# the LLVM apt source for the requested version and runs apt-get update.
if [ "${{ matrix.compiler }}" = "clang" ] && [ "${{ matrix.compiler_version }}" -ge 19 ]; then
apt-get install -y wget gnupg lsb-release
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
# `all` installs clang + libllvm + lldb + lld + the llvm-N package
# (which provides llvm-profdata-N / llvm-cov-N for coverage runs).
/tmp/llvm.sh ${{ matrix.compiler_version }} all
fi
apt-get update
apt-get install -y git python3 python-is-python3 pipx
pipx ensurepath
@@ -332,10 +359,16 @@ jobs:
pipx install "conan>=2.0,<3"
echo "$HOME/.local/bin" >> $GITHUB_PATH
# Install gcovr for coverage jobs
# Install coverage tooling
if [ "${{ matrix.job_type }}" = "coverage" ]; then
pipx install "gcovr>=7,<9"
apt-get install -y curl lcov
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
# Native LLVM source-based coverage: llvm-profdata + llvm-cov.
# The clang-N package doesn't pull these in; the llvm-N package does.
apt-get install -y "llvm-${{ matrix.compiler_version }}"
else
pipx install "gcovr>=7,<9"
fi
fi
- name: Check environment
@@ -348,10 +381,15 @@ jobs:
which ${{ matrix.cxx }} && ${{ matrix.cxx }} --version || echo "${{ matrix.cxx }} not found"
which ccache && ccache --version || echo "ccache not found"
# Check gcovr for coverage jobs
# Check coverage tooling
if [ "${{ matrix.job_type }}" = "coverage" ]; then
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
which "llvm-profdata-${{ matrix.compiler_version }}" && "llvm-profdata-${{ matrix.compiler_version }}" --version || echo "llvm-profdata not found"
which "llvm-cov-${{ matrix.compiler_version }}" && "llvm-cov-${{ matrix.compiler_version }}" --version || echo "llvm-cov not found"
else
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
fi
fi
echo "---- Full Environment ----"
@@ -378,6 +416,7 @@ jobs:
cc: ${{ matrix.cc }}
cxx: ${{ matrix.cxx }}
stdlib: ${{ matrix.stdlib }}
conan_deps_cxxflags: ${{ matrix.conan_deps_cxxflags && toJson(matrix.conan_deps_cxxflags) || '{}' }}
gha_cache_enabled: 'false' # Disable caching for self hosted runner
- name: Build
@@ -410,8 +449,9 @@ jobs:
cache_version: ${{ env.CACHE_VERSION }}
main_branch: ${{ env.MAIN_BRANCH_NAME }}
stdlib: ${{ matrix.stdlib }}
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
cmake-args: '-Dcoverage=ON -Dcoverage_format=xml -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS="-O0" -DCMAKE_C_FLAGS="-O0"'
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness.
# Use *_FLAGS_DEBUG so the build action's stdlib flag (e.g. -stdlib=libc++) in CMAKE_CXX_FLAGS isn't clobbered.
cmake-args: '-Dcoverage=ON -Dcoverage_tool=${{ matrix.coverage_tool }} -Dcoverage_format=${{ matrix.coverage_format }} -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS_DEBUG="-g -O0" -DCMAKE_C_FLAGS_DEBUG="-g -O0"'
cmake-target: 'coverage'
ccache_max_size: '100G'
@@ -443,22 +483,26 @@ jobs:
- name: Move coverage report
if: matrix.job_type == 'coverage'
shell: bash
env:
COVERAGE_FILE: ${{ matrix.coverage_tool == 'llvm' && 'coverage.lcov' || 'coverage.xml' }}
run: |
mv "${{ env.build_dir }}/coverage.xml" ./
mv "${{ env.build_dir }}/${COVERAGE_FILE}" ./
echo "COVERAGE_FILE=${COVERAGE_FILE}" >> "$GITHUB_ENV"
- name: Archive coverage report
if: matrix.job_type == 'coverage'
uses: actions/upload-artifact@v4
with:
name: coverage.xml
path: coverage.xml
name: ${{ env.COVERAGE_FILE }}-${{ matrix.compiler_id }}
path: ${{ env.COVERAGE_FILE }}
retention-days: 30
- name: Upload coverage report
if: matrix.job_type == 'coverage'
uses: codecov/codecov-action@v5
with:
files: coverage.xml
files: ${{ env.COVERAGE_FILE }}
flags: ${{ matrix.coverage_tool }}
fail_ci_if_error: true
disable_search: true
verbose: true

View File

@@ -124,9 +124,6 @@ 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)

View File

@@ -95,16 +95,8 @@ if [[ "$4" == "" ]]; then
echo "Non GH, local building, no Action runner magic"
else
# GH Action, runner
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
echo "building on the release branch... placing it in builds/candidate"
mkdir /data/builds/candidate
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
else
echo "building non-release branch, placing it in builds root"
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
fi
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
echo "Published build to: http://build.xahau.tech/"
echo $(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
fi

View File

@@ -0,0 +1,156 @@
#[===================================================================[
Native LLVM source-based code coverage helper.
Drives the -fprofile-instr-generate / -fcoverage-mapping pipeline:
1. Run instrumented binary with LLVM_PROFILE_FILE=...%m-%p.profraw
2. llvm-profdata merge -sparse -> coverage.profdata
3. llvm-cov export/show/report -> final report
Output filename per coverage_format:
lcov -> coverage.lcov
json -> coverage.json
txt | text -> coverage.txt
html | html-details -> <NAME>/index.html
#]===================================================================]
include(CMakeParseArguments)
# Locate llvm-profdata / llvm-cov, preferring versioned variants matching the
# Clang we're building with so we don't accidentally pair clang-20 with
# llvm-cov-14 (profile format mismatch -> hard failure).
function(_find_llvm_cov_tools)
if(LLVM_PROFDATA_PATH AND LLVM_COV_PATH)
return()
endif()
string(REGEX MATCH "^[0-9]+" _major "${CMAKE_CXX_COMPILER_VERSION}")
set(_pd_names llvm-profdata)
set(_cov_names llvm-cov)
if(_major)
list(PREPEND _pd_names "llvm-profdata-${_major}")
list(PREPEND _cov_names "llvm-cov-${_major}")
endif()
# Only delegate to xcrun when the *compiler* is AppleClang. On macOS with
# Homebrew/system clang-N, xcrun would resolve to Xcode's llvm tools which
# could be a different version - exactly the mismatch we want to avoid.
if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
execute_process(COMMAND xcrun -f llvm-profdata
OUTPUT_VARIABLE _pd_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _pd_rc)
if(_pd_rc EQUAL 0 AND _pd_xcrun)
set(LLVM_PROFDATA_PATH "${_pd_xcrun}" CACHE FILEPATH "llvm-profdata" FORCE)
endif()
execute_process(COMMAND xcrun -f llvm-cov
OUTPUT_VARIABLE _cov_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _cov_rc)
if(_cov_rc EQUAL 0 AND _cov_xcrun)
set(LLVM_COV_PATH "${_cov_xcrun}" CACHE FILEPATH "llvm-cov" FORCE)
endif()
endif()
if(NOT LLVM_PROFDATA_PATH)
find_program(LLVM_PROFDATA_PATH NAMES ${_pd_names})
endif()
if(NOT LLVM_COV_PATH)
find_program(LLVM_COV_PATH NAMES ${_cov_names})
endif()
endfunction()
function(setup_target_for_coverage_llvm)
set(oneValueArgs NAME FORMAT)
set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
cmake_parse_arguments(Cov "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
_find_llvm_cov_tools()
if(NOT LLVM_PROFDATA_PATH)
message(FATAL_ERROR "llvm-profdata not found (needed for coverage_tool=llvm)")
endif()
if(NOT LLVM_COV_PATH)
message(FATAL_ERROR "llvm-cov not found (needed for coverage_tool=llvm)")
endif()
if(NOT Cov_FORMAT)
set(Cov_FORMAT lcov)
endif()
set(_profraw_dir "${PROJECT_BINARY_DIR}/${Cov_NAME}-profraw")
set(_profdata "${PROJECT_BINARY_DIR}/${Cov_NAME}.profdata")
# Resolve binary path: accept either an absolute path or a bare target name
# (resolved against PROJECT_BINARY_DIR). Splice the resolved path back into
# Cov_EXECUTABLE so the run command invokes it via absolute path - bare
# names aren't on PATH and the build dir isn't `.` either.
list(GET Cov_EXECUTABLE 0 _exec_name)
if(IS_ABSOLUTE "${_exec_name}")
set(_binary "${_exec_name}")
else()
set(_binary "${PROJECT_BINARY_DIR}/${_exec_name}")
list(REMOVE_AT Cov_EXECUTABLE 0)
list(PREPEND Cov_EXECUTABLE "${_binary}")
endif()
# llvm-cov takes a single -ignore-filename-regex; OR our excludes together.
set(_ignore_regex "")
foreach(EXC IN LISTS Cov_EXCLUDE)
if(_ignore_regex)
string(APPEND _ignore_regex "|")
endif()
string(APPEND _ignore_regex "${EXC}")
endforeach()
set(_filter "")
if(_ignore_regex)
set(_filter "-ignore-filename-regex='${_ignore_regex}'")
endif()
# Pick llvm-cov subcommand + output file for the requested format. Each
# branch builds a single shell command string that we'll hand to bash -c.
if(Cov_FORMAT STREQUAL "lcov")
set(_output "${PROJECT_BINARY_DIR}/coverage.lcov")
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=lcov ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "json")
set(_output "${PROJECT_BINARY_DIR}/coverage.json")
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=text ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "txt" OR Cov_FORMAT STREQUAL "text")
set(_output "${PROJECT_BINARY_DIR}/coverage.txt")
set(_report_sh "${LLVM_COV_PATH} report -instr-profile='${_profdata}' ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "html" OR Cov_FORMAT STREQUAL "html-details")
set(_output "${PROJECT_BINARY_DIR}/${Cov_NAME}/index.html")
set(_report_sh "${LLVM_COV_PATH} show -instr-profile='${_profdata}' -format=html -output-dir='${PROJECT_BINARY_DIR}/${Cov_NAME}' ${_filter} '${_binary}'")
else()
message(FATAL_ERROR "coverage_tool=llvm: unsupported coverage_format '${Cov_FORMAT}' (use lcov|json|txt|html)")
endif()
set(_merge_sh "${LLVM_PROFDATA_PATH} merge -sparse -o '${_profdata}' '${_profraw_dir}'/*.profraw")
if(CODE_COVERAGE_VERBOSE)
message(STATUS "[coverage:llvm] binary: ${_binary}")
message(STATUS "[coverage:llvm] profraw: ${_profraw_dir}")
message(STATUS "[coverage:llvm] profdata: ${_profdata}")
message(STATUS "[coverage:llvm] format: ${Cov_FORMAT}")
message(STATUS "[coverage:llvm] output: ${_output}")
if(_ignore_regex)
message(STATUS "[coverage:llvm] ignore: ${_ignore_regex}")
endif()
message(STATUS "[coverage:llvm] merge: ${_merge_sh}")
message(STATUS "[coverage:llvm] report: ${_report_sh}")
endif()
# %m: hash of the binary, %p: pid. Wipe the dir up front so stale profraw
# files can't leak into a fresh merge.
add_custom_target(${Cov_NAME}
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_profraw_dir}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_profraw_dir}"
COMMAND ${CMAKE_COMMAND} -E env
"LLVM_PROFILE_FILE=${_profraw_dir}/rippled-%m-%p.profraw"
${Cov_EXECUTABLE} ${Cov_EXECUTABLE_ARGS}
COMMAND bash -c "${_merge_sh}"
COMMAND bash -c "${_report_sh}"
BYPRODUCTS ${_output}
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
DEPENDS ${Cov_DEPENDENCIES}
VERBATIM
COMMENT "Running llvm-cov (${Cov_FORMAT}) -> ${_output}"
)
endfunction()

View File

@@ -11,6 +11,21 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
return()
endif()
if(coverage_tool STREQUAL "llvm")
include(CodeCoverageLLVM)
setup_target_for_coverage_llvm(
NAME coverage
FORMAT ${coverage_format}
EXECUTABLE rippled
EXECUTABLE_ARGS --unittest$<$<BOOL:${coverage_test}>:=${coverage_test}> --unittest-jobs ${coverage_test_parallelism} --quiet --unittest-log
EXCLUDE "src/test" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
DEPENDENCIES rippled
)
return()
endif()
# coverage_tool == "gcov" (default): existing gcovr-driven pipeline.
include(CodeCoverage)
# The instructions for these commands come from the `CodeCoverage` module,

View File

@@ -28,15 +28,17 @@ target_compile_options (opts
$<$<AND:$<BOOL:${is_gcc}>,$<COMPILE_LANGUAGE:CXX>>:-Wsuggest-override>
$<$<BOOL:${is_gcc}>:-Wno-maybe-uninitialized>
$<$<BOOL:${perf}>:-fno-omit-frame-pointer>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
target_link_libraries (opts
INTERFACE
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)

View File

@@ -29,13 +29,25 @@ if(is_gcc OR is_clang)
"Unit tests parallelism for the purpose of coverage report.")
set(coverage_format "html-details" CACHE STRING
"Output format of the coverage report.")
set(coverage_tool "gcov" CACHE STRING
"Coverage instrumentation tool: 'gcov' (default, gcc/clang via --coverage + gcovr) or 'llvm' (clang only, native source-based coverage via -fprofile-instr-generate).")
set_property(CACHE coverage_tool PROPERTY STRINGS "gcov" "llvm")
if(NOT coverage_tool MATCHES "^(gcov|llvm)$")
message(FATAL_ERROR "coverage_tool must be 'gcov' or 'llvm', got '${coverage_tool}'")
endif()
set(coverage_extra_args "" CACHE STRING
"Additional arguments to pass to gcovr.")
"Additional arguments to pass to gcovr (gcov tool only).")
set(coverage_test "" CACHE STRING
"On gcc & clang, the specific unit test(s) to run for coverage. Default is all tests.")
if(coverage_test AND NOT coverage)
set(coverage ON CACHE BOOL "gcc/clang only" FORCE)
endif()
# Validate after coverage_test may have flipped coverage on, otherwise
# `-Dcoverage_tool=llvm -Dcoverage_test=Foo` on gcc would silently slip
# past the Clang guard and produce a broken instrumentation combo.
if(coverage AND coverage_tool STREQUAL "llvm" AND NOT is_clang)
message(FATAL_ERROR "coverage_tool=llvm requires Clang (got ${CMAKE_CXX_COMPILER_ID})")
endif()
option(wextra "compile with extra gcc/clang warnings enabled" ON)
else()
set(profile OFF CACHE BOOL "gcc/clang only" FORCE)

View File

@@ -35,7 +35,6 @@ class Xrpl(ConanFile):
'soci/4.0.3@xahaud/stable',
'xxhash/0.8.2',
'zlib/1.3.1',
'fmt/12.1.0',
]
tool_requires = [
@@ -192,7 +191,6 @@ class Xrpl(ConanFile):
'sqlite3::sqlite',
'xxhash::xxhash',
'zlib::zlib',
'fmt::fmt',
]
if self.options.rocksdb:
libxrpl.requires.append('rocksdb::librocksdb')

View File

@@ -1,557 +0,0 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -34,7 +34,6 @@
// 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)

View File

@@ -153,7 +153,6 @@ 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)
@@ -294,7 +293,6 @@ 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)

View File

@@ -630,7 +630,6 @@ 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

View File

@@ -49,8 +49,6 @@ TxFormats::TxFormats()
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfHookName, soeOPTIONAL},
{sfTime, soeOPTIONAL},
{sfJsonTxDelta, soeOPTIONAL},
};
#pragma push_macro("UNWRAP")

View File

@@ -26,17 +26,10 @@
#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
@@ -90,8 +83,7 @@ doInject(RPC::JsonContext& context)
}
// {
// tx_blob: <string> XOR tx_json: <object>
// XOR { tx: <json text>, signature: <hex> },
// tx_blob: <string> XOR tx_json: <object>,
// secret: <secret>
// }
Json::Value
@@ -99,18 +91,7 @@ doSubmit(RPC::JsonContext& context)
{
context.loadType = Resource::feeMediumBurdenRPC;
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)
if (!context.params.isMember(jss::tx_blob))
{
auto const failType = getFailHard(context);
@@ -138,73 +119,18 @@ doSubmit(RPC::JsonContext& context)
Json::Value jvResult;
std::optional<Blob> ret;
if (!isJsonTx)
{
ret = strUnHex(context.params[jss::tx_blob].asString());
auto ret = strUnHex(context.params[jss::tx_blob].asString());
if (!ret || !ret->size())
return rpcError(rpcINVALID_PARAMS);
}
if (!ret || !ret->size())
return rpcError(rpcINVALID_PARAMS);
SerialIter sitTrans(makeSlice(*ret));
std::shared_ptr<STTx const> stTx;
try
{
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");
}
stTx = std::make_shared<STTx const>(std::ref(sitTrans));
}
catch (std::exception& e)
{
@@ -215,9 +141,7 @@ doSubmit(RPC::JsonContext& context)
}
{
// 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)
if (!context.app.checkSigs())
forceValidity(
context.app.getHashRouter(),
stTx->getTransactionID(),