Compare commits

..

5 Commits

Author SHA1 Message Date
Nicholas Dudfield
1e6cda4d64 fix: add fatal log on amendment-blocked shutdown 2026-06-19 09:35:39 +07:00
Nicholas Dudfield
c146f15247 fix: rethrow runtime_error if not amendment blocked 2026-06-19 09:35:39 +07:00
Nicholas Dudfield
fb5081d1f4 fix: narrow catch to std::runtime_error in switchLastClosedLedger 2026-06-19 09:35:39 +07:00
Nicholas Dudfield
d4bec012a2 fix: skip signalStop in standalone mode for test compatibility 2026-06-19 09:35:39 +07:00
Nicholas Dudfield
f7187ba94f fix: fail fast when amendment blocked instead of zombie state
- signalStop() for graceful shutdown when unsupported amendment activates
- early shutdown ~1 minute before expected activation to avoid race
- try/catch in switchLastClosedLedger to survive unknown field crashes
  during shutdown window
- show amendment warning to all RPC users, not just admin

Fixes: #706
2026-06-19 09:35:39 +07:00
209 changed files with 410 additions and 34331 deletions

View File

@@ -40,7 +40,7 @@ jobs:
run: |
# Download install.sh
curl -o /tmp/wasienv-install.sh https://raw.githubusercontent.com/wasienv/wasienv/master/install.sh
# Replace /bin to /local/bin
sed -i 's|/bin|/local/bin|g' /tmp/wasienv-install.sh

View File

@@ -1,127 +0,0 @@
name: Formal Verification (Lean)
on:
push:
branches: ["feature-export-rng-lean"]
pull_request:
branches: ["**"]
types: [opened, synchronize, reopened]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lean-consensus:
name: Lean/C++ drift checks
runs-on: [self-hosted, macOS]
env:
BUILD_DIR: .build-formal
CMAKE_BUILD_DIR: .build-formal-cmake
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Add Homebrew to PATH
run: |
echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
echo "/opt/homebrew/sbin" >> "$GITHUB_PATH"
- name: Install core tools
run: |
brew install coreutils
echo "Num proc: $(nproc)"
- name: Setup toolchain (mise)
uses: jdx/mise-action@v3.6.1
with:
cache: false
install: true
mise_toml: |
[tools]
cmake = "3.25.3"
python = "3.12"
pipx = "latest"
conan = "2"
ninja = "latest"
- name: Install tools via mise
run: |
mise install
mise reshim
echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH"
- name: Install Lean toolchain
run: |
toolchain="$(cat formal_verification/lean-toolchain)"
curl -sSfL https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh \
| sh -s -- -y --default-toolchain "$toolchain"
echo "$HOME/.elan/bin" >> "$GITHUB_PATH"
"$HOME/.elan/bin/lake" --version
"$HOME/.elan/bin/lean" --version
- name: Build Lean proofs
run: |
cd formal_verification
"$HOME/.elan/bin/lake" build XahauConsensus:static
- name: Detect compiler version
id: detect-compiler
run: |
compiler_version=$(clang --version | grep -oE 'version [0-9]+' | grep -oE '[0-9]+')
echo "compiler_version=${compiler_version}" >> "$GITHUB_OUTPUT"
echo "Detected Apple Clang version: ${compiler_version}"
- name: Configure Conan profile
run: |
mkdir -p ~/.conan2/profiles
cat > ~/.conan2/profiles/default <<EOF
[settings]
arch=armv8
build_type=Debug
compiler=apple-clang
compiler.cppstd=20
compiler.libcxx=libc++
compiler.version=${{ steps.detect-compiler.outputs.compiler_version }}
os=Macos
[conf]
tools.build:cxxflags=["-Wno-missing-template-arg-list-after-template-kw"]
EOF
conan profile show
- name: Export custom Conan recipes
run: |
conan export external/snappy --version 1.1.10 --user xahaud --channel stable
conan export external/soci --version 4.0.3 --user xahaud --channel stable
conan export external/wasmedge --version 0.11.2 --user xahaud --channel stable
- name: Install Conan dependencies
env:
CONAN_REQUEST_TIMEOUT: 180
run: |
conan install . \
--output-folder "$BUILD_DIR" \
--build missing \
--settings build_type=Debug \
-o '&:tests=True' \
-o '&:xrpld=True' \
-o '&:formal_verification=True'
- name: Configure formal build
run: |
cmake -S . -B "$CMAKE_BUILD_DIR" -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$PWD/$BUILD_DIR/build/generators/conan_toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Debug \
-Dtests=ON \
-Dxrpld=ON \
-Dformal_verification=ON
- name: Build formal-enabled rippled
run: |
cmake --build "$CMAKE_BUILD_DIR" --target rippled --parallel "$(nproc)"
- name: Run Lean/C++ drift checks
run: |
"$CMAKE_BUILD_DIR/rippled" --unittest=LeanConsensus --unittest-log

7
.gitignore vendored
View File

@@ -127,12 +127,5 @@ bld.rippled/
generated
.vscode
# AI docs (local working documents)
.ai-docs/
# Local formal-methods workspace; kept as a separate repository and optionally
# symlinked here for navigation.
formal/lean/xahau_consensus
# Suggested in-tree build directory
/.build/

4
.testnet/.gitignore vendored
View File

@@ -1,4 +0,0 @@
output/
__pycache__/
scenarios/odd-cases/
scenarios/suite-experiments.yml

View File

@@ -1,29 +0,0 @@
"""Scenario: ConsensusEntropy amendment crashes non-supporting node.
Votes ConsensusEntropy accept on all nodes except n4, then waits for n4
to crash as the amendment activates without its support.
x-testnet run --scenario-script consensus_entropy_crash.py
"""
from helpers import CONSENSUS_ENTROPY_FEATURE
async def scenario(ctx, log):
await ctx.wait_for_ledger_close()
ctx.feature(CONSENSUS_ENTROPY_FEATURE, vetoed=False, exclude_nodes=[4])
log("Waiting for ConsensusEntropy to be voted for...")
await ctx.wait_for_feature(
CONSENSUS_ENTROPY_FEATURE,
check=lambda s: not s.get("vetoed"),
exclude_nodes=[4],
timeout=60,
)
log("Waiting for n4 to crash...")
op = await ctx.wait_for_nodes_down(nodes=[4], timeout=600)
ctx.assert_log("unsupported amendments activated", since=op.started, nodes=[4])
ctx.assert_exit_status(0, nodes=[4])
log("PASS: n4 shut down due to unsupported amendment")

View File

@@ -1,52 +0,0 @@
""":descr: entropy stays valid under transaction load"""
from __future__ import annotations
from helpers import require_entropy, get_entropy_tx, assert_valid_entropy
variants = [
{"label": "light", "min_txns": 5, "max_txns": 10},
{"label": "heavy", "min_txns": 50, "max_txns": 60},
{"label": "super_heavy", "min_txns": 90, "max_txns": 120},
]
async def scenario(ctx, log, *, min_txns=5, max_txns=10, **_):
await require_entropy(ctx, log)
gen = ctx.txn_generator(min_txns=min_txns, max_txns=max_txns)
await gen.start()
await gen.wait_until_ready()
log(f"Transaction generator ready ({min_txns}-{max_txns} txns/ledger)")
# Wait for pipeline warmup + a few txn-bearing ledgers.
await ctx.wait_for_ledgers(3, node_id=0, timeout=60)
start_seq = ctx.validated_ledger_index(0)
await ctx.wait_for_ledgers(10, node_id=0, timeout=120)
end_seq = ctx.validated_ledger_index(0)
log(f"Inspecting ledgers {start_seq + 1}{end_seq}")
digests = set()
total_user_txns = 0
for seq in range(start_seq + 1, end_seq + 1):
ce, user_txns = get_entropy_tx(ctx, seq)
digest, count = assert_valid_entropy(ce, seq, seen_digests=digests)
total_user_txns += len(user_txns)
log(
f" Ledger {seq}: EntropyCount={count} "
f"user_txns={len(user_txns)} Digest={digest[:16]}..."
)
await gen.stop()
log(
f"Verified {end_seq - start_seq} ledgers: {total_user_txns} user txns, "
f"all entropy valid and unique"
)
if total_user_txns == 0:
raise AssertionError("No user transactions were included in any ledger")
log("PASS")

View File

@@ -1,28 +0,0 @@
""":descr: healthy non-standalone testnet without UNLReport mints Tier 1 fallback"""
from __future__ import annotations
from helpers import require_entropy, get_entropy_tx, assert_consensus_fallback
async def scenario(ctx, log):
await require_entropy(ctx, log)
# Non-standalone nodes require a ledger-anchored UNLReport before assigning
# validator_quorum / participant_aligned labels. Without it, the RNG pipeline
# may still collect commits/reveals, but injection must remain Tier 1.
await ctx.wait_for_ledgers(3, node_id=0, timeout=60)
log("Pipeline warmed up without UNLReport")
start_seq = ctx.validated_ledger_index(0)
await ctx.wait_for_ledgers(5, node_id=0, timeout=90)
end_seq = ctx.validated_ledger_index(0)
log(f"Inspecting ledgers {start_seq + 1} -> {end_seq}")
for seq in range(start_seq + 1, end_seq + 1):
ce, _ = get_entropy_tx(ctx, seq)
digest, count = assert_consensus_fallback(ce, seq)
log(f" Ledger {seq}: EntropyCount={count} Digest={digest[:16]}...")
log(f"Verified {end_seq - start_seq} ledgers: all consensus_fallback")
log("PASS")

View File

@@ -1,160 +0,0 @@
""":descr: 5/6 validator_quorum, 4/6 participant_aligned (tier 2), recovery
Requires node_count: 6 (see suite.yml) — the smallest NON-degenerate Tier 2
size. At n=6: tier2 floor = 4, validator quorum = 5, validation quorum = 5. So
6/6, 5/6 present -> validator_quorum (EntropyTier=3)
4/6 present -> participant_aligned (EntropyTier=2, count 4) <-- the band
3/6 present -> consensus_fallback (EntropyTier=1)
n=5 has NO tier-2 band (tier2 == quorum == 4), which is why the existing
degradation smoke at 5 nodes only ever sees tier 3 / fallback.
KEY: the 4/6 window is BELOW the 80% validation quorum (5). The 4 survivors
keep CLOSING ledgers that carry tier-2 entropy, but those ledgers do NOT
validate until the network recovers — exactly the transition window Tier 2
serves. So validated_ledger_index() stalls; we instead inspect a surviving
node's CLOSED ledger (its LCL) directly, and cross-check the injection from the
cohort's logs.
"""
from __future__ import annotations
from helpers import (
require_entropy,
get_entropy_tx,
assert_participant_aligned,
assert_validator_quorum,
)
def _closed_entropy(result):
"""(seq, ConsensusEntropy tx) from a ctx.ledger('closed', transactions=True)
result, or (None, None) if the fetch returned no usable ledger.
Enforces the per-ledger invariant that an entropy-enabled closed ledger
carries EXACTLY ONE ConsensusEntropy pseudo-tx (mirroring get_entropy_tx):
a duplicate or missing injection raises here with a clear error instead of
being silently skipped and resurfacing later as a generic 'no tier-2 ledger'.
"""
if not result or not isinstance(result.get("ledger"), dict):
return None, None
led = result["ledger"]
try:
seq = int(led.get("ledger_index"))
except (TypeError, ValueError):
return None, None
ce = [
t
for t in led.get("transactions", [])
if isinstance(t, dict) and t.get("TransactionType") == "ConsensusEntropy"
]
if len(ce) != 1:
raise AssertionError(
f"Closed ledger {seq}: expected 1 ConsensusEntropy txn, got {len(ce)}"
)
return seq, ce[0]
async def scenario(ctx, log):
await require_entropy(ctx, log)
# Baseline: healthy 6/6 produces validator_quorum entropy.
await ctx.wait_for_ledgers(1, node_id=0, timeout=30)
# --- 5/6: settles back to validator_quorum (5 present >= quorum 5) ---
val_before_drop = ctx.validated_ledger_index(0)
ctx.stop_node(5)
await ctx.wait_for_nodes_down(nodes=[5], timeout=30)
# Settle a few ledgers past the membership change. The ledger right at a
# validator drop can carry a transient consensus_fallback (tier 1, count 0,
# deterministic and by design) before the commit/reveal pipeline re-primes,
# so we do NOT assume any single post-drop ledger is already tier 3.
await ctx.wait_for_ledgers(4, node_id=0, timeout=90)
# 5/6 is at/above the 80% quorum (5), so steady state is validator_quorum.
# Scan the post-drop validated ledgers (all carry the 5-node cohort, so a
# tier-3 here has count == 5) and require at least one clean validator_quorum
# — EntropyTier=3, count >= quorum, non-zero digest — tolerating the
# transition fallback instead of depending on where the tip happened to land.
val_5of6 = ctx.validated_ledger_index(0)
t3_seq = None
for seq in range(val_5of6, val_before_drop, -1):
ce, _ = get_entropy_tx(ctx, seq)
tier = ce.get("EntropyTier")
log(f" 5/6 ledger {seq}: tier={tier} count={ce.get('EntropyCount')}")
if tier == 3:
assert_validator_quorum(ce, seq, min_count=5)
t3_seq = seq
break
if t3_seq is None:
raise AssertionError(
f"5/6: no validator_quorum (tier 3) entropy in post-drop validated "
f"ledgers {val_before_drop + 1}..{val_5of6}"
)
log(f"5/6: validator_quorum at validated seq {t3_seq}")
#@@start test-participant-aligned-window
# --- 4/6: participant_aligned (Tier 2) degraded window ---
ctx.stop_node(4)
await ctx.wait_for_nodes_down(nodes=[4], timeout=30)
# ~12s window: confirm tier-2 INJECTION from the cohort's logs, and that the
# round is NOT the impossible/fallback path (which is what distinguishes the
# tier-2 band from the tier-1 fallback regime).
op = await ctx.sleep(12, name="tier2_window")
selected_t2 = ctx.search_logs(
r"RNG: entropy selected seq=\d+ tier=2 count=4",
within=op.window,
nodes=[0, 1, 2, 3],
)
log(f"4/6: 'entropy selected tier=2 count=4' logs: {selected_t2.count}")
if selected_t2.count == 0:
raise AssertionError(
"4/6 window injected no participant_aligned (tier 2) entropy: no "
"'RNG: entropy selected ... tier=2 count=4' on the surviving cohort"
)
ctx.assert_not_log(
r"reason=impossible-entropy-gate", within=op.window, nodes=[0, 1, 2, 3]
)
# Verify the on-ledger EntropyTier=2 DIRECTLY: validation is stalled (4 < 5),
# so sample the surviving cohort's CLOSED ledger (its LCL — built but not yet
# validated). At least one must be participant_aligned with EntropyCount=4.
tier2_on_ledger = 0
last_seq = None
for _ in range(5):
seq, ce = _closed_entropy(
ctx.ledger("closed", transactions=True, node_id=0)
)
if ce is not None and seq is not None and seq != last_seq:
last_seq = seq
tier = ce.get("EntropyTier")
count = ce.get("EntropyCount", -1)
log(f" closed ledger {seq}: tier={tier} count={count}")
if tier == 2:
assert_participant_aligned(ce, seq, expected_count=4)
tier2_on_ledger += 1
await ctx.sleep(3)
if tier2_on_ledger == 0:
raise AssertionError(
"no closed participant_aligned (tier 2) ledger observed during the "
"4/6 window (tier 2 was injected per logs, but not seen on a closed "
"ledger)"
)
log(f"4/6: {tier2_on_ledger} participant_aligned closed ledger(s) verified")
#@@end test-participant-aligned-window
# --- Recovery: liveness — validation resumes once quorum is restored ---
ctx.start_node(4)
ctx.start_node(5)
await ctx.wait_for_ledgers(1, node_id=0, timeout=120)
val_recovered = ctx.validated_ledger_index(0)
if not val_recovered or val_recovered <= val_5of6:
raise AssertionError(
f"Validated ledger did not advance after recovery "
f"({val_5of6} -> {val_recovered})"
)
log(f"Recovered: validated seq {val_5of6} -> {val_recovered}")
log("PASS")

View File

@@ -1,148 +0,0 @@
""":descr: 4/5 liveness, 3/5 fallback-entropy (consensus_fallback), recovery"""
from __future__ import annotations
from helpers import ZERO_DIGEST, require_entropy, get_entropy_tx, entropy_fields
async def scenario(ctx, log):
await require_entropy(ctx, log)
# Baseline: wait 1 ledger to confirm network is healthy.
await ctx.wait_for_ledgers(1, node_id=0, timeout=30)
# --- 4/5 liveness ---
ctx.stop_node(4)
await ctx.wait_for_nodes_down(nodes=[4], timeout=30)
await ctx.wait_for_ledgers(1, node_id=0, timeout=30)
log("4/5: liveness OK")
# Snapshot validated seq before dropping to 3/5.
val_before = ctx.validated_ledger_index(0)
# --- 3/5 degraded window ---
ctx.stop_node(3)
await ctx.wait_for_nodes_down(nodes=[3], timeout=30)
# 10s ≈ 3 rounds at 3s cadence.
await ctx.sleep(10)
val_after = ctx.validated_ledger_index(0)
log(f"3/5: validated ledger {val_before}{val_after}")
# Accepted/built ledgers may still later appear as validated once the full
# network rejoins. For ConsensusEntropy the key invariant is that every
# ledger created during this sub-quorum window carries FALLBACK entropy
# (consensus_fallback: non-zero consensus-bound digest, count 0) — never
# validator-tier entropy.
degraded_fallback = 0
degraded_end = val_after or val_before
if val_before and degraded_end and degraded_end > val_before:
for seq in range(val_before + 1, degraded_end + 1):
ce, _ = get_entropy_tx(ctx, seq)
digest, entropy_count, is_fallback = entropy_fields(ce)
tier = ce.get("EntropyTier")
# consensus_fallback (EntropyTier=1): explicit tier, count 0,
# deterministic NON-zero digest.
if tier != 1:
raise AssertionError(
f"Ledger {seq}: expected EntropyTier==1 "
f"(consensus_fallback) during 3/5 window, got {tier} "
f"(EntropyCount={entropy_count})"
)
if entropy_count != 0:
raise AssertionError(
f"Ledger {seq}: fallback EntropyCount must be 0, got "
f"{entropy_count}"
)
if not digest or digest == ZERO_DIGEST:
raise AssertionError(
f"Ledger {seq}: fallback digest must be non-zero "
f"(consensus_fallback), got {digest[:16]}..."
)
assert is_fallback # tier==1 implies fallback
degraded_fallback += 1
log(
f" Degraded ledger {seq}: EntropyCount={entropy_count} "
f"FALLBACK"
)
log(f"3/5 entropy summary: {degraded_fallback} fallback")
# Log checks tied to current transition mechanics:
# - commit-set SHAMap publication is the observable output of entering the
# commit sidecar phase
# - ConvergingCommit transition is the gateway out of seq=0-only behavior
# - reason=impossible-entropy-gate is the explicit degraded-window fallback path
ctx.log_level("LedgerConsensus", "trace")
ctx.log_level("ConsensusExtensions", "trace")
op = await ctx.sleep(6, name="stall_window")
ctx.assert_not_log(
r"RNG: transitioned to ConvergingCommit", within=op.window, nodes=[0, 1, 2]
)
ctx.assert_not_log(
r"RNG: built commitSet SHAMap", within=op.window, nodes=[0, 1, 2]
)
gate_blocked = ctx.search_logs(
r"STALLDIAG: establish gate blocked reason=(pause|no-tx-consensus)",
within=op.window,
nodes=[0, 1, 2],
)
log(f"3/5: establish gate-blocked logs in 6s: {gate_blocked.count}")
impossible = ctx.search_logs(
r"RNG: skipping commit wait reason=impossible-entropy-gate",
within=op.window,
nodes=[0, 1, 2],
)
log(f"3/5: RNG impossible-entropy-gate skips in 6s: {impossible.count}")
# --- Recovery: restart nodes, verify ledger advancement ---
ctx.start_node(3)
ctx.start_node(4)
await ctx.wait_for_ledgers(1, node_id=0, timeout=120)
val_recovered = ctx.validated_ledger_index(0)
pre_recovery = max(v for v in [val_before, val_after] if v is not None)
log(f"Recovered: validated seq {pre_recovery}{val_recovered}")
if not val_recovered or val_recovered <= pre_recovery:
raise AssertionError(
f"Validated ledger did not advance after recovery "
f"({pre_recovery}{val_recovered})"
)
# Inspect post-recovery ledgers separately from the degraded window above.
# Once the network is back at quorum, validator-tier entropy is expected
# again (transitional fallback ledgers are fine) and must be quorum-met.
fallback_count = 0
validator_count = 0
for seq in range(pre_recovery + 1, val_recovered + 1):
ce, _ = get_entropy_tx(ctx, seq)
digest, entropy_count, is_fallback = entropy_fields(ce)
if is_fallback:
fallback_count += 1
else:
validator_count += 1
if entropy_count < 4:
raise AssertionError(
f"Ledger {seq}: validator entropy with sub-quorum "
f"EntropyCount={entropy_count} (need >= 4)"
)
log(
f" Ledger {seq}: EntropyCount={entropy_count} "
f"{'FALLBACK' if is_fallback else 'VALIDATOR'}"
)
log(
f"Entropy summary: {fallback_count} fallback, "
f"{validator_count} validator"
)
log("PASS")

View File

@@ -1,44 +0,0 @@
""":descr: drop 2 nodes (3/5 stall), restart both, verify recovery"""
from __future__ import annotations
from helpers import require_entropy
async def scenario(ctx, log):
await require_entropy(ctx, log)
await ctx.wait_for_ledgers(1, node_id=0, timeout=60)
log("Baseline OK")
# Drop 2 nodes → validation stall.
ctx.stop_node(3)
ctx.stop_node(4)
await ctx.wait_for_nodes_down(nodes=[3, 4], timeout=30)
info = ctx.rpc.server_info(node_id=0)
val_before = info.get("info", {}).get("validated_ledger", {}).get("seq", 0)
log(f"Stalled at validated seq {val_before}")
# Let it sit for a few rounds in degraded state.
await ctx.sleep(6)
# Bring both nodes back.
ctx.start_node(3)
ctx.start_node(4)
log("Restarted n3 and n4, waiting for recovery...")
# Recovery: wait for ANY validated ledger advance on n0.
await ctx.wait_for_ledger_close(node_id=0, timeout=60)
info = ctx.rpc.server_info(node_id=0)
val_after = info.get("info", {}).get("validated_ledger", {}).get("seq", 0)
log(f"Recovered: validated seq {val_before}{val_after}")
if val_after <= val_before:
raise AssertionError(
f"Validated ledger did not advance after recovery "
f"({val_before}{val_after})"
)
log("PASS")

View File

@@ -1,27 +0,0 @@
""":descr: all 5 nodes healthy, every ledger has valid unique quorum-met entropy"""
from __future__ import annotations
from helpers import require_entropy, get_entropy_tx, assert_valid_entropy
async def scenario(ctx, log):
await require_entropy(ctx, log)
# Wait for RNG pipeline to warm up past bootstrap skip.
await ctx.wait_for_ledgers(3, node_id=0, timeout=60)
log("Pipeline warmed up")
start_seq = ctx.validated_ledger_index(0)
await ctx.wait_for_ledgers(10, node_id=0, timeout=120)
end_seq = ctx.validated_ledger_index(0)
log(f"Inspecting ledgers {start_seq + 1}{end_seq}")
digests = set()
for seq in range(start_seq + 1, end_seq + 1):
ce, _ = get_entropy_tx(ctx, seq)
digest, count = assert_valid_entropy(ce, seq, seen_digests=digests)
log(f" Ledger {seq}: EntropyCount={count} Digest={digest[:16]}...")
log(f"Verified {end_seq - start_seq} ledgers: all quorum entropy, all unique")
log("PASS")

View File

@@ -1,104 +0,0 @@
defaults:
network:
node_count: 5
launcher: tmux
find_ports: true
slave_delay: 0.2
features:
- ConsensusEntropy
- Export
track_features:
- ConsensusEntropy
- Export
unl_report: true
log_levels:
TxQ: info
Protocol: debug
Peer: debug
LedgerConsensus: debug
ConsensusExtensions: debug
NetworkOPs: info
env:
XAHAU_RESOURCE_PER_PORT: "1"
rc:
- rng_poll_ms=333
tests:
# --- CE + Export (80% quorum, SHAMap convergence) ---
- name: steady_state_export_ce
script: .testnet/scenarios/export/steady_state_export.py
- name: retriable_export_ce
script: .testnet/scenarios/export/retriable_export.py
- name: export_degradation_ce
script: .testnet/scenarios/export/export_degradation.py
network:
rc:
- rng_poll_ms=333
- n3:no_export_sig=true
- n4:no_export_sig=true
- name: export_without_unl_report
script: .testnet/scenarios/export/export_without_unl_report.py
network:
features:
- Export
track_features:
- Export
unl_report: false
- name: export_no_veto_missing_observation
script: .testnet/scenarios/export/export_no_veto_missing_observation.py
network:
rc:
- rng_poll_ms=333
- n4:no_export_sig_hash=true
# CE + Export: 1 node suppressed, 4/5 = 80% quorum, should succeed
- name: export_ce_one_node_down
script: .testnet/scenarios/export/export_quorum.py
params:
expect_success: true
network:
rc:
- rng_poll_ms=333
- n4:no_export_sig=true
# --- Export only, no CE (80% active-view quorum) ---
- name: export_only_all_up
script: .testnet/scenarios/export/export_quorum.py
params:
expect_success: true
network:
features:
- Export
track_features:
- Export
- name: export_only_one_node_down
script: .testnet/scenarios/export/export_quorum.py
params:
expect_success: true
network:
features:
- Export
track_features:
- Export
rc:
- rng_poll_ms=333
- n4:no_export_sig=true
- name: export_only_two_nodes_down
script: .testnet/scenarios/export/export_quorum.py
params:
expect_success: false
network:
features:
- Export
track_features:
- Export
rc:
- rng_poll_ms=333
- n3:no_export_sig=true
- n4:no_export_sig=true

View File

@@ -1,123 +0,0 @@
""":descr: Submit ttEXPORT with 2 nodes suppressing export sigs, verify it
retries via terRETRY_EXPORT until LLS expiry (insufficient signatures).
Nodes 3 and 4 have runtime_config no_export_sig=true, so only 3/5 nodes
provide export signatures. With 80% quorum = ceil(5*0.8) = 4 required,
the export cannot reach quorum and should expire via tecEXPORT_EXPIRED.
Flow:
1. Fund alice and bob
2. alice submits ttEXPORT with tight LLS
3. Export retries (only 3/5 sigs available, need 4)
4. Verify export expires with tecEXPORT_EXPIRED
5. Verify subsequent payment still works (sequence not permanently blocked)
"""
from __future__ import annotations
from export_helpers import require_export, assert_shadow_ticket
async def scenario(ctx, log):
await require_export(ctx, log)
# --- Setup ---
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
log("Accounts funded")
alice = ctx.account("alice")
bob = ctx.account("bob")
current_seq = ctx.validated_ledger_index(0)
log(f"Current ledger: {current_seq}")
log("Nodes 3,4 have runtime_config no_export_sig=true (3/5 sigs, need 4)")
#@@start test-export-below-quorum-expiry
# --- Submit ttEXPORT (should retry then expire -- only 3/5 sigs) ---
export_start = ctx.mark("export-degradation-submit-start")
result = await ctx.submit_and_wait(
{
"TransactionType": "Export",
"LastLedgerSequence": current_seq + 8,
"Fee": "1000000",
"ExportedTxn": {
"TransactionType": "Payment",
"Account": alice.address,
"Destination": bob.address,
"Amount": "1000000",
"Fee": "10",
"Sequence": 0,
"TicketSequence": 1,
"FirstLedgerSequence": current_seq + 1,
"LastLedgerSequence": current_seq + 6,
"Flags": 2147483648,
"SigningPubKey": "",
},
},
alice.wallet,
timeout=60,
)
export_end = ctx.mark("export-degradation-submit-end")
final_seq = ctx.validated_ledger_index(0)
engine_result = result.get("engine_result", "")
log(f"Export completed at ledger {final_seq}, result: {engine_result}")
# With only 3/5 sigs and 80% quorum (4 required), export MUST fail
if engine_result == "tesSUCCESS":
raise AssertionError(
"Export should NOT have succeeded with only 3/5 sigs "
"(need 4 for 80% quorum) -- check runtime_config no_export_sig"
)
# Should be tecEXPORT_EXPIRED (LLS reached without quorum). Be exact here:
# any other non-success means the retry/expiry boundary regressed.
if engine_result != "tecEXPORT_EXPIRED":
raise AssertionError(
f"Expected tecEXPORT_EXPIRED below quorum, got {engine_result}"
)
log(f"Export failed as expected ({engine_result})")
retry_logs = ctx.assert_log(
r"Export: insufficient signatures .*result=terRETRY_EXPORT",
since=export_start,
until=export_end,
)
log(f"Export insufficient-signature retries: {retry_logs.count}")
expired_logs = ctx.assert_log(
r"Export: last ledger expired .*result=tecEXPORT_EXPIRED",
since=export_start,
until=export_end,
)
log(f"Export LLS expiry logs: {expired_logs.count}")
# No shadow ticket should exist (export never reached quorum)
assert_shadow_ticket(ctx, alice.address, log, expect_exists=False)
#@@end test-export-below-quorum-expiry
# --- Verify subsequent payment works regardless ---
log("Submitting payment from alice to bob...")
pay_result = await ctx.submit_and_wait(
{
"TransactionType": "Payment",
"Destination": bob.address,
"Amount": "1000000",
"Fee": "12",
},
alice.wallet,
timeout=30,
)
pay_engine = pay_result.get("engine_result", "")
log(f"Payment result: {pay_engine}")
if pay_engine != "tesSUCCESS":
raise AssertionError(
f"Payment failed after expired export: {pay_engine} "
f"-- sequence may be blocked"
)
log("Payment succeeded -- account not permanently blocked")
log("PASS")

View File

@@ -1,181 +0,0 @@
"""Shared helpers for Export scenario tests."""
from __future__ import annotations
from xahaud_scripts.testnet.config import _unl_report_index, feature_name_to_hash
async def require_export(
ctx, log, *, require_unl_report=True, require_runtime_config=True
):
"""Wait for first ledger and assert Export is enabled.
Network-mode Export success requires a parent-ledger UNLReport-backed
active validator view. Most export scenarios seed that report in genesis;
assert it here so a success-path test cannot accidentally pass setup
without the condition Export::doApply requires. The no-UNLReport retry
scenario opts out deliberately.
The tracked export suite also uses XAHAUD_RUNTIME_TEST_CONFIG for polling
and fault-injection knobs. Default binaries reject the runtime_config RPC,
so check it up front rather than silently running without those knobs.
"""
await ctx.wait_for_ledger_close(timeout=120)
if require_runtime_config:
result = ctx.rpc.runtime_config(0)
if not result or result.get("error"):
raise AssertionError(
"Export suite requires a binary built with "
"xahaud_runtime_test_config=ON; runtime_config RPC returned "
f"{result}"
)
log("RuntimeConfig RPC active")
feature = ctx.feature_check(feature_name_to_hash("Export"), node_id=0)
if not feature or not feature.get("enabled", False):
raise AssertionError(f"Export not enabled: {feature}")
log("Export enabled")
if require_unl_report:
result = ctx.rpc.ledger_entry(0, _unl_report_index())
node = (result or {}).get("node", {})
active = node.get("ActiveValidators", [])
if node.get("LedgerEntryType") != "UNLReport" or not active:
raise AssertionError(
"Export success scenario requires a ledger UNLReport with "
f"ActiveValidators, got: {result}"
)
log(f"UNLReport active validators: {len(active)}")
def find_export_txns(ctx, seq):
"""Find Export transactions in a ledger.
Returns list of Export transaction dicts.
"""
result = ctx.ledger(seq, transactions=True)
if not result:
return []
txns = result.get("ledger", {}).get("transactions", [])
return [tx for tx in txns if tx.get("TransactionType") == "Export"]
def dst_param(address):
"""Encode an address as a HookParameter entry for the DST param."""
from xrpl.core.addresscodec import decode_classic_address
dst_hex = decode_classic_address(address).hex().upper()
return {
"HookParameter": {
"HookParameterName": "445354", # "DST"
"HookParameterValue": dst_hex,
}
}
def assert_hook_accepted(meta, log, *, expected_emits=1):
"""Assert hook executed with ACCEPT and the expected emit count.
Checks sfHookExecutions in transaction metadata.
Returns the hook execution entry for further inspection.
"""
hook_execs = meta.get("HookExecutions", [])
if not hook_execs:
raise AssertionError("No HookExecutions in metadata")
exec_entry = hook_execs[0].get("HookExecution", {})
hook_result = exec_entry.get("HookResult", -1)
emit_count = exec_entry.get("HookEmitCount", -1)
return_code = exec_entry.get("HookReturnCode", "")
log(f" HookResult={hook_result} EmitCount={emit_count} ReturnCode={return_code}")
# HookResult 3 = ExitType::ACCEPT
if hook_result != 3:
raise AssertionError(
f"Hook did not ACCEPT: HookResult={hook_result} "
f"ReturnCode={return_code}"
)
if emit_count != expected_emits:
raise AssertionError(
f"Expected {expected_emits} emits, got {emit_count}"
)
# ReturnCode 0 = success; non-zero = ASSERT line number in hook
if return_code and str(return_code) != "0":
raise AssertionError(
f"Hook returned error code {return_code} "
f"(likely ASSERT failure at that line)"
)
return exec_entry
def assert_export_result(meta, log, *, require_signers=True):
"""Assert ExportResult is present and well-formed in metadata.
Returns the ExportResult dict.
"""
export_result = meta.get("ExportResult", {})
if not export_result:
raise AssertionError("ExportResult not found in metadata")
# Must have LedgerSequence and TransactionHash
if "LedgerSequence" not in export_result:
raise AssertionError("ExportResult missing LedgerSequence")
if "TransactionHash" not in export_result:
raise AssertionError("ExportResult missing TransactionHash")
# Must have the inner ExportedTxn object
inner = export_result.get("ExportedTxn", {})
if not inner:
raise AssertionError("ExportResult missing ExportedTxn (multisigned blob)")
log(f" ExportResult: seq={export_result['LedgerSequence']} "
f"hash={export_result['TransactionHash'][:16]}...")
# Inner tx should have Account, Destination, TransactionType
if "Account" not in inner:
raise AssertionError("ExportedTxn missing Account")
if "TransactionType" not in inner:
raise AssertionError("ExportedTxn missing TransactionType")
# Should have empty SigningPubKey (multisigned)
if inner.get("SigningPubKey", "NOT_EMPTY") != "":
raise AssertionError(
f"ExportedTxn SigningPubKey should be empty, "
f"got '{inner.get('SigningPubKey')}'"
)
if require_signers:
signers = inner.get("Signers", [])
if not signers:
raise AssertionError("ExportedTxn has no Signers (multisig not applied)")
log(f" Signers: {len(signers)} validator(s)")
return export_result
def assert_shadow_ticket(ctx, account_address, log, *, expect_exists=True):
"""Assert shadow ticket exists (or doesn't) for the account."""
obj_result = ctx.rpc.request(
0, "account_objects", {"account": account_address}
)
all_objects = (obj_result or {}).get("account_objects", [])
shadow_tickets = [
obj for obj in all_objects
if obj.get("LedgerEntryType") == "ShadowTicket"
]
log(f" Shadow tickets: {len(shadow_tickets)}")
if expect_exists and not shadow_tickets:
raise AssertionError("Expected shadow ticket but none found")
if not expect_exists and shadow_tickets:
raise AssertionError(
f"Expected no shadow tickets but found {len(shadow_tickets)}"
)
return shadow_tickets

View File

@@ -1,87 +0,0 @@
""":descr: Export succeeds when quorum sidecar material exists but one active
validator withholds exportSigSetHash observation.
Node 4 has runtime_config no_export_sig_hash=true. It still attaches export
signatures, but it does not publish its exportSigSetHash in proposals. The
remaining 4/5 active validators can still align on the same export sidecar
hash, so the round must not retry/expire just because fullObservation is false.
"""
from __future__ import annotations
from export_helpers import (
require_export,
assert_export_result,
assert_shadow_ticket,
)
async def scenario(ctx, log):
await require_export(ctx, log)
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
log("Accounts funded")
alice = ctx.account("alice")
bob = ctx.account("bob")
current_seq = ctx.validated_ledger_index(0)
log(f"Current ledger: {current_seq}")
log("Node 4 withholds exportSigSetHash but still attaches export signatures")
export_start = ctx.mark("export-no-veto-submit-start")
result = await ctx.submit_and_wait(
{
"TransactionType": "Export",
"LastLedgerSequence": current_seq + 10,
"Fee": "1000000",
"ExportedTxn": {
"TransactionType": "Payment",
"Account": alice.address,
"Destination": bob.address,
"Amount": "1000000",
"Fee": "10",
"Sequence": 0,
"TicketSequence": 1,
"FirstLedgerSequence": current_seq + 1,
"LastLedgerSequence": current_seq + 8,
"Flags": 2147483648,
"SigningPubKey": "",
},
},
alice.wallet,
timeout=60,
)
export_end = ctx.mark("export-no-veto-submit-end")
final_seq = ctx.validated_ledger_index(0)
engine_result = result.get("engine_result", "")
meta = result.get("meta", {})
log(f"Export completed at ledger {final_seq}, result: {engine_result}")
if engine_result != "tesSUCCESS":
raise AssertionError(f"Expected tesSUCCESS, got {engine_result}")
export_result = assert_export_result(meta, log, require_signers=True)
signers = export_result.get("ExportedTxn", {}).get("Signers", [])
if len(signers) < 4:
raise AssertionError(f"Expected at least 4 signers, got {len(signers)}")
log(f"Export signer count: {len(signers)}")
no_veto_logs = ctx.assert_log(
r"Export: missing exportSigSetHash observation ignored",
since=export_start,
until=export_end,
)
log(f"Export no-veto missing-observation logs: {no_veto_logs.count}")
withhold_logs = ctx.assert_log(
r"Export: withholding exportSigSetHash",
since=export_start,
until=export_end,
)
log(f"Export sidecar hash withholding logs: {withhold_logs.count}")
assert_shadow_ticket(ctx, alice.address, log, expect_exists=True)
log("PASS")

View File

@@ -1,117 +0,0 @@
""":descr: Test Export quorum behavior. When enough active validators sign,
the export should succeed whether or not CE is enabled. When fewer than the
active-view quorum sign, the export should expire.
Parameterized via `expect_success` kwarg from suite.yml.
Flow:
1. Fund alice and bob
2. alice submits ttEXPORT
3. Verify result matches expectation (tesSUCCESS or tecEXPORT_EXPIRED)
4. Verify ExportResult + shadow ticket on success, absence on failure
5. Verify subsequent payment works regardless
"""
from __future__ import annotations
from export_helpers import (
require_export,
assert_export_result,
assert_shadow_ticket,
)
async def scenario(ctx, log, expect_success=True):
await require_export(ctx, log)
# --- Setup ---
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
log("Accounts funded")
alice = ctx.account("alice")
bob = ctx.account("bob")
current_seq = ctx.validated_ledger_index(0)
log(f"Current ledger: {current_seq}")
outcome = "success" if expect_success else "failure (below quorum)"
log(f"Expecting export {outcome}")
# --- Submit ttEXPORT ---
result = await ctx.submit_and_wait(
{
"TransactionType": "Export",
"LastLedgerSequence": current_seq + 10,
"Fee": "1000000",
"ExportedTxn": {
"TransactionType": "Payment",
"Account": alice.address,
"Destination": bob.address,
"Amount": "1000000",
"Fee": "10",
"Sequence": 0,
"TicketSequence": 1,
"FirstLedgerSequence": current_seq + 1,
"LastLedgerSequence": current_seq + 8,
"Flags": 2147483648,
"SigningPubKey": "",
},
},
alice.wallet,
timeout=60,
)
final_seq = ctx.validated_ledger_index(0)
engine_result = result.get("engine_result", "")
meta = result.get("meta", {})
log(f"Export at ledger {final_seq}, result: {engine_result}")
if expect_success:
if engine_result != "tesSUCCESS":
raise AssertionError(
f"Expected tesSUCCESS, got {engine_result}"
)
# Assert ExportResult is well-formed with signers
assert_export_result(meta, log, require_signers=True)
# Assert shadow ticket was created
assert_shadow_ticket(ctx, alice.address, log, expect_exists=True)
log("Export succeeded as expected (active-view quorum reached)")
else:
if engine_result == "tesSUCCESS":
raise AssertionError(
"Export should NOT have succeeded below active-view quorum"
)
if engine_result != "tecEXPORT_EXPIRED":
raise AssertionError(
"Expected tecEXPORT_EXPIRED below active-view quorum, "
f"got {engine_result}"
)
log(f"Export failed as expected ({engine_result})")
# No shadow ticket should exist
assert_shadow_ticket(ctx, alice.address, log, expect_exists=False)
# --- Verify subsequent payment works ---
log("Submitting payment from alice to bob...")
pay_result = await ctx.submit_and_wait(
{
"TransactionType": "Payment",
"Destination": bob.address,
"Amount": "1000000",
"Fee": "12",
},
alice.wallet,
timeout=30,
)
pay_engine = pay_result.get("engine_result", "")
log(f"Payment result: {pay_engine}")
if pay_engine != "tesSUCCESS":
raise AssertionError(f"Payment failed: {pay_engine}")
log("Payment succeeded -- account not blocked")
log("PASS")

View File

@@ -1,92 +0,0 @@
""":descr: Export retries/expires without a ledger-anchored UNLReport view.
All validators may sign, but network-mode Export must not assemble quorum
material from a node-local trusted-config view. Without UNLReport, the export
should retry until LastLedgerSequence and expire without creating a shadow
ticket.
"""
from __future__ import annotations
from export_helpers import require_export, assert_shadow_ticket
async def scenario(ctx, log):
await require_export(ctx, log, require_unl_report=False)
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
log("Accounts funded")
alice = ctx.account("alice")
bob = ctx.account("bob")
current_seq = ctx.validated_ledger_index(0)
log(f"Current ledger: {current_seq}")
log("UNLReport intentionally absent; export must not use local config view")
export_start = ctx.mark("export-without-unlreport-submit-start")
result = await ctx.submit_and_wait(
{
"TransactionType": "Export",
"LastLedgerSequence": current_seq + 8,
"Fee": "1000000",
"ExportedTxn": {
"TransactionType": "Payment",
"Account": alice.address,
"Destination": bob.address,
"Amount": "1000000",
"Fee": "10",
"Sequence": 0,
"TicketSequence": 1,
"FirstLedgerSequence": current_seq + 1,
"LastLedgerSequence": current_seq + 6,
"Flags": 2147483648,
"SigningPubKey": "",
},
},
alice.wallet,
timeout=60,
)
export_end = ctx.mark("export-without-unlreport-submit-end")
final_seq = ctx.validated_ledger_index(0)
engine_result = result.get("engine_result", "")
log(f"Export completed at ledger {final_seq}, result: {engine_result}")
if engine_result == "tesSUCCESS":
raise AssertionError(
"Export should not succeed without a ledger-anchored UNLReport view"
)
# Be exact: without a UNLReport view the export should retry until LLS and
# expire, not fail by some unrelated terminal code.
if engine_result != "tecEXPORT_EXPIRED":
raise AssertionError(
"Expected tecEXPORT_EXPIRED without UNLReport view, "
f"got {engine_result}"
)
warning_logs = ctx.assert_log(
r"Export: retrying without ledger-anchored validator view",
since=export_start,
until=export_end,
)
log(f"Export no-UNLReport retry warnings: {warning_logs.count}")
retry_logs = ctx.assert_log(
r"Export: insufficient signatures .*result=terRETRY_EXPORT",
since=export_start,
until=export_end,
)
log(f"Export retry logs: {retry_logs.count}")
expired_logs = ctx.assert_log(
r"Export: last ledger expired .*result=tecEXPORT_EXPIRED",
since=export_start,
until=export_end,
)
log(f"Export expiry logs: {expired_logs.count}")
assert_shadow_ticket(ctx, alice.address, log, expect_exists=False)
log("PASS")

View File

@@ -1,94 +0,0 @@
""":descr: Submit ttEXPORT directly (no hook), verify it succeeds with
ExportResult in metadata. Then submit a payment from the same account
to verify sequence handling doesn't block subsequent transactions.
Flow:
1. Fund alice and bob
2. alice submits ttEXPORT with inner payment -> tesSUCCESS (provisional)
3. Validators attach sigs via proposals -> quorum -> ExportResult in metadata
4. alice submits a Payment to bob -> should succeed (sequence not blocked)
"""
from __future__ import annotations
from export_helpers import require_export, assert_export_result, assert_shadow_ticket
async def scenario(ctx, log):
await require_export(ctx, log)
# --- Setup ---
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
log("Accounts funded")
alice = ctx.account("alice")
bob = ctx.account("bob")
current_seq = ctx.validated_ledger_index(0)
log(f"Current ledger: {current_seq}")
# --- 1. Submit ttEXPORT ---
result = await ctx.submit_and_wait(
{
"TransactionType": "Export",
"LastLedgerSequence": current_seq + 15,
"Fee": "1000000",
"ExportedTxn": {
"TransactionType": "Payment",
"Account": alice.address,
"Destination": bob.address,
"Amount": "1000000",
"Fee": "10",
"Sequence": 0,
"TicketSequence": 1,
"FirstLedgerSequence": current_seq + 1,
"LastLedgerSequence": current_seq + 10,
"Flags": 2147483648,
"SigningPubKey": "",
},
},
alice.wallet,
timeout=60,
)
export_seq = ctx.validated_ledger_index(0)
engine_result = result.get("engine_result", "")
log(f"Export completed at ledger {export_seq}, result: {engine_result}")
if engine_result != "tesSUCCESS":
raise AssertionError(
f"Expected tesSUCCESS for export, got {engine_result}"
)
# Assert ExportResult is well-formed with signers
meta = result.get("meta", {})
assert_export_result(meta, log, require_signers=True)
# Assert shadow ticket was created
assert_shadow_ticket(ctx, alice.address, log, expect_exists=True)
# --- 2. Submit Payment from same account ---
log("Submitting payment from alice to bob...")
pay_result = await ctx.submit_and_wait(
{
"TransactionType": "Payment",
"Destination": bob.address,
"Amount": "1000000",
"Fee": "12",
},
alice.wallet,
timeout=30,
)
pay_engine = pay_result.get("engine_result", "")
log(f"Payment result: {pay_engine}")
if pay_engine != "tesSUCCESS":
raise AssertionError(f"Payment failed: {pay_engine}")
log(
f"Both transactions succeeded: "
f"Export at ledger {export_seq}, Payment at ledger {ctx.validated_ledger_index(0)}"
)
log("Sequence handling OK - export didn't block subsequent txns")
log("PASS")

View File

@@ -1,211 +0,0 @@
""":descr: install xport hook, trigger export, verify emitted ttEXPORT lifecycle
1. Fund alice (hook holder), bob (trigger), carol (export destination)
2. Install xport hook on alice
3. bob pays alice with DST=carol → hook calls xport() → emits ttEXPORT
4. Emitted ttEXPORT enters open ledger, validators attach sigs via proposals
5. Verify Export transaction appears in a subsequent ledger
"""
from __future__ import annotations
from export_helpers import (
require_export,
find_export_txns,
dst_param,
assert_hook_accepted,
assert_export_result,
assert_shadow_ticket,
)
# C source for the xport hook — verbatim from src/test/app/Export_test_hooks.h
# On Payment to the hook account, exports a 1 XAH payment to the DST param.
XPORT_HOOK_C = r"""
#include <stdint.h>
extern int32_t _g(uint32_t id, uint32_t maxiter);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t xport_reserve(uint32_t count);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
extern int64_t otxn_type(void);
extern int64_t ledger_seq(void);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
#define ttPAYMENT 0
#define tfCANONICAL 0x80000000UL
#define amAMOUNT 1
#define amFEE 8
#define atACCOUNT 1
#define atDESTINATION 3
#define ENCODE_TT(buf_out, tt) \
buf_out[0] = 0x12U; buf_out[1] = (tt >> 8) & 0xFFU; buf_out[2] = tt & 0xFFU; buf_out += 3;
#define ENCODE_FLAGS(buf_out, flags) \
buf_out[0] = 0x22U; buf_out[1] = (flags >> 24) & 0xFFU; buf_out[2] = (flags >> 16) & 0xFFU; \
buf_out[3] = (flags >> 8) & 0xFFU; buf_out[4] = flags & 0xFFU; buf_out += 5;
#define ENCODE_SEQUENCE(buf_out, seq) \
buf_out[0] = 0x24U; buf_out[1] = (seq >> 24) & 0xFFU; buf_out[2] = (seq >> 16) & 0xFFU; \
buf_out[3] = (seq >> 8) & 0xFFU; buf_out[4] = seq & 0xFFU; buf_out += 5;
#define ENCODE_FLS(buf_out, fls) \
buf_out[0] = 0x20U; buf_out[1] = 0x1AU; buf_out[2] = (fls >> 24) & 0xFFU; \
buf_out[3] = (fls >> 16) & 0xFFU; buf_out[4] = (fls >> 8) & 0xFFU; \
buf_out[5] = fls & 0xFFU; buf_out += 6;
#define ENCODE_LLS(buf_out, lls) \
buf_out[0] = 0x20U; buf_out[1] = 0x1BU; buf_out[2] = (lls >> 24) & 0xFFU; \
buf_out[3] = (lls >> 16) & 0xFFU; buf_out[4] = (lls >> 8) & 0xFFU; \
buf_out[5] = lls & 0xFFU; buf_out += 6;
#define ENCODE_DROPS(buf_out, drops, amt_type) \
buf_out[0] = 0x60U + amt_type; buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
buf_out[2] = (drops >> 48) & 0xFFU; buf_out[3] = (drops >> 40) & 0xFFU; \
buf_out[4] = (drops >> 32) & 0xFFU; buf_out[5] = (drops >> 24) & 0xFFU; \
buf_out[6] = (drops >> 16) & 0xFFU; buf_out[7] = (drops >> 8) & 0xFFU; \
buf_out[8] = drops & 0xFFU; buf_out += 9;
#define ENCODE_SIGNING_PUBKEY_EMPTY(buf_out) \
buf_out[0] = 0x73U; buf_out[1] = 0x00U; buf_out += 2;
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
buf_out[0] = 0x80U + acc_type; buf_out[1] = 0x14U; \
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; buf_out += 22;
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
int64_t hook(uint32_t reserved) {
_g(1, 1);
if (otxn_type() != ttPAYMENT)
return accept(0, 0, 0);
ASSERT(xport_reserve(1) == 1);
uint8_t dst[20];
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
ASSERT(dst_len == 20);
uint8_t acc[20];
ASSERT(hook_account(SBUF(acc)) == 20);
uint32_t cls = (uint32_t)ledger_seq();
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
uint8_t* buf = tx;
ENCODE_TT(buf, ttPAYMENT);
ENCODE_FLAGS(buf, tfCANONICAL);
ENCODE_SEQUENCE(buf, 0);
ENCODE_FLS(buf, cls + 1);
ENCODE_LLS(buf, cls + 5);
// sfTicketSequence = UINT32 field 41 = 0x20 0x29
buf[0] = 0x20U; buf[1] = 0x29U;
buf[2] = 0; buf[3] = 0; buf[4] = 0; buf[5] = 1;
buf += 6;
uint64_t drops = 1000000;
ENCODE_DROPS(buf, drops, amAMOUNT);
ENCODE_DROPS(buf, 10, amFEE);
ENCODE_SIGNING_PUBKEY_EMPTY(buf);
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
uint8_t hash[32];
int64_t xport_result = xport(SBUF(hash), (uint32_t)tx, buf - tx);
ASSERT(xport_result == 32);
return accept(0, 0, 0);
}
"""
async def scenario(ctx, log):
# Wait for network to start and amendments to activate
await require_export(ctx, log)
# --- Setup ---
await ctx.fund_accounts({"alice": 10000, "bob": 10000, "carol": 1000})
log("Accounts funded")
alice = ctx.account("alice")
carol = ctx.account("carol")
# Compile and install xport hook on alice
wasm = ctx.compile_hook(XPORT_HOOK_C, label="xport")
await ctx.submit_and_wait(
{
"TransactionType": "SetHook",
"Hooks": [
{
"Hook": {
"CreateCode": wasm.hex().upper(),
"HookOn": "0" * 64,
"HookNamespace": "0" * 64,
"HookApiVersion": 0,
"Flags": 1, # hsfOVERRIDE
}
}
],
"Fee": "100000000",
},
alice.wallet,
)
log(
f"Hook installed on alice ({alice.address[:12]}...) "
f"ledger {ctx.validated_ledger_index(0)}"
)
# --- Trigger ---
# bob pays alice → hook calls xport() → emits ttEXPORT
trigger_result = await ctx.submit_and_wait(
{
"TransactionType": "Payment",
"Destination": alice.address,
"Amount": "100000000",
"Fee": "1000000",
"HookParameters": [dst_param(carol.address)],
},
ctx.account("bob").wallet,
)
trigger_seq = ctx.validated_ledger_index(0)
log(f"Export triggered at ledger {trigger_seq}")
# Assert hook fired with ACCEPT and emitted 1 tx
trigger_meta = trigger_result.get("meta", {})
assert_hook_accepted(trigger_meta, log, expected_emits=1)
# --- Verify: check each ledger close for the Export transaction ---
max_ledgers = 10
for i in range(max_ledgers):
await ctx.wait_for_ledgers(1, node_id=0, timeout=30)
seq = ctx.validated_ledger_index(0)
exports = find_export_txns(ctx, seq)
if exports:
export_tx = exports[0]
meta = export_tx.get("meta", export_tx.get("metaData", {}))
result = meta.get("TransactionResult", "")
log(f"Ledger {seq}: Export txn found, result={result}")
if result != "tesSUCCESS":
raise AssertionError(f"Export did not succeed: {result}")
# Assert ExportResult is well-formed with signers and inner tx
assert_export_result(meta, log, require_signers=True)
# Assert shadow ticket was created
assert_shadow_ticket(ctx, alice.address, log, expect_exists=True)
log("PASS")
return
log(f"Ledger {seq}: no Export txn yet")
raise AssertionError(
f"No Export transaction found after {max_ledgers} ledger closes"
)

View File

@@ -1,180 +0,0 @@
"""Shared helpers for ConsensusEntropy scenario tests."""
from __future__ import annotations
from xahaud_scripts.testnet.config import feature_name_to_hash
ZERO_DIGEST = "0" * 64
CONSENSUS_ENTROPY_FEATURE = feature_name_to_hash("ConsensusEntropy")
def feature_hash(name: str) -> str:
"""Return the amendment hash accepted by feature RPC."""
return feature_name_to_hash(name)
def feature_status(ctx, name: str, node_id=0):
"""Query a feature by amendment hash; feature RPC names are ambiguous."""
return ctx.feature_check(feature_hash(name), node_id=node_id)
def consensus_entropy_feature(ctx, node_id=0):
"""Query ConsensusEntropy by amendment hash."""
return feature_status(ctx, "ConsensusEntropy", node_id=node_id)
async def require_entropy(ctx, log):
"""Wait for first ledger and assert ConsensusEntropy is enabled."""
await ctx.wait_for_ledger_close(timeout=120)
feature = consensus_entropy_feature(ctx, node_id=0)
if not feature or not feature.get("enabled", False):
raise AssertionError(f"ConsensusEntropy not enabled: {feature}")
log("ConsensusEntropy enabled")
def get_entropy_tx(ctx, seq):
"""Fetch ledger and return (ce_tx, user_txns) or raise."""
result = ctx.ledger(seq, transactions=True)
if not result:
raise AssertionError(f"Ledger {seq}: fetch failed")
ledger = result.get("ledger")
if not isinstance(ledger, dict):
raise AssertionError(f"Ledger {seq}: fetch returned no ledger: {result}")
txns = ledger.get("transactions", [])
ce = [tx for tx in txns if tx.get("TransactionType") == "ConsensusEntropy"]
user = [tx for tx in txns if tx.get("TransactionType") != "ConsensusEntropy"]
if len(ce) != 1:
raise AssertionError(
f"Ledger {seq}: expected 1 ConsensusEntropy txn, got {len(ce)}"
)
return ce[0], user
def entropy_fields(ce_tx):
"""Return (digest, entropy_count, is_fallback) from a ConsensusEntropy tx.
consensus_fallback rounds carry a deterministic non-zero consensus-bound
digest with EntropyCount=0 and EntropyTier=1 (consensus_fallback).
Validator entropy has EntropyTier=3 (validator_quorum).
WARNING: is_fallback is ``tier != 3``, so it lumps participant_aligned
(Tier 2) in with fallback. It is only safe where no Tier 2 band exists
(e.g. 5-node networks, where tier2 == quorum). For band-aware scenarios use
the explicit assert_consensus_fallback / assert_participant_aligned /
assert_validator_quorum helpers, which check EntropyTier directly.
"""
digest = ce_tx.get("Digest", "")
entropy_count = ce_tx.get("EntropyCount", -1)
tier = ce_tx.get("EntropyTier", None)
if tier is not None:
is_fallback = tier != 3
else:
is_fallback = entropy_count == 0
return digest, entropy_count, is_fallback
def assert_participant_aligned(ce_tx, seq, expected_count=None):
"""Assert participant_aligned (Tier 2) entropy on a ConsensusEntropy tx.
Tier 2 is the sub-quorum band: the agreed reveal cohort is >= the
participant floor but < the 80% validator quorum, so it carries
EntropyTier=2 with a deterministic non-zero digest. NOTE entropy_fields()'s
is_fallback lumps tier 2 in with fallback (is_fallback = tier != 3), so the
tier must be checked EXPLICITLY here.
"""
digest = ce_tx.get("Digest", "")
count = ce_tx.get("EntropyCount", -1)
tier = ce_tx.get("EntropyTier", None)
if tier != 2:
raise AssertionError(
f"Ledger {seq}: expected EntropyTier==2 (participant_aligned), "
f"got {tier} (EntropyCount={count})"
)
if not digest or digest == ZERO_DIGEST:
raise AssertionError(
f"Ledger {seq}: participant_aligned digest must be non-zero, got "
f"{digest[:16]}..."
)
if expected_count is not None and count != expected_count:
raise AssertionError(
f"Ledger {seq}: participant_aligned EntropyCount must be "
f"{expected_count} (the surviving cohort), got {count}"
)
return digest, count
def assert_validator_quorum(ce_tx, seq, min_count=None):
"""Assert validator_quorum (Tier 3) entropy on a ConsensusEntropy tx:
EntropyTier=3, a deterministic non-zero digest, and (optionally)
EntropyCount >= min_count (the active quorum). The count can EXCEED the
quorum (e.g. a still-full 6/6 ledger caught at a 6->5 transition), so check
>=, not ==.
"""
digest = ce_tx.get("Digest", "")
count = ce_tx.get("EntropyCount", -1)
tier = ce_tx.get("EntropyTier", None)
if tier != 3:
raise AssertionError(
f"Ledger {seq}: expected EntropyTier==3 (validator_quorum), got "
f"{tier} (EntropyCount={count})"
)
if not digest or digest == ZERO_DIGEST:
raise AssertionError(
f"Ledger {seq}: validator_quorum digest must be non-zero, got "
f"{digest[:16]}..."
)
if min_count is not None and count < min_count:
raise AssertionError(
f"Ledger {seq}: validator_quorum EntropyCount={count} < quorum "
f"{min_count}"
)
return digest, count
def assert_consensus_fallback(ce_tx, seq):
"""Assert consensus_fallback (Tier 1) entropy on a ConsensusEntropy tx:
EntropyTier=1, EntropyCount=0, and a deterministic NON-zero digest.
"""
digest = ce_tx.get("Digest", "")
count = ce_tx.get("EntropyCount", -1)
tier = ce_tx.get("EntropyTier", None)
if tier != 1:
raise AssertionError(
f"Ledger {seq}: expected EntropyTier==1 (consensus_fallback), got "
f"{tier} (EntropyCount={count})"
)
if count != 0:
raise AssertionError(
f"Ledger {seq}: consensus_fallback EntropyCount must be 0, got "
f"{count}"
)
if not digest or digest == ZERO_DIGEST:
raise AssertionError(
f"Ledger {seq}: consensus_fallback digest must be non-zero, got "
f"{digest[:16]}..."
)
return digest, count
def assert_valid_entropy(ce_tx, seq, seen_digests=None):
"""Assert quorum-met validator entropy. Optionally check uniqueness."""
digest, entropy_count, is_fallback = entropy_fields(ce_tx)
if is_fallback or not digest or digest == ZERO_DIGEST:
raise AssertionError(f"Ledger {seq}: fallback/empty Digest")
if entropy_count < 4:
raise AssertionError(
f"Ledger {seq}: EntropyCount={entropy_count} < 4 (sub-quorum)"
)
if seen_digests is not None:
if digest in seen_digests:
raise AssertionError(f"Ledger {seq}: duplicate Digest {digest[:16]}...")
seen_digests.add(digest)
return digest, entropy_count

View File

@@ -1,92 +0,0 @@
defaults:
network:
node_count: 5
launcher: tmux
find_ports: true
slave_delay: 0.2
features:
- ConsensusEntropy
- Export
track_features:
- ConsensusEntropy
- Export
unl_report: true
log_levels:
TxQ: info
Protocol: debug
Peer: debug
LedgerConsensus: debug
ConsensusExtensions: debug
NetworkOPs: info
env:
XAHAU_RESOURCE_PER_PORT: "1"
rc:
- rng_poll_ms=250
tests:
- name: latency_baseline_ce
script: .testnet/scenarios/perf/ce_export_latency_probe.py
params:
warmup_ledgers: 3
ledgers: 8
submit_export: false
- name: latency_baseline_export
script: .testnet/scenarios/perf/ce_export_latency_probe.py
params:
warmup_ledgers: 3
ledgers: 8
submit_export: true
- name: latency_proposal_delay_export
script: .testnet/scenarios/perf/ce_export_latency_probe.py
params:
warmup_ledgers: 3
ledgers: 8
submit_export: true
network:
rc:
- rng_poll_ms=250
- delay=100,jitter=25,msg=proposal
- name: latency_directed_pair_delay_export
script: .testnet/scenarios/perf/ce_export_latency_probe.py
params:
warmup_ledgers: 3
ledgers: 8
submit_export: true
network:
rc:
- rng_poll_ms=250
- n0->n2:delay=750,jitter=100,msg=proposal
- n2->n0:delay=750,jitter=100,msg=proposal
- name: latency_slow_minority_export
script: .testnet/scenarios/perf/ce_export_latency_probe.py
params:
warmup_ledgers: 3
ledgers: 8
submit_export: true
export_timeout: 120
network:
rc:
- rng_poll_ms=250
- n3->n0:delay=500,jitter=100,msg=proposal
- n3->n1:delay=500,jitter=100,msg=proposal
- n3->n2:delay=500,jitter=100,msg=proposal
- n4->n0:delay=500,jitter=100,msg=proposal
- n4->n1:delay=500,jitter=100,msg=proposal
- n4->n2:delay=500,jitter=100,msg=proposal
- n0->n3:delay=500,jitter=100,msg=proposal
- n1->n3:delay=500,jitter=100,msg=proposal
- n2->n3:delay=500,jitter=100,msg=proposal
- n0->n4:delay=500,jitter=100,msg=proposal
- n1->n4:delay=500,jitter=100,msg=proposal
- n2->n4:delay=500,jitter=100,msg=proposal
- name: latency_export_no_veto_with_delay
script: .testnet/scenarios/export/export_no_veto_missing_observation.py
network:
rc:
- rng_poll_ms=250
- delay=300,jitter=100,msg=proposal
- n4:no_export_sig_hash=true

View File

@@ -1,196 +0,0 @@
""":descr: measure CE/export behavior while RuntimeConfig injects latency/drop.
The suite supplies runtime fault injection through network.rc. This scenario
does not mutate RuntimeConfig itself; it observes what the launched network does
under that condition and logs enough counters to compare variants.
"""
from __future__ import annotations
from collections import Counter
import json
from export.export_helpers import assert_export_result, require_export
from helpers import consensus_entropy_feature, get_entropy_tx
async def _require_runtime_config(ctx, log):
result = ctx.rpc.runtime_config(0)
if not result or result.get("error"):
raise AssertionError(
"Latency probe requires a binary built with "
"xahaud_runtime_test_config=ON; runtime_config RPC returned "
f"{result}"
)
log("RuntimeConfig RPC active")
async def _require_consensus_entropy(ctx, log):
feature = consensus_entropy_feature(ctx, node_id=0)
if not feature or not feature.get("enabled", False):
raise AssertionError(f"ConsensusEntropy not enabled: {feature}")
log("ConsensusEntropy enabled")
def _log_runtime_config(ctx, log):
for node_id in range(ctx.node_count):
cfg = ctx.rpc.runtime_config(node_id)
if cfg is None:
raise AssertionError(f"runtime_config RPC failed on node {node_id}")
log(
f"runtime_config n{node_id}: "
f"{json.dumps(cfg, sort_keys=True, separators=(',', ':'))}"
)
async def _submit_direct_export(ctx, log, *, timeout):
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
alice = ctx.account("alice")
bob = ctx.account("bob")
current_seq = ctx.validated_ledger_index(0)
if current_seq is None:
raise AssertionError("validated ledger is not available before Export")
log(f"Submitting direct Export at validated ledger {current_seq}")
started = ctx.mark("latency-export-submit-start")
result = await ctx.submit_and_wait(
{
"TransactionType": "Export",
"LastLedgerSequence": current_seq + 12,
"Fee": "1000000",
"ExportedTxn": {
"TransactionType": "Payment",
"Account": alice.address,
"Destination": bob.address,
"Amount": "1000000",
"Fee": "10",
"Sequence": 0,
"TicketSequence": 1,
"FirstLedgerSequence": current_seq + 1,
"LastLedgerSequence": current_seq + 10,
"Flags": 2147483648,
"SigningPubKey": "",
},
},
alice.wallet,
timeout=timeout,
)
ended = ctx.mark("latency-export-submit-end")
elapsed = (ended.monotonic_ns - started.monotonic_ns) / 1_000_000_000
engine_result = result.get("engine_result", "")
log(f"Export result={engine_result} elapsed={elapsed:.3f}s")
if engine_result != "tesSUCCESS":
raise AssertionError(f"Expected Export tesSUCCESS, got {engine_result}")
export_result = assert_export_result(result.get("meta", {}), log)
signers = export_result.get("ExportedTxn", {}).get("Signers", [])
log(f"Export signer count={len(signers)}")
return started, ended
def _summarize_logs(ctx, log, *, label, started, ended):
patterns = {
"rng_selected": r"RNG: entropy selected",
"rng_fallback": r"tier=1",
"rng_participant_aligned": r"tier=2",
"rng_validator_quorum": r"tier=3",
"export_retry": r"terRETRY_EXPORT",
"export_quorum_timeout": r"Export: exportSigSet quorum alignment timeout",
"export_missing_observation_ignored": (
r"Export: missing exportSigSetHash observation ignored"
),
}
for name, pattern in patterns.items():
result = ctx.search_logs(pattern, since=started, until=ended, limit=500)
log(f"log_count {label}.{name}={result.count}")
async def scenario(
ctx,
log,
*,
warmup_ledgers=3,
ledgers=8,
submit_export=False,
export_timeout=90,
):
await ctx.wait_for_ledger_close(timeout=120)
await _require_runtime_config(ctx, log)
_log_runtime_config(ctx, log)
await _require_consensus_entropy(ctx, log)
if submit_export:
# require_export also asserts the UNLReport precondition for successful
# network-mode Export. Keep that explicit in perf runs so a missing
# report does not masquerade as a latency failure.
await require_export(ctx, log, require_runtime_config=False)
await ctx.wait_for_ledgers(warmup_ledgers, node_id=0, timeout=120)
warm_seq = ctx.validated_ledger_index(0)
log(f"Warmup complete at validated ledger {warm_seq}")
export_window = None
if submit_export:
export_window = await _submit_direct_export(
ctx, log, timeout=export_timeout
)
started = ctx.mark("latency-probe-start")
start_seq = ctx.validated_ledger_index(0)
await ctx.wait_for_ledgers(ledgers, node_id=0, timeout=max(120, ledgers * 30))
ended = ctx.mark("latency-probe-end")
end_seq = ctx.validated_ledger_index(0)
if start_seq is None or end_seq is None:
raise AssertionError("validated ledger index unavailable during probe")
elapsed = (ended.monotonic_ns - started.monotonic_ns) / 1_000_000_000
closed = max(0, end_seq - start_seq)
cadence = elapsed / closed if closed else 0.0
log(
f"Observed validated ledgers {start_seq + 1}..{end_seq} "
f"closed={closed} elapsed={elapsed:.3f}s cadence={cadence:.3f}s/ledger"
)
tiers: Counter[int] = Counter()
counts: Counter[int] = Counter()
missing_entropy = 0
for seq in range(start_seq + 1, end_seq + 1):
try:
ce, user_txns = get_entropy_tx(ctx, seq)
except AssertionError as exc:
missing_entropy += 1
log(f" Ledger {seq}: no ConsensusEntropy tx ({exc})")
continue
tier = ce.get("EntropyTier", -1)
count = ce.get("EntropyCount", -1)
tiers[tier] += 1
counts[count] += 1
log(
f" Ledger {seq}: tier={tier} count={count} "
f"user_txns={len(user_txns)} digest={ce.get('Digest', '')[:16]}..."
)
log(
"SUMMARY "
f"closed={closed} elapsed_s={elapsed:.3f} cadence_s={cadence:.3f} "
f"tiers={dict(sorted(tiers.items()))} "
f"counts={dict(sorted(counts.items()))} "
f"missing_entropy={missing_entropy}"
)
_summarize_logs(ctx, log, label="probe", started=started, ended=ended)
if export_window is not None:
_summarize_logs(
ctx,
log,
label="export",
started=export_window[0],
ended=export_window[1],
)
log("PASS")

View File

@@ -1,62 +0,0 @@
defaults:
network:
node_count: 5
launcher: tmux
find_ports: true
slave_delay: 0.2
features:
- ConsensusEntropy
track_features:
- ConsensusEntropy
unl_report: true
log_levels:
TxQ: info
Protocol: debug
Peer: debug
LedgerConsensus: debug
ConsensusExtensions: debug
NetworkOPs: info
env:
XAHAU_RESOURCE_PER_PORT: "1"
rc:
- rng_poll_ms=333
tests:
- name: steady_state_entropy
script: .testnet/scenarios/entropy/steady_state_entropy.py
- name: fallback_without_unl_report
script: .testnet/scenarios/entropy/fallback_without_unl_report.py
network:
unl_report: false
- name: steady_state_entropy_fast_start
script: .testnet/scenarios/entropy/steady_state_entropy.py
network:
env:
XAHAUD_RUNTIME_TEST_CONFIG: '{"set":{"global":{"rng_poll_ms":333,"bootstrap_fast_start":true}}}'
- name: entropy_with_transactions
script: .testnet/scenarios/entropy/entropy_with_transactions.py
- name: quorum_recovery_smoke
script: .testnet/scenarios/entropy/quorum_recovery_smoke.py
- name: quorum_degradation_smoke
script: .testnet/scenarios/entropy/quorum_degradation_smoke.py
network:
log_levels:
LedgerConsensus: trace
ConsensusExtensions: trace
# Tier 2 (participant_aligned) needs 6 nodes: n=5 has no band (tier2 ==
# quorum). At 6, the 4/6 window is the participant_aligned band.
- name: participant_aligned_smoke
script: .testnet/scenarios/entropy/participant_aligned_smoke.py
network:
node_count: 6
log_levels:
LedgerConsensus: trace
ConsensusExtensions: trace
# Export scenarios: see export-suite.yml

View File

@@ -26,7 +26,7 @@ Loop: xrpld.app xrpld.nodestore
xrpld.app > xrpld.nodestore
Loop: xrpld.app xrpld.overlay
xrpld.overlay == xrpld.app
xrpld.overlay ~= xrpld.app
Loop: xrpld.app xrpld.peerfinder
xrpld.app > xrpld.peerfinder

View File

@@ -12,7 +12,6 @@ libxrpl.server > xrpl.basics
libxrpl.server > xrpl.json
libxrpl.server > xrpl.protocol
libxrpl.server > xrpl.server
test.app > test.shamap
test.app > test.toplevel
test.app > test.unit_test
test.app > xrpl.basics
@@ -22,7 +21,6 @@ test.app > xrpld.ledger
test.app > xrpld.nodestore
test.app > xrpld.overlay
test.app > xrpld.rpc
test.app > xrpld.shamap
test.app > xrpl.hook
test.app > xrpl.json
test.app > xrpl.protocol
@@ -45,7 +43,6 @@ test.consensus > xrpld.app
test.consensus > xrpld.consensus
test.consensus > xrpld.core
test.consensus > xrpld.ledger
test.consensus > xrpl.json
test.consensus > xrpl.protocol
test.core > test.jtx
test.core > test.toplevel
@@ -59,8 +56,6 @@ test.csf > xrpl.basics
test.csf > xrpld.consensus
test.csf > xrpl.json
test.csf > xrpl.protocol
test.formal_verification > xrpld.app
test.formal_verification > xrpld.consensus
test.json > test.jtx
test.json > xrpl.json
test.jtx > xrpl.basics

View File

@@ -2,7 +2,7 @@
**Note:** Throughout this README, references to "we" or "our" pertain to the community and contributors involved in the Xahau network. It does not imply a legal entity or a specific collection of individuals.
[Xahau](https://xahau.network/) is a decentralized cryptographic ledger that builds upon the robust foundation of the XRP Ledger. It inherits the XRP Ledger's Byzantine Fault Tolerant consensus algorithm under the normal XRPL assumptions about configured validator-list overlap, timing, and fault bounds, and enhances it with additional features and functionalities. Developers and users familiar with the XRP Ledger will find that most documentation and tutorials available on [xrpl.org](https://xrpl.org) are relevant and applicable to Xahau, including those related to running validators and managing validator keys. For Xahau specific documentation you can visit our [documentation](https://xahau.network/)
[Xahau](https://xahau.network/) is a decentralized cryptographic ledger that builds upon the robust foundation of the XRP Ledger. It inherits the XRP Ledger's Byzantine Fault Tolerant consensus algorithm and enhances it with additional features and functionalities. Developers and users familiar with the XRP Ledger will find that most documentation and tutorials available on [xrpl.org](https://xrpl.org) are relevant and applicable to Xahau, including those related to running validators and managing validator keys. For Xahau specific documentation you can visit our [documentation](https://xahau.network/)
## XAH
XAH is the public, counterparty-free asset native to Xahau and functions primarily as network gas. Transactions submitted to the Xahau network must supply an appropriate amount of XAH, to be burnt by the network as a fee, in order to be successfully included in a validated ledger. In addition, XAH also acts as a bridge currency within the Xahau DEX. XAH is traded on the open-market and is available for anyone to access. Xahau was created in 2023 with a supply of 600 million units of XAH.

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

@@ -160,18 +160,11 @@ target_link_modules(xrpl PUBLIC
# $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
# $<INSTALL_INTERFACE:include>)
if(formal_verification AND NOT xrpld)
message(FATAL_ERROR "formal_verification requires xrpld=ON")
endif()
if(xrpld)
add_executable(rippled)
if(tests)
target_compile_definitions(rippled PUBLIC ENABLE_TESTS)
endif()
if(xahaud_runtime_test_config)
target_compile_definitions(rippled PUBLIC XAHAUD_ENABLE_RUNTIME_TEST_CONFIG=1)
endif()
target_include_directories(rippled
PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
@@ -187,21 +180,6 @@ if(xrpld)
"${CMAKE_CURRENT_SOURCE_DIR}/src/test/*.cpp"
)
target_sources(rippled PRIVATE ${sources})
set(HOOKS_TEST_DIR "" CACHE PATH "External hook Env-test directory")
if(NOT HOOKS_TEST_DIR AND DEFINED ENV{HOOKS_TEST_DIR})
set(HOOKS_TEST_DIR "$ENV{HOOKS_TEST_DIR}")
endif()
if(HOOKS_TEST_DIR)
file(GLOB_RECURSE hook_test_sources CONFIGURE_DEPENDS
"${HOOKS_TEST_DIR}/*_test.cpp"
)
if(hook_test_sources)
message(STATUS "Including external hook Env tests from ${HOOKS_TEST_DIR}")
target_sources(rippled PRIVATE ${hook_test_sources})
target_include_directories(rippled PRIVATE "${HOOKS_TEST_DIR}")
endif()
endif()
endif()
target_link_libraries(rippled
@@ -215,7 +193,6 @@ if(xrpld)
# This is likely not strictly necessary, but listed explicitly as a good practice.
m
)
include(XahaudFormalVerification)
exclude_if_included(rippled)
# define a macro for tests that might need to
# be exluded or run differently in CI environment

View File

@@ -22,9 +22,6 @@ target_compile_definitions (opts
$<$<BOOL:${beast_no_unit_test_inline}>:BEAST_NO_UNIT_TEST_INLINE=1>
$<$<BOOL:${beast_disable_autolink}>:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1>
$<$<BOOL:${single_io_service_thread}>:RIPPLE_SINGLE_IO_SERVICE_THREAD=1>
# Enhanced logging is enabled for Debug builds, or explicitly via
# -DBEAST_ENHANCED_LOGGING=ON for other build types.
$<$<OR:$<CONFIG:Debug>,$<BOOL:${BEAST_ENHANCED_LOGGING}>>:BEAST_ENHANCED_LOGGING=1>
$<$<BOOL:${voidstar}>:ENABLE_VOIDSTAR>)
target_compile_options (opts
INTERFACE

View File

@@ -12,21 +12,6 @@ option(xrpld "Build xrpld" ON)
option(tests "Build tests" ON)
option(xahaud_runtime_test_config
"Enable XAHAUD_RUNTIME_TEST_CONFIG env and runtime_config RPC fault-injection controls"
OFF)
# Conan 2 local opt-in:
# [conf]
# tools.cmake.cmaketoolchain:extra_variables={"xahaud_runtime_test_config":"ON"}
option(formal_verification
"Enable Lean-backed formal-verification cross-check tests"
OFF)
# Default off: this pulls the Lean runtime and the vendored formal model into
# the test binary. Conan/local opt-in mirrors the runtime-test-config pattern:
# [conf]
# tools.cmake.cmaketoolchain:extra_variables={"formal_verification":"ON"}
option(unity "Creates a build using UNITY support in cmake. This is the default" ON)
if(unity)
if(NOT is_ci)

View File

@@ -1,65 +0,0 @@
if(NOT formal_verification)
return()
endif()
if(NOT xrpld)
message(FATAL_ERROR "formal_verification requires xrpld=ON")
endif()
if(NOT tests)
message(FATAL_ERROR "formal_verification requires tests=ON")
endif()
if(CMAKE_CROSSCOMPILING)
message(FATAL_ERROR "formal_verification currently supports native builds only")
endif()
if(WIN32)
message(FATAL_ERROR "formal_verification currently supports Unix-like native builds only")
endif()
set(XAHAU_FORMAL_VERIFICATION_DIR
"${CMAKE_CURRENT_SOURCE_DIR}/formal_verification"
CACHE PATH
"Lean formal-verification project used by formal_verification=ON")
include(XahaudLean)
xahaud_require_lean_toolchain("${XAHAU_FORMAL_VERIFICATION_DIR}")
set(XAHAU_FORMAL_ARCHIVE
"${XAHAU_FORMAL_VERIFICATION_DIR}/.lake/build/lib/libxahau__consensus_XahauConsensus.a")
file(GLOB_RECURSE XAHAU_FORMAL_SOURCES CONFIGURE_DEPENDS
"${XAHAU_FORMAL_VERIFICATION_DIR}/*.lean")
# Lake currently writes package artifacts under the Lean workspace's .lake/
# directory. Keep this option native/test-only until the build is moved to a
# copied CMake-binary-dir workspace or Lake grows a stable external build-dir
# interface we can rely on here.
#
# This target deliberately invokes Lake whenever the formal-enabled `rippled`
# target is built. Lake still performs its own incremental rebuild, but CMake
# must not trust a source-tree `.lake` archive purely by timestamp.
add_custom_target(xahaud_formal_verification_lean
COMMAND "${LAKE_EXECUTABLE}" build XahauConsensus:static
WORKING_DIRECTORY "${XAHAU_FORMAL_VERIFICATION_DIR}"
DEPENDS
"${XAHAU_FORMAL_VERIFICATION_DIR}/lakefile.toml"
"${XAHAU_FORMAL_VERIFICATION_DIR}/lean-toolchain"
"${XAHAU_FORMAL_VERIFICATION_DIR}/lake-manifest.json"
${XAHAU_FORMAL_SOURCES}
BYPRODUCTS "${XAHAU_FORMAL_ARCHIVE}"
COMMENT "Building Lean formal-verification archive"
VERBATIM)
add_dependencies(rippled xahaud_formal_verification_lean)
target_compile_definitions(rippled PRIVATE XAHAUD_ENABLE_FORMAL_VERIFICATION=1)
target_include_directories(rippled PRIVATE "${LEAN_INCLUDE_DIR}")
target_link_libraries(rippled "${XAHAU_FORMAL_ARCHIVE}" "${LEAN_SHARED_LIBRARY}")
if(UNIX)
set_property(TARGET rippled APPEND PROPERTY BUILD_RPATH "${LEAN_SYSROOT}/lib/lean")
endif()
message(STATUS "Formal verification enabled: ${XAHAU_FORMAL_VERIFICATION_DIR}")
message(STATUS "Lean ${LEAN_EXPECTED_VERSION} sysroot: ${LEAN_SYSROOT}")

View File

@@ -1,113 +0,0 @@
include_guard(GLOBAL)
function(xahaud_require_lean_toolchain project_dir)
if(NOT EXISTS "${project_dir}/lean-toolchain")
message(FATAL_ERROR "Lean project is missing lean-toolchain: ${project_dir}")
endif()
file(READ "${project_dir}/lean-toolchain" lean_toolchain)
string(STRIP "${lean_toolchain}" lean_toolchain)
if(NOT lean_toolchain MATCHES "^leanprover/lean4:v([0-9]+\\.[0-9]+\\.[0-9]+([-+._A-Za-z0-9]+)?)$")
message(FATAL_ERROR
"Unsupported lean-toolchain format `${lean_toolchain}` in ${project_dir}")
endif()
set(expected_lean_version "${CMAKE_MATCH_1}")
find_program(LAKE_EXECUTABLE
NAMES lake
HINTS "$ENV{HOME}/.elan/bin")
if(NOT LAKE_EXECUTABLE)
message(FATAL_ERROR
"formal_verification=ON requires Lake on PATH or in ~/.elan/bin. "
"Install elan, then run `lake build` once in ${project_dir}.")
endif()
execute_process(
COMMAND "${LAKE_EXECUTABLE}" env lean --version
WORKING_DIRECTORY "${project_dir}"
OUTPUT_VARIABLE lean_version_output
ERROR_VARIABLE lean_version_error
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE lean_version_result)
if(NOT lean_version_result EQUAL 0)
message(FATAL_ERROR
"Could not run `${LAKE_EXECUTABLE} env lean --version`: "
"${lean_version_error}")
endif()
if(NOT lean_version_output MATCHES "^Lean \\(version ([^,)]+)[,)]")
message(FATAL_ERROR
"Could not parse Lean version from `${lean_version_output}`")
endif()
set(actual_lean_version "${CMAKE_MATCH_1}")
if(NOT actual_lean_version STREQUAL expected_lean_version)
message(FATAL_ERROR
"Lean version mismatch for formal_verification=ON. "
"Expected ${expected_lean_version} from ${project_dir}/lean-toolchain, "
"but `${LAKE_EXECUTABLE} env lean --version` returned "
"`${lean_version_output}`")
endif()
execute_process(
COMMAND "${LAKE_EXECUTABLE}" --version
WORKING_DIRECTORY "${project_dir}"
OUTPUT_VARIABLE lake_version_output
ERROR_VARIABLE lake_version_error
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE lake_version_result)
if(NOT lake_version_result EQUAL 0)
message(FATAL_ERROR
"Could not run `${LAKE_EXECUTABLE} --version`: ${lake_version_error}")
endif()
if(NOT lake_version_output MATCHES "Lean version ([^)]+)\\)")
message(FATAL_ERROR
"Could not parse Lake's Lean version from `${lake_version_output}`")
endif()
set(lake_lean_version "${CMAKE_MATCH_1}")
if(NOT lake_lean_version STREQUAL expected_lean_version)
message(FATAL_ERROR
"Lake version mismatch for formal_verification=ON. "
"Expected Lean ${expected_lean_version} from ${project_dir}/lean-toolchain, "
"but `${LAKE_EXECUTABLE} --version` returned `${lake_version_output}`")
endif()
if(NOT EXISTS "${project_dir}/lakefile.toml")
message(FATAL_ERROR
"formal_verification=ON requires ${project_dir}/lakefile.toml")
endif()
execute_process(
COMMAND "${LAKE_EXECUTABLE}" env printenv LEAN_SYSROOT
WORKING_DIRECTORY "${project_dir}"
OUTPUT_VARIABLE lean_sysroot
ERROR_VARIABLE lean_sysroot_error
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE lean_sysroot_result)
if(NOT lean_sysroot_result EQUAL 0 OR NOT lean_sysroot)
message(FATAL_ERROR
"Could not determine Lean sysroot via "
"`${LAKE_EXECUTABLE} env printenv LEAN_SYSROOT`: ${lean_sysroot_error}")
endif()
set(lean_include_dir "${lean_sysroot}/include")
if(NOT EXISTS "${lean_include_dir}/lean/lean.h")
message(FATAL_ERROR "Lean header not found: ${lean_include_dir}/lean/lean.h")
endif()
find_library(lean_shared_library
NAMES leanshared libleanshared
PATHS "${lean_sysroot}/lib/lean"
NO_DEFAULT_PATH)
if(NOT lean_shared_library)
message(FATAL_ERROR
"Lean shared runtime not found under ${lean_sysroot}/lib/lean")
endif()
set(LAKE_EXECUTABLE "${LAKE_EXECUTABLE}" PARENT_SCOPE)
set(LEAN_SYSROOT "${lean_sysroot}" PARENT_SCOPE)
set(LEAN_INCLUDE_DIR "${lean_include_dir}" PARENT_SCOPE)
set(LEAN_SHARED_LIBRARY "${lean_shared_library}" PARENT_SCOPE)
set(LEAN_EXPECTED_VERSION "${expected_lean_version}" PARENT_SCOPE)
endfunction()

View File

@@ -1,5 +1,4 @@
from conan import ConanFile
from conan.errors import ConanInvalidConfiguration
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
import re
@@ -15,7 +14,6 @@ class Xrpl(ConanFile):
'assertions': [True, False],
'coverage': [True, False],
'fPIC': [True, False],
'formal_verification': [True, False],
'jemalloc': [True, False],
'rocksdb': [True, False],
'shared': [True, False],
@@ -47,7 +45,6 @@ class Xrpl(ConanFile):
'assertions': False,
'coverage': False,
'fPIC': True,
'formal_verification': False,
'jemalloc': False,
'rocksdb': True,
'shared': False,
@@ -113,14 +110,6 @@ class Xrpl(ConanFile):
if self.settings.compiler == 'apple-clang':
self.options['boost/*'].visibility = 'global'
def validate(self):
if self.options.formal_verification and (
not self.options.tests or not self.options.xrpld
):
raise ConanInvalidConfiguration(
'formal_verification=True requires tests=True and xrpld=True'
)
def requirements(self):
# Force sqlite3 version to avoid conflicts with soci
self.requires('sqlite3/3.47.0', override=True)
@@ -143,18 +132,6 @@ class Xrpl(ConanFile):
'cfg/*',
'cmake/*',
'external/*',
'formal_verification/*.json',
'formal_verification/*.lean',
'formal_verification/*.md',
'formal_verification/*.toml',
'formal_verification/lean-toolchain',
'formal_verification/XahauConsensus/*.lean',
'!formal_verification/.lake',
'!formal_verification/.lake/*',
'!formal_verification/.lake/**',
'!formal_verification/**/.lake',
'!formal_verification/**/.lake/*',
'!formal_verification/**/.lake/**',
'include/*',
'src/*',
)
@@ -171,7 +148,6 @@ class Xrpl(ConanFile):
tc.variables['tests'] = self.options.tests
tc.variables['assert'] = self.options.assertions
tc.variables['coverage'] = self.options.coverage
tc.variables['formal_verification'] = self.options.formal_verification
tc.variables['jemalloc'] = self.options.jemalloc
tc.variables['rocksdb'] = self.options.rocksdb
tc.variables['BUILD_SHARED_LIBS'] = self.options.shared
@@ -187,11 +163,6 @@ class Xrpl(ConanFile):
cmake.build()
def package(self):
if self.options.formal_verification:
raise ConanInvalidConfiguration(
'formal_verification=True is a local/CI test build option and '
'is not supported for Conan packages'
)
cmake = CMake(self)
cmake.verbose = True
cmake.install()

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
/.lake

View File

@@ -1,166 +0,0 @@
# xahau_consensus
Lean proofs for small Xahau consensus invariants.
This package is intentionally narrow. It does **not** try to verify the C++
implementation directly. It mirrors small formulas and decision ladders from
the consensus-extension code so the safety arguments can be checked as theorems
instead of repeatedly re-derived in review notes.
Current modules:
- `XahauConsensus.Threshold`
- mirrors `calculateParticipantThreshold`
- proves the Tier-2 intersection inequality:
`count + floor(count / 5) < 2 * participantThreshold count`
- proves the threshold is minimal for that strict inequality
- proves the original-view threshold remains safe when nUNL shrinks the
effective view
- includes the `original=10`, `effective=8` regression example showing why
using the effective view for the Tier-2 floor is forkable
- proves `participantThreshold count <= quorumThreshold count` for
non-empty views
- distinguishes raw formula helpers from the live safety-wrapped gate
thresholds used by `ConsensusExtensions`
- `XahauConsensus.ThresholdFacts`
- records small-network values and band-empty/band-present examples
- proves exact multiple-of-five behavior
- proves threshold monotonicity facts
- `XahauConsensus.SixtyPercent`
- defines a naive `ceil(60%)` threshold
- proves naive 60% is unsafe at exact multiples of five
- proves the live derived floor is one higher there and restores strict
intersection safety
- `XahauConsensus.Intersection`
- proves the abstract cardinality argument behind quorum intersection
- shows two threshold-sized cohorts must overlap above the fault bound
whenever `n + f < 2t`
- specializes that argument to the live participant threshold, including
nUNL-shrunk effective views
- `XahauConsensus.HonestOverlap`
- bridges overlap arithmetic to the consensus claim that two cohorts share at
least one honest validator
- specializes that bridge to the participant threshold and `floor(n/5)` fault
bound
- `XahauConsensus.ViewUniverse`
- proves original-view anchoring remains safe under nUNL shrink
- separates strict safety from threshold reachability
- defines cross-view participant-band presence/absence
- shows effective-view thresholds can be unsafe against the original fault
bound
- shows trusted-superset counting universes erode the intersection margin
- `XahauConsensus.NunlCap`
- models the protocol's ceil-25% nUNL disablement cap
- proves 8/6 and 10/8 band collapse examples
- records that 10 at max cap has effective view 7, below the original
participant floor
- records the important counterexample: original `20`, effective `15` does
**not** make validator quorum meet the original participant floor
- `XahauConsensus.SidecarAlignment`
- models aligned participant counting for sidecar hashes
- proves non-active peers and non-active local publication cannot pad the
alignment count
- proves changing nonmember reports cannot change quorum alignment
- `XahauConsensus.EntropySelector`
- models the tier-label ladder from `ConsensusExtensions::selectEntropy`
- proves non-UNLReport views select fallback
- proves the quorum / participant / fallback bands select the expected tier
- `XahauConsensus.SelectorDeterminism`
- models labeled digest output
- proves digest payload bytes do not affect the label when consensus metadata
is fixed
- records examples where changing view provenance or view sizes changes labels
- `XahauConsensus.ExportGate`
- models export's quorum-aligned success rule
- models export's sidecar-gate outcome as `proceed` or `retryOrExpire`, with
no deterministic fallback signature set
- proves missing minority observation does not block a quorum-aligned export
- proves `fullObservation` alone cannot change the export decision
- `XahauConsensus.ExportQuorum`
- proves two 80% export quorums overlap above the standard Byzantine bound
in nonempty active universes
- proves export quorum overlap remains above the original-view Byzantine
bound when nUNL shrinkage is within the protocol cap
- proves Byzantine validators at the standard bound cannot veto quorum
- records concrete overlap margins for 5/10/20-validator universes
- `XahauConsensus.FinsetIntersection`
- uses Mathlib finite sets to prove the cardinality premise behind the
arithmetic intersection theorems
- specializes that bridge for Tier-2 cohorts, nUNL-shrunk cohorts, and export
80% quorums
- `XahauConsensus.Invariants`
- restates cross-module design contracts in one place
- pins the live safety-wrapped threshold relationship
- proves the cross-view entropy gate is exactly the selector's non-fallback
boundary
- pins non-UNLReport fallback and export full-observation independence
Run:
```sh
~/.elan/bin/lake build
```
## Optional C++ cross-checks
The xahaud CMake build can also compile a Lean-backed unit-test path, but it is
off by default and is not part of normal release builds:
Install Lean through `elan` first. The CMake integration intentionally keeps the
tooling rule simple: when `formal_verification=ON`, it looks for `lake` on
`PATH` or in `~/.elan/bin`, asks that Lake environment to run `lean --version`,
verifies the exact version specified by this package's `lean-toolchain`, then
asks Lake for `LEAN_SYSROOT` and checks that `lean.h` and `libleanshared`
exist.
```sh
conan install . --output-folder=build-formal --build=missing \
-s build_type=Release \
-o '&:tests=True' \
-o '&:xrpld=True' \
-o '&:formal_verification=True'
cmake -S . -B build-formal-cmake \
-DCMAKE_TOOLCHAIN_FILE=$PWD/build-formal/build/generators/conan_toolchain.cmake \
-Dtests=ON \
-Dxrpld=ON \
-Dformal_verification=ON
cmake --build build-formal-cmake --target rippled
./build-formal-cmake/rippled --unittest=LeanConsensus
```
This path currently supports native test builds only. It builds
`XahauConsensus:static`, links the resulting Lean archive and runtime into the
test binary, and runs C++ drift tests over selected scalar formulas and helper
predicates. Some checks compare directly to named production helpers; others are
review-oriented safety predicates computed from those helpers. The exported
surface is intentionally scalar and reviewable:
- Byzantine bound, participant threshold, and validator quorum threshold.
- The safety-wrapped zero-view thresholds used by the live gates.
- The cross-view entropy gate threshold, with effective and original view
denominators kept separate.
- The entropy tier selector policy for `(fromUNLReport, participantCount,
effectiveView, originalView)`.
- Sidecar aligned-participant counting, full-observation, quorum-aligned
predicates, and active-view mask-counting samples.
- Export's quorum-only sidecar-gate proceed predicate, where `fullObservation`
is diagnostic rather than success-gating; a small final-apply snapshot model
makes explicit that gate proceed is not the same as closed-ledger
`Export::doApply` success.
- NegativeUNL cap/effective-view arithmetic.
- View-universe safety predicates and naive-60% regression anchors.
This is still a model-to-code cross-check, not a proof that the C++ implements
the Lean model. Its value is narrower and practical: if a production formula,
decision ladder, or helper predicate changes without the formal model changing
too, the gated unit test fails. The formal CMake target invokes Lake on each
formal-enabled `rippled` build and lets Lake decide whether its own artifacts
are current; CMake does not trust an existing source-tree archive by timestamp.
Lake still writes build artifacts under the Lean workspace's `.lake/`
directory, and the Conan recipe intentionally excludes that directory from
exported sources, so keep this option as a local/CI confidence build rather
than a release packaging input. The Conan recipe rejects
`formal_verification=True` unless `tests=True` and `xrpld=True`, and refuses to
package formal-enabled builds.

View File

@@ -1,32 +0,0 @@
# Xahau Lean Roadmap
This package should stay focused on invariants that are compact enough to be
reviewable and stable enough to mirror from C++.
Good targets:
1. Threshold arithmetic
- Tier-2 participant threshold formula
- quorum threshold relation
- nUNL original-view anchoring
- small-network boundary examples
2. Sidecar alignment
- active-view-only counting
- quorum-aligned predicate
- full-observation as diagnostic vs success precondition where applicable
3. Entropy selector
- non-UNLReport fallback
- tier ladder from agreed participant count
- no local pending-state dependency in the tier decision
4. Export gate
- quorum-aligned success without full observation
- no deterministic fallback value
- retry/expire as liveness behavior, not ledger-content substitution
Poor targets for this package:
- direct verification of C++ implementation details
- wall-clock timing and network scheduling liveness
- full ledger execution semantics
Those belong in C++ tests, CSF/testnet scenarios, or a dedicated temporal model.

View File

@@ -1,17 +0,0 @@
-- This module serves as the root of the `XahauConsensus` library.
-- Import modules here that should be built as part of the library.
import XahauConsensus.Threshold
import XahauConsensus.ThresholdFacts
import XahauConsensus.SixtyPercent
import XahauConsensus.Intersection
import XahauConsensus.HonestOverlap
import XahauConsensus.ViewUniverse
import XahauConsensus.NunlCap
import XahauConsensus.SidecarAlignment
import XahauConsensus.EntropySelector
import XahauConsensus.SelectorDeterminism
import XahauConsensus.ExportGate
import XahauConsensus.ExportQuorum
import XahauConsensus.FinsetIntersection
import XahauConsensus.Invariants
import XahauConsensus.FFI

View File

@@ -1,74 +0,0 @@
import XahauConsensus.Threshold
namespace XahauConsensus
inductive EntropyTier where
| consensusFallback
| participantAligned
| validatorQuorum
deriving DecidableEq, Repr
/-- Minimal model of `ConsensusExtensions::selectEntropy`'s network,
non-failed, non-empty tier ladder.
The real C++ also computes a digest. This model deliberately focuses on the
part that can fork by labeling the same agreed set differently: the tier
decision from `(fromUNLReport, participantCount, effectiveView, originalView)`.
It does not model the standalone development shortcut, timeout-driven
`entropyFailed_` downgrade, or empty-map fallback; those paths all bypass or
downgrade this ladder rather than producing a stronger non-fallback label.
-/
def selectEntropyTier
(fromUNLReport : Bool)
(participantCount effectiveView originalView : Nat) : EntropyTier :=
if !fromUNLReport then
EntropyTier.consensusFallback
else if participantCount >= safeQuorumThreshold effectiveView then
EntropyTier.validatorQuorum
else if participantCount >= safeParticipantThreshold originalView then
EntropyTier.participantAligned
else
EntropyTier.consensusFallback
/-- Non-standalone nodes must fail closed to fallback until the validator view
is ledger-anchored by a UNLReport. -/
theorem no_unl_report_selects_fallback
(participantCount effectiveView originalView : Nat) :
selectEntropyTier false participantCount effectiveView originalView =
EntropyTier.consensusFallback := by
rfl
/-- At or above the effective-view quorum threshold, the ladder selects the
strongest entropy tier. -/
theorem quorum_count_selects_validator_quorum
{participantCount effectiveView originalView : Nat}
(hQuorum : safeQuorumThreshold effectiveView <= participantCount) :
selectEntropyTier true participantCount effectiveView originalView =
EntropyTier.validatorQuorum := by
unfold selectEntropyTier
simp [hQuorum]
/-- Below validator quorum but at or above the original-view participant floor,
the ladder selects Tier 2. -/
theorem participant_band_selects_tier2
{participantCount effectiveView originalView : Nat}
(hBelowQuorum : participantCount < safeQuorumThreshold effectiveView)
(hParticipant : safeParticipantThreshold originalView <= participantCount) :
selectEntropyTier true participantCount effectiveView originalView =
EntropyTier.participantAligned := by
unfold selectEntropyTier
simp [Nat.not_le_of_gt hBelowQuorum, hParticipant]
/-- Below both thresholds, the ladder falls back. -/
theorem below_participant_floor_selects_fallback
{participantCount effectiveView originalView : Nat}
(hBelowQuorum : participantCount < safeQuorumThreshold effectiveView)
(hBelowParticipant : participantCount < safeParticipantThreshold originalView) :
selectEntropyTier true participantCount effectiveView originalView =
EntropyTier.consensusFallback := by
unfold selectEntropyTier
simp [
Nat.not_le_of_gt hBelowQuorum,
Nat.not_le_of_gt hBelowParticipant]
end XahauConsensus

View File

@@ -1,139 +0,0 @@
namespace XahauConsensus
/-- Minimal model of the sidecar export gate.
`alignedParticipants` is the number of participants observed on the export
sidecar, `quorumThreshold` is the required aligned count, and
`fullObservation` records whether every participant was observed. The C++ gate
must use quorum alignment for success; full observation is only diagnostic.
-/
structure ExportGate where
alignedParticipants : Nat
quorumThreshold : Nat
fullObservation : Bool
deriving DecidableEq, Repr
/-- Export sidecar-gate outcome. This is not the final `Export::doApply`
result: closed-ledger apply re-validates the frozen agreed signature snapshot
before it can create a shadow ticket. -/
inductive ExportOutcome where
| proceed
| retryOrExpire
deriving DecidableEq, Repr
/-- The success predicate used by export: enough participants are aligned. -/
def ExportGate.quorumAligned (gate : ExportGate) : Bool :=
decide (gate.quorumThreshold <= gate.alignedParticipants)
/-- Export proceeds exactly when quorum alignment is met. -/
def ExportGate.proceed (gate : ExportGate) : Bool :=
gate.quorumAligned
/-- Export's externally visible decision shape. -/
def ExportGate.outcome (gate : ExportGate) : ExportOutcome :=
if gate.proceed then ExportOutcome.proceed else ExportOutcome.retryOrExpire
/-- Minimal model of the additional closed-ledger apply preconditions.
The sidecar gate only proves that one `exportSigSetHash` had quorum alignment.
Network-mode `Export::doApply` then independently requires a ledger-anchored
validator view, no convergence failure for the round, a frozen agreed sidecar
map, a parseable/valid signature set, and enough verified signers in that map.
The model intentionally excludes cryptography and metadata construction; it
exists to prevent reading `ExportGate.proceed` as final apply success.
-/
structure ExportApplySnapshot where
fromUNLReport : Bool
convergenceFailed : Bool
agreedSetPresent : Bool
agreedSetValid : Bool
signerCount : Nat
quorumThreshold : Nat
deriving DecidableEq, Repr
/-- Closed-ledger apply can use only a valid, frozen agreed sidecar snapshot. -/
def ExportApplySnapshot.validAgreedSnapshot
(snapshot : ExportApplySnapshot) : Bool :=
snapshot.fromUNLReport &&
!snapshot.convergenceFailed &&
snapshot.agreedSetPresent &&
snapshot.agreedSetValid &&
decide (snapshot.quorumThreshold <= snapshot.signerCount)
/-- Minimal network-mode apply decision: valid agreed snapshot applies; all
other cases retry or expire. -/
def ExportApplySnapshot.outcome
(snapshot : ExportApplySnapshot) : ExportOutcome :=
if snapshot.validAgreedSnapshot then
ExportOutcome.proceed
else
ExportOutcome.retryOrExpire
theorem apply_success_iff_valid_agreed_snapshot
(snapshot : ExportApplySnapshot) :
snapshot.outcome = ExportOutcome.proceed
snapshot.validAgreedSnapshot = true := by
unfold ExportApplySnapshot.outcome
by_cases h : snapshot.validAgreedSnapshot <;> simp [h]
/-- Gate success alone is not final apply success. For example, the sidecar
gate may have quorum alignment while the final apply path has no frozen agreed
sidecar map available and therefore retries. -/
theorem gate_proceed_does_not_imply_apply_success :
gate : ExportGate, snapshot : ExportApplySnapshot,
ExportGate.proceed gate = true
ExportApplySnapshot.outcome snapshot =
ExportOutcome.retryOrExpire := by
refine
ExportGate.mk 4 4 false,
ExportApplySnapshot.mk true false false true 4 4,
?_,
?_ <;> rfl
/-- A missing minority, represented by `fullObservation = false`, does not
prevent export when the quorum threshold is met. -/
theorem missing_minority_does_not_prevent_proceed
{alignedParticipants quorumThreshold : Nat}
(hQuorum : quorumThreshold <= alignedParticipants) :
(ExportGate.mk alignedParticipants quorumThreshold false).proceed = true := by
unfold ExportGate.proceed ExportGate.quorumAligned
simp [hQuorum]
theorem missing_minority_proceeds
{alignedParticipants quorumThreshold : Nat}
(hQuorum : quorumThreshold <= alignedParticipants) :
(ExportGate.mk alignedParticipants quorumThreshold false).outcome =
ExportOutcome.proceed := by
unfold ExportGate.outcome
simp [missing_minority_does_not_prevent_proceed hQuorum]
/-- Export must not proceed below the aligned-participant quorum threshold. -/
theorem below_quorum_does_not_proceed
{alignedParticipants quorumThreshold : Nat}
(fullObservation : Bool)
(hBelow : alignedParticipants < quorumThreshold) :
(ExportGate.mk alignedParticipants quorumThreshold fullObservation).proceed =
false := by
unfold ExportGate.proceed ExportGate.quorumAligned
simp [Nat.not_le_of_gt hBelow]
/-- Below quorum, export retries or expires. There is no deterministic fallback
signature set analogous to RNG's Tier 1 fallback digest. -/
theorem below_quorum_retries_or_expires
{alignedParticipants quorumThreshold : Nat}
(fullObservation : Bool)
(hBelow : alignedParticipants < quorumThreshold) :
(ExportGate.mk alignedParticipants quorumThreshold fullObservation).outcome =
ExportOutcome.retryOrExpire := by
unfold ExportGate.outcome
simp [below_quorum_does_not_proceed fullObservation hBelow]
/-- Flipping only the diagnostic `fullObservation` field cannot change the
export decision. -/
theorem changing_fullObservation_alone_does_not_change_proceed
(alignedParticipants quorumThreshold : Nat) :
(ExportGate.mk alignedParticipants quorumThreshold true).proceed =
(ExportGate.mk alignedParticipants quorumThreshold false).proceed := by
rfl
end XahauConsensus

View File

@@ -1,254 +0,0 @@
import XahauConsensus.Intersection
import XahauConsensus.NunlCap
import XahauConsensus.ThresholdFacts
namespace XahauConsensus
/-!
Nat-cardinality arithmetic for export sidecar quorum uniqueness.
The model deliberately stays at the level used by `Intersection.lean`:
* `n` is the active validator universe size.
* `a` and `b` are the numbers of validators supporting two export sidecar
hashes in that same universe.
* `overlap` is the size of the intersection between those two support sets.
* `faultyOverlap + honestOverlap = overlap` splits that intersection.
No `Finset` structure is needed here; callers supply the usual
inclusion-exclusion cardinality inequality `a + b <= n + overlap`.
-/
theorem disabled_le_cap_mul_four_le
{originalView disabled : Nat}
(hCap : disabled <= disabledCap originalView) :
disabled * 4 <= originalView + 3 := by
unfold disabledCap ceilDiv at hCap
have hFour : 0 < 4 := by decide
simp at hCap
have hMul :=
(Nat.le_div_iff_mul_le hFour).mp hCap
omega
theorem quorumThreshold_mul_five_ge_four_mul (n : Nat) :
4 * n <= 5 * quorumThreshold n := by
unfold quorumThreshold
have hHundred : 0 < 100 := by decide
have hDiv :
(n * 80 + 99) / 100 <= (n * 80 + 99) / 100 :=
Nat.le_refl _
have hBound :=
(Nat.div_le_iff_le_mul hHundred).mp hDiv
omega
theorem byzantineBound_mul_five_le (n : Nat) :
byzantineBound n * 5 <= n := by
unfold byzantineBound
exact Nat.div_mul_le_self n 5
/-- Two 80% export quorums in one active universe overlap by at least
`2 * quorumThreshold n - n`. -/
theorem two_export_quorums_overlap_lower_bound
{n a b overlap : Nat}
(hCardinality : a + b <= n + overlap)
(hA : quorumThreshold n <= a)
(hB : quorumThreshold n <= b) :
2 * quorumThreshold n - n <= overlap := by
omega
/-- The 80% quorum threshold is intersection-safe against the standard
`floor(n / 5)` fault bound for every nonempty active universe. -/
theorem quorumThreshold_intersection_safe
{n : Nat} (hPositive : 0 < n) :
n + byzantineBound n < 2 * quorumThreshold n := by
unfold quorumThreshold byzantineBound
omega
/-- The unconditional version is false: the empty active universe has raw
quorum threshold zero, so there is no strict intersection margin. -/
theorem quorumThreshold_empty_not_intersection_safe :
¬ 0 + byzantineBound 0 < 2 * quorumThreshold 0 := by
native_decide
/-- Two export sidecar hashes both clearing 80% quorum in the same nonempty
active universe must have overlap larger than the standard fault bound. -/
theorem export_hash_quorums_overlap_gt_byzantine
{n a b overlap : Nat}
(hPositive : 0 < n)
(hCardinality : a + b <= n + overlap)
(hA : quorumThreshold n <= a)
(hB : quorumThreshold n <= b) :
byzantineBound n < overlap := by
exact overlap_gt_fault_of_two_threshold_cohorts
hCardinality
hA
hB
(quorumThreshold_intersection_safe hPositive)
/-- If the overlap between two quorum-clearing export hashes is split into
faulty and honest validators, and at most `floor(n / 5)` validators in that
overlap are faulty, then the overlap contains an honest validator. -/
theorem export_hash_quorums_force_honest_overlap
{n a b overlap faultyOverlap honestOverlap : Nat}
(hPositive : 0 < n)
(hCardinality : a + b <= n + overlap)
(hA : quorumThreshold n <= a)
(hB : quorumThreshold n <= b)
(hSplit : overlap = faultyOverlap + honestOverlap)
(hFaulty : faultyOverlap <= byzantineBound n) :
0 < honestOverlap := by
have hOverlap :
byzantineBound n < overlap :=
export_hash_quorums_overlap_gt_byzantine
hPositive
hCardinality
hA
hB
omega
/-- Export quorum intersection remains above the original-view Byzantine bound
when nUNL shrinkage is within the protocol's ceil-25% cap. -/
theorem export_quorum_intersection_safe_under_nunl_cap
{originalView effectiveView disabled : Nat}
(hEffective : effectiveView = originalView - disabled)
(hCap : disabled <= disabledCap originalView)
(hPositive : 0 < effectiveView) :
effectiveView + byzantineBound originalView <
2 * quorumThreshold effectiveView := by
have hCapBound :
disabled * 4 <= originalView + 3 :=
disabled_le_cap_mul_four_le hCap
have hQuorumBound :
4 * effectiveView <= 5 * quorumThreshold effectiveView :=
quorumThreshold_mul_five_ge_four_mul effectiveView
have hByzBound :
byzantineBound originalView * 5 <= originalView :=
byzantineBound_mul_five_le originalView
omega
/-- Two export sidecar hashes both clearing 80% quorum in an nUNL-shrunk
effective view must still overlap above the original-view Byzantine bound,
provided the shrinkage stays within the protocol cap. -/
theorem export_hash_quorums_overlap_gt_original_byzantine_under_nunl_cap
{originalView effectiveView disabled a b overlap : Nat}
(hEffective : effectiveView = originalView - disabled)
(hCap : disabled <= disabledCap originalView)
(hPositive : 0 < effectiveView)
(hCardinality : a + b <= effectiveView + overlap)
(hA : quorumThreshold effectiveView <= a)
(hB : quorumThreshold effectiveView <= b) :
byzantineBound originalView < overlap := by
exact overlap_gt_fault_of_two_threshold_cohorts
hCardinality
hA
hB
(export_quorum_intersection_safe_under_nunl_cap
hEffective
hCap
hPositive)
/-- A Byzantine minority at the standard bound cannot veto export quorum:
after removing `floor(n / 5)` validators, enough validators remain to meet the
80% quorum threshold. -/
theorem byzantineBound_cannot_veto_quorum (n : Nat) :
byzantineBound n + quorumThreshold n <= n := by
unfold byzantineBound quorumThreshold
omega
/-- Equivalent no-veto form using subtraction. -/
theorem quorumThreshold_le_universe_minus_byzantineBound (n : Nat) :
quorumThreshold n <= n - byzantineBound n := by
have hNoVeto := byzantineBound_cannot_veto_quorum n
omega
/-- Concrete regression anchor: in a 5-validator active universe, two 80%
export quorums overlap in at least three validators. -/
theorem export_quorum_five_overlap_at_least_three
{a b overlap : Nat}
(hCardinality : a + b <= 5 + overlap)
(hA : quorumThreshold 5 <= a)
(hB : quorumThreshold 5 <= b) :
3 <= overlap := by
have hLower :
2 * quorumThreshold 5 - 5 <= overlap :=
two_export_quorums_overlap_lower_bound
hCardinality
hA
hB
have hExact : 2 * quorumThreshold 5 - 5 = 3 := by
native_decide
omega
/-- Concrete regression anchor: in a 10-validator active universe, two 80%
export quorums overlap in at least six validators. -/
theorem export_quorum_ten_overlap_at_least_six
{a b overlap : Nat}
(hCardinality : a + b <= 10 + overlap)
(hA : quorumThreshold 10 <= a)
(hB : quorumThreshold 10 <= b) :
6 <= overlap := by
have hLower :
2 * quorumThreshold 10 - 10 <= overlap :=
two_export_quorums_overlap_lower_bound
hCardinality
hA
hB
have hExact : 2 * quorumThreshold 10 - 10 = 6 := by
native_decide
omega
/-- Concrete regression anchor: in a 20-validator active universe, two 80%
export quorums overlap in at least twelve validators. -/
theorem export_quorum_twenty_overlap_at_least_twelve
{a b overlap : Nat}
(hCardinality : a + b <= 20 + overlap)
(hA : quorumThreshold 20 <= a)
(hB : quorumThreshold 20 <= b) :
12 <= overlap := by
have hLower :
2 * quorumThreshold 20 - 20 <= overlap :=
two_export_quorums_overlap_lower_bound
hCardinality
hA
hB
have hExact : 2 * quorumThreshold 20 - 20 = 12 := by
native_decide
omega
/-- On exact multiples of five, two 80% export quorums overlap in at least
`3 * k` validators. -/
theorem export_quorum_five_mul_overlap_at_least_three_mul
{k a b overlap : Nat}
(hCardinality : a + b <= 5 * k + overlap)
(hA : quorumThreshold (5 * k) <= a)
(hB : quorumThreshold (5 * k) <= b) :
3 * k <= overlap := by
have hLower :
2 * quorumThreshold (5 * k) - 5 * k <= overlap :=
two_export_quorums_overlap_lower_bound
hCardinality
hA
hB
rw [quorumThreshold_five_mul] at hLower
omega
/-- On exact multiples of five, quorum overlap strictly exceeds the standard
fault bound by at least `2 * k`. For `k = 0` this is only a non-strict
difference statement; strict safety is provided by
`export_hash_quorums_overlap_gt_byzantine` for nonempty universes. -/
theorem export_quorum_five_mul_overlap_margin
{k a b overlap : Nat}
(hCardinality : a + b <= 5 * k + overlap)
(hA : quorumThreshold (5 * k) <= a)
(hB : quorumThreshold (5 * k) <= b) :
byzantineBound (5 * k) + 2 * k <= overlap := by
have hOverlap :
3 * k <= overlap :=
export_quorum_five_mul_overlap_at_least_three_mul
hCardinality
hA
hB
rw [byzantineBound_five_mul]
omega
end XahauConsensus

View File

@@ -1,188 +0,0 @@
import XahauConsensus.Threshold
import XahauConsensus.Invariants
import XahauConsensus.NunlCap
import XahauConsensus.SidecarAlignment
import XahauConsensus.ViewUniverse
import XahauConsensus.ExportQuorum
import XahauConsensus.SixtyPercent
namespace XahauConsensus
/-! Scalar C ABI exports used by the optional C++ drift tests.
These functions intentionally expose only plain integer formulas. The broader
Lean project proves properties about these definitions; the C++ tests then
check that selected production formulas and helper predicates still compute the
same values.
-/
-- @@start ffi-scalar-export-surface
@[export xahau_byzantine_bound]
def xahauByzantineBound (count : UInt64) : UInt64 :=
(byzantineBound count.toNat).toUInt64
@[export xahau_participant_threshold]
def xahauParticipantThreshold (count : UInt64) : UInt64 :=
(participantThreshold count.toNat).toUInt64
@[export xahau_quorum_threshold]
def xahauQuorumThreshold (count : UInt64) : UInt64 :=
(quorumThreshold count.toNat).toUInt64
@[export xahau_safe_quorum_threshold]
def xahauSafeQuorumThreshold (count : UInt64) : UInt64 :=
(safeQuorumThreshold count.toNat).toUInt64
@[export xahau_safe_participant_threshold]
def xahauSafeParticipantThreshold (count : UInt64) : UInt64 :=
(safeParticipantThreshold count.toNat).toUInt64
@[export xahau_entropy_gate_threshold_for_view]
def xahauEntropyGateThresholdForView
(effectiveView originalView : UInt64) : UInt64 :=
(entropyGateThresholdForView effectiveView.toNat originalView.toNat).toUInt64
def entropyTierCode : EntropyTier UInt8
| EntropyTier.consensusFallback => 1
| EntropyTier.participantAligned => 2
| EntropyTier.validatorQuorum => 3
@[export xahau_select_entropy_tier]
def xahauSelectEntropyTier
(fromUNLReport participantCount effectiveView originalView : UInt64) : UInt8 :=
entropyTierCode <|
selectEntropyTier
(fromUNLReport != 0)
participantCount.toNat
effectiveView.toNat
originalView.toNat
@[export xahau_aligned_participants]
def xahauAlignedParticipants
(aligned localIsMember localPublished : UInt64) : UInt64 :=
(alignedParticipants
aligned.toNat
(localIsMember != 0)
(localPublished != 0)).toUInt64
@[export xahau_quorum_aligned]
def xahauQuorumAligned
(threshold aligned localIsMember localPublished : UInt64) : UInt8 :=
if quorumAligned
threshold.toNat
aligned.toNat
(localIsMember != 0)
(localPublished != 0) then
1
else
0
@[export xahau_full_observation]
def xahauFullObservation (peersSeen txConverged : UInt64) : UInt8 :=
if fullObservation peersSeen.toNat txConverged.toNat then 1 else 0
@[export xahau_export_gate_proceed]
def xahauExportGateProceed
(alignedParticipants quorumThreshold fullObservation : UInt64) : UInt8 :=
if (ExportGate.mk
alignedParticipants.toNat
quorumThreshold.toNat
(fullObservation != 0)).proceed then
1
else
0
@[export xahau_strict_intersection_safe]
def xahauStrictIntersectionSafe
(activeView byzantineUniverse threshold : UInt64) : UInt8 :=
if activeView.toNat + byzantineBound byzantineUniverse.toNat <
2 * threshold.toNat then
1
else
0
@[export xahau_nonvacuous_strict_intersection_safe]
def xahauNonvacuousStrictIntersectionSafe
(activeView byzantineUniverse threshold : UInt64) : UInt8 :=
if threshold.toNat <= activeView.toNat
activeView.toNat + byzantineBound byzantineUniverse.toNat <
2 * threshold.toNat then
1
else
0
@[export xahau_participant_band_nonempty]
def xahauParticipantBandNonempty
(effectiveView originalView : UInt64) : UInt8 :=
if participantThreshold originalView.toNat < quorumThreshold effectiveView.toNat then
1
else
0
@[export xahau_export_quorum_overlap_lower_bound]
def xahauExportQuorumOverlapLowerBound (activeView : UInt64) : UInt64 :=
(2 * quorumThreshold activeView.toNat - activeView.toNat).toUInt64
@[export xahau_export_quorum_safe_under_nunl_cap]
def xahauExportQuorumSafeUnderNunlCap
(originalView effectiveView disabled : UInt64) : UInt8 :=
if effectiveView.toNat = originalView.toNat - disabled.toNat
disabled.toNat <= disabledCap originalView.toNat
0 < effectiveView.toNat
effectiveView.toNat + byzantineBound originalView.toNat <
2 * quorumThreshold effectiveView.toNat then
1
else
0
private def maskBit (mask : UInt64) (peer : Nat) : Bool :=
((mask.toNat / (2 ^ peer)) % 2) == 1
@[export xahau_active_aligned_count_mask]
def xahauActiveAlignedCountMask
(count activeMask alignedMask : UInt64) : UInt64 :=
(activeAlignedCount
(maskBit activeMask)
(maskBit alignedMask)
count.toNat).toUInt64
@[export xahau_quorum_aligned_mask]
def xahauQuorumAlignedMask
(threshold count activeMask alignedMask localIsMember localPublished : UInt64) : UInt8 :=
let aligned :=
activeAlignedCount
(maskBit activeMask)
(maskBit alignedMask)
count.toNat
if quorumAligned
threshold.toNat
aligned
(localIsMember != 0)
(localPublished != 0) then
1
else
0
@[export xahau_naive_sixty_percent_threshold]
def xahauNaiveSixtyPercentThreshold (count : UInt64) : UInt64 :=
(naiveSixtyPercentThreshold count.toNat).toUInt64
@[export xahau_naive_sixty_percent_is_safe]
def xahauNaiveSixtyPercentIsSafe (count : UInt64) : UInt8 :=
if count.toNat + byzantineBound count.toNat <
2 * naiveSixtyPercentThreshold count.toNat then
1
else
0
@[export xahau_disabled_cap]
def xahauDisabledCap (originalView : UInt64) : UInt64 :=
(disabledCap originalView.toNat).toUInt64
@[export xahau_effective_view]
def xahauEffectiveView (originalView disabled : UInt64) : UInt64 :=
(effectiveView originalView.toNat disabled.toNat).toUInt64
-- @@end ffi-scalar-export-surface
end XahauConsensus

View File

@@ -1,88 +0,0 @@
import Mathlib.Data.Finset.Card
import XahauConsensus.ExportQuorum
import XahauConsensus.Intersection
namespace XahauConsensus
/-!
Finite-set bridge for the quorum-intersection arithmetic.
The arithmetic modules prove useful facts from the premise
`a + b <= n + overlap`. This module discharges that premise for actual finite
cohorts `A` and `B` that are both subsets of a common validator universe `U`.
-/
open Finset
/-- Inclusion-exclusion bridge: two finite cohorts inside one universe satisfy
the cardinality premise used by `Intersection.lean`. -/
theorem finset_cardinality_bound
[DecidableEq α]
{U A B : Finset α}
(hA : A U)
(hB : B U) :
A.card + B.card <= U.card + (A B).card := by
have hUnionSubset : A B U := by
intro x hx
rcases Finset.mem_union.mp hx with hxA | hxB
· exact hA hxA
· exact hB hxB
have hUnionCard : (A B).card <= U.card :=
Finset.card_le_card hUnionSubset
have hInclusion :
(A B).card + (A B).card = A.card + B.card :=
Finset.card_union_add_card_inter A B
omega
/-- Set-level Tier-2 form: two participant-threshold cohorts in the same
validator universe overlap above the Byzantine bound. -/
theorem finset_participant_threshold_cohorts_overlap_gt_byzantine
[DecidableEq α]
{U A B : Finset α}
(hAUniverse : A U)
(hBUniverse : B U)
(hAThreshold : participantThreshold U.card <= A.card)
(hBThreshold : participantThreshold U.card <= B.card) :
byzantineBound U.card < (A B).card := by
exact participant_threshold_cohorts_overlap_gt_byzantine
(finset_cardinality_bound hAUniverse hBUniverse)
hAThreshold
hBThreshold
/-- nUNL/set-level form: two original-view participant-threshold cohorts in a
shrunk effective universe still overlap above the original Byzantine bound. -/
theorem finset_participant_threshold_cohorts_overlap_gt_byzantine_under_shrink
[DecidableEq α]
{Original Effective A B : Finset α}
(hEffectiveSubset : Effective Original)
(hAUniverse : A Effective)
(hBUniverse : B Effective)
(hAThreshold : participantThreshold Original.card <= A.card)
(hBThreshold : participantThreshold Original.card <= B.card) :
byzantineBound Original.card < (A B).card := by
have hShrink : Effective.card <= Original.card :=
Finset.card_le_card hEffectiveSubset
exact participant_threshold_cohorts_overlap_gt_byzantine_under_shrink
hShrink
(finset_cardinality_bound hAUniverse hBUniverse)
hAThreshold
hBThreshold
/-- Set-level export form: two 80% export sidecar quorums in the same nonempty
active universe overlap above the standard Byzantine bound. -/
theorem finset_export_hash_quorums_overlap_gt_byzantine
[DecidableEq α]
{U A B : Finset α}
(hNonempty : 0 < U.card)
(hAUniverse : A U)
(hBUniverse : B U)
(hAThreshold : quorumThreshold U.card <= A.card)
(hBThreshold : quorumThreshold U.card <= B.card) :
byzantineBound U.card < (A B).card := by
exact export_hash_quorums_overlap_gt_byzantine
hNonempty
(finset_cardinality_bound hAUniverse hBUniverse)
hAThreshold
hBThreshold
end XahauConsensus

View File

@@ -1,70 +0,0 @@
import XahauConsensus.Intersection
namespace XahauConsensus
/-!
Bridge from cardinality arithmetic to the consensus-language statement:
if cohort overlap is larger than the maximum faulty overlap, then the overlap
contains at least one honest validator.
-/
/-- If the overlap is larger than the number of faulty validators in it, then
some honest validator remains in the overlap. -/
theorem honest_overlap_exists
{overlap faultyInOverlap : Nat}
(hFaultyLtOverlap : faultyInOverlap < overlap) :
0 < overlap - faultyInOverlap := by
omega
/-- If total faulty validators are bounded by `faultBound`, and the overlap is
larger than `faultBound`, then the overlap contains an honest validator. -/
theorem honest_overlap_exists_of_fault_bound
{overlap faultyInOverlap faultBound : Nat}
(hFaultyBound : faultyInOverlap <= faultBound)
(hOverlapGtFaultBound : faultBound < overlap) :
0 < overlap - faultyInOverlap := by
omega
/-- Direct bridge from the abstract two-cohort intersection theorem: two
threshold-sized cohorts under the strict safety inequality have honest overlap,
provided faulty validators in the overlap are bounded by `f`.
-/
theorem honest_overlap_of_two_threshold_cohorts
{n a b overlap threshold faultBound faultyInOverlap : Nat}
(hCardinality : a + b <= n + overlap)
(hA : threshold <= a)
(hB : threshold <= b)
(hSafety : n + faultBound < 2 * threshold)
(hFaultyBound : faultyInOverlap <= faultBound) :
0 < overlap - faultyInOverlap := by
have hOverlapGtFaultBound :
faultBound < overlap :=
overlap_gt_fault_of_two_threshold_cohorts
hCardinality
hA
hB
hSafety
exact honest_overlap_exists_of_fault_bound
hFaultyBound
hOverlapGtFaultBound
/-- Direct participant-threshold form: two Tier-2-sized cohorts in the same
view have honest overlap under the `floor(n/5)` Byzantine bound. -/
theorem honest_overlap_of_participant_threshold_cohorts
{count a b overlap faultyInOverlap : Nat}
(hCardinality : a + b <= count + overlap)
(hA : participantThreshold count <= a)
(hB : participantThreshold count <= b)
(hFaultyBound : faultyInOverlap <= byzantineBound count) :
0 < overlap - faultyInOverlap := by
have hOverlapGtBound :
byzantineBound count < overlap :=
participant_threshold_cohorts_overlap_gt_byzantine
hCardinality
hA
hB
exact honest_overlap_exists_of_fault_bound
hFaultyBound
hOverlapGtBound
end XahauConsensus

View File

@@ -1,96 +0,0 @@
import XahauConsensus.Threshold
namespace XahauConsensus
/-!
Abstract cardinality arithmetic for quorum intersection arguments.
The variables are plain natural-number cardinalities:
* `n`: universe size
* `a`, `b`: cohort sizes
* `o`: overlap size
* `t`: quorum threshold
* `f`: tolerated faulty overlap
The shape `a + b <= n + o` captures the inclusion-exclusion upper bound
without committing to a concrete `Finset` model.
-/
/-- If two threshold-sized cohorts fit in an `n`-sized universe only by
overlapping by `o`, and `n + f < 2 * t`, then the overlap is larger than the
fault bound `f`. -/
theorem overlap_gt_fault_of_two_threshold_cohorts
{n a b o t f : Nat}
(hCardinality : a + b <= n + o)
(hA : t <= a)
(hB : t <= b)
(hSafety : n + f < 2 * t) :
f < o := by
omega
/-- Reviewer-facing contrapositive form: if the overlap is no larger than the
fault bound, then under the strict safety inequality the two cohorts cannot
both meet threshold. -/
theorem not_both_threshold_cohorts_of_overlap_le_fault
{n a b o t f : Nat}
(hOverlap : o <= f)
(hCardinality : a + b <= n + o)
(hSafety : n + f < 2 * t) :
¬ (t <= a t <= b) := by
intro hBoth
have hStrict :
f < o :=
overlap_gt_fault_of_two_threshold_cohorts
hCardinality hBoth.1 hBoth.2 hSafety
omega
/-- Equivalent disjunctive form of the reviewer fact: with insufficient
overlap, at least one candidate cohort must be below threshold. -/
theorem overlap_le_fault_forces_cohort_below_threshold
{n a b o t f : Nat}
(hOverlap : o <= f)
(hCardinality : a + b <= n + o)
(hSafety : n + f < 2 * t) :
a < t b < t := by
have hNotBoth :
¬ (t <= a t <= b) :=
not_both_threshold_cohorts_of_overlap_le_fault
hOverlap hCardinality hSafety
omega
/-- Direct Tier-2 form: two cohorts at the participant threshold in the same
original-view universe must overlap by more than the tolerated Byzantine bound.
-/
theorem participant_threshold_cohorts_overlap_gt_byzantine
{count a b overlap : Nat}
(hCardinality : a + b <= count + overlap)
(hA : participantThreshold count <= a)
(hB : participantThreshold count <= b) :
byzantineBound count < overlap := by
exact overlap_gt_fault_of_two_threshold_cohorts
hCardinality
hA
hB
(participantThreshold_intersection_safe count)
/-- nUNL form: when the effective universe shrinks, the original-view
participant threshold still forces overlap above the original Byzantine bound.
-/
theorem participant_threshold_cohorts_overlap_gt_byzantine_under_shrink
{originalView effectiveView a b overlap : Nat}
(hShrink : effectiveView <= originalView)
(hCardinality : a + b <= effectiveView + overlap)
(hA : participantThreshold originalView <= a)
(hB : participantThreshold originalView <= b) :
byzantineBound originalView < overlap := by
exact overlap_gt_fault_of_two_threshold_cohorts
hCardinality
hA
hB
(participantThreshold_safe_under_effective_shrink
originalView
effectiveView
hShrink)
end XahauConsensus

View File

@@ -1,112 +0,0 @@
import XahauConsensus.Threshold
import XahauConsensus.EntropySelector
import XahauConsensus.ExportGate
namespace XahauConsensus
/-!
Small cross-module invariants that state the design contract in one place.
These do not verify C++ directly. They pin the consensus arguments that the C++
is intended to implement.
-/
/-- Same-count band fact: with both thresholds computed from one view size,
Tier 2 is never stricter than validator quorum. Production nUNL rounds use
cross-view thresholds instead; see `entropyGateThresholdForView`. -/
theorem same_count_tier2_not_stricter_than_validator_quorum (count : Nat) :
safeParticipantThreshold count <= safeQuorumThreshold count :=
safeParticipantThreshold_le_safeQuorumThreshold count
/-- Same-view shorthand: the live entropy gate is the weaker of Tier 2 and
validator quorum, so it is never above validator quorum. -/
def entropyGateThresholdModel (count : Nat) : Nat :=
min (safeQuorumThreshold count) (safeParticipantThreshold count)
theorem entropy_gate_le_validator_quorum (count : Nat) :
entropyGateThresholdModel count <= safeQuorumThreshold count := by
unfold entropyGateThresholdModel
exact Nat.min_le_left _ _
theorem entropy_gate_le_participant_threshold (count : Nat) :
entropyGateThresholdModel count <= safeParticipantThreshold count := by
unfold entropyGateThresholdModel
exact Nat.min_le_right _ _
/-- Production shape: validator quorum is over the effective post-nUNL view,
while Tier 2 is over the original pre-nUNL view. -/
def entropyGateThresholdForView (effectiveView originalView : Nat) : Nat :=
min (safeQuorumThreshold effectiveView) (safeParticipantThreshold originalView)
theorem entropy_gate_for_view_le_validator_quorum
(effectiveView originalView : Nat) :
entropyGateThresholdForView effectiveView originalView <=
safeQuorumThreshold effectiveView := by
unfold entropyGateThresholdForView
exact Nat.min_le_left _ _
theorem entropy_gate_for_view_le_participant_threshold
(effectiveView originalView : Nat) :
entropyGateThresholdForView effectiveView originalView <=
safeParticipantThreshold originalView := by
unfold entropyGateThresholdForView
exact Nat.min_le_right _ _
/-- The entropy gate is exactly the selector's non-fallback boundary: reaching
the lower of the validator-quorum and participant-aligned thresholds is enough
to select a non-fallback tier, and below it the selector falls back. -/
theorem selectEntropyTier_nonfallback_iff_entropy_gate
(participantCount effectiveView originalView : Nat) :
selectEntropyTier true participantCount effectiveView originalView
EntropyTier.consensusFallback
entropyGateThresholdForView effectiveView originalView <=
participantCount := by
unfold selectEntropyTier entropyGateThresholdForView
by_cases hQuorum : safeQuorumThreshold effectiveView <= participantCount
· constructor
· intro _
exact Nat.le_trans (Nat.min_le_left _ _) hQuorum
· intro _
simp [hQuorum]
· by_cases hParticipant :
safeParticipantThreshold originalView <= participantCount
· constructor
· intro _
exact Nat.le_trans (Nat.min_le_right _ _) hParticipant
· intro _
simp [hQuorum, hParticipant]
· constructor
· intro hNonfallback
simp [hQuorum, hParticipant] at hNonfallback
· intro hGate
have hBelowQuorum :
participantCount < safeQuorumThreshold effectiveView :=
Nat.lt_of_not_ge hQuorum
have hBelowParticipant :
participantCount < safeParticipantThreshold originalView :=
Nat.lt_of_not_ge hParticipant
have hBelowGate :
participantCount <
min (safeQuorumThreshold effectiveView)
(safeParticipantThreshold originalView) :=
(Nat.lt_min).mpr hBelowQuorum, hBelowParticipant
exact False.elim (Nat.not_lt_of_ge hGate hBelowGate)
/-- Until the view is ledger-anchored, entropy tier labeling fails closed. -/
theorem non_unl_report_cannot_mint_nonfallback
(participantCount effectiveView originalView : Nat) :
selectEntropyTier false participantCount effectiveView originalView =
EntropyTier.consensusFallback :=
no_unl_report_selects_fallback participantCount effectiveView originalView
/-- Export success is a quorum-alignment property, not a full-observation
property. -/
theorem export_success_independent_of_full_observation
(alignedParticipants quorumThreshold : Nat) :
(ExportGate.mk alignedParticipants quorumThreshold true).proceed =
(ExportGate.mk alignedParticipants quorumThreshold false).proceed :=
changing_fullObservation_alone_does_not_change_proceed
alignedParticipants
quorumThreshold
end XahauConsensus

View File

@@ -1,147 +0,0 @@
import XahauConsensus.Threshold
namespace XahauConsensus
/-!
Arithmetic facts for nUNL-capped view shrinkage.
The examples here intentionally use the original view for the participant
floor and the effective post-nUNL view for validator quorum. That is the
cross-view comparison that matters when disabled validators collapse the space
between the Tier-2 participant floor and the Tier-3 validator-quorum floor.
-/
/-- Integer ceiling division, defined defensively for `d = 0`. -/
def ceilDiv (n d : Nat) : Nat :=
if d = 0 then 0 else (n + d - 1) / d
/-- The protocol's ceil-25% nUNL disablement cap for an original validator view. -/
def disabledCap (originalView : Nat) : Nat :=
ceilDiv originalView 4
/-- The post-nUNL effective validator view after `disabled` validators drop. -/
def effectiveView (originalView disabled : Nat) : Nat :=
originalView - disabled
theorem ceilDiv_zero_right (n : Nat) : ceilDiv n 0 = 0 := by
simp [ceilDiv]
theorem ceilDiv_four_eight : ceilDiv 8 4 = 2 := by
native_decide
theorem ceilDiv_four_ten : ceilDiv 10 4 = 3 := by
native_decide
theorem ceilDiv_four_twenty : ceilDiv 20 4 = 5 := by
native_decide
theorem disabledCap_eight : disabledCap 8 = 2 := by
native_decide
theorem disabledCap_ten : disabledCap 10 = 3 := by
native_decide
theorem disabledCap_twenty : disabledCap 20 = 5 := by
native_decide
theorem effectiveView_eight_at_disabledCap :
effectiveView 8 (disabledCap 8) = 6 := by
native_decide
theorem effectiveView_ten_at_disabledCap :
effectiveView 10 (disabledCap 10) = 7 := by
native_decide
theorem effectiveView_twenty_at_disabledCap :
effectiveView 20 (disabledCap 20) = 15 := by
native_decide
/-- Original 8 with two disabled validators collapses the participant/quorum band. -/
theorem band_collapse_original8_effective6 :
quorumThreshold 6 = participantThreshold 8 := by
native_decide
theorem quorum_original8_effective6_meets_participant_floor :
participantThreshold 8 <= quorumThreshold 6 := by
native_decide
/-- Original 10 with two disabled validators collapses the participant/quorum band. -/
theorem band_collapse_original10_effective8 :
quorumThreshold 8 = participantThreshold 10 := by
native_decide
theorem quorum_original10_effective8_meets_participant_floor :
participantThreshold 10 <= quorumThreshold 8 := by
native_decide
/-- Original 10 at the full ceil-25% cap leaves effective view 7, below the participant floor. -/
theorem quorum_original10_effective7_below_participant_floor :
quorumThreshold 7 < participantThreshold 10 := by
native_decide
theorem max_cap_original10_below_participant_floor :
quorumThreshold (effectiveView 10 (disabledCap 10)) <
participantThreshold 10 := by
native_decide
/-- At original 20, the full ceil-25% cap leaves effective view 15, which is too small. -/
theorem quorum_original20_effective15_below_participant_floor :
quorumThreshold 15 < participantThreshold 20 := by
native_decide
theorem quorum_original20_effective15_does_not_meet_participant_floor :
¬ participantThreshold 20 <= quorumThreshold 15 := by
native_decide
/-- Original 20 with four disabled validators collapses the participant/quorum band. -/
theorem band_collapse_original20_effective16 :
quorumThreshold 16 = participantThreshold 20 := by
native_decide
theorem quorum_original20_effective16_meets_participant_floor :
participantThreshold 20 <= quorumThreshold 16 := by
native_decide
/-- The ceil-25% cap does not by itself guarantee collapse at size 20. -/
theorem max_cap_original20_below_participant_floor :
quorumThreshold (effectiveView 20 (disabledCap 20)) <
participantThreshold 20 := by
native_decide
/--
General cross-view comparison: an effective-view quorum satisfies the
original-view participant floor whenever that quorum clears the original
intersection boundary.
-/
theorem quorumThreshold_meets_participantThreshold_of_intersection_premise
{originalView effectiveView : Nat}
(h :
originalView + byzantineBound originalView <
2 * quorumThreshold effectiveView) :
participantThreshold originalView <= quorumThreshold effectiveView := by
exact participantThreshold_minimal originalView (quorumThreshold effectiveView) h
/--
Once the effective-view quorum threshold meets the original-view participant
floor, any validator count meeting validator quorum also meets the participant
floor anchored to the original view.
-/
theorem validators_meet_participant_floor_of_meet_quorum
{originalView effectiveView validators : Nat}
(hBand : participantThreshold originalView <= quorumThreshold effectiveView)
(hQuorum : quorumThreshold effectiveView <= validators) :
participantThreshold originalView <= validators :=
Nat.le_trans hBand hQuorum
/-- If cross-view quorum is no higher than the participant floor, the in-between band is empty. -/
theorem cross_view_participant_band_empty
{originalView effectiveView : Nat}
(hCollapse : quorumThreshold effectiveView <= participantThreshold originalView) :
¬ participants,
participantThreshold originalView <= participants
participants < quorumThreshold effectiveView := by
intro hExists
rcases hExists with participants, hParticipant, hBelowQuorum
omega
end XahauConsensus

View File

@@ -1,64 +0,0 @@
import XahauConsensus.EntropySelector
namespace XahauConsensus
/-- A minimal digest model: the payload is opaque to the selector, while the
label is the entropy tier chosen from the consensus metadata. -/
structure LabeledDigest (α : Type) where
payload : α
label : EntropyTier
deriving Repr
def labelDigest
(fromUNLReport : Bool)
(participantCount effectiveView originalView : Nat)
(payload : α) : LabeledDigest α :=
{ payload
label :=
selectEntropyTier
fromUNLReport
participantCount
effectiveView
originalView }
/-- The digest payload itself does not affect the selected tier. The label is
entirely determined by the consensus metadata. -/
theorem payload_does_not_affect_tier
{α : Type}
{payloadA payloadB : α}
(fromUNLReport : Bool)
(participantCount effectiveView originalView : Nat) :
(labelDigest
fromUNLReport
participantCount
effectiveView
originalView
payloadA).label =
(labelDigest
fromUNLReport
participantCount
effectiveView
originalView
payloadB).label := by
rfl
/-- Without a UNLReport anchor the same count and views can receive a different
label. -/
theorem label_can_differ_when_fromUNLReport_differs :
(labelDigest true 8 10 10 0).label
(labelDigest false 8 10 10 0).label := by
native_decide
/-- Changing the effective validator view can change the digest label. -/
theorem label_can_differ_when_effective_view_differs :
(labelDigest true 7 8 10 0).label
(labelDigest true 7 10 10 0).label := by
native_decide
/-- Changing the original validator view can change the digest label. -/
theorem label_can_differ_when_original_view_differs :
(labelDigest true 6 10 8 0).label
(labelDigest true 6 10 10 0).label := by
native_decide
end XahauConsensus

View File

@@ -1,241 +0,0 @@
namespace XahauConsensus
/-- Count a local boolean contribution as the `Nat` value used in threshold
comparisons. -/
def localPublishedCount (localPublished : Bool) : Nat :=
if localPublished then 1 else 0
/-- The proof-level participant count behind sidecar alignment.
`aligned` is the count of aligned remote active-view participants; a local
publication contributes one more participant. -/
def alignedParticipants
(aligned : Nat)
(localIsMember localPublished : Bool) : Nat :=
aligned + localPublishedCount (localIsMember && localPublished)
/-- Sidecar quorum predicate, kept boolean to mirror the implementation check. -/
def quorumAligned
(threshold aligned : Nat)
(localIsMember localPublished : Bool) : Bool :=
decide (threshold <= alignedParticipants aligned localIsMember localPublished)
/-- Full sidecar observation means every converged transaction has been seen. -/
def fullObservation (peersSeen txConverged : Nat) : Bool :=
peersSeen == txConverged
/-- Count aligned peers from a finite peer prefix, filtering through the active
view before any alignment bit contributes. -/
def activeAlignedCount
(inActiveView peerAligned : Nat Bool) : Nat Nat
| 0 => 0
| peer + 1 =>
activeAlignedCount inActiveView peerAligned peer +
localPublishedCount (inActiveView peer && peerAligned peer)
theorem localPublishedCount_true :
localPublishedCount true = 1 := by
rfl
theorem localPublishedCount_false :
localPublishedCount false = 0 := by
rfl
theorem localPublishedCount_le_one (published : Bool) :
localPublishedCount published <= 1 := by
cases published <;> simp [localPublishedCount]
/-- Core participant-count equation: aligned remotes plus the local published
contribution. -/
theorem alignedParticipants_eq_aligned_plus_localPublished
(aligned : Nat) (localIsMember localPublished : Bool) :
alignedParticipants aligned localIsMember localPublished =
aligned + localPublishedCount (localIsMember && localPublished) := by
rfl
/-- A non-active local node cannot pad the participant count. -/
theorem alignedParticipants_local_nonmember
(aligned : Nat) (localPublished : Bool) :
alignedParticipants aligned false localPublished = aligned := by
cases localPublished <;> rfl
/-- An active local node contributes exactly when it published the sidecar hash. -/
theorem alignedParticipants_local_member
(aligned : Nat) (localPublished : Bool) :
alignedParticipants aligned true localPublished =
aligned + localPublishedCount localPublished := by
cases localPublished <;> rfl
/-- The local node can add at most one participant to the remote aligned count. -/
theorem alignedParticipants_le_aligned_succ
(aligned : Nat) (localIsMember localPublished : Bool) :
alignedParticipants aligned localIsMember localPublished <= aligned + 1 := by
cases localIsMember <;> cases localPublished <;>
simp [alignedParticipants, localPublishedCount]
/-- The boolean quorum predicate is exactly the threshold comparison over
`alignedParticipants`. -/
theorem quorumAligned_iff_threshold_le_alignedParticipants
(threshold aligned : Nat) (localIsMember localPublished : Bool) :
quorumAligned threshold aligned localIsMember localPublished = true
threshold <= alignedParticipants aligned localIsMember localPublished := by
unfold quorumAligned
simp
/-- The boolean full-observation predicate is exactly equality of the observed
and converged counts. -/
theorem fullObservation_iff_peersSeen_eq_txConverged
(peersSeen txConverged : Nat) :
fullObservation peersSeen txConverged = true
peersSeen = txConverged := by
unfold fullObservation
simp
/-- A peer outside the active view contributes zero, even if its sidecar
alignment bit is set. -/
theorem activeAlignedCount_succ_nonmember
{inActiveView peerAligned : Nat Bool} {peer : Nat}
(hNonmember : inActiveView peer = false) :
activeAlignedCount inActiveView peerAligned (peer + 1) =
activeAlignedCount inActiveView peerAligned peer := by
simp [activeAlignedCount, hNonmember, localPublishedCount]
/-- A prefix of `n` peer positions can contribute at most `n` aligned active
remote participants. -/
theorem activeAlignedCount_le_prefix
(inActiveView peerAligned : Nat Bool) (n : Nat) :
activeAlignedCount inActiveView peerAligned n <= n := by
induction n with
| zero =>
simp [activeAlignedCount]
| succ n ih =>
cases hAligned : inActiveView n && peerAligned n
· simp [activeAlignedCount, hAligned, localPublishedCount]
exact Nat.le_trans ih (Nat.le_succ n)
· simp [activeAlignedCount, hAligned, localPublishedCount]
exact ih
/-- With the optional local contribution included, the participant count is
bounded by the inspected remote prefix plus one. -/
theorem alignedParticipants_le_prefix_succ
(inActiveView peerAligned : Nat Bool)
(n : Nat)
(localIsMember localPublished : Bool) :
alignedParticipants
(activeAlignedCount inActiveView peerAligned n)
localIsMember
localPublished <= n + 1 := by
have hRemote := activeAlignedCount_le_prefix inActiveView peerAligned n
cases localIsMember <;> cases localPublished <;>
simp [alignedParticipants, localPublishedCount]
· exact Nat.le_trans hRemote (Nat.le_succ n)
· exact Nat.le_trans hRemote (Nat.le_succ n)
· exact Nat.le_trans hRemote (Nat.le_succ n)
· exact hRemote
/-- Adding a nonmember peer to the inspected prefix cannot increase
`alignedParticipants`. -/
theorem alignedParticipants_succ_nonmember
{inActiveView peerAligned : Nat Bool} {peer : Nat}
(localIsMember localPublished : Bool)
(hNonmember : inActiveView peer = false) :
alignedParticipants
(activeAlignedCount inActiveView peerAligned (peer + 1))
localIsMember
localPublished =
alignedParticipants
(activeAlignedCount inActiveView peerAligned peer)
localIsMember
localPublished := by
simp [alignedParticipants, activeAlignedCount_succ_nonmember hNonmember]
/-- Consequently, a nonmember peer cannot change the quorum-aligned result. -/
theorem quorumAligned_succ_nonmember
{inActiveView peerAligned : Nat Bool} {peer threshold : Nat}
(localIsMember localPublished : Bool)
(hNonmember : inActiveView peer = false) :
quorumAligned threshold
(activeAlignedCount inActiveView peerAligned (peer + 1))
localIsMember
localPublished =
quorumAligned threshold
(activeAlignedCount inActiveView peerAligned peer)
localIsMember
localPublished := by
simp [
quorumAligned,
alignedParticipants_succ_nonmember
localIsMember
localPublished
hNonmember]
/-- Active-view filtering: only member peers' alignment bits can affect the
aligned remote count. -/
theorem activeAlignedCount_ext_on_members
{n : Nat} {inActiveView alignedA alignedB : Nat Bool}
(hSameOnMembers :
peer, peer < n inActiveView peer = true
alignedA peer = alignedB peer) :
activeAlignedCount inActiveView alignedA n =
activeAlignedCount inActiveView alignedB n := by
induction n with
| zero =>
rfl
| succ n ih =>
have hPrefix :
peer, peer < n inActiveView peer = true
alignedA peer = alignedB peer := by
intro peer hLt hMember
exact hSameOnMembers peer (Nat.lt_trans hLt (Nat.lt_succ_self n)) hMember
have hAt :
localPublishedCount (inActiveView n && alignedA n) =
localPublishedCount (inActiveView n && alignedB n) := by
cases hMember : inActiveView n
· simp [localPublishedCount]
· have hEq := hSameOnMembers n (Nat.lt_succ_self n) hMember
simp [hEq, localPublishedCount]
simp [activeAlignedCount, ih hPrefix, hAt]
/-- Changing sidecar alignment reports for nonmembers cannot change the final
participant count. -/
theorem alignedParticipants_ext_on_members
{n : Nat} {inActiveView alignedA alignedB : Nat Bool}
{localIsMember : Bool}
{localPublished : Bool}
(hSameOnMembers :
peer, peer < n inActiveView peer = true
alignedA peer = alignedB peer) :
alignedParticipants
(activeAlignedCount inActiveView alignedA n)
localIsMember
localPublished =
alignedParticipants
(activeAlignedCount inActiveView alignedB n)
localIsMember
localPublished := by
simp [
alignedParticipants,
activeAlignedCount_ext_on_members hSameOnMembers]
/-- Changing sidecar alignment reports for nonmembers cannot turn quorum on or
off. -/
theorem quorumAligned_ext_on_members
{n threshold : Nat} {inActiveView alignedA alignedB : Nat Bool}
{localIsMember : Bool}
{localPublished : Bool}
(hSameOnMembers :
peer, peer < n inActiveView peer = true
alignedA peer = alignedB peer) :
quorumAligned threshold
(activeAlignedCount inActiveView alignedA n)
localIsMember
localPublished =
quorumAligned threshold
(activeAlignedCount inActiveView alignedB n)
localIsMember
localPublished := by
simp [
quorumAligned,
alignedParticipants_ext_on_members hSameOnMembers]
end XahauConsensus

View File

@@ -1,56 +0,0 @@
import XahauConsensus.Threshold
namespace XahauConsensus
/-!
Review-oriented facts about the tempting `ceil(60%)` participant threshold.
The live `participantThreshold` is one higher than naive 60% at exact
multiples of five. That extra vote is what turns equality at the
Byzantine-overlap boundary into strict intersection safety.
-/
/-- A naive `ceil(0.6 * count)` threshold. -/
def naiveSixtyPercentThreshold (count : Nat) : Nat :=
(count * 60 + 99) / 100
theorem naiveSixtyPercentThreshold_five_mul (k : Nat) :
naiveSixtyPercentThreshold (5 * k) = 3 * k := by
unfold naiveSixtyPercentThreshold
omega
theorem participantThreshold_five_mul_eq_naiveSixtyPercentThreshold_succ
(k : Nat) :
participantThreshold (5 * k) =
naiveSixtyPercentThreshold (5 * k) + 1 := by
unfold participantThreshold byzantineBound naiveSixtyPercentThreshold
omega
/-- At exact multiples of five, naive 60% only reaches the unsafe boundary. -/
theorem naiveSixtyPercentThreshold_five_mul_hits_intersection_boundary
(k : Nat) :
2 * naiveSixtyPercentThreshold (5 * k) =
5 * k + byzantineBound (5 * k) := by
unfold naiveSixtyPercentThreshold byzantineBound
omega
theorem naiveSixtyPercentThreshold_five_mul_not_intersection_safe
(k : Nat) :
¬ 5 * k + byzantineBound (5 * k) <
2 * naiveSixtyPercentThreshold (5 * k) := by
rw [naiveSixtyPercentThreshold_five_mul_hits_intersection_boundary k]
omega
theorem participantThreshold_five_mul_intersection_safe (k : Nat) :
5 * k + byzantineBound (5 * k) <
2 * participantThreshold (5 * k) := by
exact participantThreshold_intersection_safe (5 * k)
/-- At exact multiples of five, the live threshold clears the boundary by two. -/
theorem participantThreshold_five_mul_intersection_margin (k : Nat) :
2 * participantThreshold (5 * k) =
(5 * k + byzantineBound (5 * k)) + 2 := by
unfold participantThreshold byzantineBound
omega
end XahauConsensus

View File

@@ -1,124 +0,0 @@
namespace XahauConsensus
/-- C++: `count / 5`, the conservative Byzantine bound used by
`calculateParticipantThreshold`. -/
def byzantineBound (count : Nat) : Nat :=
count / 5
/-- C++: `calculateParticipantThreshold(count)`.
This is the smallest integer `t` satisfying `2 * t > count + floor(count / 5)`.
-/
def participantThreshold (count : Nat) : Nat :=
(count + byzantineBound count) / 2 + 1
/-- C++: `calculateQuorumThreshold(count)`, i.e. `ceil(0.8 * count)`. -/
def quorumThreshold (count : Nat) : Nat :=
(count * 80 + 99) / 100
/-- C++: `ConsensusExtensions::quorumThreshold()`.
The raw formula gives `0` for an empty view, but the live consensus-extension
gate requires at least one aligned participant for safety.
-/
def safeQuorumThreshold (count : Nat) : Nat :=
if count = 0 then 1 else quorumThreshold count
/-- C++: `ConsensusExtensions::tier2Threshold()`.
`participantThreshold 0` already returns `1`; this wrapper makes the
zero-view safety rule explicit and mirrors the C++ method shape.
-/
def safeParticipantThreshold (count : Nat) : Nat :=
if count = 0 then 1 else participantThreshold count
/-- The Tier-2 threshold strictly exceeds the Byzantine-overlap boundary.
This is the load-bearing equivocation invariant behind participant-aligned
entropy: two cohorts of this size in a `count`-sized universe overlap in more
than `floor(count / 5)` validators.
-/
theorem participantThreshold_intersection_safe (count : Nat) :
count + byzantineBound count < 2 * participantThreshold count := by
unfold participantThreshold byzantineBound
omega
/-- Anchoring the Tier-2 threshold to the original pre-nUNL view remains safe
when the effective post-nUNL view shrinks.
This is the arithmetic reason `originalViewSize` is the right denominator:
smaller effective universes only increase the intersection margin.
-/
theorem participantThreshold_safe_under_effective_shrink
(originalView effectiveView : Nat)
(hShrink : effectiveView <= originalView) :
effectiveView + byzantineBound originalView <
2 * participantThreshold originalView := by
have hSafe := participantThreshold_intersection_safe originalView
omega
/-- Concrete regression example: if `originalView = 10` and `effectiveView = 8`,
using the effective view's participant threshold (`5`) leaves the overlap equal
to the original-view Byzantine bound (`2`), not strictly greater than it.
This is why the C++ must not replace `originalViewSize` with `size()` for the
Tier-2 floor.
-/
theorem effective_threshold_regression_hits_boundary_example :
2 * participantThreshold 8 <= 8 + byzantineBound 10 := by
native_decide
theorem threshold_minimal_for_boundary (boundary threshold : Nat) :
boundary < 2 * threshold boundary / 2 + 1 <= threshold := by
omega
theorem below_threshold_not_safe_for_boundary (boundary threshold : Nat) :
threshold < boundary / 2 + 1 2 * threshold <= boundary := by
omega
/-- `participantThreshold` is the smallest threshold satisfying the strict
intersection-safety inequality. -/
theorem participantThreshold_minimal (count threshold : Nat) :
count + byzantineBound count < 2 * threshold
participantThreshold count <= threshold := by
intro hSafe
unfold participantThreshold
exact threshold_minimal_for_boundary
(count + byzantineBound count)
threshold
hSafe
/-- Anything below `participantThreshold` fails the strict intersection-safety
inequality. -/
theorem below_participantThreshold_not_safe (count threshold : Nat) :
threshold < participantThreshold count
2 * threshold <= count + byzantineBound count := by
intro hBelow
unfold participantThreshold at hBelow
exact below_threshold_not_safe_for_boundary
(count + byzantineBound count)
threshold
hBelow
/-- The participant threshold never exceeds the 80% validator-quorum threshold.
This is useful because Tier 2 should form a band below Tier 3, not a stricter
condition than validator quorum.
-/
theorem participantThreshold_le_quorumThreshold (count : Nat) :
0 < count participantThreshold count <= quorumThreshold count := by
intro hCount
unfold participantThreshold quorumThreshold byzantineBound
omega
/-- With the live safety wrappers, the participant threshold never exceeds the
validator-quorum threshold, including the empty-view edge case. -/
theorem safeParticipantThreshold_le_safeQuorumThreshold (count : Nat) :
safeParticipantThreshold count <= safeQuorumThreshold count := by
unfold safeParticipantThreshold safeQuorumThreshold
by_cases hZero : count = 0
· simp [hZero]
· have hPositive : 0 < count := Nat.pos_of_ne_zero hZero
simp [hZero, participantThreshold_le_quorumThreshold count hPositive]
end XahauConsensus

View File

@@ -1,223 +0,0 @@
import XahauConsensus.Threshold
namespace XahauConsensus
/-!
Additional arithmetic facts about the Xahau consensus thresholds.
These lemmas are deliberately small and review-oriented: they expose concrete
edge cases, exact multiples-of-five behavior, participant/quorum band facts,
and monotonicity of the threshold functions.
-/
theorem byzantineBound_zero : byzantineBound 0 = 0 := by
native_decide
theorem participantThreshold_zero : participantThreshold 0 = 1 := by
native_decide
theorem quorumThreshold_zero : quorumThreshold 0 = 0 := by
native_decide
theorem safeQuorumThreshold_zero : safeQuorumThreshold 0 = 1 := by
native_decide
theorem safeParticipantThreshold_zero : safeParticipantThreshold 0 = 1 := by
native_decide
theorem byzantineBound_one : byzantineBound 1 = 0 := by
native_decide
theorem participantThreshold_one : participantThreshold 1 = 1 := by
native_decide
theorem quorumThreshold_one : quorumThreshold 1 = 1 := by
native_decide
theorem safeQuorumThreshold_one : safeQuorumThreshold 1 = 1 := by
native_decide
theorem safeParticipantThreshold_one : safeParticipantThreshold 1 = 1 := by
native_decide
theorem participantThreshold_two : participantThreshold 2 = 2 := by
native_decide
theorem quorumThreshold_two : quorumThreshold 2 = 2 := by
native_decide
theorem participantThreshold_three : participantThreshold 3 = 2 := by
native_decide
theorem quorumThreshold_three : quorumThreshold 3 = 3 := by
native_decide
theorem participantThreshold_four : participantThreshold 4 = 3 := by
native_decide
theorem quorumThreshold_four : quorumThreshold 4 = 4 := by
native_decide
theorem byzantineBound_five : byzantineBound 5 = 1 := by
native_decide
theorem participantThreshold_five : participantThreshold 5 = 4 := by
native_decide
theorem quorumThreshold_five : quorumThreshold 5 = 4 := by
native_decide
theorem byzantineBound_ten : byzantineBound 10 = 2 := by
native_decide
theorem participantThreshold_ten : participantThreshold 10 = 7 := by
native_decide
theorem quorumThreshold_ten : quorumThreshold 10 = 8 := by
native_decide
theorem byzantineBound_twenty : byzantineBound 20 = 4 := by
native_decide
theorem participantThreshold_twenty : participantThreshold 20 = 13 := by
native_decide
theorem quorumThreshold_twenty : quorumThreshold 20 = 16 := by
native_decide
theorem byzantineBound_five_mul (k : Nat) :
byzantineBound (5 * k) = k := by
unfold byzantineBound
omega
theorem participantThreshold_five_mul (k : Nat) :
participantThreshold (5 * k) = 3 * k + 1 := by
unfold participantThreshold byzantineBound
omega
theorem quorumThreshold_five_mul (k : Nat) :
quorumThreshold (5 * k) = 4 * k := by
unfold quorumThreshold
omega
/-- On exact multiples of five, the strict safety margin is exactly two. -/
theorem participantThreshold_five_mul_margin (k : Nat) :
2 * participantThreshold (5 * k) =
(5 * k + byzantineBound (5 * k)) + 2 := by
rw [participantThreshold_five_mul, byzantineBound_five_mul]
omega
/-- One below the multiple-of-five participant threshold reaches only equality
with the unsafe boundary, so the strict safety inequality fails. -/
theorem below_participantThreshold_five_mul_hits_boundary (k : Nat) :
2 * (participantThreshold (5 * k) - 1) =
5 * k + byzantineBound (5 * k) := by
rw [participantThreshold_five_mul, byzantineBound_five_mul]
omega
theorem participantThreshold_five_mul_lt_quorumThreshold_five_mul
{k : Nat} (h : 1 < k) :
participantThreshold (5 * k) < quorumThreshold (5 * k) := by
rw [participantThreshold_five_mul, quorumThreshold_five_mul]
omega
theorem participantThreshold_five_eq_quorumThreshold_five :
participantThreshold 5 = quorumThreshold 5 := by
native_decide
theorem participantThreshold_ten_lt_quorumThreshold_ten :
participantThreshold 10 < quorumThreshold 10 := by
native_decide
theorem participant_band_nonempty {count : Nat}
(h : participantThreshold count < quorumThreshold count) :
participants,
participantThreshold count <= participants
participants < quorumThreshold count := by
exact participantThreshold count, Nat.le_refl _, h
theorem participant_band_empty {count : Nat}
(h : quorumThreshold count <= participantThreshold count) :
¬ participants,
participantThreshold count <= participants
participants < quorumThreshold count := by
intro hExists
rcases hExists with participants, hParticipant, hBelowQuorum
omega
theorem participant_band_empty_zero :
¬ participants,
participantThreshold 0 <= participants
participants < quorumThreshold 0 := by
apply participant_band_empty
native_decide
theorem participant_band_empty_one :
¬ participants,
participantThreshold 1 <= participants
participants < quorumThreshold 1 := by
apply participant_band_empty
native_decide
theorem participant_band_empty_two :
¬ participants,
participantThreshold 2 <= participants
participants < quorumThreshold 2 := by
apply participant_band_empty
native_decide
theorem participant_band_empty_five :
¬ participants,
participantThreshold 5 <= participants
participants < quorumThreshold 5 := by
apply participant_band_empty
native_decide
theorem participant_band_nonempty_three :
participants,
participantThreshold 3 <= participants
participants < quorumThreshold 3 := by
apply participant_band_nonempty
native_decide
theorem participant_band_nonempty_four :
participants,
participantThreshold 4 <= participants
participants < quorumThreshold 4 := by
apply participant_band_nonempty
native_decide
theorem participant_band_nonempty_ten :
participants,
participantThreshold 10 <= participants
participants < quorumThreshold 10 := by
apply participant_band_nonempty
native_decide
theorem participant_band_nonempty_five_mul {k : Nat} (h : 1 < k) :
participants,
participantThreshold (5 * k) <= participants
participants < quorumThreshold (5 * k) := by
exact participant_band_nonempty
(participantThreshold_five_mul_lt_quorumThreshold_five_mul h)
theorem byzantineBound_mono {a b : Nat} (h : a <= b) :
byzantineBound a <= byzantineBound b := by
unfold byzantineBound
exact Nat.div_le_div_right h
theorem participantThreshold_mono {a b : Nat} (h : a <= b) :
participantThreshold a <= participantThreshold b := by
unfold participantThreshold
apply Nat.succ_le_succ
apply Nat.div_le_div_right
have hByzantine := byzantineBound_mono h
omega
theorem quorumThreshold_mono {a b : Nat} (h : a <= b) :
quorumThreshold a <= quorumThreshold b := by
unfold quorumThreshold
apply Nat.div_le_div_right
omega
end XahauConsensus

View File

@@ -1,201 +0,0 @@
import XahauConsensus.ThresholdFacts
namespace XahauConsensus
/-!
Concrete arithmetic examples for the distinction between the active effective
view, the original pre-nUNL view, and any larger trusted counting universe.
The safety shape is deliberately Nat-only: two cohorts of size `threshold` in
an `activeView` overlap strictly beyond the Byzantine bound charged to
`byzantineUniverse` when
`activeView + byzantineBound byzantineUniverse < 2 * threshold`.
-/
def strictIntersectionSafe
(activeView byzantineUniverse threshold : Nat) : Prop :=
activeView + byzantineBound byzantineUniverse < 2 * threshold
/-- Strict intersection safety plus reachability of the threshold inside the
active view. This separates "safe if it happens" from "possible to happen". -/
def nonvacuousStrictIntersectionSafe
(activeView byzantineUniverse threshold : Nat) : Prop :=
threshold <= activeView strictIntersectionSafe activeView byzantineUniverse threshold
/-- Cross-view Tier-2 band: participant floor is anchored to the original view,
validator quorum to the effective view. -/
def participantBandNonempty
(effectiveView originalView : Nat) : Prop :=
participants,
participantThreshold originalView <= participants
participants < quorumThreshold effectiveView
theorem participantBandNonempty_iff
(effectiveView originalView : Nat) :
participantBandNonempty effectiveView originalView
participantThreshold originalView < quorumThreshold effectiveView := by
constructor
· intro h
rcases h with participants, hParticipant, hBelowQuorum
omega
· intro h
exact participantThreshold originalView, Nat.le_refl _, h
/-- The original-view participant threshold remains safe when nUNL shrinks the
active effective view. -/
theorem original_threshold_safe_under_nunl_shrink
{originalView effectiveView : Nat}
(hShrink : effectiveView <= originalView) :
strictIntersectionSafe
effectiveView
originalView
(participantThreshold originalView) := by
unfold strictIntersectionSafe
exact participantThreshold_safe_under_effective_shrink
originalView
effectiveView
hShrink
theorem original_threshold_nonvacuous_under_nunl_shrink
{originalView effectiveView : Nat}
(hShrink : effectiveView <= originalView)
(hReachable : participantThreshold originalView <= effectiveView) :
nonvacuousStrictIntersectionSafe
effectiveView
originalView
(participantThreshold originalView) := by
constructor
· exact hReachable
· exact original_threshold_safe_under_nunl_shrink hShrink
/-- The original-view threshold is also safe if the Byzantine counting universe
is no larger than the original view. -/
theorem original_threshold_safe_for_no_larger_counting_universe
{originalView effectiveView countingUniverse : Nat}
(hShrink : effectiveView <= originalView)
(hCounting : countingUniverse <= originalView) :
strictIntersectionSafe
effectiveView
countingUniverse
(participantThreshold originalView) := by
unfold strictIntersectionSafe
have hOriginal :=
participantThreshold_safe_under_effective_shrink
originalView
effectiveView
hShrink
have hBound := byzantineBound_mono hCounting
omega
/-- Any threshold at or below the overlap boundary is not strictly safe. -/
theorem not_strictIntersectionSafe_of_threshold_le_boundary
{activeView byzantineUniverse threshold : Nat}
(hBoundary : 2 * threshold <= activeView + byzantineBound byzantineUniverse) :
¬ strictIntersectionSafe activeView byzantineUniverse threshold := by
unfold strictIntersectionSafe
omega
/-- If the effective-view threshold is below what the original Byzantine bound
requires, it cannot prove strict intersection safety against that original
bound. -/
theorem effective_threshold_not_safe_against_original_bound
{originalView effectiveView : Nat}
(hBelow :
participantThreshold effectiveView <
(effectiveView + byzantineBound originalView) / 2 + 1) :
¬ strictIntersectionSafe
effectiveView
originalView
(participantThreshold effectiveView) := by
apply not_strictIntersectionSafe_of_threshold_le_boundary
exact below_threshold_not_safe_for_boundary
(effectiveView + byzantineBound originalView)
(participantThreshold effectiveView)
hBelow
/-- A larger trusted counting universe increases the Byzantine side of the
boundary, eroding the strict-intersection margin. -/
theorem original_boundary_le_trusted_superset_boundary
{originalView effectiveView trustedUniverse : Nat}
(hSuperset : originalView <= trustedUniverse) :
effectiveView + byzantineBound originalView <=
effectiveView + byzantineBound trustedUniverse := by
have hBound := byzantineBound_mono hSuperset
omega
/-- Concrete nUNL example: `originalView = 10`, `effectiveView = 8`, and the
original threshold still clears the original Byzantine bound. -/
theorem original_ten_effective_eight_original_threshold_safe :
strictIntersectionSafe 8 10 (participantThreshold 10) := by
unfold strictIntersectionSafe
native_decide
theorem original_ten_effective_eight_participant_band_empty :
¬ participantBandNonempty 8 10 := by
rw [participantBandNonempty_iff]
native_decide
theorem original_ten_effective_eight_original_threshold_reachable :
nonvacuousStrictIntersectionSafe 8 10 (participantThreshold 10) := by
apply original_threshold_nonvacuous_under_nunl_shrink
· native_decide
· native_decide
/-- Concrete regression: for `originalView = 10` and `effectiveView = 8`, the
effective threshold does not strictly clear the original Byzantine bound. -/
theorem original_ten_effective_eight_effective_threshold_not_safe :
¬ strictIntersectionSafe 8 10 (participantThreshold 8) := by
apply not_strictIntersectionSafe_of_threshold_le_boundary
native_decide
/-- The same failure as a direct boundary comparison, useful when reviewing the
raw arithmetic. -/
theorem original_ten_effective_eight_effective_threshold_hits_boundary :
2 * participantThreshold 8 <= 8 + byzantineBound 10 := by
native_decide
/-- Larger concrete nUNL example with the original threshold anchored at
`20`. -/
theorem original_twenty_effective_sixteen_original_threshold_safe :
strictIntersectionSafe 16 20 (participantThreshold 20) := by
unfold strictIntersectionSafe
native_decide
theorem original_twenty_effective_sixteen_participant_band_empty :
¬ participantBandNonempty 16 20 := by
rw [participantBandNonempty_iff]
native_decide
theorem original_twenty_effective_fifteen_participant_band_empty :
¬ participantBandNonempty 15 20 := by
rw [participantBandNonempty_iff]
native_decide
theorem original_twenty_effective_fifteen_original_threshold_reachable :
nonvacuousStrictIntersectionSafe 15 20 (participantThreshold 20) := by
apply original_threshold_nonvacuous_under_nunl_shrink
· native_decide
· native_decide
/-- With `originalView = 20` and `effectiveView = 16`, using the effective
threshold again reaches the unsafe boundary. -/
theorem original_twenty_effective_sixteen_effective_threshold_not_safe :
¬ strictIntersectionSafe 16 20 (participantThreshold 16) := by
apply not_strictIntersectionSafe_of_threshold_le_boundary
native_decide
/-- Counting Byzantine stake over a trusted universe of `20` instead of the
original view of `10` erodes the margin all the way to equality. -/
theorem trusted_superset_twenty_erodes_original_ten_margin_to_boundary :
2 * participantThreshold 10 = 10 + byzantineBound 20 := by
native_decide
/-- The equality above means the original threshold for `10` is not strictly
safe if Byzantine weight is counted over the larger trusted universe `20`. -/
theorem trusted_superset_twenty_original_ten_threshold_not_safe :
¬ strictIntersectionSafe 10 20 (participantThreshold 10) := by
apply not_strictIntersectionSafe_of_threshold_le_boundary
native_decide
end XahauConsensus

View File

@@ -1,96 +0,0 @@
{"version": "1.2.0",
"packagesDir": ".lake/packages",
"packages":
[{"url": "https://github.com/leanprover-community/mathlib4.git",
"type": "git",
"subDir": null,
"scope": "",
"rev": "fabf563a7c95a166b8d7b6efca11c8b4dc9d911f",
"name": "mathlib",
"manifestFile": "lake-manifest.json",
"inputRev": "v4.31.0",
"inherited": false,
"configFile": "lakefile.lean"},
{"url": "https://github.com/leanprover-community/plausible",
"type": "git",
"subDir": null,
"scope": "leanprover-community",
"rev": "63045536fe95024e6c18fc7b48e03f506701c5bc",
"name": "plausible",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
"inherited": true,
"configFile": "lakefile.toml"},
{"url": "https://github.com/leanprover-community/LeanSearchClient",
"type": "git",
"subDir": null,
"scope": "leanprover-community",
"rev": "c5d5b8fe6e5158def25cd28eb94e4141ad97c843",
"name": "LeanSearchClient",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
"inherited": true,
"configFile": "lakefile.toml"},
{"url": "https://github.com/leanprover-community/import-graph",
"type": "git",
"subDir": null,
"scope": "leanprover-community",
"rev": "5c7542ed018c78194f1e2b903eaf6a792b74c03d",
"name": "importGraph",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
"inherited": true,
"configFile": "lakefile.toml"},
{"url": "https://github.com/leanprover-community/ProofWidgets4",
"type": "git",
"subDir": null,
"scope": "leanprover-community",
"rev": "24b0d9dc081c5423f8eec7e866c441e5184f29d9",
"name": "proofwidgets",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
"inherited": true,
"configFile": "lakefile.lean"},
{"url": "https://github.com/leanprover-community/aesop",
"type": "git",
"subDir": null,
"scope": "leanprover-community",
"rev": "e3cb2f741431ce31bf73549fb52316a57368b06f",
"name": "aesop",
"manifestFile": "lake-manifest.json",
"inputRev": "master",
"inherited": true,
"configFile": "lakefile.toml"},
{"url": "https://github.com/leanprover-community/quote4",
"type": "git",
"subDir": null,
"scope": "leanprover-community",
"rev": "f46324995fca5f0483b742e4eb4daec7f4ee50d2",
"name": "Qq",
"manifestFile": "lake-manifest.json",
"inputRev": "master",
"inherited": true,
"configFile": "lakefile.toml"},
{"url": "https://github.com/leanprover-community/batteries",
"type": "git",
"subDir": null,
"scope": "leanprover-community",
"rev": "fa08db58b30eb033edcdab331bba000827f9f785",
"name": "batteries",
"manifestFile": "lake-manifest.json",
"inputRev": "main",
"inherited": true,
"configFile": "lakefile.toml"},
{"url": "https://github.com/leanprover/lean4-cli",
"type": "git",
"subDir": null,
"scope": "leanprover",
"rev": "92564e5770e4d09f2d86dfbf8ada1e9c715b384c",
"name": "Cli",
"manifestFile": "lake-manifest.json",
"inputRev": "v4.31.0",
"inherited": true,
"configFile": "lakefile.toml"}],
"name": "xahau_consensus",
"lakeDir": ".lake",
"fixedToolchain": false}

View File

@@ -1,11 +0,0 @@
name = "xahau_consensus"
version = "0.1.0"
defaultTargets = ["XahauConsensus"]
[[require]]
name = "mathlib"
git = "https://github.com/leanprover-community/mathlib4.git"
rev = "v4.31.0"
[[lean_lib]]
name = "XahauConsensus"

View File

@@ -1 +0,0 @@
leanprover/lean4:v4.31.0

View File

@@ -47,8 +47,5 @@
#define MEM_OVERLAP -43
#define TOO_MANY_STATE_MODIFICATIONS -44
#define TOO_MANY_NAMESPACES -45
#define EXPORT_FAILURE -46
#define TOO_MANY_EXPORTED_TXN -47
#define TOO_LITTLE_ENTROPY -48
#define HOOK_ERROR_CODES
#endif //HOOK_ERROR_CODES

View File

@@ -339,41 +339,6 @@ prepare(
uint32_t read_ptr,
uint32_t read_len);
extern int64_t
xport_reserve(uint32_t count);
extern int64_t
xport(
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len);
extern int64_t
xport_cancel(uint32_t ticket_seq);
/*
Consensus entropy APIs.
min_tier is a fail-closed floor:
1 = consensus_fallback, 2 = participant_aligned, 3 = validator_quorum.
min_count is the minimum validator/reveal count the caller accepts.
If the most recent finalized entropy object does not satisfy both floors,
these APIs return TOO_LITTLE_ENTROPY. Open-ledger and simulate execution
are provisional previews over the entropy currently visible to the node;
final ordered ledger execution may see a different entropy object.
*/
extern int64_t
dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t
random(
uint32_t write_ptr,
uint32_t write_len,
uint32_t min_tier,
uint32_t min_count);
#ifdef __cplusplus
}
#endif

View File

@@ -41,21 +41,6 @@ APPLY_HOOK="$SCRIPT_DIR/../include/xrpl/hook/hook_api.macro"
# Insert __attribute__((noduplicate)) before _g
sub(/[[:space:]]+_g/, " __attribute__((noduplicate)) _g", line);
}
if (line ~ /[[:space:]]+dice[[:space:]]*\(/) {
print "/*";
print " Consensus entropy APIs.";
print "";
print " min_tier is a fail-closed floor:";
print " 1 = consensus_fallback, 2 = participant_aligned, 3 = validator_quorum.";
print " min_count is the minimum validator/reveal count the caller accepts.";
print "";
print " If the most recent finalized entropy object does not satisfy both floors,";
print " these APIs return TOO_LITTLE_ENTROPY. Open-ledger and simulate execution";
print " are provisional previews over the entropy currently visible to the node;";
print " final ordered ledger execution may see a different entropy object.";
print "*/";
}
# printf("\n");

View File

@@ -607,37 +607,31 @@ int out_len = 0;\
#define PREPARE_PAYMENT_SIMPLE_SIZE 248U
#endif
#define PREPARE_PAYMENT_SIMPLE( \
buf_out_master, drops_amount_raw, to_address, dest_tag_raw, src_tag_raw) \
{ \
uint8_t* buf_out = buf_out_master; \
uint8_t acc[20]; \
uint64_t drops_amount = (drops_amount_raw); \
uint32_t dest_tag = (dest_tag_raw); \
uint32_t src_tag = (src_tag_raw); \
uint32_t cls = (uint32_t)ledger_seq(); \
hook_account(SBUF(acc)); \
_01_02_ENCODE_TT(buf_out, ttPAYMENT); /* uint16 | size 3 */ \
_02_02_ENCODE_FLAGS(buf_out, tfCANONICAL); /* uint32 | size 5 */ \
_02_03_ENCODE_TAG_SRC(buf_out, src_tag); /* uint32 | size 5 */ \
_02_04_ENCODE_SEQUENCE(buf_out, 0); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST(buf_out, dest_tag); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS(buf_out, cls + 1); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS(buf_out, cls + 5); /* uint32 | size 6 */ \
_06_01_ENCODE_DROPS_AMOUNT( \
buf_out, drops_amount); /* amount | size 9 */ \
uint8_t* fee_ptr = buf_out; \
_06_08_ENCODE_DROPS_FEE(buf_out, 0); /* amount | size 9 */ \
_07_03_ENCODE_SIGNING_PUBKEY_NULL(buf_out); /* pk | size 35 */ \
_08_01_ENCODE_ACCOUNT_SRC(buf_out, acc); /* account | size 22 */ \
_08_03_ENCODE_ACCOUNT_DST( \
buf_out, to_address); /* account | size 22 */ \
int64_t edlen = etxn_details( \
(uint32_t)buf_out, \
PREPARE_PAYMENT_SIMPLE_SIZE); /* emitdet | size 1?? */ \
int64_t fee = \
etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_SIZE); \
_06_08_ENCODE_DROPS_FEE(fee_ptr, fee); \
#define PREPARE_PAYMENT_SIMPLE(buf_out_master, drops_amount_raw, to_address, dest_tag_raw, src_tag_raw)\
{\
uint8_t* buf_out = buf_out_master;\
uint8_t acc[20];\
uint64_t drops_amount = (drops_amount_raw);\
uint32_t dest_tag = (dest_tag_raw);\
uint32_t src_tag = (src_tag_raw);\
uint32_t cls = (uint32_t)ledger_seq();\
hook_account(SBUF(acc));\
_01_02_ENCODE_TT (buf_out, ttPAYMENT ); /* uint16 | size 3 */ \
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
_02_03_ENCODE_TAG_SRC (buf_out, src_tag ); /* uint32 | size 5 */ \
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST (buf_out, dest_tag ); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
_06_01_ENCODE_DROPS_AMOUNT (buf_out, drops_amount ); /* amount | size 9 */ \
uint8_t* fee_ptr = buf_out;\
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
_08_03_ENCODE_ACCOUNT_DST (buf_out, to_address ); /* account | size 22 */ \
int64_t edlen = etxn_details((uint32_t)buf_out, PREPARE_PAYMENT_SIMPLE_SIZE); /* emitdet | size 1?? */ \
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_SIZE); \
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
}
#ifdef HAS_CALLBACK
@@ -645,35 +639,33 @@ int out_len = 0;\
#else
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE 287
#endif
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE( \
buf_out_master, tlamt, to_address, dest_tag_raw, src_tag_raw) \
{ \
uint8_t* buf_out = buf_out_master; \
uint8_t acc[20]; \
uint32_t dest_tag = (dest_tag_raw); \
uint32_t src_tag = (src_tag_raw); \
uint32_t cls = (uint32_t)ledger_seq(); \
hook_account(SBUF(acc)); \
_01_02_ENCODE_TT(buf_out, ttPAYMENT); /* uint16 | size 3 */ \
_02_02_ENCODE_FLAGS(buf_out, tfCANONICAL); /* uint32 | size 5 */ \
_02_03_ENCODE_TAG_SRC(buf_out, src_tag); /* uint32 | size 5 */ \
_02_04_ENCODE_SEQUENCE(buf_out, 0); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST(buf_out, dest_tag); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS(buf_out, cls + 1); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS(buf_out, cls + 5); /* uint32 | size 6 */ \
_06_01_ENCODE_TL_AMOUNT(buf_out, tlamt); /* amount | size 48 */ \
uint8_t* fee_ptr = buf_out; \
_06_08_ENCODE_DROPS_FEE(buf_out, 0); /* amount | size 9 */ \
_07_03_ENCODE_SIGNING_PUBKEY_NULL(buf_out); /* pk | size 35 */ \
_08_01_ENCODE_ACCOUNT_SRC(buf_out, acc); /* account | size 22 */ \
_08_03_ENCODE_ACCOUNT_DST( \
buf_out, to_address); /* account | size 22 */ \
etxn_details( \
(uint32_t)buf_out, \
PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); /* emitdet | size 1?? */ \
int64_t fee = etxn_fee_base( \
buf_out_master, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); \
_06_08_ENCODE_DROPS_FEE(fee_ptr, fee); \
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE(buf_out_master, tlamt, to_address, dest_tag_raw, src_tag_raw)\
{\
uint8_t* buf_out = buf_out_master;\
uint8_t acc[20];\
uint32_t dest_tag = (dest_tag_raw);\
uint32_t src_tag = (src_tag_raw);\
uint32_t cls = (uint32_t)ledger_seq();\
hook_account(SBUF(acc));\
_01_02_ENCODE_TT (buf_out, ttPAYMENT ); /* uint16 | size 3 */ \
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
_02_03_ENCODE_TAG_SRC (buf_out, src_tag ); /* uint32 | size 5 */ \
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST (buf_out, dest_tag ); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
_06_01_ENCODE_TL_AMOUNT (buf_out, tlamt ); /* amount | size 48 */ \
uint8_t* fee_ptr = buf_out;\
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
_08_03_ENCODE_ACCOUNT_DST (buf_out, to_address ); /* account | size 22 */ \
etxn_details((uint32_t)buf_out, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); /* emitdet | size 1?? */ \
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); \
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
}
#endif

View File

@@ -9,8 +9,6 @@
#define sfUNLModifyDisabling ((16U << 16U) + 17U)
#define sfHookResult ((16U << 16U) + 18U)
#define sfWasLockingChainSend ((16U << 16U) + 19U)
#define sfSidecarType ((16U << 16U) + 20U)
#define sfEntropyTier ((16U << 16U) + 21U)
#define sfLedgerEntryType ((1U << 16U) + 1U)
#define sfTransactionType ((1U << 16U) + 2U)
#define sfSignerWeight ((1U << 16U) + 3U)
@@ -24,8 +22,6 @@
#define sfHookApiVersion ((1U << 16U) + 20U)
#define sfHookStateScale ((1U << 16U) + 21U)
#define sfLedgerFixType ((1U << 16U) + 22U)
#define sfHookExportCount ((1U << 16U) + 98U)
#define sfEntropyCount ((1U << 16U) + 99U)
#define sfNetworkID ((2U << 16U) + 1U)
#define sfFlags ((2U << 16U) + 2U)
#define sfSourceTag ((2U << 16U) + 3U)
@@ -84,7 +80,6 @@
#define sfRewardTime ((2U << 16U) + 98U)
#define sfRewardLgrFirst ((2U << 16U) + 99U)
#define sfRewardLgrLast ((2U << 16U) + 100U)
#define sfCancelTicketSequence ((2U << 16U) + 101U)
#define sfIndexNext ((3U << 16U) + 1U)
#define sfIndexPrevious ((3U << 16U) + 2U)
#define sfBookNode ((3U << 16U) + 3U)
@@ -164,7 +159,6 @@
#define sfEmittedTxnID ((5U << 16U) + 97U)
#define sfGovernanceMarks ((5U << 16U) + 98U)
#define sfGovernanceFlags ((5U << 16U) + 99U)
#define sfEntropyDigest ((5U << 16U) + 100U)
#define sfNumber ((9U << 16U) + 1U)
#define sfAmount ((6U << 16U) + 1U)
#define sfBalance ((6U << 16U) + 2U)
@@ -294,7 +288,6 @@
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
#define sfPriceData ((14U << 16U) + 32U)
#define sfCredential ((14U << 16U) + 33U)
#define sfExportedTxn ((14U << 16U) + 90U)
#define sfAmountEntry ((14U << 16U) + 91U)
#define sfMintURIToken ((14U << 16U) + 92U)
#define sfHookEmission ((14U << 16U) + 93U)
@@ -304,7 +297,6 @@
#define sfRemark ((14U << 16U) + 97U)
#define sfHighReward ((14U << 16U) + 98U)
#define sfLowReward ((14U << 16U) + 99U)
#define sfExportResult ((14U << 16U) + 100U)
#define sfSigners ((15U << 16U) + 3U)
#define sfSignerEntries ((15U << 16U) + 4U)
#define sfTemplate ((15U << 16U) + 5U)

View File

@@ -61,7 +61,6 @@
#define ttNFTOKEN_MODIFY 70
#define ttPERMISSIONED_DOMAIN_SET 71
#define ttPERMISSIONED_DOMAIN_DELETE 72
#define ttEXPORT 91
#define ttCRON 92
#define ttCRON_SET 93
#define ttREMARKS_SET 94
@@ -75,4 +74,3 @@
#define ttUNL_MODIFY 102
#define ttEMIT_FAILURE 103
#define ttUNL_REPORT 104
#define ttCONSENSUS_ENTROPY 105

View File

@@ -115,8 +115,3 @@ enum AMMClawbackFlags : uint32_t {
enum BridgeModifyFlags : uint32_t {
tfClearAccountCreateAmount = 0x00010000,
};
enum ConsensusEntropyFlags : uint32_t {
tfEntropyCommit = 0x00000001, // entry is a commitment in commitSet
tfEntropyReveal = 0x00000002, // entry is a reveal in entropySet
};

View File

@@ -15,8 +15,6 @@
#define uint256 std::string
#define featureHooksUpdate1 "1"
#define featureHooksUpdate2 "1"
#define featureExport "1"
#define featureConsensusEntropy "1"
#define fix20250131 "1"
#define fixGuardDepth32 "1"
namespace hook_api {
@@ -386,10 +384,7 @@ enum class hook_return_code : int64_t {
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state
// entires in the combined hook chains
TOO_MANY_NAMESPACES = -45,
EXPORT_FAILURE = -46,
TOO_MANY_EXPORTED_TXN = -47,
TOO_LITTLE_ENTROPY = -48,
TOO_MANY_NAMESPACES = -45
};
enum class ExitType : uint8_t {
@@ -403,7 +398,6 @@ const uint16_t max_state_modifications = 256;
const uint8_t max_slots = 255;
const uint8_t max_nonce = 255;
const uint8_t max_emit = 255;
const uint8_t max_export = 2;
const uint8_t max_params = 16;
const double fee_base_multiplier = 1.1f;
@@ -444,6 +438,10 @@ getImportWhitelist(Rules const& rules)
return whitelist;
}
#undef HOOK_API_DEFINITION
#undef I32
#undef I64
enum GuardRulesVersion : uint64_t {
GuardRuleFix20250131 = 0x00000001,
GuardRuleDepth32 = 0x00000002,

View File

@@ -372,28 +372,3 @@ HOOK_API_DEFINITION(
HOOK_API_DEFINITION(
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
featureHooksUpdate2)
// int64_t xport_reserve(uint32_t count);
HOOK_API_DEFINITION(
int64_t, xport_reserve, (uint32_t),
featureExport)
// int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, xport, (uint32_t, uint32_t, uint32_t, uint32_t),
featureExport)
// int64_t xport_cancel(uint32_t ticket_seq);
HOOK_API_DEFINITION(
int64_t, xport_cancel, (uint32_t),
featureExport)
// int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
HOOK_API_DEFINITION(
int64_t, dice, (uint32_t, uint32_t, uint32_t),
featureConsensusEntropy)
// int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t min_count);
HOOK_API_DEFINITION(
int64_t, random, (uint32_t, uint32_t, uint32_t, uint32_t),
featureConsensusEntropy)

View File

@@ -1,2 +0,0 @@
---
DisableFormat: true

View File

@@ -153,11 +153,7 @@ message TMStatusChange
message TMProposeSet
{
required uint32 proposeSeq = 1;
// Proposed transaction-set identity. Legacy/plain proposals carry the
// tx-set hash directly; ConsensusExtensions proposals carry a serialized
// ExtendedPosition whose first field is that tx-set hash, followed by
// signed RNG/Export sidecar fields.
required bytes currentTxHash = 2;
required bytes currentTxHash = 2; // the hash of the ledger we are proposing
required bytes nodePubKey = 3;
required uint32 closeTime = 4;
required bytes signature = 5; // signature of above fields
@@ -170,14 +166,6 @@ message TMProposeSet
// Number of hops traveled
optional uint32 hops = 12 [deprecated=true];
// Export signatures for pending exports seen in the proposal set. The
// proposal's ExtendedPosition includes a digest of this repeated field, so
// these side-channel blobs are covered by the proposal signature.
// Each entry is: txnHash (32 bytes) + validator pubkey (33 bytes)
// + multisign signature (variable length). Validators attach these
// so export quorum can be reached within the same consensus round.
repeated bytes exportSignatures = 13;
}
enum TxSetStatus
@@ -396,3 +384,4 @@ message TMHaveTransactions
{
repeated bytes hashes = 1;
}

View File

@@ -1,42 +0,0 @@
#ifndef RIPPLE_PROTOCOL_ENTROPY_TIER_H_INCLUDED
#define RIPPLE_PROTOCOL_ENTROPY_TIER_H_INCLUDED
#include <cstdint>
namespace ripple {
/// Which gate the ledger's entropy passed. Stored in sfEntropyTier (UINT8)
/// on the ttCONSENSUS_ENTROPY pseudo-transaction and the ConsensusEntropy
/// ledger entry.
///
/// EntropyCount says how many validators contributed; EntropyTier says which
/// gate the result passed. Values are strength-ordered so consumers can gate
/// with a numeric comparison (tier >= required).
enum EntropyTier : std::uint8_t {
/// No usable entropy (reserved; a fresh ConsensusEntropy entry should
/// always carry one of the tiers below).
entropyTierNone = 0,
/// Consensus-bound deterministic fallback: derived from already-agreed
/// round inputs (parent ledger hash, base tx set hash, sequence) under
/// HashPrefix::entropyFallback when no agreed reveal set reaches either
/// participant_aligned or validator_quorum. Unpredictable in practice but
/// user-influenceable via transaction submission — never suitable for
/// value-bearing outcomes.
entropyTierConsensusFallback = 1,
/// Participant-aligned sub-quorum entropy: the agreed reveal set aligned at
/// the tier-2 participant threshold — below the 80% validator quorum but at
/// or above the equivocation-intersection floor over the original
/// (pre-nUNL)
/// view. Weaker than validator_quorum; opt-in for hooks via min_tier.
entropyTierParticipantAligned = 2,
/// Validator commit/reveal entropy whose sidecar set passed the
/// active-validator-view quorum alignment gate.
entropyTierValidatorQuorum = 3,
};
} // namespace ripple
#endif

View File

@@ -1,33 +0,0 @@
#ifndef RIPPLE_PROTOCOL_EXPORT_LIMITS_H_INCLUDED
#define RIPPLE_PROTOCOL_EXPORT_LIMITS_H_INCLUDED
#include <cstdint>
namespace ripple {
// Export system caps.
//
// These limits bound the DoS surface of the export signature system:
// - Each pending export requires every validator to sign it every round
// (sign-once, attach once via TMProposeSet)
// - Inbound signature processing involves crypto verification per sig
// - The open-ledger cap (maxPendingExports) is the root constraint;
// signing throughput and inbound processing are transitively bounded by it
struct ExportLimits
{
// Maximum exports a single hook execution may produce
// (also enforced by hook_api::max_export in Enum.h)
static constexpr std::uint8_t maxExportsPerHook = 2;
// Maximum pending export transactions in an open/apply ledger.
// Hook-emitted export backlog drains into the open ledger at this cap.
// This transitively caps:
// - signatures per TMProposeSet message (1 per pending export)
// - inbound proposal signature processing (clamped to this)
// - validator signing work per round
static constexpr std::uint8_t maxPendingExports = 8;
};
} // namespace ripple
#endif

View File

@@ -96,15 +96,6 @@ enum class HashPrefix : std::uint32_t {
/** Credentials signature */
credential = detail::make_hash_prefix('C', 'R', 'D'),
/** consensus extension sidecar object */
sidecar = detail::make_hash_prefix('S', 'C', 'R'),
/** consensus-bound fallback entropy digest (Tier 1: derived from
already-agreed round inputs when no agreed reveal set reaches an
accepted validator-participant tier; never to be confused with
validator entropy) */
entropyFallback = detail::make_hash_prefix('E', 'F', 'B'),
};
template <class Hasher>

View File

@@ -62,9 +62,6 @@ emittedDir() noexcept;
Keylet
emittedTxn(uint256 const& id) noexcept;
Keylet
shadowTicket(AccountID const& account, std::uint32_t ticketSeq) noexcept;
Keylet
hookDefinition(uint256 const& hash) noexcept;
@@ -121,10 +118,6 @@ negativeUNL() noexcept;
Keylet const&
UNLReport() noexcept;
/** The (fixed) index of the object containing consensus-derived entropy. */
Keylet const&
consensusEntropy() noexcept;
/** The beginning of an order book */
struct book_t
{

View File

@@ -1,21 +0,0 @@
#ifndef RIPPLE_PROTOCOL_SIDECAR_TYPE_H_INCLUDED
#define RIPPLE_PROTOCOL_SIDECAR_TYPE_H_INCLUDED
#include <cstdint>
namespace ripple {
/// Discriminator for sidecar set entries (SHAMap leaves used for
/// consensus extension data: RNG commit/reveal, export signatures).
///
/// Stored in sfSidecarType (UINT8) on each STObject entry.
/// Makes sidecar sets self-describing — no content-sniffing needed.
enum SidecarType : std::uint8_t {
sidecarRngCommit = 1,
sidecarRngReveal = 2,
sidecarExportSig = 3,
};
} // namespace ripple
#endif

View File

@@ -68,7 +68,6 @@ enum TELcodes : TERUnderlyingType {
telNON_LOCAL_EMITTED_TXN,
telIMPORT_VL_KEY_NOT_RECOGNISED,
telCAN_NOT_QUEUE_IMPORT,
telSHADOW_TICKET_REQUIRED,
telENV_RPC_FAILED,
};
@@ -235,10 +234,8 @@ enum TERcodes : TERUnderlyingType {
terQUEUED, // Transaction is being held in TxQ until fee drops
terPRE_TICKET, // Ticket is not yet in ledger but might be on its way
terNO_AMM, // AMM doesn't exist for the asset pair
terNO_HOOK, // Transaction requires a non-existent hook definition
terNO_HOOK // Transaction requires a non-existent hook definition
// (referenced by sfHookHash)
terRETRY_EXPORT // Export does not yet have enough validator signatures.
// Retained in retriable set for next ledger.
};
//------------------------------------------------------------------------------
@@ -366,7 +363,6 @@ enum TECcodes : TERUnderlyingType {
tecARRAY_TOO_LARGE = 197,
tecLOCKED = 198,
tecBAD_CREDENTIALS = 199,
tecEXPORT_EXPIRED = 200,
tecLAST_POSSIBLE_ENTRY = 255,
};

View File

@@ -274,13 +274,6 @@ enum BridgeModifyFlags : uint32_t {
tfClearAccountCreateAmount = 0x00010000,
};
constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount);
// ConsensusEntropy flags (used on ttCONSENSUS_ENTROPY SHAMap entries):
enum ConsensusEntropyFlags : uint32_t {
tfEntropyCommit = 0x00000001, // entry is a commitment in commitSet
tfEntropyReveal = 0x00000002, // entry is a reveal in entropySet
};
// flag=0 (no tfEntropyCommit/tfEntropyReveal) = final injected pseudo-tx
// clang-format on
} // namespace ripple

View File

@@ -140,12 +140,6 @@ public:
mHookEmissions = hookEmissions;
}
void
setExportResult(STObject const& exportResult)
{
mExportResult = exportResult;
}
bool
hasHookExecutions() const
{
@@ -158,12 +152,6 @@ public:
return static_cast<bool>(mHookEmissions);
}
bool
hasExportResult() const
{
return static_cast<bool>(mExportResult);
}
STAmount
getDeliveredAmount() const
{
@@ -188,7 +176,6 @@ private:
std::optional<STAmount> mDelivered;
std::optional<STArray> mHookExecutions;
std::optional<STArray> mHookEmissions;
std::optional<STObject> mExportResult;
STArray mNodes;
};

View File

@@ -65,8 +65,6 @@ XRPL_FEATURE(AMM, Supported::no, VoteBehavior::DefaultNo
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Export, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConsensusEntropy, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -223,21 +223,6 @@ LEDGER_ENTRY(ltURI_TOKEN, 0x0055, URIToken, uri_token, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** The ledger object which stores consensus-derived entropy.
\note This is a singleton: only one such object exists in the ledger.
\sa keylet::consensusEntropy
*/
LEDGER_ENTRY_DUPLICATE(ltCONSENSUS_ENTROPY, 0x0058, ConsensusEntropy, consensus_entropy, ({
{sfDigest, soeREQUIRED},
{sfEntropyCount, soeREQUIRED},
{sfEntropyTier, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object which describes an account.
\sa keylet::account
@@ -607,22 +592,6 @@ LEDGER_ENTRY(ltDID, 0x008D, DID, did, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
//@@start shadow-ticket-ledger-entry
/** A shadow ticket for export replay protection.
Created when a transaction is exported. Consumed when
proof-of-execution is imported back. Account-owned (pays reserve).
\sa keylet::shadowTicket
*/
LEDGER_ENTRY(ltSHADOW_TICKET, 0x5374, ShadowTicket, shadow_ticket, ({
{sfAccount, soeREQUIRED},
{sfTicketSequence, soeREQUIRED},
{sfTransactionHash, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
}))
//@@end shadow-ticket-ledger-entry
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -42,8 +42,6 @@ TYPED_SFIELD(sfTickSize, UINT8, 16)
TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17)
TYPED_SFIELD(sfHookResult, UINT8, 18)
TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19)
TYPED_SFIELD(sfSidecarType, UINT8, 20)
TYPED_SFIELD(sfEntropyTier, UINT8, 21)
// 16-bit integers (common)
TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::sMD_Never)
@@ -61,8 +59,6 @@ TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
TYPED_SFIELD(sfHookStateScale, UINT16, 21)
TYPED_SFIELD(sfLedgerFixType, UINT16, 22)
TYPED_SFIELD(sfHookExportCount, UINT16, 98)
TYPED_SFIELD(sfEntropyCount, UINT16, 99)
// 32-bit integers (common)
TYPED_SFIELD(sfNetworkID, UINT32, 1)
@@ -127,7 +123,6 @@ TYPED_SFIELD(sfImportSequence, UINT32, 97)
TYPED_SFIELD(sfRewardTime, UINT32, 98)
TYPED_SFIELD(sfRewardLgrFirst, UINT32, 99)
TYPED_SFIELD(sfRewardLgrLast, UINT32, 100)
TYPED_SFIELD(sfCancelTicketSequence, UINT32, 101)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -222,7 +217,6 @@ TYPED_SFIELD(sfHookCanEmit, UINT256, 96)
TYPED_SFIELD(sfEmittedTxnID, UINT256, 97)
TYPED_SFIELD(sfGovernanceMarks, UINT256, 98)
TYPED_SFIELD(sfGovernanceFlags, UINT256, 99)
TYPED_SFIELD(sfEntropyDigest, UINT256, 100)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -388,7 +382,6 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
UNTYPED_SFIELD(sfExportedTxn, OBJECT, 90)
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
UNTYPED_SFIELD(sfHookEmission, OBJECT, 93)
@@ -398,7 +391,6 @@ UNTYPED_SFIELD(sfGenesisMint, OBJECT, 96)
UNTYPED_SFIELD(sfRemark, OBJECT, 97)
UNTYPED_SFIELD(sfHighReward, OBJECT, 98)
UNTYPED_SFIELD(sfLowReward, OBJECT, 99)
UNTYPED_SFIELD(sfExportResult, OBJECT, 100)
// array of objects (common)
// ARRAY/1 is reserved for end of array

View File

@@ -500,17 +500,6 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
{sfDomainID, soeREQUIRED},
}))
//@@start export-transaction-types
/* User-submittable export: creates a cross-chain transaction for
validator signing. Retries via terRETRY_EXPORT until quorum.
Also supports shadow ticket cancellation via sfCancelTicketSequence.
At least one of sfExportedTxn or sfCancelTicketSequence must be present. */
TRANSACTION(ttEXPORT, 91, Export, ({
{sfExportedTxn, soeOPTIONAL},
{sfCancelTicketSequence, soeOPTIONAL},
}))
//@@end export-transaction-types
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
TRANSACTION(ttCRON, 92, Cron, ({
{sfOwner, soeREQUIRED},
@@ -617,11 +606,3 @@ TRANSACTION(ttUNL_REPORT, 104, UNLReport, ({
{sfActiveValidator, soeOPTIONAL},
{sfImportVLKey, soeOPTIONAL},
}))
TRANSACTION(ttCONSENSUS_ENTROPY, 105, ConsensusEntropy, ({
{sfLedgerSequence, soeREQUIRED},
{sfDigest, soeREQUIRED},
{sfEntropyCount, soeREQUIRED},
{sfEntropyTier, soeREQUIRED},
{sfBlob, soeOPTIONAL},
}))

View File

@@ -109,22 +109,14 @@ public:
Consumer
newInboundEndpoint(beast::IP::Endpoint const& address)
{
//@@start rng-local-testnet-resource-bucket
// Inbound connections from the same IP normally share one
// resource bucket (port stripped) for DoS protection. For
// loopback addresses, preserve the port so local testnet nodes
// each get their own bucket instead of all sharing one.
auto const key = is_loopback(address) ? address : address.at_port(0);
//@@end rng-local-testnet-resource-bucket
Entry* entry(nullptr);
{
std::lock_guard _(lock_);
auto [resultIt, resultInserted] = table_.emplace(
std::piecewise_construct,
std::make_tuple(kindInbound, key),
std::make_tuple(m_clock.now()));
std::make_tuple(kindInbound, address.at_port(0)), // Key
std::make_tuple(m_clock.now())); // Entry
entry = &resultIt->second;
entry->key = &resultIt->first;

View File

@@ -31,7 +31,6 @@
#include <cassert>
#include <cstring>
#include <ctime>
#include <exception>
#include <fstream>
#include <functional>
#include <iostream>
@@ -352,18 +351,9 @@ Logs::format(
if (useLocalTime)
{
try
{
auto now = std::chrono::system_clock::now();
auto local = date::make_zoned(date::current_zone(), now);
output = date::format(fmt, local);
}
catch (std::exception const&)
{
// Enhanced logging should not make startup fatal if tzdb lookup is
// unavailable or misconfigured. Fall back to UTC formatting.
output = date::format(fmt, std::chrono::system_clock::now());
}
auto now = std::chrono::system_clock::now();
auto local = date::make_zoned(date::current_zone(), now);
output = date::format(fmt, local);
}
else
{

View File

@@ -74,7 +74,6 @@ enum class LedgerNameSpace : std::uint16_t {
HOOK_DEFINITION = 'D',
EMITTED_TXN = 'E',
EMITTED_DIR = 'F',
SHADOW_TICKET = 0x5374, // St
NFTOKEN_OFFER = 'q',
NFTOKEN_BUY_OFFERS = 'h',
NFTOKEN_SELL_OFFERS = 'i',
@@ -82,7 +81,6 @@ enum class LedgerNameSpace : std::uint16_t {
IMPORT_VLSEQ = 'I',
UNL_REPORT = 'R',
CRON = 'L',
CONSENSUS_ENTROPY = 'X',
AMM = 'A',
BRIDGE = LEDGER_NAMESPACE2(0x01, 'H'),
XCHAIN_CLAIM_ID = 'Q',
@@ -190,15 +188,6 @@ emittedTxn(uint256 const& id) noexcept
return {ltEMITTED_TXN, indexHash(LedgerNameSpace::EMITTED_TXN, id)};
}
Keylet
shadowTicket(AccountID const& account, std::uint32_t ticketSeq) noexcept
{
return {
ltSHADOW_TICKET,
indexHash(
LedgerNameSpace::SHADOW_TICKET, account, std::uint32_t(ticketSeq))};
}
Keylet
hook(AccountID const& id) noexcept
{
@@ -557,14 +546,6 @@ cron(uint32_t timestamp, std::optional<AccountID> const& id)
return {ltCRON, uint256::fromVoid(h)};
}
Keylet const&
consensusEntropy() noexcept
{
static Keylet const ret{
ltCONSENSUS_ENTROPY, indexHash(LedgerNameSpace::CONSENSUS_ENTROPY)};
return ret;
}
Keylet
amm(Asset const& issue1, Asset const& issue2) noexcept
{

View File

@@ -78,7 +78,6 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookExecutionIndex, soeREQUIRED},
{sfHookStateChangeCount, soeREQUIRED},
{sfHookEmitCount, soeREQUIRED},
{sfHookExportCount, soeOPTIONAL},
{sfFlags, soeOPTIONAL}});
add(sfHookEmission.jsonName,

View File

@@ -684,8 +684,7 @@ isPseudoTx(STObject const& tx)
auto tt = safe_cast<TxType>(*t);
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
tt == ttCONSENSUS_ENTROPY;
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
}
} // namespace ripple

View File

@@ -124,7 +124,6 @@ transResults()
MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."),
MAKE_ERROR(tecLOCKED, "Fund is locked."),
MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."),
MAKE_ERROR(tecEXPORT_EXPIRED, "Export expired without reaching signature quorum."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
@@ -172,7 +171,6 @@ transResults()
MAKE_ERROR(telNON_LOCAL_EMITTED_TXN, "Emitted transaction cannot be applied because it was not generated locally."),
MAKE_ERROR(telIMPORT_VL_KEY_NOT_RECOGNISED, "Import vl key was not recognized."),
MAKE_ERROR(telCAN_NOT_QUEUE_IMPORT, "Import transaction was not able to be directly applied and cannot be queued."),
MAKE_ERROR(telSHADOW_TICKET_REQUIRED, "The imported transaction uses a TicketSequence but no shadow ticket exists."),
MAKE_ERROR(telENV_RPC_FAILED, "Unit test RPC failure."),
MAKE_ERROR(temMALFORMED, "Malformed transaction."),
@@ -240,7 +238,6 @@ transResults()
MAKE_ERROR(terPRE_TICKET, "Ticket is not yet in ledger."),
MAKE_ERROR(terNO_HOOK, "No hook with that hash exists on the ledger."),
MAKE_ERROR(terNO_AMM, "AMM doesn't exist for the asset pair."),
MAKE_ERROR(terRETRY_EXPORT, "Export awaiting validator signatures."),
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
MAKE_ERROR(tesPARTIAL, "The transaction was applied but should be submitted again until returning tesSUCCESS."),

View File

@@ -49,11 +49,6 @@ TxMeta::TxMeta(
if (obj.isFieldPresent(sfHookEmissions))
setHookEmissions(obj.getFieldArray(sfHookEmissions));
if (obj.isFieldPresent(sfExportResult))
setExportResult(const_cast<STObject&>(obj)
.getField(sfExportResult)
.downcast<STObject>());
}
TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj)
@@ -80,11 +75,6 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj)
if (obj.isFieldPresent(sfHookEmissions))
setHookEmissions(obj.getFieldArray(sfHookEmissions));
if (obj.isFieldPresent(sfExportResult))
setExportResult(const_cast<STObject&>(obj)
.getField(sfExportResult)
.downcast<STObject>());
}
TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec)
@@ -255,14 +245,6 @@ TxMeta::getAsObject() const
if (hasHookEmissions())
metaData.setFieldArray(sfHookEmissions, getHookEmissions());
if (hasExportResult())
{
Serializer s;
mExportResult->add(s);
SerialIter sit(s.slice());
metaData.emplace_back(STObject(sit, sfExportResult));
}
return metaData;
}

View File

@@ -1,589 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL Labs
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.
*/
//==============================================================================
#include <test/app/ConsensusEntropy_test_hooks.h>
#include <test/jtx.h>
#include <test/jtx/hook.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/hook/Enum.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
using TestHook = std::vector<uint8_t> const&;
#define BEAST_REQUIRE(x) \
{ \
BEAST_EXPECT(!!(x)); \
if (!(x)) \
return; \
}
#define HSFEE fee(100'000'000)
#define M(m) memo(m, "", "")
class ConsensusEntropy_test : public beast::unit_test::suite
{
static void
overrideFlag(Json::Value& jv)
{
jv[jss::Flags] = hsfOVERRIDE;
}
static int64_t
hookReturnCode(STObject const& hookExecution)
{
auto const rawCode = hookExecution.getFieldU64(sfHookReturnCode);
return (rawCode & 0x8000000000000000ULL)
? -static_cast<int64_t>(rawCode & 0x7FFFFFFFFFFFFFFFULL)
: static_cast<int64_t>(rawCode);
}
void
testSLECreated()
{
testcase("SLE created on ledger close");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
BEAST_EXPECT(!env.le(keylet::consensusEntropy()));
env.close();
auto const sle = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(sle);
auto const digest = sle->getFieldH256(sfDigest);
BEAST_EXPECT(digest != uint256{});
auto const count = sle->getFieldU16(sfEntropyCount);
BEAST_EXPECT(count >= 5);
auto const sleSeq = sle->getFieldU32(sfLedgerSequence);
BEAST_EXPECT(sleSeq == env.closed()->seq());
}
void
testSLEUpdatedOnSubsequentClose()
{
testcase("SLE updated on subsequent ledger close");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
env.close();
auto const sle1 = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(sle1);
auto const digest1 = sle1->getFieldH256(sfDigest);
auto const seq1 = sle1->getFieldU32(sfLedgerSequence);
env.close();
auto const sle2 = env.le(keylet::consensusEntropy());
BEAST_REQUIRE(sle2);
auto const digest2 = sle2->getFieldH256(sfDigest);
auto const seq2 = sle2->getFieldU32(sfLedgerSequence);
BEAST_EXPECT(digest2 != digest1);
BEAST_EXPECT(seq2 == seq1 + 1);
}
void
testNoSLEWithoutAmendment()
{
testcase("No SLE without amendment");
using namespace jtx;
Env env{*this};
env.close();
env.close();
BEAST_EXPECT(!env.le(keylet::consensusEntropy()));
}
void
testDice()
{
testcase("Hook dice() API");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
// Entropy SLE must exist before hook can use dice()
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// Set the hook
TestHook hook = consensusentropy_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
int64_t hook(uint32_t r)
{
_g(1,1);
// dice(6) should return 0..5
int64_t result = dice(6, 3, 5);
// negative means error
if (result < 0)
rollback(0, 0, result);
if (result >= 6)
rollback(0, 0, -1);
// return the dice result as the accept code
return accept(0, 0, result);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice hook"),
HSFEE);
env.close();
// Invoke the hook
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice"), fee(XRP(1)));
auto meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
auto const returnCode = hookExecutions[0].getFieldU64(sfHookReturnCode);
std::cerr << " dice(6) returnCode = " << returnCode << " (hex 0x"
<< std::hex << returnCode << std::dec << ")\n";
// dice(6) returns 0..5
BEAST_EXPECT(returnCode <= 5);
// Result should be 3 (accept)
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testRandom()
{
testcase("Hook random() API");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// Hook calls random() to fill a 32-byte buffer, then checks
// the buffer is not all zeroes.
TestHook hook = consensusentropy_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t min_count);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t buf[32];
for (int i = 0; GUARD(32), i < 32; ++i)
buf[i] = 0;
int64_t result = random((uint32_t)buf, 32, 3, 5);
// Should return 32 (bytes written)
if (result != 32)
rollback(0, 0, result);
// Verify buffer is not all zeroes
int nonzero = 0;
for (int i = 0; GUARD(32), i < 32; ++i)
if (buf[i] != 0) nonzero = 1;
if (!nonzero)
rollback(0, 0, -2);
return accept(0, 0, 0);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set random hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test random"), fee(XRP(1)));
auto meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
// Return code 0 = all checks passed in the hook
BEAST_EXPECT(hookExecutions[0].getFieldU64(sfHookReturnCode) == 0);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testDiceConsecutiveCallsDiffer()
{
testcase("Hook dice() consecutive calls return different values");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// dice(1000000) twice — large range makes collision near-impossible
// encode r1 in low 20 bits, r2 in high bits
TestHook hook = consensusentropy_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t r1 = dice(1000000, 3, 5);
if (r1 < 0)
rollback(0, 0, r1);
int64_t r2 = dice(1000000, 3, 5);
if (r2 < 0)
rollback(0, 0, r2);
// consecutive calls should differ (rngCallCounter)
if (r1 == r2)
rollback(0, 0, -1);
return accept(0, 0, r1 | (r2 << 20));
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice consecutive"), fee(XRP(1)));
auto meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
auto const rc = hookExecutions[0].getFieldU64(sfHookReturnCode);
auto const r1 = rc & 0xFFFFF;
auto const r2 = (rc >> 20) & 0xFFFFF;
std::cerr << " two-call dice(1000000): returnCode=" << rc << " hex=0x"
<< std::hex << rc << std::dec << " r1=" << r1 << " r2=" << r2
<< "\n";
// hookResult 3 = accept (would be 1 if r1==r2 triggered rollback)
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
BEAST_EXPECT(r1 < 1000000);
BEAST_EXPECT(r2 < 1000000);
BEAST_EXPECT(r1 != r2);
}
void
testDiceZeroSides()
{
testcase("Hook dice(0) returns INVALID_ARGUMENT");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// Hook calls dice(0) and returns whatever dice returns.
// dice(0) should return INVALID_ARGUMENT (-7).
TestHook hook = consensusentropy_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(0, 3, 5);
// dice(0) should return negative error code, pass it through
return accept(0, 0, result);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice0 hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice(0)"), fee(XRP(1)));
auto meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
// INVALID_ARGUMENT = -7, encoded as 0x8000000000000000 + abs(code)
// (see applyHook.cpp unsigned_exit_code encoding)
auto const rawCode = hookExecutions[0].getFieldU64(sfHookReturnCode);
int64_t returnCode = (rawCode & 0x8000000000000000ULL)
? -static_cast<int64_t>(rawCode & 0x7FFFFFFFFFFFFFFFULL)
: static_cast<int64_t>(rawCode);
std::cerr << " dice(0) returnCode = " << returnCode << " (raw 0x"
<< std::hex << rawCode << std::dec << ")\n";
BEAST_EXPECT(returnCode == -7);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testDiceRequirementNotMet()
{
testcase("Hook dice() returns TOO_LITTLE_ENTROPY below requirement");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
// Standalone entropy carries EntropyCount=20 / tier validator_quorum.
// A hook demanding min_count=21 states a requirement this ledger
// cannot meet, so dice must fail closed with TOO_LITTLE_ENTROPY (-48)
// rather than silently serving weaker entropy.
TestHook hook = consensusentropy_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(6, 3, 21);
// requirement unmet: pass the error code through
return accept(0, 0, result);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set dice-requirement hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test dice min_count unmet"), fee(XRP(1)));
auto meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
auto const rawCode = hookExecutions[0].getFieldU64(sfHookReturnCode);
int64_t returnCode = (rawCode & 0x8000000000000000ULL)
? -static_cast<int64_t>(rawCode & 0x7FFFFFFFFFFFFFFFULL)
: static_cast<int64_t>(rawCode);
std::cerr << " dice(6,3,21) returnCode = " << returnCode << " (raw 0x"
<< std::hex << rawCode << std::dec << ")\n";
BEAST_EXPECT(returnCode == -48); // TOO_LITTLE_ENTROPY
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
testInvalidEntropyRequirements()
{
testcase("Hook dice/random reject invalid entropy requirements");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureConsensusEntropy,
nullptr};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
BEAST_REQUIRE(env.le(keylet::consensusEntropy()));
TestHook hook = consensusentropy_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t min_count);
#define INVALID_ARGUMENT (-7)
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t buf[32];
int64_t bad_min_tier = dice(6, 0, 0);
if (bad_min_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_min_tier);
int64_t bad_high_tier = dice(6, 4, 0);
if (bad_high_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_high_tier);
int64_t bad_min_count = dice(6, 3, 70000);
if (bad_min_count != INVALID_ARGUMENT)
return accept(0, 0, bad_min_count);
int64_t bad_random_tier = random((uint32_t)buf, 32, 4, 0);
if (bad_random_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_random_tier);
// Sentinel distinct from any dice (0..5) / random result and
// from INVALID_ARGUMENT, so a regression that lets a bad
// requirement through returns its own code, not this one.
return accept(0, 0, 42);
}
)[test.hook]"];
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set invalid entropy requirement hook"),
HSFEE);
env.close();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = alice.human();
env(invoke, M("test invalid entropy requirements"), fee(XRP(1)));
auto meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
// 42 only if all four invalid requirements were rejected; any bad
// requirement leaking through returns its own (non-42) code.
BEAST_EXPECT(hookReturnCode(hookExecutions[0]) == 42);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
}
void
run() override
{
testSLECreated();
testSLEUpdatedOnSubsequentClose();
testNoSLEWithoutAmendment();
testDice();
testDiceZeroSides();
testDiceRequirementNotMet();
testInvalidEntropyRequirements();
testRandom();
testDiceConsecutiveCallsDiffer();
}
};
BEAST_DEFINE_TESTSUITE(ConsensusEntropy, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -1,326 +0,0 @@
// This file is generated by hookz build-test-hooks
#ifndef CONSENSUSENTROPY_TEST_WASM_INCLUDED
#define CONSENSUSENTROPY_TEST_WASM_INCLUDED
#include <map>
#include <stdint.h>
#include <string>
#include <vector>
namespace ripple {
namespace test {
inline std::map<std::string, std::vector<uint8_t>> consensusentropy_test_wasm =
{
/* ==== WASM: 0 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
int64_t hook(uint32_t r)
{
_g(1,1);
// dice(6) should return 0..5
int64_t result = dice(6, 3, 5);
// negative means error
if (result < 0)
rollback(0, 0, result);
if (result >= 6)
rollback(0, 0, -1);
// return the dice result as the accept code
return accept(0, 0, result);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U,
0x1AU, 0x04U, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU,
0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU,
0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU,
0x02U, 0x31U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU,
0x67U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x04U, 0x64U,
0x69U, 0x63U, 0x65U, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU,
0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U,
0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x00U, 0x03U, 0x02U, 0x01U,
0x03U, 0x05U, 0x03U, 0x01U, 0x00U, 0x01U, 0x07U, 0x08U, 0x01U,
0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x04U, 0x0AU, 0x40U,
0x01U, 0x3EU, 0x01U, 0x02U, 0x7EU, 0x41U, 0x01U, 0x41U, 0x01U,
0x10U, 0x00U, 0x1AU, 0x41U, 0x06U, 0x41U, 0x03U, 0x41U, 0x05U,
0x10U, 0x01U, 0x22U, 0x01U, 0x21U, 0x02U, 0x02U, 0x40U, 0x20U,
0x01U, 0x42U, 0x00U, 0x59U, 0x04U, 0x40U, 0x42U, 0x7FU, 0x21U,
0x02U, 0x20U, 0x01U, 0x42U, 0x06U, 0x54U, 0x0DU, 0x01U, 0x0BU,
0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x10U, 0x02U, 0x1AU,
0x0BU, 0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x01U, 0x10U, 0x03U,
0x0BU,
}},
/* ==== WASM: 1 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t min_count);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t buf[32];
for (int i = 0; GUARD(32), i < 32; ++i)
buf[i] = 0;
int64_t result = random((uint32_t)buf, 32, 3, 5);
// Should return 32 (bytes written)
if (result != 32)
rollback(0, 0, result);
// Verify buffer is not all zeroes
int nonzero = 0;
for (int i = 0; GUARD(32), i < 32; ++i)
if (buf[i] != 0) nonzero = 1;
if (!nonzero)
rollback(0, 0, -2);
return accept(0, 0, 0);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U,
0x1BU, 0x04U, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU,
0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x04U, 0x7FU,
0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U,
0x7EU, 0x02U, 0x33U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U,
0x5FU, 0x67U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U,
0x72U, 0x61U, 0x6EU, 0x64U, 0x6FU, 0x6DU, 0x00U, 0x02U, 0x03U,
0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U,
0x61U, 0x63U, 0x6BU, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x00U,
0x03U, 0x02U, 0x01U, 0x03U, 0x05U, 0x03U, 0x01U, 0x00U, 0x01U,
0x06U, 0x08U, 0x01U, 0x7FU, 0x01U, 0x41U, 0x80U, 0x80U, 0x04U,
0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU,
0x00U, 0x04U, 0x0AU, 0xD0U, 0x01U, 0x01U, 0xCDU, 0x01U, 0x02U,
0x03U, 0x7FU, 0x01U, 0x7EU, 0x23U, 0x00U, 0x41U, 0x20U, 0x6BU,
0x22U, 0x01U, 0x24U, 0x00U, 0x41U, 0x01U, 0x41U, 0x01U, 0x10U,
0x00U, 0x1AU, 0x41U, 0x8EU, 0x80U, 0x80U, 0x80U, 0x78U, 0x41U,
0x21U, 0x10U, 0x00U, 0x1AU, 0x41U, 0x00U, 0x21U, 0x00U, 0x03U,
0x40U, 0x41U, 0x8EU, 0x80U, 0x80U, 0x80U, 0x78U, 0x41U, 0x21U,
0x10U, 0x00U, 0x1AU, 0x20U, 0x00U, 0x20U, 0x01U, 0x6AU, 0x41U,
0x00U, 0x3AU, 0x00U, 0x00U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U,
0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x20U, 0x00U, 0x41U,
0x01U, 0x6AU, 0x22U, 0x00U, 0x41U, 0x20U, 0x47U, 0x0DU, 0x00U,
0x0BU, 0x20U, 0x01U, 0x41U, 0x20U, 0x41U, 0x03U, 0x41U, 0x05U,
0x10U, 0x01U, 0x22U, 0x04U, 0x42U, 0x20U, 0x52U, 0x04U, 0x40U,
0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x04U, 0x10U, 0x02U, 0x1AU,
0x0BU, 0x41U, 0x99U, 0x80U, 0x80U, 0x80U, 0x78U, 0x41U, 0x21U,
0x10U, 0x00U, 0x1AU, 0x41U, 0x00U, 0x21U, 0x00U, 0x03U, 0x40U,
0x41U, 0x99U, 0x80U, 0x80U, 0x80U, 0x78U, 0x41U, 0x21U, 0x10U,
0x00U, 0x1AU, 0x20U, 0x00U, 0x20U, 0x01U, 0x6AU, 0x2DU, 0x00U,
0x00U, 0x21U, 0x03U, 0x41U, 0x01U, 0x20U, 0x02U, 0x20U, 0x03U,
0x1BU, 0x21U, 0x02U, 0x20U, 0x00U, 0x41U, 0x01U, 0x6AU, 0x22U,
0x00U, 0x41U, 0x20U, 0x47U, 0x0DU, 0x00U, 0x0BU, 0x20U, 0x02U,
0x45U, 0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x7EU,
0x10U, 0x02U, 0x1AU, 0x0BU, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U,
0x00U, 0x10U, 0x03U, 0x21U, 0x04U, 0x20U, 0x01U, 0x41U, 0x20U,
0x6AU, 0x24U, 0x00U, 0x20U, 0x04U, 0x0BU,
}},
/* ==== WASM: 2 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t r1 = dice(1000000, 3, 5);
if (r1 < 0)
rollback(0, 0, r1);
int64_t r2 = dice(1000000, 3, 5);
if (r2 < 0)
rollback(0, 0, r2);
// consecutive calls should differ (rngCallCounter)
if (r1 == r2)
rollback(0, 0, -1);
return accept(0, 0, r1 | (r2 << 20));
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U,
0x1AU, 0x04U, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU,
0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU,
0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU,
0x02U, 0x31U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU,
0x67U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x04U, 0x64U,
0x69U, 0x63U, 0x65U, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU,
0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U,
0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x00U, 0x03U, 0x02U, 0x01U,
0x03U, 0x05U, 0x03U, 0x01U, 0x00U, 0x01U, 0x07U, 0x08U, 0x01U,
0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x04U, 0x0AU, 0x62U,
0x01U, 0x60U, 0x01U, 0x02U, 0x7EU, 0x41U, 0x01U, 0x41U, 0x01U,
0x10U, 0x00U, 0x1AU, 0x41U, 0xC0U, 0x84U, 0x3DU, 0x41U, 0x03U,
0x41U, 0x05U, 0x10U, 0x01U, 0x22U, 0x01U, 0x42U, 0x00U, 0x53U,
0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x01U, 0x10U,
0x02U, 0x1AU, 0x0BU, 0x41U, 0xC0U, 0x84U, 0x3DU, 0x41U, 0x03U,
0x41U, 0x05U, 0x10U, 0x01U, 0x22U, 0x02U, 0x42U, 0x00U, 0x53U,
0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x10U,
0x02U, 0x1AU, 0x0BU, 0x20U, 0x01U, 0x20U, 0x02U, 0x51U, 0x04U,
0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x7FU, 0x10U, 0x02U,
0x1AU, 0x0BU, 0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x42U,
0x14U, 0x86U, 0x20U, 0x01U, 0x84U, 0x10U, 0x03U, 0x0BU,
}},
/* ==== WASM: 3 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(0, 3, 5);
// dice(0) should return negative error code, pass it through
return accept(0, 0, result);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U,
0x1AU, 0x04U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U,
0x03U, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU,
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU,
0x02U, 0x22U, 0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU,
0x67U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x04U, 0x64U,
0x69U, 0x63U, 0x65U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x02U,
0x03U, 0x02U, 0x01U, 0x03U, 0x05U, 0x03U, 0x01U, 0x00U, 0x01U,
0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U,
0x03U, 0x0AU, 0x19U, 0x01U, 0x17U, 0x00U, 0x41U, 0x01U, 0x41U,
0x01U, 0x10U, 0x00U, 0x1AU, 0x41U, 0x00U, 0x41U, 0x00U, 0x41U,
0x00U, 0x41U, 0x03U, 0x41U, 0x05U, 0x10U, 0x01U, 0x10U, 0x02U,
0x0BU,
}},
/* ==== WASM: 4 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
int64_t hook(uint32_t r)
{
_g(1,1);
int64_t result = dice(6, 3, 21);
// requirement unmet: pass the error code through
return accept(0, 0, result);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U,
0x1AU, 0x04U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U,
0x03U, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU,
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU,
0x02U, 0x22U, 0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU,
0x67U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x04U, 0x64U,
0x69U, 0x63U, 0x65U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x02U,
0x03U, 0x02U, 0x01U, 0x03U, 0x05U, 0x03U, 0x01U, 0x00U, 0x01U,
0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U,
0x03U, 0x0AU, 0x19U, 0x01U, 0x17U, 0x00U, 0x41U, 0x01U, 0x41U,
0x01U, 0x10U, 0x00U, 0x1AU, 0x41U, 0x00U, 0x41U, 0x00U, 0x41U,
0x06U, 0x41U, 0x03U, 0x41U, 0x15U, 0x10U, 0x01U, 0x10U, 0x02U,
0x0BU,
}},
/* ==== WASM: 5 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
extern int64_t random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t min_count);
#define INVALID_ARGUMENT (-7)
int64_t hook(uint32_t r)
{
_g(1,1);
uint8_t buf[32];
int64_t bad_min_tier = dice(6, 0, 0);
if (bad_min_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_min_tier);
int64_t bad_high_tier = dice(6, 4, 0);
if (bad_high_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_high_tier);
int64_t bad_min_count = dice(6, 3, 70000);
if (bad_min_count != INVALID_ARGUMENT)
return accept(0, 0, bad_min_count);
int64_t bad_random_tier = random((uint32_t)buf, 32, 4, 0);
if (bad_random_tier != INVALID_ARGUMENT)
return accept(0, 0, bad_random_tier);
// Sentinel distinct from any dice (0..5) / random result and
// from INVALID_ARGUMENT, so a regression that lets a bad
// requirement through returns its own code, not this one.
return accept(0, 0, 42);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U,
0x22U, 0x05U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U,
0x03U, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU,
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU,
0x7FU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U,
0x2FU, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U,
0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x04U, 0x64U, 0x69U,
0x63U, 0x65U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U,
0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x02U, 0x03U,
0x65U, 0x6EU, 0x76U, 0x06U, 0x72U, 0x61U, 0x6EU, 0x64U, 0x6FU,
0x6DU, 0x00U, 0x03U, 0x03U, 0x02U, 0x01U, 0x04U, 0x05U, 0x03U,
0x01U, 0x00U, 0x01U, 0x06U, 0x08U, 0x01U, 0x7FU, 0x01U, 0x41U,
0x80U, 0x80U, 0x04U, 0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, 0x68U,
0x6FU, 0x6FU, 0x6BU, 0x00U, 0x04U, 0x0AU, 0x99U, 0x01U, 0x01U,
0x96U, 0x01U, 0x01U, 0x01U, 0x7EU, 0x23U, 0x00U, 0x41U, 0x20U,
0x6BU, 0x22U, 0x00U, 0x24U, 0x00U, 0x41U, 0x01U, 0x41U, 0x01U,
0x10U, 0x00U, 0x1AU, 0x02U, 0x7EU, 0x41U, 0x06U, 0x41U, 0x00U,
0x41U, 0x00U, 0x10U, 0x01U, 0x22U, 0x01U, 0x42U, 0x79U, 0x52U,
0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x01U, 0x10U,
0x02U, 0x0CU, 0x01U, 0x0BU, 0x41U, 0x06U, 0x41U, 0x04U, 0x41U,
0x00U, 0x10U, 0x01U, 0x22U, 0x01U, 0x42U, 0x79U, 0x52U, 0x04U,
0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x01U, 0x10U, 0x02U,
0x0CU, 0x01U, 0x0BU, 0x41U, 0x06U, 0x41U, 0x03U, 0x41U, 0xF0U,
0xA2U, 0x04U, 0x10U, 0x01U, 0x22U, 0x01U, 0x42U, 0x79U, 0x52U,
0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x01U, 0x10U,
0x02U, 0x0CU, 0x01U, 0x0BU, 0x20U, 0x00U, 0x41U, 0x20U, 0x41U,
0x04U, 0x41U, 0x00U, 0x10U, 0x03U, 0x22U, 0x01U, 0x42U, 0x79U,
0x52U, 0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x20U, 0x01U,
0x10U, 0x02U, 0x0CU, 0x01U, 0x0BU, 0x41U, 0x00U, 0x41U, 0x00U,
0x42U, 0x2AU, 0x10U, 0x02U, 0x0BU, 0x21U, 0x01U, 0x20U, 0x00U,
0x41U, 0x20U, 0x6AU, 0x24U, 0x00U, 0x20U, 0x01U, 0x0BU,
}},
};
}
} // namespace ripple
#endif

View File

@@ -1,221 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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.
*/
//==============================================================================
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/digest.h>
#include <cstring>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
STTx
makeSTTx(STObject const& obj)
{
Serializer s;
obj.add(s);
SerialIter sit{s.slice()};
return STTx{std::ref(sit)};
}
STTx
makeExportedPayment(AccountID const& src, AccountID const& dst)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYMENT);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfTicketSequence, 1);
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setAccountID(sfDestination, dst);
return makeSTTx(obj);
}
} // namespace
class ExportResultBuilder_test : public beast::unit_test::suite
{
public:
void
testAssemblesSignedMetadata()
{
testcase("assembles signed metadata");
auto const signerA = randomKeyPair(KeyType::secp256k1);
auto const signerB = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(signerA.first), calcAccountID(signerB.first));
auto const exportTxHash = makeHash("outer-export-tx");
ExportResultBuilder::SignatureSnapshot signatures;
signatures.emplace(
signerA.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerA.first, signerA.second));
signatures.emplace(
signerB.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerB.first, signerB.second));
auto assembled = ExportResultBuilder::assemble(
innerTx, signatures, 123, exportTxHash);
BEAST_EXPECT(assembled.signerCount == 2);
BEAST_EXPECT(assembled.metadata.getFieldU32(sfLedgerSequence) == 123);
BEAST_EXPECT(
assembled.metadata.getFieldH256(sfTransactionHash) == exportTxHash);
auto const& multiSigned =
assembled.metadata.peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
auto const& signers = multiSigned.getFieldArray(sfSigners);
BEAST_EXPECT(signers.size() == 2);
if (signers.size() == 2)
{
BEAST_EXPECT(
signers[0].getAccountID(sfAccount) <
signers[1].getAccountID(sfAccount));
}
for (auto const& signer : signers)
{
auto const pkVL = signer.getFieldVL(sfSigningPubKey);
PublicKey const pk{makeSlice(pkVL)};
auto const sigVL = signer.getFieldVL(sfTxnSignature);
auto const signerAcctID = signer.getAccountID(sfAccount);
auto const sigData = buildMultiSigningData(innerTx, signerAcctID);
BEAST_EXPECT(verify(pk, sigData.slice(), makeSlice(sigVL)));
}
BEAST_EXPECT(
assembled.signedTxHash ==
multiSigned.getHash(HashPrefix::transactionID));
Serializer serialized;
multiSigned.add(serialized);
SerialIter sit(serialized.slice());
STTx signedTx{std::ref(sit)};
BEAST_EXPECT(signedTx.getTransactionID() == assembled.signedTxHash);
}
void
testSkipsEmptySignatures()
{
testcase("skips empty signatures");
auto const signer = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(signer.first), calcAccountID(dst.first));
ExportResultBuilder::SignatureSnapshot signatures;
signatures.emplace(signer.first, Buffer{});
auto assembled = ExportResultBuilder::assemble(
innerTx, signatures, 456, makeHash("empty-sig-export"));
BEAST_EXPECT(assembled.signerCount == 0);
auto const& multiSigned =
assembled.metadata.peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
BEAST_EXPECT(!multiSigned.isFieldPresent(sfSigners));
BEAST_EXPECT(
assembled.signedTxHash ==
multiSigned.getHash(HashPrefix::transactionID));
}
void
testBuildMultiSignedExportedTxnDirect()
{
testcase("builds multisigned exported transaction directly");
auto const signerA = randomKeyPair(KeyType::secp256k1);
auto const signerB = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(signerA.first), calcAccountID(dst.first));
ExportResultBuilder::SignatureSnapshot signatures;
signatures.emplace(signerB.first, Buffer{});
signatures.emplace(
signerA.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerA.first, signerA.second));
auto multiSigned = ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, signatures);
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
auto const& signers = multiSigned.getFieldArray(sfSigners);
BEAST_EXPECT(signers.size() == 1);
if (signers.size() == 1)
{
BEAST_EXPECT(
signers[0].getAccountID(sfAccount) ==
calcAccountID(signerA.first));
BEAST_EXPECT(
makeSlice(signers[0].getFieldVL(sfSigningPubKey)) ==
signerA.first.slice());
}
ExportResultBuilder::SignatureSnapshot none;
auto unsignedMulti =
ExportResultBuilder::buildMultiSignedExportedTxn(innerTx, none);
BEAST_EXPECT(unsignedMulti.getFieldVL(sfSigningPubKey).empty());
BEAST_EXPECT(!unsignedMulti.isFieldPresent(sfSigners));
}
void
run() override
{
testAssemblesSignedMetadata();
testSkipsEmptySignatures();
testBuildMultiSignedExportedTxnDirect();
}
};
BEAST_DEFINE_TESTSUITE(ExportResultBuilder, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -1,299 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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.
*/
//==============================================================================
#include <xrpld/app/misc/ExportSigCollector.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/digest.h>
#include <cstring>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
PublicKey
makePublicKey(char const* hex)
{
auto const raw = strUnHex(hex);
return PublicKey{makeSlice(*raw)};
}
Buffer
makeSignature(std::uint8_t seed)
{
std::uint8_t bytes[] = {
seed,
static_cast<std::uint8_t>(seed + 1),
static_cast<std::uint8_t>(seed + 2)};
return Buffer(bytes, sizeof(bytes));
}
} // namespace
class ExportSigCollector_test : public beast::unit_test::suite
{
PublicKey const validator_ = makePublicKey(
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C05"
"2");
public:
void
testCleanupUsesFirstSeenSeq()
{
testcase("cleanup uses first seen sequence");
ExportSigCollector collector;
auto const tx = makeHash("cleanup-verified");
auto const sig = makeSignature(1);
collector.addVerifiedSignature(tx, validator_, sig, 10);
BEAST_EXPECT(collector.signatureCount(tx) == 1);
collector.cleanupStale(266);
BEAST_EXPECT(collector.signatureCount(tx) == 1);
collector.cleanupStale(267);
BEAST_EXPECT(collector.signatureCount(tx) == 0);
}
void
testUpgradeSetsFirstSeenSeq()
{
testcase("upgrade sets first seen sequence");
ExportSigCollector collector;
auto const tx = makeHash("cleanup-upgraded");
auto const sig = makeSignature(5);
collector.addUnverifiedSignature(tx, validator_, sig);
BEAST_EXPECT(collector.hasUnverifiedSignatures());
collector.upgradeSignature(tx, validator_, sig, 10);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(tx) == 1);
collector.cleanupStale(266);
BEAST_EXPECT(collector.signatureCount(tx) == 1);
collector.cleanupStale(267);
BEAST_EXPECT(collector.signatureCount(tx) == 0);
}
void
testRemoveInvalidUnverifiedSignature()
{
testcase("remove invalid unverified signature");
ExportSigCollector collector;
auto const tx = makeHash("remove-invalid");
auto const sig = makeSignature(9);
auto const otherSig = makeSignature(10);
collector.addUnverifiedSignature(tx, validator_, sig, 10);
BEAST_EXPECT(collector.hasUnverifiedSignatures());
BEAST_EXPECT(!collector.removeSignature(tx, validator_, otherSig));
BEAST_EXPECT(collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.removeSignature(tx, validator_, sig));
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(tx) == 0);
}
void
testSnapshotsAndFilteredCounts()
{
testcase("snapshots and filtered counts use verified signatures only");
auto const other = randomKeyPair(KeyType::secp256k1).first;
ExportSigCollector collector;
auto const tx = makeHash("snapshot-filtered");
auto const verifiedSig = makeSignature(20);
auto const unverifiedSig = makeSignature(30);
BEAST_EXPECT(!collector.hasVerifiedSignature(tx, validator_));
BEAST_EXPECT(collector.unverifiedSignatures(tx).empty());
BEAST_EXPECT(!collector.checkQuorumAndSnapshot(tx, 1));
collector.addVerifiedSignature(tx, validator_, verifiedSig, 10);
collector.addUnverifiedSignature(tx, other, unverifiedSig, 11);
BEAST_EXPECT(collector.hasVerifiedSignature(tx, validator_));
BEAST_EXPECT(!collector.hasVerifiedSignature(tx, other));
BEAST_EXPECT(collector.signatureCount(tx) == 1);
BEAST_EXPECT(collector.signatureCount(tx, [&](PublicKey const& pk) {
return pk == validator_;
}) == 1);
BEAST_EXPECT(collector.signatureCount(tx, [&](PublicKey const& pk) {
return pk == other;
}) == 0);
auto unverified = collector.unverifiedSignatures(tx);
BEAST_EXPECT(unverified.size() == 1);
BEAST_EXPECT(unverified.count(other) == 1);
auto snapshot = collector.snapshot();
BEAST_EXPECT(snapshot.size() == 1);
BEAST_EXPECT(snapshot[tx].count(validator_) == 1);
BEAST_EXPECT(snapshot[tx].count(other) == 0);
auto sigSnapshot = collector.snapshotWithSigs();
BEAST_EXPECT(sigSnapshot[tx].size() == 1);
BEAST_EXPECT(sigSnapshot[tx][validator_] == verifiedSig);
auto filteredSnapshot = collector.snapshotWithSigs(
[&](PublicKey const& pk) { return pk == other; });
BEAST_EXPECT(filteredSnapshot.empty());
BEAST_EXPECT(!collector.checkQuorumAndSnapshot(tx, 2));
auto quorum = collector.checkQuorumAndSnapshot(tx, 1);
BEAST_EXPECT(quorum.has_value());
if (quorum)
{
BEAST_EXPECT(quorum->size() == 1);
BEAST_EXPECT((*quorum)[validator_] == verifiedSig);
}
collector.upgradeSignature(tx, other, makeSignature(31), 12);
BEAST_EXPECT(collector.signatureCount(tx) == 1);
collector.upgradeSignature(tx, other, unverifiedSig, 12);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(tx) == 2);
auto filteredQuorum = collector.checkQuorumAndSnapshot(
tx, 1, [&](PublicKey const& pk) { return pk == other; });
BEAST_EXPECT(filteredQuorum.has_value());
if (filteredQuorum)
BEAST_EXPECT((*filteredQuorum)[other] == unverifiedSig);
collector.clear(tx);
BEAST_EXPECT(collector.signatureCount(tx) == 0);
BEAST_EXPECT(collector.snapshot().empty());
}
void
testStandaloneAndRoundState()
{
testcase("standalone signatures and round state");
ExportSigCollector collector;
auto const tx = makeHash("standalone-round");
collector.addStandaloneSignature(tx, validator_, 10);
BEAST_EXPECT(collector.hasVerifiedSignature(tx, validator_));
BEAST_EXPECT(collector.signatureCount(tx) == 1);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
auto snapshot = collector.snapshot();
BEAST_EXPECT(snapshot.size() == 1);
BEAST_EXPECT(snapshot[tx].count(validator_) == 1);
auto sigSnapshot = collector.snapshotWithSigs();
BEAST_EXPECT(sigSnapshot.size() == 1);
BEAST_EXPECT(sigSnapshot[tx].count(validator_) == 1);
BEAST_EXPECT(sigSnapshot[tx][validator_].empty());
BEAST_EXPECT(collector.markSent(tx));
BEAST_EXPECT(!collector.markSent(tx));
collector.clearRound();
BEAST_EXPECT(collector.markSent(tx));
}
void
testClearAll()
{
testcase("clear all signatures and round state");
ExportSigCollector collector;
auto const verifiedTx = makeHash("clear-all-verified");
auto const unverifiedTx = makeHash("clear-all-unverified");
auto const sig = makeSignature(12);
collector.addVerifiedSignature(verifiedTx, validator_, sig, 10);
collector.addUnverifiedSignature(unverifiedTx, validator_, sig, 10);
BEAST_EXPECT(collector.signatureCount(verifiedTx) == 1);
BEAST_EXPECT(collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.markSent(verifiedTx));
BEAST_EXPECT(!collector.markSent(verifiedTx));
collector.clearAll();
BEAST_EXPECT(collector.signatureCount(verifiedTx) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.markSent(verifiedTx));
}
void
testDefensiveNoOps()
{
testcase("defensive no-op paths");
ExportSigCollector collector;
auto const missingTx = makeHash("missing-defensive");
auto const standaloneTx = makeHash("standalone-defensive");
auto const sig = makeSignature(40);
collector.upgradeSignature(missingTx, validator_, sig, 10);
BEAST_EXPECT(collector.signatureCount(missingTx) == 0);
BEAST_EXPECT(!collector.removeSignature(missingTx, validator_, sig));
BEAST_EXPECT(!collector.checkQuorumAndSnapshot(missingTx, 1));
BEAST_EXPECT(collector.signatureCount(missingTx, [](PublicKey const&) {
return true;
}) == 0);
collector.addStandaloneSignature(standaloneTx, validator_, 10);
collector.upgradeSignature(standaloneTx, validator_, Buffer{}, 11);
BEAST_EXPECT(collector.signatureCount(standaloneTx) == 1);
BEAST_EXPECT(collector.snapshotWithSigs()
.at(standaloneTx)
.at(validator_)
.empty());
auto filtered =
collector.snapshotWithSigs([](PublicKey const&) { return false; });
BEAST_EXPECT(filtered.empty());
BEAST_EXPECT(!collector.checkQuorumAndSnapshot(
standaloneTx, 1, [](PublicKey const&) { return false; }));
}
void
run() override
{
testCleanupUsesFirstSeenSeq();
testUpgradeSetsFirstSeenSeq();
testRemoveInvalidUnverifiedSignature();
testSnapshotsAndFilteredCounts();
testStandaloneAndRoundState();
testClearAll();
testDefensiveNoOps();
}
};
BEAST_DEFINE_TESTSUITE(ExportSigCollector, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -1,400 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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.
*/
//==============================================================================
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/app/consensus/ExportSignatureHarvester.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/digest.h>
#include <cstring>
#include <memory>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
STTx
makeSTTx(STObject const& obj)
{
Serializer s;
obj.add(s);
SerialIter sit{s.slice()};
return STTx{std::ref(sit)};
}
STObject
makeExportedPayment(AccountID const& src, AccountID const& dst)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYMENT);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfTicketSequence, 1);
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setAccountID(sfDestination, dst);
return obj;
}
std::shared_ptr<STTx const>
makeExportTx(STObject const& inner, AccountID const& account)
{
STObject exportObj(sfGeneric);
exportObj.setFieldU16(sfTransactionType, ttEXPORT);
exportObj.setAccountID(sfAccount, account);
exportObj.setFieldU32(sfSequence, 0);
exportObj.setFieldVL(sfSigningPubKey, Blob{});
exportObj.setFieldU32(sfFirstLedgerSequence, 2);
exportObj.setFieldU32(sfLastLedgerSequence, 6);
exportObj.setFieldAmount(sfFee, XRPAmount{0});
exportObj.set(std::make_unique<STObject>(inner));
return std::make_shared<STTx const>(makeSTTx(exportObj));
}
std::shared_ptr<STTx const>
makeExportTxWithoutInner(AccountID const& account)
{
STObject exportObj(sfGeneric);
exportObj.setFieldU16(sfTransactionType, ttEXPORT);
exportObj.setAccountID(sfAccount, account);
exportObj.setFieldU32(sfSequence, 0);
exportObj.setFieldVL(sfSigningPubKey, Blob{});
exportObj.setFieldU32(sfFirstLedgerSequence, 2);
exportObj.setFieldU32(sfLastLedgerSequence, 6);
exportObj.setFieldAmount(sfFee, XRPAmount{0});
return std::make_shared<STTx const>(makeSTTx(exportObj));
}
std::string
makeBlob(uint256 const& txHash, PublicKey const& pk, Slice sig)
{
std::string blob;
blob.append(reinterpret_cast<char const*>(txHash.data()), txHash.size());
blob.append(reinterpret_cast<char const*>(pk.data()), pk.size());
blob.append(reinterpret_cast<char const*>(sig.data()), sig.size());
return blob;
}
std::string
makeBlob(uint256 const& txHash, PublicKey const& pk, Buffer const& sig)
{
return makeBlob(txHash, pk, Slice(sig.data(), sig.size()));
}
} // namespace
class ExportSignatureHarvester_test : public beast::unit_test::suite
{
SuiteJournal journal_{"ExportSignatureHarvester_test", *this};
std::pair<PublicKey, SecretKey> const sender_ =
randomKeyPair(KeyType::secp256k1);
std::pair<PublicKey, SecretKey> const other_ =
randomKeyPair(KeyType::secp256k1);
uint256 const prevLedger_ = makeHash("export-harvester-prev-ledger");
char const* source_ = "unit-test";
ExportSignatureHarvestInput
makeInput(
std::vector<std::string> const& blobs,
ExportTxnLookup const& exportTxns,
bool active = true,
std::optional<uint256> sourceLedgerHash = std::nullopt,
PublicKey const* sender = nullptr,
std::size_t maxEntries = 2) const
{
return ExportSignatureHarvestInput{
sender ? *sender : sender_.first,
prevLedger_,
blobs,
sourceLedgerHash,
[active](PublicKey const&) { return active; },
exportTxns,
42,
source_,
maxEntries};
}
beast::Journal&
journal()
{
return journal_;
}
public:
void
testRejectsTooManyEntries()
{
testcase("rejects too many entries");
auto const txHash = makeHash("too-many");
auto const blob = makeBlob(txHash, sender_.first, Slice("sig", 3));
std::vector<std::string> const blobs{blob, blob, blob};
ExportTxnLookup lookup;
ExportSigCollector collector;
auto input = makeInput(blobs, lookup);
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
}
void
testEmptyInputAndDirectVerification()
{
testcase("empty input and direct verification");
std::vector<std::string> const empty;
ExportTxnLookup lookup;
ExportSigCollector collector;
auto input = makeInput(empty, lookup, true, prevLedger_);
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
auto const senderAccount = calcAccountID(sender_.first);
auto const dstAccount = calcAccountID(other_.first);
auto const innerObj = makeExportedPayment(senderAccount, dstAccount);
auto const innerTx = makeSTTx(innerObj);
auto const sigData = buildMultiSigningData(innerTx, senderAccount);
auto const sig = sign(sender_.first, sender_.second, sigData.slice());
auto const exportTx = makeExportTx(innerObj, senderAccount);
auto const txHash = exportTx->getTransactionID();
BEAST_EXPECT(verifyExportSignatureAgainstTx(
*exportTx,
sender_.first,
Slice(sig.data(), sig.size()),
txHash,
journal(),
source_));
BEAST_EXPECT(!verifyExportSignatureAgainstTx(
*exportTx,
sender_.first,
Slice("bad-sig", 7),
txHash,
journal(),
source_));
auto const noInner = makeExportTxWithoutInner(senderAccount);
BEAST_EXPECT(!verifyExportSignatureAgainstTx(
*noInner,
sender_.first,
Slice(sig.data(), sig.size()),
noInner->getTransactionID(),
journal(),
source_));
}
void
testIgnoresEmptyAndMalformedEntries()
{
testcase("ignores empty and malformed entries");
auto const txHash = makeHash("malformed");
std::string invalidPubkeyBlob;
invalidPubkeyBlob.append(
reinterpret_cast<char const*>(txHash.data()), txHash.size());
invalidPubkeyBlob.append(33, '\0');
invalidPubkeyBlob.append("sig", 3);
std::vector<std::string> const blobs{
"",
std::string(64, 'x'),
makeBlob(txHash, sender_.first, Slice{}),
invalidPubkeyBlob};
ExportTxnLookup lookup;
ExportSigCollector collector;
auto input =
makeInput(blobs, lookup, true, prevLedger_, nullptr, blobs.size());
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
}
void
testRejectsInactiveOrWrongParent()
{
testcase("rejects inactive or wrong-parent senders");
auto const txHash = makeHash("inactive");
std::vector<std::string> const blobs{
makeBlob(txHash, sender_.first, Slice("sig", 3))};
ExportTxnLookup lookup;
ExportSigCollector collector;
auto inactive = makeInput(blobs, lookup, false);
BEAST_EXPECT(
harvestExportSignatures(inactive, collector, journal()) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
auto wrongParent =
makeInput(blobs, lookup, true, makeHash("different-parent"));
BEAST_EXPECT(
harvestExportSignatures(wrongParent, collector, journal()) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
}
void
testRejectsPubkeyMismatchAtomically()
{
testcase("rejects embedded pubkey mismatch atomically");
auto const txHash = makeHash("mismatch");
std::vector<std::string> const blobs{
makeBlob(txHash, sender_.first, Slice("sig-a", 5)),
makeBlob(txHash, other_.first, Slice("sig-b", 5))};
ExportTxnLookup lookup;
ExportSigCollector collector;
auto input = makeInput(blobs, lookup, true, prevLedger_);
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
}
void
testMissingTxStoresUnverified()
{
testcase("missing tx stores unverified");
auto const txHash = makeHash("missing-tx");
std::vector<std::string> const blobs{
makeBlob(txHash, sender_.first, Slice("sig", 3))};
ExportTxnLookup lookup;
ExportSigCollector collector;
auto input = makeInput(blobs, lookup, true, prevLedger_);
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 1);
BEAST_EXPECT(collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
}
void
testOpenLedgerTxStoresVerifiedAndSkipsDuplicate()
{
testcase("open-ledger tx stores verified and skips duplicate");
auto const senderAccount = calcAccountID(sender_.first);
auto const dstAccount = calcAccountID(other_.first);
auto const innerObj = makeExportedPayment(senderAccount, dstAccount);
auto const innerTx = makeSTTx(innerObj);
auto const sigData = buildMultiSigningData(innerTx, senderAccount);
auto const sig = sign(sender_.first, sender_.second, sigData.slice());
auto const exportTx = makeExportTx(innerObj, senderAccount);
auto const txHash = exportTx->getTransactionID();
ExportTxnLookup lookup;
lookup.emplace(txHash, exportTx);
std::vector<std::string> const blobs{
makeBlob(txHash, sender_.first, sig)};
ExportSigCollector collector;
auto input = makeInput(blobs, lookup, true, prevLedger_);
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 1);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(txHash) == 1);
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
BEAST_EXPECT(collector.signatureCount(txHash) == 1);
}
void
testRejectsInvalidOpenLedgerSignatures()
{
testcase("rejects invalid open-ledger signatures");
auto const senderAccount = calcAccountID(sender_.first);
auto const dstAccount = calcAccountID(other_.first);
auto const innerObj = makeExportedPayment(senderAccount, dstAccount);
auto const exportTx = makeExportTx(innerObj, senderAccount);
auto const txHash = exportTx->getTransactionID();
ExportTxnLookup lookup;
lookup.emplace(txHash, exportTx);
std::vector<std::string> const blobs{
makeBlob(txHash, sender_.first, Slice("bad-sig", 7))};
ExportSigCollector collector;
auto input = makeInput(blobs, lookup, true, prevLedger_);
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
}
void
testRejectsOpenLedgerTxWithoutExportedTxn()
{
testcase("rejects open-ledger tx without exported transaction");
auto const senderAccount = calcAccountID(sender_.first);
auto const exportTx = makeExportTxWithoutInner(senderAccount);
auto const txHash = exportTx->getTransactionID();
ExportTxnLookup lookup;
lookup.emplace(txHash, exportTx);
std::vector<std::string> const blobs{
makeBlob(txHash, sender_.first, Slice("sig", 3))};
ExportSigCollector collector;
auto input = makeInput(blobs, lookup, true, prevLedger_);
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
}
void
run() override
{
testRejectsTooManyEntries();
testEmptyInputAndDirectVerification();
testIgnoresEmptyAndMalformedEntries();
testRejectsInactiveOrWrongParent();
testRejectsPubkeyMismatchAtomically();
testMissingTxStoresUnverified();
testOpenLedgerTxStoresVerifiedAndSkipsDuplicate();
testRejectsInvalidOpenLedgerSignatures();
testRejectsOpenLedgerTxWithoutExportedTxn();
}
};
BEAST_DEFINE_TESTSUITE(ExportSignatureHarvester, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -1,210 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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.
*/
//==============================================================================
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
#include <xrpld/app/tx/detail/ExportSignatureUpgrader.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/digest.h>
#include <cstdint>
#include <cstring>
#include <set>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
STTx
makeSTTx(STObject const& obj)
{
Serializer s;
obj.add(s);
SerialIter sit{s.slice()};
return STTx{std::ref(sit)};
}
STTx
makeExportedPayment(AccountID const& src, AccountID const& dst)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYMENT);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfTicketSequence, 1);
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setAccountID(sfDestination, dst);
return makeSTTx(obj);
}
Buffer
makeInvalidSignature(std::uint8_t first = 1)
{
std::uint8_t bytes[] = {
first,
static_cast<std::uint8_t>(first + 1),
static_cast<std::uint8_t>(first + 2),
static_cast<std::uint8_t>(first + 3),
static_cast<std::uint8_t>(first + 4)};
return Buffer(bytes, sizeof(bytes));
}
beast::Journal
nullJournal()
{
return beast::Journal{beast::Journal::getNullSink()};
}
} // namespace
class ExportSignatureUpgrader_test : public beast::unit_test::suite
{
public:
void
testUpgradeFiltersAndRemovesInvalid()
{
testcase("upgrade filters and removes invalid signatures");
auto const validSigner = randomKeyPair(KeyType::secp256k1);
auto const invalidSigner = randomKeyPair(KeyType::secp256k1);
auto const inactiveSigner = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(validSigner.first), calcAccountID(dst.first));
auto const txHash = makeHash("export-upgrade");
auto validSig = ExportResultBuilder::signExportedTxn(
innerTx, validSigner.first, validSigner.second);
auto invalidSig = makeInvalidSignature();
auto inactiveSig = ExportResultBuilder::signExportedTxn(
innerTx, inactiveSigner.first, inactiveSigner.second);
ExportSigCollector collector;
collector.addUnverifiedSignature(
txHash, validSigner.first, validSig, 7);
collector.addUnverifiedSignature(
txHash, invalidSigner.first, invalidSig, 7);
collector.addUnverifiedSignature(
txHash, inactiveSigner.first, inactiveSig, 7);
std::set<PublicKey> active{
validSigner.first,
invalidSigner.first,
};
auto stats = ExportSignatureUpgrader::upgradeUnverifiedSignatures(
collector,
innerTx,
txHash,
12,
[&active](PublicKey const& pk) { return active.count(pk) > 0; },
nullJournal());
BEAST_EXPECT(stats.inspected == 3);
BEAST_EXPECT(stats.inactiveSkipped == 1);
BEAST_EXPECT(stats.upgraded == 1);
BEAST_EXPECT(stats.removedInvalid == 1);
BEAST_EXPECT(collector.hasVerifiedSignature(txHash, validSigner.first));
BEAST_EXPECT(
!collector.hasVerifiedSignature(txHash, invalidSigner.first));
BEAST_EXPECT(
!collector.hasVerifiedSignature(txHash, inactiveSigner.first));
auto const unverified = collector.unverifiedSignatures(txHash);
BEAST_EXPECT(!unverified.contains(invalidSigner.first));
BEAST_EXPECT(unverified.contains(inactiveSigner.first));
BEAST_EXPECT(collector.signatureCount(txHash) == 1);
}
void
testInvalidRemovalRequiresStoredBufferMatch()
{
testcase("invalid removal requires stored buffer match");
auto const invalidSigner = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(invalidSigner.first), calcAccountID(dst.first));
auto const txHash = makeHash("export-upgrade-race");
auto invalidSig = makeInvalidSignature();
auto replacementSig = makeInvalidSignature(20);
ExportSigCollector collector;
collector.addUnverifiedSignature(
txHash, invalidSigner.first, invalidSig, 7);
bool mutated = false;
auto stats = ExportSignatureUpgrader::upgradeUnverifiedSignatures(
collector,
innerTx,
txHash,
12,
[&](PublicKey const& pk) {
if (pk == invalidSigner.first && !mutated)
{
mutated = true;
collector.addUnverifiedSignature(
txHash, invalidSigner.first, replacementSig, 12);
}
return true;
},
nullJournal());
BEAST_EXPECT(mutated);
BEAST_EXPECT(stats.inspected == 1);
BEAST_EXPECT(stats.upgraded == 0);
BEAST_EXPECT(stats.removedInvalid == 0);
BEAST_EXPECT(
!collector.hasVerifiedSignature(txHash, invalidSigner.first));
auto const unverified = collector.unverifiedSignatures(txHash);
auto const it = unverified.find(invalidSigner.first);
BEAST_EXPECT(it != unverified.end());
if (it != unverified.end())
BEAST_EXPECT(it->second == replacementSig);
}
void
run() override
{
testUpgradeFiltersAndRemovesInvalid();
testInvalidRemovalRequiresStoredBufferMatch();
}
};
BEAST_DEFINE_TESTSUITE(ExportSignatureUpgrader, app, ripple);
} // namespace test
} // namespace ripple

File diff suppressed because it is too large Load Diff

View File

@@ -1,483 +0,0 @@
// This file is generated by build_test_hooks.py
#ifndef EXPORT_TEST_WASM_INCLUDED
#define EXPORT_TEST_WASM_INCLUDED
#include <map>
#include <stdint.h>
#include <string>
#include <vector>
namespace ripple {
namespace test {
std::map<std::string, std::vector<uint8_t>> export_test_wasm = {
/* ==== WASM: 0 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t id, uint32_t maxiter);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t xport_reserve(uint32_t count);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
extern int64_t otxn_type(void);
extern int64_t ledger_seq(void);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
#define ttPAYMENT 0
#define tfCANONICAL 0x80000000UL
#define amAMOUNT 1
#define amFEE 8
#define atACCOUNT 1
#define atDESTINATION 3
#define ENCODE_TT(buf_out, tt) \
buf_out[0] = 0x12U; \
buf_out[1] = (tt >> 8) & 0xFFU; \
buf_out[2] = tt & 0xFFU; \
buf_out += 3;
#define ENCODE_FLAGS(buf_out, flags) \
buf_out[0] = 0x22U; \
buf_out[1] = (flags >> 24) & 0xFFU; \
buf_out[2] = (flags >> 16) & 0xFFU; \
buf_out[3] = (flags >> 8) & 0xFFU; \
buf_out[4] = flags & 0xFFU; \
buf_out += 5;
#define ENCODE_SEQUENCE(buf_out, seq) \
buf_out[0] = 0x24U; \
buf_out[1] = (seq >> 24) & 0xFFU; \
buf_out[2] = (seq >> 16) & 0xFFU; \
buf_out[3] = (seq >> 8) & 0xFFU; \
buf_out[4] = seq & 0xFFU; \
buf_out += 5;
#define ENCODE_FLS(buf_out, fls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1AU; \
buf_out[2] = (fls >> 24) & 0xFFU; \
buf_out[3] = (fls >> 16) & 0xFFU; \
buf_out[4] = (fls >> 8) & 0xFFU; \
buf_out[5] = fls & 0xFFU; \
buf_out += 6;
#define ENCODE_LLS(buf_out, lls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1BU; \
buf_out[2] = (lls >> 24) & 0xFFU; \
buf_out[3] = (lls >> 16) & 0xFFU; \
buf_out[4] = (lls >> 8) & 0xFFU; \
buf_out[5] = lls & 0xFFU; \
buf_out += 6;
#define ENCODE_DROPS(buf_out, drops, amt_type) \
buf_out[0] = 0x60U + amt_type; \
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
buf_out[2] = (drops >> 48) & 0xFFU; \
buf_out[3] = (drops >> 40) & 0xFFU; \
buf_out[4] = (drops >> 32) & 0xFFU; \
buf_out[5] = (drops >> 24) & 0xFFU; \
buf_out[6] = (drops >> 16) & 0xFFU; \
buf_out[7] = (drops >> 8) & 0xFFU; \
buf_out[8] = drops & 0xFFU; \
buf_out += 9;
#define ENCODE_SIGNING_PUBKEY_EMPTY(buf_out) \
buf_out[0] = 0x73U; \
buf_out[1] = 0x00U; \
buf_out += 2;
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
buf_out[0] = 0x80U + acc_type; \
buf_out[1] = 0x14U; \
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
buf_out += 22;
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
int64_t hook(uint32_t reserved) {
_g(1, 1);
if (otxn_type() != ttPAYMENT)
return accept(0, 0, 0);
ASSERT(xport_reserve(1) == 1);
uint8_t dst[20];
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
ASSERT(dst_len == 20);
uint8_t acc[20];
ASSERT(hook_account(SBUF(acc)) == 20);
uint32_t cls = (uint32_t)ledger_seq();
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
uint8_t* buf = tx;
ENCODE_TT(buf, ttPAYMENT);
ENCODE_FLAGS(buf, tfCANONICAL);
ENCODE_SEQUENCE(buf, 0);
ENCODE_FLS(buf, cls + 1);
ENCODE_LLS(buf, cls + 5);
// sfTicketSequence = UINT32 field 41 = 0x20 0x29
buf[0] = 0x20U; buf[1] = 0x29U;
buf[2] = 0; buf[3] = 0; buf[4] = 0; buf[5] = 1;
buf += 6;
uint64_t drops = 1000000;
ENCODE_DROPS(buf, drops, amAMOUNT);
ENCODE_DROPS(buf, 10, amFEE);
ENCODE_SIGNING_PUBKEY_EMPTY(buf);
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
uint8_t hash[32];
int64_t xport_result = xport(SBUF(hash), (uint32_t)tx, buf - tx);
ASSERT(xport_result == 32);
return accept(0, 0, 0);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x25U,
0x06U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x00U, 0x01U,
0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U,
0x7FU, 0x01U, 0x7EU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U,
0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x8BU, 0x01U,
0x09U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x00U,
0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU,
0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x02U, 0x03U,
0x65U, 0x6EU, 0x76U, 0x0DU, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU,
0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x03U, 0x03U,
0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U,
0x63U, 0x6BU, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6FU,
0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU, 0x00U,
0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU, 0x6BU,
0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U, 0x05U,
0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U,
0x72U, 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU,
0x76U, 0x05U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x00U, 0x04U, 0x03U,
0x02U, 0x01U, 0x03U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U, 0x21U,
0x05U, 0x7FU, 0x01U, 0x41U, 0xE0U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U,
0x41U, 0xD9U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU,
0x7FU, 0x00U, 0x41U, 0xE0U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U,
0x80U, 0x08U, 0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU,
0x6BU, 0x00U, 0x09U, 0x0AU, 0xC5U, 0x84U, 0x00U, 0x01U, 0xC1U, 0x84U,
0x00U, 0x03U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x7FU, 0x23U, 0x80U,
0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0xF0U, 0x02U, 0x6BU, 0x22U, 0x01U,
0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x01U, 0x41U, 0x01U,
0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U, 0x02U,
0x40U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x50U, 0x0DU, 0x00U,
0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x82U, 0x80U, 0x80U,
0x80U, 0x00U, 0x21U, 0x02U, 0x0CU, 0x01U, 0x0BU, 0x02U, 0x40U, 0x41U,
0x01U, 0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x01U, 0x51U,
0x0DU, 0x00U, 0x41U, 0x80U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x16U,
0x42U, 0xDFU, 0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0xD0U, 0x02U, 0x6AU, 0x41U,
0x14U, 0x41U, 0x96U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x03U, 0x10U,
0x85U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU, 0x00U,
0x41U, 0x9AU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x0EU, 0x42U, 0xE3U,
0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U,
0x40U, 0x20U, 0x01U, 0x41U, 0xB0U, 0x02U, 0x6AU, 0x41U, 0x14U, 0x10U,
0x86U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU, 0x00U,
0x41U, 0xA8U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x1EU, 0x42U, 0xE6U,
0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x10U,
0x87U, 0x80U, 0x80U, 0x80U, 0x00U, 0x21U, 0x02U, 0x20U, 0x01U, 0x41U,
0xCEU, 0x00U, 0x6AU, 0x41U, 0x00U, 0x3BU, 0x01U, 0x00U, 0x20U, 0x01U,
0x41U, 0xC0U, 0x00U, 0x3AU, 0x00U, 0x49U, 0x20U, 0x01U, 0x42U, 0x80U,
0x80U, 0x80U, 0x80U, 0xF0U, 0xC1U, 0x90U, 0xA0U, 0xE8U, 0x00U, 0x37U,
0x00U, 0x41U, 0x20U, 0x01U, 0x42U, 0xA0U, 0xD2U, 0x80U, 0x80U, 0x80U,
0xA0U, 0xC0U, 0xB0U, 0xC0U, 0x00U, 0x37U, 0x00U, 0x39U, 0x20U, 0x01U,
0x41U, 0xA0U, 0x36U, 0x3BU, 0x00U, 0x33U, 0x20U, 0x01U, 0x41U, 0xA0U,
0x34U, 0x3BU, 0x00U, 0x2DU, 0x20U, 0x01U, 0x41U, 0x00U, 0x36U, 0x00U,
0x29U, 0x20U, 0x01U, 0x41U, 0x24U, 0x3AU, 0x00U, 0x28U, 0x20U, 0x01U,
0x42U, 0x92U, 0x80U, 0x80U, 0x90U, 0x82U, 0x10U, 0x37U, 0x03U, 0x20U,
0x20U, 0x01U, 0x41U, 0x00U, 0x36U, 0x01U, 0x4AU, 0x20U, 0x01U, 0x20U,
0x02U, 0xA7U, 0x22U, 0x03U, 0x41U, 0x05U, 0x6AU, 0x22U, 0x04U, 0x3AU,
0x00U, 0x38U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x08U, 0x76U, 0x3AU,
0x00U, 0x37U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x10U, 0x76U, 0x3AU,
0x00U, 0x36U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x18U, 0x76U, 0x3AU,
0x00U, 0x35U, 0x20U, 0x01U, 0x20U, 0x03U, 0x41U, 0x01U, 0x6AU, 0x22U,
0x04U, 0x3AU, 0x00U, 0x32U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x08U,
0x76U, 0x3AU, 0x00U, 0x31U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x10U,
0x76U, 0x3AU, 0x00U, 0x30U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x18U,
0x76U, 0x3AU, 0x00U, 0x2FU, 0x20U, 0x01U, 0x41U, 0xDDU, 0x00U, 0x6AU,
0x20U, 0x01U, 0x29U, 0x03U, 0xB8U, 0x02U, 0x37U, 0x00U, 0x00U, 0x20U,
0x01U, 0x41U, 0xE5U, 0x00U, 0x6AU, 0x20U, 0x01U, 0x41U, 0xB0U, 0x02U,
0x6AU, 0x41U, 0x10U, 0x6AU, 0x28U, 0x02U, 0x00U, 0x36U, 0x00U, 0x00U,
0x20U, 0x01U, 0x41U, 0xF3U, 0x00U, 0x6AU, 0x20U, 0x01U, 0x29U, 0x03U,
0xD8U, 0x02U, 0x37U, 0x00U, 0x00U, 0x20U, 0x01U, 0x41U, 0xFBU, 0x00U,
0x6AU, 0x20U, 0x01U, 0x41U, 0xD0U, 0x02U, 0x6AU, 0x41U, 0x10U, 0x6AU,
0x28U, 0x02U, 0x00U, 0x36U, 0x00U, 0x00U, 0x20U, 0x01U, 0x41U, 0x14U,
0x3AU, 0x00U, 0x54U, 0x20U, 0x01U, 0x41U, 0x8AU, 0xE6U, 0x81U, 0x88U,
0x78U, 0x36U, 0x02U, 0x50U, 0x20U, 0x01U, 0x41U, 0x83U, 0x29U, 0x3BU,
0x00U, 0x69U, 0x20U, 0x01U, 0x20U, 0x01U, 0x29U, 0x03U, 0xB0U, 0x02U,
0x37U, 0x00U, 0x55U, 0x20U, 0x01U, 0x20U, 0x01U, 0x29U, 0x03U, 0xD0U,
0x02U, 0x37U, 0x00U, 0x6BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x20U,
0x20U, 0x01U, 0x41U, 0x20U, 0x6AU, 0x41U, 0xDFU, 0x00U, 0x10U, 0x88U,
0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x20U, 0x51U, 0x0DU, 0x00U, 0x41U,
0xC6U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x13U, 0x42U, 0x81U, 0x01U,
0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x00U,
0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U,
0x21U, 0x02U, 0x0BU, 0x20U, 0x01U, 0x41U, 0xF0U, 0x02U, 0x6AU, 0x24U,
0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x02U, 0x0BU, 0x0BU, 0x60U,
0x01U, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x59U, 0x78U, 0x70U, 0x6FU,
0x72U, 0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U,
0x28U, 0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x44U,
0x53U, 0x54U, 0x00U, 0x64U, 0x73U, 0x74U, 0x5FU, 0x6CU, 0x65U, 0x6EU,
0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x68U, 0x6FU, 0x6FU,
0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x28U,
0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x61U, 0x63U, 0x63U, 0x29U, 0x29U,
0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x78U, 0x70U, 0x6FU,
0x72U, 0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x75U, 0x6CU, 0x74U, 0x20U,
0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 1 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t id, uint32_t maxiter);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t xport_reserve(uint32_t count);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
extern int64_t otxn_type(void);
extern int64_t ledger_seq(void);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
#define ttPAYMENT 0
#define tfCANONICAL 0x80000000UL
#define amAMOUNT 1
#define amFEE 8
#define atACCOUNT 1
#define atDESTINATION 3
#define ENCODE_TT(buf_out, tt) \
buf_out[0] = 0x12U; \
buf_out[1] = (tt >> 8) & 0xFFU; \
buf_out[2] = tt & 0xFFU; \
buf_out += 3;
#define ENCODE_FLAGS(buf_out, flags) \
buf_out[0] = 0x22U; \
buf_out[1] = (flags >> 24) & 0xFFU; \
buf_out[2] = (flags >> 16) & 0xFFU; \
buf_out[3] = (flags >> 8) & 0xFFU; \
buf_out[4] = flags & 0xFFU; \
buf_out += 5;
#define ENCODE_SEQUENCE(buf_out, seq) \
buf_out[0] = 0x24U; \
buf_out[1] = (seq >> 24) & 0xFFU; \
buf_out[2] = (seq >> 16) & 0xFFU; \
buf_out[3] = (seq >> 8) & 0xFFU; \
buf_out[4] = seq & 0xFFU; \
buf_out += 5;
// sfNetworkID = UINT32 field 1 = 0x21
#define ENCODE_NETWORK_ID(buf_out, id) \
buf_out[0] = 0x21U; \
buf_out[1] = (id >> 24) & 0xFFU; \
buf_out[2] = (id >> 16) & 0xFFU; \
buf_out[3] = (id >> 8) & 0xFFU; \
buf_out[4] = id & 0xFFU; \
buf_out += 5;
#define ENCODE_FLS(buf_out, fls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1AU; \
buf_out[2] = (fls >> 24) & 0xFFU; \
buf_out[3] = (fls >> 16) & 0xFFU; \
buf_out[4] = (fls >> 8) & 0xFFU; \
buf_out[5] = fls & 0xFFU; \
buf_out += 6;
#define ENCODE_LLS(buf_out, lls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1BU; \
buf_out[2] = (lls >> 24) & 0xFFU; \
buf_out[3] = (lls >> 16) & 0xFFU; \
buf_out[4] = (lls >> 8) & 0xFFU; \
buf_out[5] = lls & 0xFFU; \
buf_out += 6;
#define ENCODE_DROPS(buf_out, drops, amt_type) \
buf_out[0] = 0x60U + amt_type; \
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
buf_out[2] = (drops >> 48) & 0xFFU; \
buf_out[3] = (drops >> 40) & 0xFFU; \
buf_out[4] = (drops >> 32) & 0xFFU; \
buf_out[5] = (drops >> 24) & 0xFFU; \
buf_out[6] = (drops >> 16) & 0xFFU; \
buf_out[7] = (drops >> 8) & 0xFFU; \
buf_out[8] = drops & 0xFFU; \
buf_out += 9;
#define ENCODE_SIGNING_PUBKEY_EMPTY(buf_out) \
buf_out[0] = 0x73U; \
buf_out[1] = 0x00U; \
buf_out += 2;
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
buf_out[0] = 0x80U + acc_type; \
buf_out[1] = 0x14U; \
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
buf_out += 22;
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
int64_t hook(uint32_t reserved) {
_g(1, 1);
if (otxn_type() != ttPAYMENT)
return accept(0, 0, 0);
ASSERT(xport_reserve(1) == 1);
uint8_t dst[20];
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
ASSERT(dst_len == 20);
uint8_t acc[20];
ASSERT(hook_account(SBUF(acc)) == 20);
uint32_t cls = (uint32_t)ledger_seq();
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
uint8_t* buf = tx;
ENCODE_TT(buf, ttPAYMENT);
ENCODE_NETWORK_ID(buf, 21337); // must precede Sequence (canonical order)
ENCODE_FLAGS(buf, tfCANONICAL);
ENCODE_SEQUENCE(buf, 0);
ENCODE_FLS(buf, cls + 1);
ENCODE_LLS(buf, cls + 5);
uint64_t drops = 1000000;
ENCODE_DROPS(buf, drops, amAMOUNT);
ENCODE_DROPS(buf, 10, amFEE);
ENCODE_SIGNING_PUBKEY_EMPTY(buf);
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
uint8_t hash[32];
int64_t xport_result = xport(SBUF(hash), (uint32_t)tx, buf - tx);
// xport should return EXPORT_FAILURE (-46), ASSERT will rollback
ASSERT(xport_result == 32);
return accept(0, 0, 0);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x25U,
0x06U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x00U, 0x01U,
0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U,
0x7FU, 0x01U, 0x7EU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U,
0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x8BU, 0x01U,
0x09U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x00U,
0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU,
0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x02U, 0x03U,
0x65U, 0x6EU, 0x76U, 0x0DU, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU,
0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x03U, 0x03U,
0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U,
0x63U, 0x6BU, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6FU,
0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU, 0x00U,
0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU, 0x6BU,
0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U, 0x05U,
0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U,
0x72U, 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU,
0x76U, 0x05U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x00U, 0x04U, 0x03U,
0x02U, 0x01U, 0x03U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U, 0x21U,
0x05U, 0x7FU, 0x01U, 0x41U, 0xE0U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U,
0x41U, 0xD9U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU,
0x7FU, 0x00U, 0x41U, 0xE0U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U,
0x80U, 0x08U, 0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU,
0x6BU, 0x00U, 0x09U, 0x0AU, 0xCDU, 0x84U, 0x00U, 0x01U, 0xC9U, 0x84U,
0x00U, 0x03U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x7FU, 0x23U, 0x80U,
0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0xF0U, 0x02U, 0x6BU, 0x22U, 0x01U,
0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x01U, 0x41U, 0x01U,
0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U, 0x02U,
0x40U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x50U, 0x0DU, 0x00U,
0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x82U, 0x80U, 0x80U,
0x80U, 0x00U, 0x21U, 0x02U, 0x0CU, 0x01U, 0x0BU, 0x02U, 0x40U, 0x41U,
0x01U, 0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x01U, 0x51U,
0x0DU, 0x00U, 0x41U, 0x80U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x16U,
0x42U, 0xE8U, 0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0xD0U, 0x02U, 0x6AU, 0x41U,
0x14U, 0x41U, 0x96U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x03U, 0x10U,
0x85U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU, 0x00U,
0x41U, 0x9AU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x0EU, 0x42U, 0xECU,
0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U,
0x40U, 0x20U, 0x01U, 0x41U, 0xB0U, 0x02U, 0x6AU, 0x41U, 0x14U, 0x10U,
0x86U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU, 0x00U,
0x41U, 0xA8U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x1EU, 0x42U, 0xEFU,
0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x10U,
0x87U, 0x80U, 0x80U, 0x80U, 0x00U, 0x21U, 0x02U, 0x20U, 0x01U, 0x41U,
0xC0U, 0x00U, 0x3AU, 0x00U, 0x48U, 0x20U, 0x01U, 0x42U, 0x80U, 0x80U,
0x80U, 0x80U, 0xF0U, 0xC1U, 0x90U, 0xA0U, 0xE8U, 0x00U, 0x37U, 0x03U,
0x40U, 0x20U, 0x01U, 0x41U, 0xE1U, 0x80U, 0x01U, 0x3BU, 0x01U, 0x3EU,
0x20U, 0x01U, 0x41U, 0xA0U, 0x36U, 0x3BU, 0x01U, 0x38U, 0x20U, 0x01U,
0x41U, 0xA0U, 0x34U, 0x3BU, 0x01U, 0x32U, 0x20U, 0x01U, 0x41U, 0x00U,
0x36U, 0x01U, 0x2EU, 0x20U, 0x01U, 0x41U, 0x80U, 0xC8U, 0x00U, 0x3BU,
0x01U, 0x2CU, 0x20U, 0x01U, 0x41U, 0xA2U, 0x80U, 0x02U, 0x36U, 0x02U,
0x28U, 0x20U, 0x01U, 0x42U, 0x92U, 0x80U, 0x80U, 0x88U, 0x82U, 0x80U,
0xC0U, 0xA9U, 0xD9U, 0x00U, 0x37U, 0x03U, 0x20U, 0x20U, 0x01U, 0x20U,
0x02U, 0xA7U, 0x22U, 0x03U, 0x41U, 0x05U, 0x6AU, 0x22U, 0x04U, 0x3AU,
0x00U, 0x3DU, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x08U, 0x76U, 0x3AU,
0x00U, 0x3CU, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x10U, 0x76U, 0x3AU,
0x00U, 0x3BU, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x18U, 0x76U, 0x3AU,
0x00U, 0x3AU, 0x20U, 0x01U, 0x20U, 0x03U, 0x41U, 0x01U, 0x6AU, 0x22U,
0x04U, 0x3AU, 0x00U, 0x37U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x08U,
0x76U, 0x3AU, 0x00U, 0x36U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x10U,
0x76U, 0x3AU, 0x00U, 0x35U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U, 0x18U,
0x76U, 0x3AU, 0x00U, 0x34U, 0x20U, 0x01U, 0x41U, 0xCDU, 0x00U, 0x6AU,
0x41U, 0x00U, 0x3BU, 0x00U, 0x00U, 0x20U, 0x01U, 0x41U, 0xDCU, 0x00U,
0x6AU, 0x20U, 0x01U, 0x29U, 0x03U, 0xB8U, 0x02U, 0x37U, 0x02U, 0x00U,
0x20U, 0x01U, 0x41U, 0xE4U, 0x00U, 0x6AU, 0x20U, 0x01U, 0x41U, 0xB0U,
0x02U, 0x6AU, 0x41U, 0x10U, 0x6AU, 0x28U, 0x02U, 0x00U, 0x36U, 0x02U,
0x00U, 0x20U, 0x01U, 0x41U, 0xF2U, 0x00U, 0x6AU, 0x20U, 0x01U, 0x29U,
0x03U, 0xD8U, 0x02U, 0x37U, 0x01U, 0x00U, 0x20U, 0x01U, 0x41U, 0xFAU,
0x00U, 0x6AU, 0x20U, 0x01U, 0x41U, 0xD0U, 0x02U, 0x6AU, 0x41U, 0x10U,
0x6AU, 0x28U, 0x02U, 0x00U, 0x36U, 0x01U, 0x00U, 0x20U, 0x01U, 0x41U,
0x00U, 0x36U, 0x00U, 0x49U, 0x20U, 0x01U, 0x41U, 0x8AU, 0xE6U, 0x81U,
0x88U, 0x78U, 0x36U, 0x00U, 0x4FU, 0x20U, 0x01U, 0x41U, 0x14U, 0x3AU,
0x00U, 0x53U, 0x20U, 0x01U, 0x41U, 0x83U, 0x29U, 0x3BU, 0x01U, 0x68U,
0x20U, 0x01U, 0x20U, 0x01U, 0x29U, 0x03U, 0xB0U, 0x02U, 0x37U, 0x02U,
0x54U, 0x20U, 0x01U, 0x20U, 0x01U, 0x29U, 0x03U, 0xD0U, 0x02U, 0x37U,
0x01U, 0x6AU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x20U, 0x20U, 0x01U,
0x41U, 0x20U, 0x6AU, 0x41U, 0xDEU, 0x00U, 0x10U, 0x88U, 0x80U, 0x80U,
0x80U, 0x00U, 0x42U, 0x20U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xC6U, 0x88U,
0x80U, 0x80U, 0x00U, 0x41U, 0x13U, 0x42U, 0x88U, 0x01U, 0x10U, 0x84U,
0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x00U, 0x41U, 0x00U,
0x42U, 0x00U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x21U, 0x02U,
0x0BU, 0x20U, 0x01U, 0x41U, 0xF0U, 0x02U, 0x6AU, 0x24U, 0x80U, 0x80U,
0x80U, 0x80U, 0x00U, 0x20U, 0x02U, 0x0BU, 0x0BU, 0x60U, 0x01U, 0x00U,
0x41U, 0x80U, 0x08U, 0x0BU, 0x59U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U,
0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x28U, 0x31U,
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x44U, 0x53U, 0x54U,
0x00U, 0x64U, 0x73U, 0x74U, 0x5FU, 0x6CU, 0x65U, 0x6EU, 0x20U, 0x3DU,
0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU,
0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x28U, 0x53U, 0x42U,
0x55U, 0x46U, 0x28U, 0x61U, 0x63U, 0x63U, 0x29U, 0x29U, 0x20U, 0x3DU,
0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U,
0x5FU, 0x72U, 0x65U, 0x73U, 0x75U, 0x6CU, 0x74U, 0x20U, 0x3DU, 0x3DU,
0x20U, 0x33U, 0x32U, 0x00U,
}},
};
}
} // namespace ripple
#endif

View File

@@ -5766,16 +5766,6 @@ private:
BEAST_EXPECT(!features[featurePermissionedDomains]);
}
void
testExportTSH(FeatureBitset features)
{
testcase("export tsh");
// ttEXPORT is a wrapper/consensus-driven path, not a hook-dispatched
// user transaction for triggered strong/weak hook execution.
pass();
}
void
testSetFeeTSH(FeatureBitset features)
{
@@ -6216,15 +6206,6 @@ private:
pass();
}
void
testConsensusEntropyTSH(FeatureBitset features)
{
testcase("consensus entropy tsh");
// pseudo transaction
pass();
}
// | otxn | tfBurnable | tsh | mint | burn | buy | sell | cancel
// | O | false | O | N/A | S | N/A | S | S
// | O | false | I | N/A | N | N/A | W | N/A

View File

@@ -1,456 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL Labs
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.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/import.h>
#include <test/jtx/xpop.h>
#include <test/shamap/common.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/proof/LedgerProof.h>
#include <xrpld/app/proof/ProofBuilder.h>
#include <xrpld/app/proof/XPOPv1.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/protocol/Import.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <cstring>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
} // namespace
struct XPOP_test : public beast::unit_test::suite
{
void
testBuildLedgerProof()
{
testcase("Build LedgerProof from a payment");
using namespace jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
// Submit a payment and close the ledger.
env(pay(alice, bob, XRP(100)));
env.close();
// Get the tx hash from the last closed ledger.
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
BEAST_EXPECT(lcl);
// Find a payment tx in the ledger.
uint256 paymentHash;
bool found = false;
lcl->txMap().visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
if (!found)
{
paymentHash = item->key();
found = true;
}
});
BEAST_EXPECT(found);
// Build the proof.
auto const lp = proof::buildLedgerProof(*lcl, paymentHash);
BEAST_EXPECT(lp.has_value());
if (lp)
{
// Verify header fields are populated.
BEAST_EXPECT(lp->ledgerIndex > 0);
BEAST_EXPECT(lp->totalCoins > 0);
BEAST_EXPECT(lp->parentHash != uint256{});
BEAST_EXPECT(lp->txRoot != uint256{});
BEAST_EXPECT(lp->accountRoot != uint256{});
// Verify tx blob is non-empty.
BEAST_EXPECT(!lp->txBlob.empty());
BEAST_EXPECT(!lp->metaBlob.empty());
// Verify merkle proof exists and is valid.
BEAST_EXPECT(lp->txProof.has_value());
if (lp->txProof)
{
auto const computedRoot = lp->txProof->computeRoot();
BEAST_EXPECT(computedRoot.has_value());
if (computedRoot)
BEAST_EXPECT(*computedRoot == lp->txRoot);
}
// Verify ledger hash reconstruction.
auto const computedHash = lp->computeLedgerHash();
BEAST_EXPECT(computedHash == lcl->info().hash);
}
auto missing = proof::buildLedgerProof(*lcl, makeHash("missing-tx"));
BEAST_EXPECT(!missing);
auto const missingProof =
proof::extractProofV1(lcl->txMap(), makeHash("missing-proof"));
BEAST_EXPECT(!missingProof);
}
void
testProofBuilderEdgeCases()
{
testcase("ProofBuilder edge cases");
proof::MerkleProof empty;
BEAST_EXPECT(!empty.computeRoot());
BEAST_EXPECT(!empty.verify(makeHash("root")));
BEAST_EXPECT(empty.toJsonV1().isNull());
proof::MerkleProof manual;
manual.key = makeHash("manual-key");
manual.leafHash = makeHash("manual-leaf");
proof::ProofNode leafParent;
leafParent.targetBranch = 3;
leafParent.isLeafParent = true;
leafParent.branches[0] = makeHash("manual-sibling-0");
manual.path.push_back(leafParent);
auto const computedRoot = manual.computeRoot();
BEAST_EXPECT(computedRoot.has_value());
if (computedRoot)
BEAST_EXPECT(manual.verify(*computedRoot));
auto const proofJson = manual.toJsonV1();
BEAST_EXPECT(proofJson.isArray());
BEAST_EXPECT(proofJson.size() == 16);
BEAST_EXPECT(proofJson[3].asString() == to_string(manual.leafHash));
}
void
testProofBuilderSyntheticTrie()
{
testcase("ProofBuilder synthetic trie collisions");
tests::TestNodeFamily f{beast::Journal{beast::Journal::getNullSink()}};
SHAMap map{SHAMapType::TRANSACTION, f};
auto const keyA = uint256{
"1000000000000000000000000000000000000000000000000000000000000001"};
auto const keyB = uint256{
"1800000000000000000000000000000000000000000000000000000000000002"};
auto const keyC = uint256{
"2000000000000000000000000000000000000000000000000000000000000003"};
auto const keyD = uint256{
"1f00000000000000000000000000000000000000000000000000000000000004"};
auto add = [&](uint256 const& key, Blob data) {
return map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(key, makeSlice(data)));
};
auto payload = [](std::uint8_t first) {
Blob data;
data.reserve(12);
for (std::uint8_t i = 0; i < 12; ++i)
data.push_back(first + i);
return data;
};
BEAST_EXPECT(add(keyA, payload(0x01)));
BEAST_EXPECT(add(keyB, payload(0x11)));
BEAST_EXPECT(add(keyC, payload(0x21)));
BEAST_EXPECT(add(keyD, payload(0x31)));
map.invariants();
auto const proof = proof::extractProofV1(map, keyA);
BEAST_EXPECT(proof.has_value());
if (proof)
{
BEAST_EXPECT(proof->path.size() == 2);
auto const computedRoot = proof->computeRoot();
BEAST_EXPECT(computedRoot.has_value());
if (computedRoot)
BEAST_EXPECT(proof->verify(*computedRoot));
auto const json = proof->toJsonV1();
BEAST_EXPECT(json.isArray());
BEAST_EXPECT(json[proof->path.front().targetBranch].isArray());
}
auto const nearMiss = uint256{
"10000000000000000000000000000000000000000000000000000000000000ff"};
BEAST_EXPECT(!proof::extractProofV1(map, nearMiss));
}
void
testBuildXPOPv1()
{
testcase("Build XPOP v1 JSON from a payment");
using namespace jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
env(pay(alice, bob, XRP(100)));
env.close();
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
BEAST_EXPECT(lcl);
// Find a tx.
uint256 txHash;
lcl->txMap().visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
txHash = item->key();
});
// Build XPOP using the test helper.
auto const xpopCtx = xpop::TestXPOPContext::create(3);
auto const xpop = xpopCtx.buildXPOP(*lcl, txHash);
BEAST_EXPECT(!xpop.isNull());
// Verify structure.
BEAST_EXPECT(xpop.isMember(jss::ledger));
BEAST_EXPECT(xpop.isMember(jss::transaction));
BEAST_EXPECT(xpop.isMember(jss::validation));
// Ledger section.
auto const& lgr = xpop[jss::ledger];
BEAST_EXPECT(lgr.isMember(jss::index));
BEAST_EXPECT(lgr.isMember(jss::coins));
BEAST_EXPECT(lgr.isMember(jss::phash));
BEAST_EXPECT(lgr.isMember(jss::txroot));
BEAST_EXPECT(lgr.isMember(jss::acroot));
BEAST_EXPECT(lgr.isMember(jss::close));
BEAST_EXPECT(lgr.isMember(jss::pclose));
BEAST_EXPECT(lgr.isMember(jss::cres));
BEAST_EXPECT(lgr.isMember(jss::flags));
// Transaction section.
auto const& txn = xpop[jss::transaction];
BEAST_EXPECT(txn.isMember(jss::blob));
BEAST_EXPECT(txn.isMember(jss::meta));
BEAST_EXPECT(txn.isMember(jss::proof));
BEAST_EXPECT(txn[jss::blob].asString().size() > 0);
BEAST_EXPECT(txn[jss::meta].asString().size() > 0);
// Validation section.
auto const& val = xpop[jss::validation];
BEAST_EXPECT(val.isMember(jss::data));
BEAST_EXPECT(val.isMember(jss::unl));
BEAST_EXPECT(val[jss::data].size() == 3); // 3 validators
auto const& unl = val[jss::unl];
BEAST_EXPECT(unl.isMember(jss::public_key));
BEAST_EXPECT(unl.isMember(jss::manifest));
BEAST_EXPECT(unl.isMember(jss::blob));
BEAST_EXPECT(unl.isMember(jss::signature));
BEAST_EXPECT(unl.isMember(jss::version));
auto const encoded = proof::xpopToHex(xpop);
BEAST_EXPECT(!encoded.empty());
BEAST_EXPECT(strUnHex(encoded).has_value());
auto const missing = proof::buildXPOPv1(
*lcl,
makeHash("missing-xpop-tx"),
std::vector<proof::ValidatorKeys>{},
xpopCtx.vlData);
BEAST_EXPECT(missing.isNull());
}
void
testBuildXPOPv1WithoutMerkleProof()
{
testcase("Build XPOP v1 without merkle proof");
auto const xpopCtx = jtx::xpop::TestXPOPContext::create(0);
proof::LedgerProof lp;
lp.ledgerIndex = 17;
lp.totalCoins = 12345;
lp.parentHash = makeHash("xpop-parent");
lp.txRoot = makeHash("xpop-tx-root");
lp.accountRoot = makeHash("xpop-account-root");
lp.parentCloseTime = 100;
lp.closeTime = 200;
lp.closeTimeResolution = 10;
lp.closeFlags = 1;
lp.txBlob = Blob{0x12, 0x00, 0x00};
lp.metaBlob = Blob{0x01, 0x02};
auto const xpop = proof::buildXPOPv1(
lp, std::vector<proof::ValidatorKeys>{}, xpopCtx.vlData);
BEAST_EXPECT(!xpop.isNull());
BEAST_EXPECT(xpop[jss::transaction][jss::proof].isArray());
BEAST_EXPECT(xpop[jss::transaction][jss::proof].size() == 0);
BEAST_EXPECT(xpop[jss::validation][jss::data].size() == 0);
auto const encoded = proof::xpopToHex(xpop);
BEAST_EXPECT(!encoded.empty());
BEAST_EXPECT(strUnHex(encoded).has_value());
}
void
testMerkleProofVerification()
{
testcase("Merkle proof verifies against tx root");
using namespace jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, bob, carol);
env.close();
// Multiple transactions to create a deeper trie.
env(pay(alice, bob, XRP(10)));
env(pay(bob, carol, XRP(5)));
env(pay(carol, alice, XRP(1)));
env.close();
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
BEAST_EXPECT(lcl);
// Verify proof for each transaction in the ledger.
int proofCount = 0;
lcl->txMap().visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
auto const lp = proof::buildLedgerProof(*lcl, item->key());
BEAST_EXPECT(lp.has_value());
if (lp && lp->txProof)
{
// Proof must verify against the ledger's tx root.
BEAST_EXPECT(lp->txProof->verify(lp->txRoot));
// JSON v1 serialization must round-trip.
auto const json = lp->txProof->toJsonV1();
BEAST_EXPECT(!json.isNull());
BEAST_EXPECT(json.isArray());
++proofCount;
}
});
// We should have proven at least 3 transactions.
BEAST_EXPECT(proofCount >= 3);
}
void
testImportWithGeneratedXPOP()
{
testcase("Import accepts dynamically generated XPOP");
using namespace jtx;
// Create XPOP context (VL publisher + validators).
auto const xpopCtx = xpop::TestXPOPContext::create(3);
// --- Source "network": generate a payment and build XPOP ---
Env srcEnv{*this};
Account const alice{"alice"};
Account const bob{"bob"};
srcEnv.fund(XRP(10000), alice, bob);
srcEnv.close();
// Import requires: no sfNetworkID + sfOperationLimit = dest NETWORK_ID.
Json::Value payTx;
payTx[jss::TransactionType] = jss::Payment;
payTx[jss::Account] = alice.human();
payTx[jss::Destination] = bob.human();
payTx[jss::Amount] = "100000000";
payTx[sfOperationLimit.jsonName] = 21337;
srcEnv(payTx, fee(XRP(1)));
srcEnv.close();
// Find the tx hash and build the XPOP.
auto const srcLcl = srcEnv.app().getLedgerMaster().getClosedLedger();
BEAST_EXPECT(srcLcl);
uint256 paymentHash;
srcLcl->txMap().visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
paymentHash = item->key();
});
auto const xpopJson = xpopCtx.buildXPOP(*srcLcl, paymentHash);
BEAST_EXPECT(!xpopJson.isNull());
// --- Destination "network": import the XPOP ---
Env dstEnv{*this, xpopCtx.makeEnvConfig(21337)};
// Burn some XRP so B2M can credit.
auto const master = Account("masterpassphrase");
dstEnv(noop(master), fee(10'000'000'000), ter(tesSUCCESS));
dstEnv.close();
Account const importAlice{"alice"};
dstEnv.fund(XRP(1000), importAlice);
dstEnv.close();
auto const feeDrops = dstEnv.current()->fees().base;
// Submit the import — should succeed (B2M path).
dstEnv(
import::import(importAlice, xpopJson),
fee(feeDrops * 10),
ter(tesSUCCESS));
dstEnv.close();
}
void
run() override
{
testBuildLedgerProof();
testProofBuilderEdgeCases();
testProofBuilderSyntheticTrie();
testBuildXPOPv1();
testBuildXPOPv1WithoutMerkleProof();
testMerkleProofVerification();
testImportWithGeneratedXPOP();
}
};
BEAST_DEFINE_TESTSUITE(XPOP, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -1,446 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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.
*/
//==============================================================================
#include <xrpld/app/hook/detail/XportWrapperBuilder.h>
#include <xrpl/basics/Expected.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/digest.h>
#include <cstring>
#include <optional>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
STTx
makeSTTx(STObject const& obj)
{
Serializer s;
obj.add(s);
SerialIter sit{s.slice()};
return STTx{std::ref(sit)};
}
Blob
serialize(STTx const& tx)
{
Serializer s;
tx.add(s);
return {s.begin(), s.end()};
}
STTx
makeExportedPayment(
AccountID const& src,
AccountID const& dst,
std::optional<std::uint32_t> networkID = std::nullopt,
std::optional<std::uint32_t> ticketSequence = 1,
std::uint32_t sequence = 0)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYMENT);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, sequence);
if (ticketSequence)
obj.setFieldU32(sfTicketSequence, *ticketSequence);
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setAccountID(sfDestination, dst);
if (networkID)
obj.setFieldU32(sfNetworkID, *networkID);
return makeSTTx(obj);
}
beast::Journal
nullJournal()
{
return beast::Journal{beast::Journal::getNullSink()};
}
hook::XportWrapperBuilder::Input
makeInput(
Slice innerTxBlob,
AccountID const& exporter,
std::uint32_t networkID = 21337,
hook::XportWrapperBuilder::NonceGenerator generateNonce =
[] {
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
},
hook::XportWrapperBuilder::FeeCalculator calculateFee =
[](Slice const&) {
return Expected<std::uint64_t, ::hook_api::hook_return_code>{12345};
})
{
return hook::XportWrapperBuilder::Input{
innerTxBlob,
exporter,
networkID,
10,
makeHash("parent-tx"),
makeHash("hook-hash"),
true,
3,
7,
std::move(generateNonce),
std::move(calculateFee),
nullJournal()};
}
} // namespace
class XportWrapperBuilder_test : public beast::unit_test::suite
{
public:
void
testBuildsWrapper()
{
testcase("builds xport wrapper");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first)));
BEAST_EXPECT(result);
if (!result)
return;
auto const& wrapper = result->wrapperTx;
BEAST_EXPECT(result->innerTxHash == innerTx.getTransactionID());
BEAST_EXPECT(wrapper.getTxnType() == ttEXPORT);
BEAST_EXPECT(
wrapper.getAccountID(sfAccount) == calcAccountID(exporter.first));
BEAST_EXPECT(wrapper.getFieldU32(sfSequence) == 0);
BEAST_EXPECT(wrapper.getFieldU32(sfFirstLedgerSequence) == 11);
BEAST_EXPECT(wrapper.getFieldU32(sfLastLedgerSequence) == 15);
BEAST_EXPECT(wrapper.getFieldAmount(sfFee) == STAmount{12345});
BEAST_EXPECT(wrapper.getFieldVL(sfSigningPubKey).empty());
auto const& exported =
wrapper.peekAtField(sfExportedTxn).downcast<STObject>();
Serializer exportedSer;
exported.add(exportedSer);
STTx parsedInner{SerialIter{exportedSer.slice()}};
BEAST_EXPECT(
parsedInner.getTransactionID() == innerTx.getTransactionID());
auto const& emitDetails =
wrapper.peekAtField(sfEmitDetails).downcast<STObject>();
BEAST_EXPECT(emitDetails.getFieldU32(sfEmitGeneration) == 3);
BEAST_EXPECT(emitDetails.getFieldU64(sfEmitBurden) == 7);
BEAST_EXPECT(
emitDetails.getFieldH256(sfEmitParentTxnID) ==
makeHash("parent-tx"));
BEAST_EXPECT(
emitDetails.getFieldH256(sfEmitNonce) == makeHash("nonce"));
BEAST_EXPECT(
emitDetails.getFieldH256(sfEmitHookHash) == makeHash("hook-hash"));
BEAST_EXPECT(
emitDetails.getAccountID(sfEmitCallback) ==
calcAccountID(exporter.first));
}
void
testBuildsWrapperWithoutCallback()
{
testcase("builds xport wrapper without callback");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
auto input = makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first));
input.hasCallback = false;
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(result);
if (!result)
return;
auto const& emitDetails =
result->wrapperTx.peekAtField(sfEmitDetails).downcast<STObject>();
BEAST_EXPECT(!emitDetails.isFieldPresent(sfEmitCallback));
}
void
testRejectsInvalidInputs()
{
testcase("rejects invalid inputs");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const other = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
{
Blob malformed{1, 2, 3};
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(malformed.data(), malformed.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(other.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
auto const networkTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first), 21337);
auto const serializedNetwork = serialize(networkTx);
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serializedNetwork.data(), serializedNetwork.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first),
0,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
auto const noTicketTx = makeExportedPayment(
calcAccountID(exporter.first),
calcAccountID(dst.first),
std::nullopt,
std::nullopt);
auto const serializedNoTicket = serialize(noTicketTx);
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serializedNoTicket.data(), serializedNoTicket.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
auto const sequencedTicketTx = makeExportedPayment(
calcAccountID(exporter.first),
calcAccountID(dst.first),
std::nullopt,
1,
9);
auto const serializedSequencedTicket = serialize(sequencedTicketTx);
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(
serializedSequencedTicket.data(),
serializedSequencedTicket.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
}
void
testRejectsMissingCallbacks()
{
testcase("rejects missing callbacks");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
{
auto input = makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first));
input.generateNonce = {};
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::INTERNAL_ERROR);
}
{
auto input = makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first));
input.calculateFee = {};
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
}
}
void
testMapsNonceFailureToInternalError()
{
testcase("maps nonce failure to internal error");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first),
21337,
[] {
return Expected<uint256, ::hook_api::hook_return_code>{
Unexpected(::hook_api::hook_return_code::TOO_MANY_NONCES)};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::INTERNAL_ERROR);
}
void
testRejectsFeeFailure()
{
testcase("rejects fee failure");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first),
21337,
[] {
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
},
[](Slice const&) {
return Expected<std::uint64_t, ::hook_api::hook_return_code>{
Unexpected(::hook_api::hook_return_code::EXPORT_FAILURE)};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
}
void
run() override
{
testBuildsWrapper();
testBuildsWrapperWithoutCallback();
testRejectsInvalidInputs();
testRejectsMissingCallbacks();
testMapsNonceFailureToInternalError();
testRejectsFeeFailure();
}
};
BEAST_DEFINE_TESTSUITE(XportWrapperBuilder, app, ripple);
} // namespace test
} // namespace ripple

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,6 @@
#include <xrpld/consensus/ConsensusProposal.h>
#include <xrpl/beast/clock/manual_clock.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/json/to_string.h>
#include <optional>
#include <utility>
namespace ripple {
@@ -38,18 +36,10 @@ public:
{
}
static void
enableSilentTracing(csf::Sim& sim)
{
sim.sink.silent(true);
sim.sink.threshold(beast::severities::kTrace);
}
void
testShouldCloseLedger()
{
using namespace std::chrono_literals;
testcase("should close ledger");
// Use default parameters
ConsensusParms const p{};
@@ -88,102 +78,46 @@ public:
testCheckConsensus()
{
using namespace std::chrono_literals;
testcase("check consensus");
// Use default parameterss
ConsensusParms const p{};
///////////////
// Disputes still in doubt
//
// Not enough time has elapsed
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 2s, false, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 2s, p, true, journal_));
// If not enough peers have propsed, ensure
// more time for proposals
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 4s, false, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 4s, p, true, journal_));
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 2, 0, 3s, 10s, false, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 10s, p, true, journal_));
// Enough time has elapsed and we don't yet agree
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 1, 0, 3s, 10s, false, p, true, journal_));
checkConsensus(10, 2, 1, 0, 3s, 10s, p, true, journal_));
// Our peers have moved on
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::MovedOn ==
checkConsensus(10, 2, 1, 8, 3s, 10s, false, p, true, journal_));
checkConsensus(10, 2, 1, 8, 3s, 10s, p, true, journal_));
// If no peers, don't agree until time has passed.
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(0, 0, 0, 0, 3s, 10s, false, p, true, journal_));
checkConsensus(0, 0, 0, 0, 3s, 10s, p, true, journal_));
// Agree if no peers and enough time has passed.
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(0, 0, 0, 0, 3s, 16s, false, p, true, journal_));
// Expire if too much time has passed without agreement
BEAST_EXPECT(
ConsensusState::Expired ==
checkConsensus(10, 8, 1, 0, 1s, 19s, false, p, true, journal_));
///////////////
// Stalled
//
// Not enough time has elapsed
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 2s, true, p, true, journal_));
// If not enough peers have propsed, ensure
// more time for proposals
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 4s, true, p, true, journal_));
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 2, 0, 3s, 10s, true, p, true, journal_));
// Enough time has elapsed and we don't yet agree, but there's nothing
// left to dispute
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 1, 0, 3s, 10s, true, p, true, journal_));
// Our peers have moved on
// Enough time has elapsed and we all agree, nothing left to dispute
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 1, 8, 3s, 10s, true, p, true, journal_));
// If no peers, don't agree until time has passed.
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(0, 0, 0, 0, 3s, 10s, true, p, true, journal_));
// Agree if no peers and enough time has passed.
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(0, 0, 0, 0, 3s, 16s, true, p, true, journal_));
// We are done if there's nothing left to dispute, no matter how much
// time has passed
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 8, 1, 0, 1s, 19s, true, p, true, journal_));
checkConsensus(0, 0, 0, 0, 3s, 16s, p, true, journal_));
}
void
@@ -191,7 +125,6 @@ public:
{
using namespace std::chrono_literals;
using namespace csf;
testcase("standalone");
Sim s;
PeerGroup peers = s.createGroup(1);
@@ -216,12 +149,9 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("peers agree");
//@@start peers-agree
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup peers = sim.createGroup(5);
// Connected trust and network graphs with single fixed delay
@@ -249,7 +179,6 @@ public:
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
}
}
//@@end peers-agree
}
void
@@ -257,18 +186,15 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("slow peers");
// Several tests of a complete trust graph with a subset of peers
// that have significantly longer network delays to the rest of the
// network
//@@start slow-peer-scenario
// Test when a slow peer doesn't delay a consensus quorum (4/5 agree)
{
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup slow = sim.createGroup(1);
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
@@ -303,18 +229,16 @@ public:
BEAST_EXPECT(
peer->prevRoundTime == network[0]->prevRoundTime);
// Slow peer's transaction (Tx{0}) didn't make it in time
BEAST_EXPECT(lcl.txs().find(Tx{0}) == lcl.txs().end());
for (std::uint32_t i = 2; i < network.size(); ++i)
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
// Tx 0 is still in the open transaction set for next round
// Tx 0 didn't make it
BEAST_EXPECT(
peer->openTxs.find(Tx{0}) != peer->openTxs.end());
}
}
}
//@@end slow-peer-scenario
// Test when the slow peers delay a consensus quorum (4/6 agree)
{
@@ -327,7 +251,6 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup slow = sim.createGroup(2);
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
@@ -428,7 +351,6 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("close time disagree");
// This is a very specialized test to get ledgers to disagree on
// the close time. It unfortunately assumes knowledge about current
@@ -457,7 +379,6 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup groupA = sim.createGroup(2);
PeerGroup groupB = sim.createGroup(2);
@@ -491,59 +412,11 @@ public:
}
}
void
testBootstrapFastStart()
{
using namespace csf;
using namespace std::chrono;
testcase("bootstrap fast start");
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup peers = sim.createGroup(4);
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
for (Peer* peer : peers)
{
peer->ce().bootstrapFastStartEnabled_ = true;
peer->targetLedgers =
static_cast<int>(parms.bootstrapStableRoundsRequired);
peer->start();
auto const json = peer->consensus.getJson(true);
BEAST_EXPECT(json.isMember("bootstrap_fast_start"));
BEAST_EXPECT(json["bootstrap_fast_start"].asBool());
BEAST_EXPECT(
json["previous_mseconds"].asInt() ==
parms.bootstrapRoundTimeSeed.count());
BEAST_EXPECT(json["bootstrap_stable_rounds"].asInt() == 0);
}
sim.scheduler.step();
if (BEAST_EXPECT(sim.synchronized()))
{
for (Peer* peer : peers)
{
BEAST_EXPECT(
peer->completedLedgers ==
static_cast<int>(parms.bootstrapStableRoundsRequired));
auto const json = peer->consensus.getJson(true);
BEAST_EXPECT(!json.isMember("bootstrap_fast_start"));
BEAST_EXPECT(peer->prevRoundTime < parms.ledgerIDLE_INTERVAL);
}
}
}
void
testWrongLCL()
{
using namespace csf;
using namespace std::chrono;
testcase("wrong LCL");
// Specialized test to exercise a temporary fork in which some peers
// are working on an incorrect prior ledger.
@@ -553,7 +426,6 @@ public:
// the wrong LCL at different phases of consensus
for (auto validationDelay : {0ms, parms.ledgerMIN_CLOSE})
{
//@@start wrong-lcl-scenario
// Consider 10 peers:
// 0 1 2 3 4 5 6 7 8 9
// minority majorityA majorityB
@@ -574,10 +446,8 @@ public:
// This topology can potentially fork with the above trust relations
// but that is intended for this test.
//@@end wrong-lcl-scenario
Sim sim;
enableSilentTracing(sim);
PeerGroup minority = sim.createGroup(2);
PeerGroup majorityA = sim.createGroup(3);
@@ -682,7 +552,6 @@ public:
// after it is already in the establish phase of the next round.
Sim sim;
enableSilentTracing(sim);
PeerGroup loner = sim.createGroup(1);
PeerGroup friends = sim.createGroup(3);
loner.trust(loner + friends);
@@ -720,7 +589,6 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("consensus close time rounding");
// This is a specialized test engineered to yield ledgers with different
// close times even though the peers believe they had close time
@@ -728,7 +596,6 @@ public:
ConsensusParms parms;
Sim sim;
enableSilentTracing(sim);
// This requires a group of 4 fast and 2 slow peers to create a
// situation in which a subset of peers requires seeing additional
@@ -737,6 +604,9 @@ public:
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
for (Peer* peer : network)
peer->consensusParms = parms;
// Connected trust graph
network.trust(network);
@@ -822,7 +692,6 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("fork");
std::uint32_t numPeers = 10;
// Vary overlap between two UNLs
@@ -830,7 +699,6 @@ public:
{
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
std::uint32_t numA = (numPeers - overlap) / 2;
std::uint32_t numB = numPeers - numA - overlap;
@@ -861,13 +729,9 @@ public:
}
sim.run(1);
//@@start fork-threshold
// Historical CSF topology regression: in this symmetric two-clique
// setup, the overlapped nodes trust the union of both cliques and
// the legacy simulator expects synchronization above the 40%
// overlap boundary. This is not a general XRP LCP safety theorem;
// modern consensus analysis has stricter heterogeneous-UNL
// assumptions.
// Fork should not happen for 40% or greater overlap
// Since the overlapped nodes have a UNL that is the union of the
// two cliques, the maximum sized UNL list is the number of peers
if (overlap > 0.4 * numPeers)
BEAST_EXPECT(sim.synchronized());
else
@@ -876,7 +740,6 @@ public:
// One for cliqueA, one for cliqueB and one for nodes in both
BEAST_EXPECT(sim.branches() <= 3);
}
//@@end fork-threshold
}
}
@@ -885,14 +748,12 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("hub network");
// Simulate a set of 5 validators that aren't directly connected but
// rely on a single hub node for communication
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup validators = sim.createGroup(5);
PeerGroup center = sim.createGroup(1);
validators.trust(validators);
@@ -974,7 +835,6 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("preferred by branch");
// Simulate network splits that are prevented from forking when using
// preferred ledger by trie. This is a contrived example that involves
@@ -1008,7 +868,6 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
// Goes A->B->D
PeerGroup groupABD = sim.createGroup(2);
@@ -1108,7 +967,6 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("pause for laggards");
// Test that validators that jump ahead of the network slow
// down.
@@ -1132,7 +990,6 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
SimDuration delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup behind = sim.createGroup(3);
@@ -1195,410 +1052,6 @@ public:
BEAST_EXPECT(sim.synchronized());
}
// RNG consensus tests in ConsensusRng_test.cpp
// MERGE NOTE (sync-2.5.0): upstream testDisputes() is already present
// below with j/clog stalled() params from 86ef16dbeb. If upstream
// auto-merges a duplicate, delete it — keep only this version.
void
testDisputes()
{
testcase("disputes");
using namespace csf;
// Test dispute objects directly
using Dispute = DisputedTx<Tx, PeerID>;
Tx const txTrue{99};
Tx const txFalse{98};
Tx const txFollowingTrue{97};
Tx const txFollowingFalse{96};
int const numPeers = 100;
ConsensusParms p;
std::size_t peersUnchanged = 0;
auto logs = std::make_unique<Logs>(beast::severities::kError);
auto j = logs->journal("Test");
auto clog = std::make_unique<std::stringstream>();
// Three cases:
// 1 proposing, initial vote yes
// 2 proposing, initial vote no
// 3 not proposing, initial vote doesn't matter after the first update,
// use yes
{
Dispute proposingTrue{txTrue.id(), true, numPeers, journal_};
Dispute proposingFalse{txFalse.id(), false, numPeers, journal_};
Dispute followingTrue{
txFollowingTrue.id(), true, numPeers, journal_};
Dispute followingFalse{
txFollowingFalse.id(), false, numPeers, journal_};
BEAST_EXPECT(proposingTrue.ID() == 99);
BEAST_EXPECT(proposingFalse.ID() == 98);
BEAST_EXPECT(followingTrue.ID() == 97);
BEAST_EXPECT(followingFalse.ID() == 96);
// Create an even split in the peer votes
for (int i = 0; i < numPeers; ++i)
{
BEAST_EXPECT(proposingTrue.setVote(PeerID(i), i < 50));
BEAST_EXPECT(proposingFalse.setVote(PeerID(i), i < 50));
BEAST_EXPECT(followingTrue.setVote(PeerID(i), i < 50));
BEAST_EXPECT(followingFalse.setVote(PeerID(i), i < 50));
}
// Switch the middle vote to match mine
BEAST_EXPECT(proposingTrue.setVote(PeerID(50), true));
BEAST_EXPECT(proposingFalse.setVote(PeerID(49), false));
BEAST_EXPECT(followingTrue.setVote(PeerID(50), true));
BEAST_EXPECT(followingFalse.setVote(PeerID(49), false));
// no changes yet
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(clog->str() == "");
// I'm in the majority, my vote should not change
BEAST_EXPECT(!proposingTrue.updateVote(5, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(5, true, p));
BEAST_EXPECT(!followingTrue.updateVote(5, false, p));
BEAST_EXPECT(!followingFalse.updateVote(5, false, p));
BEAST_EXPECT(!proposingTrue.updateVote(10, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(10, true, p));
BEAST_EXPECT(!followingTrue.updateVote(10, false, p));
BEAST_EXPECT(!followingFalse.updateVote(10, false, p));
peersUnchanged = 2;
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(clog->str() == "");
// Right now, the vote is 51%. The requirement is about to jump to
// 65%
BEAST_EXPECT(proposingTrue.updateVote(55, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(55, true, p));
BEAST_EXPECT(!followingTrue.updateVote(55, false, p));
BEAST_EXPECT(!followingFalse.updateVote(55, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 16 validators change their vote to match my original vote
for (int i = 0; i < 16; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 66%, threshold is 65%
BEAST_EXPECT(proposingTrue.updateVote(60, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(60, true, p));
BEAST_EXPECT(!followingTrue.updateVote(60, false, p));
BEAST_EXPECT(!followingFalse.updateVote(60, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// Threshold jumps to 70%
BEAST_EXPECT(proposingTrue.updateVote(86, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(86, true, p));
BEAST_EXPECT(!followingTrue.updateVote(86, false, p));
BEAST_EXPECT(!followingFalse.updateVote(86, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 5 more validators change their vote to match my original vote
for (int i = 16; i < 21; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(proposingTrue.updateVote(90, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(90, true, p));
BEAST_EXPECT(!followingTrue.updateVote(90, false, p));
BEAST_EXPECT(!followingFalse.updateVote(90, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(!proposingTrue.updateVote(150, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(150, true, p));
BEAST_EXPECT(!followingTrue.updateVote(150, false, p));
BEAST_EXPECT(!followingFalse.updateVote(150, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(!proposingTrue.updateVote(190, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(190, true, p));
BEAST_EXPECT(!followingTrue.updateVote(190, false, p));
BEAST_EXPECT(!followingFalse.updateVote(190, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
peersUnchanged = 3;
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(clog->str() == "");
// Threshold jumps to 95%
BEAST_EXPECT(proposingTrue.updateVote(220, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(220, true, p));
BEAST_EXPECT(!followingTrue.updateVote(220, false, p));
BEAST_EXPECT(!followingFalse.updateVote(220, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 25 more validators change their vote to match my original vote
for (int i = 21; i < 46; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 96%, threshold is 95%
BEAST_EXPECT(proposingTrue.updateVote(250, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250, false, p));
BEAST_EXPECT(!followingFalse.updateVote(250, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
for (peersUnchanged = 0; peersUnchanged < 6; ++peersUnchanged)
{
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(clog->str() == "");
}
auto expectStalled = [this, &clog](
int txid,
bool ourVote,
int ourTime,
int peerTime,
int support,
std::uint32_t line) {
using namespace std::string_literals;
auto const s = clog->str();
expect(s.find("stalled"), s, __FILE__, line);
expect(
s.starts_with("Transaction "s + std::to_string(txid)),
s,
__FILE__,
line);
expect(
s.find("voting "s + (ourVote ? "YES" : "NO")) != s.npos,
s,
__FILE__,
line);
expect(
s.find("for "s + std::to_string(ourTime) + " rounds."s) !=
s.npos,
s,
__FILE__,
line);
expect(
s.find(
"votes in "s + std::to_string(peerTime) + " rounds.") !=
s.npos,
s,
__FILE__,
line);
expect(
s.ends_with(
"has "s + std::to_string(support) + "% support. "s),
s,
__FILE__,
line);
clog = std::make_unique<std::stringstream>();
};
for (int i = 0; i < 1; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// true vote has changed recently, so not stalled
BEAST_EXPECT(!proposingTrue.stalled(p, true, 0, j, clog));
BEAST_EXPECT(clog->str() == "");
// remaining votes have been unchanged in so long that we only
// need to hit the second round at 95% to be stalled, regardless
// of peers
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
expectStalled(98, false, 11, 0, 2, __LINE__);
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
expectStalled(97, true, 11, 0, 97, __LINE__);
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
expectStalled(96, false, 11, 0, 3, __LINE__);
// true vote has changed recently, so not stalled
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECTS(clog->str() == "", clog->str());
// remaining votes have been unchanged in so long that we only
// need to hit the second round at 95% to be stalled, regardless
// of peers
BEAST_EXPECT(
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
expectStalled(98, false, 11, 6, 2, __LINE__);
BEAST_EXPECT(
followingTrue.stalled(p, false, peersUnchanged, j, clog));
expectStalled(97, true, 11, 6, 97, __LINE__);
BEAST_EXPECT(
followingFalse.stalled(p, false, peersUnchanged, j, clog));
expectStalled(96, false, 11, 6, 3, __LINE__);
}
for (int i = 1; i < 3; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// true vote changed 2 rounds ago, and peers are changing, so
// not stalled
BEAST_EXPECT(!proposingTrue.stalled(p, true, 0, j, clog));
BEAST_EXPECTS(clog->str() == "", clog->str());
// still stalled
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
expectStalled(98, false, 11 + i, 0, 2, __LINE__);
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
expectStalled(97, true, 11 + i, 0, 97, __LINE__);
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
expectStalled(96, false, 11 + i, 0, 3, __LINE__);
// true vote changed 2 rounds ago, and peers are NOT changing,
// so stalled
BEAST_EXPECT(
proposingTrue.stalled(p, true, peersUnchanged, j, clog));
expectStalled(99, true, 1 + i, 6, 97, __LINE__);
// still stalled
BEAST_EXPECT(
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
expectStalled(98, false, 11 + i, 6, 2, __LINE__);
BEAST_EXPECT(
followingTrue.stalled(p, false, peersUnchanged, j, clog));
expectStalled(97, true, 11 + i, 6, 97, __LINE__);
BEAST_EXPECT(
followingFalse.stalled(p, false, peersUnchanged, j, clog));
expectStalled(96, false, 11 + i, 6, 3, __LINE__);
}
for (int i = 3; i < 5; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
BEAST_EXPECT(proposingTrue.stalled(p, true, 0, j, clog));
expectStalled(99, true, 1 + i, 0, 97, __LINE__);
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
expectStalled(98, false, 11 + i, 0, 2, __LINE__);
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
expectStalled(97, true, 11 + i, 0, 97, __LINE__);
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
expectStalled(96, false, 11 + i, 0, 3, __LINE__);
BEAST_EXPECT(
proposingTrue.stalled(p, true, peersUnchanged, j, clog));
expectStalled(99, true, 1 + i, 6, 97, __LINE__);
BEAST_EXPECT(
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
expectStalled(98, false, 11 + i, 6, 2, __LINE__);
BEAST_EXPECT(
followingTrue.stalled(p, false, peersUnchanged, j, clog));
expectStalled(97, true, 11 + i, 6, 97, __LINE__);
BEAST_EXPECT(
followingFalse.stalled(p, false, peersUnchanged, j, clog));
expectStalled(96, false, 11 + i, 6, 3, __LINE__);
}
}
}
void
run() override
{
@@ -1609,15 +1062,12 @@ public:
testPeersAgree();
testSlowPeers();
testCloseTimeDisagree();
testBootstrapFastStart();
testWrongLCL();
testConsensusCloseTimeRounding();
testFork();
testHubNetwork();
testPreferredByBranch();
testPauseForLaggards();
// RNG consensus tests moved to ConsensusRng_test.cpp
testDisputes();
}
};

Some files were not shown because too many files have changed in this diff Show More