mirror of
https://github.com/Xahau/xahaud.git
synced 2026-06-26 12:06:38 +00:00
Compare commits
378 Commits
subscripti
...
feature-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20815f11a0 | ||
|
|
938d3055ce | ||
|
|
ff24f10b9f | ||
|
|
aee3a638ee | ||
|
|
e6ac99624e | ||
|
|
b9f54b9ddb | ||
|
|
d54ad32d65 | ||
|
|
feb2123e2c | ||
|
|
56d196739c | ||
|
|
79c2562492 | ||
|
|
a509de3d39 | ||
|
|
4fb91ea9f5 | ||
|
|
691e2b07eb | ||
|
|
ffe7b51336 | ||
|
|
d7a5863b93 | ||
|
|
acb492a2c9 | ||
|
|
92fe444323 | ||
|
|
9549901014 | ||
|
|
9f6e7dd315 | ||
|
|
f6d986bdbc | ||
|
|
e55bf43986 | ||
|
|
b4beb92c34 | ||
|
|
40edbfc7f2 | ||
|
|
ff205b1b81 | ||
|
|
ddfb1dbeb6 | ||
|
|
8bf1ece0a0 | ||
|
|
639e153f3b | ||
|
|
8e542c32e0 | ||
|
|
38f4d53ebf | ||
|
|
fe66a11c69 | ||
|
|
77e48d553c | ||
|
|
439031dc92 | ||
|
|
1a5b934881 | ||
|
|
7f0d2959e8 | ||
|
|
1440d1495f | ||
|
|
57f5a9d6cc | ||
|
|
cacd1f71fe | ||
|
|
93a6f0fbec | ||
|
|
c5b1cb222d | ||
|
|
e447f9f021 | ||
|
|
0757094ed2 | ||
|
|
4b8107d57c | ||
|
|
d4b9e2f22c | ||
|
|
3fc199017e | ||
|
|
4d55443976 | ||
|
|
0d1d649867 | ||
|
|
60469dbd86 | ||
|
|
3970912735 | ||
|
|
456f4144ba | ||
|
|
570cad4c44 | ||
|
|
b13868b71e | ||
|
|
288b9e6d25 | ||
|
|
e55c2c6dc8 | ||
|
|
35e981e509 | ||
|
|
bb244ef772 | ||
|
|
b9a733c831 | ||
|
|
301e546aa9 | ||
|
|
e2da1db6d2 | ||
|
|
16cd02156e | ||
|
|
fb9e2710cc | ||
|
|
c8fad50d66 | ||
|
|
d9b5fc26fc | ||
|
|
72d03620f9 | ||
|
|
8368e12ab3 | ||
|
|
23f745bd01 | ||
|
|
27ae39b91a | ||
|
|
20d52d8b66 | ||
|
|
b070785dee | ||
|
|
def617e3f9 | ||
|
|
c322b59961 | ||
|
|
c3cc3513c9 | ||
|
|
a0be935227 | ||
|
|
6b1b18dd38 | ||
|
|
946f25249b | ||
|
|
a95306142e | ||
|
|
8d43154061 | ||
|
|
156a8cbb85 | ||
|
|
14ebe74a56 | ||
|
|
580f07ce35 | ||
|
|
d4c0ba3769 | ||
|
|
378d6b78c8 | ||
|
|
4b219cdef8 | ||
|
|
9d4b97c824 | ||
|
|
d6481a3869 | ||
|
|
0cf6f73441 | ||
|
|
08a6f3cd57 | ||
|
|
77d78236e8 | ||
|
|
c92c0656ec | ||
|
|
fbdec3be66 | ||
|
|
bb619cc100 | ||
|
|
10f22c84f2 | ||
|
|
dd21024c0e | ||
|
|
16a72172b4 | ||
|
|
265012e16a | ||
|
|
d072527bc5 | ||
|
|
5f5ce12fa6 | ||
|
|
0804a01b9b | ||
|
|
80cd1bed34 | ||
|
|
b4e98ac1d7 | ||
|
|
4f7e751fbd | ||
|
|
748fef6267 | ||
|
|
4d48c9f949 | ||
|
|
03c1216661 | ||
|
|
537474cb5f | ||
|
|
ec086d6765 | ||
|
|
7362c1dac1 | ||
|
|
f0550ca625 | ||
|
|
526b60bf3d | ||
|
|
9347b47639 | ||
|
|
9988568a08 | ||
|
|
13260b9ef7 | ||
|
|
65dab780de | ||
|
|
ee70e4cdbb | ||
|
|
7fb1509673 | ||
|
|
64620e2825 | ||
|
|
a87a7896ca | ||
|
|
443aca8611 | ||
|
|
804b76b4ab | ||
|
|
ab6571a20f | ||
|
|
331e1606a3 | ||
|
|
24d6dea1a2 | ||
|
|
03e0bb5fc3 | ||
|
|
0a77dbf68e | ||
|
|
60a9a2c9fb | ||
|
|
445d0070d8 | ||
|
|
61a8d8bba7 | ||
|
|
fbedb8a73a | ||
|
|
8ae541fcc1 | ||
|
|
c8f3f6f05f | ||
|
|
b12cee5d47 | ||
|
|
a3b1e45f4d | ||
|
|
3938ba7af4 | ||
|
|
96b1104646 | ||
|
|
92bdd2ed9f | ||
|
|
d87cfdc604 | ||
|
|
a956abb2d1 | ||
|
|
aa36a80ab7 | ||
|
|
e729aa11eb | ||
|
|
c58da3da58 | ||
|
|
0c2c59d258 | ||
|
|
15662eb1b1 | ||
|
|
492fe90643 | ||
|
|
ea413873b2 | ||
|
|
625419eab7 | ||
|
|
2218bdd7f3 | ||
|
|
f13233b00a | ||
|
|
a61f334ca2 | ||
|
|
53a119ce30 | ||
|
|
63d1197345 | ||
|
|
aafd5b940b | ||
|
|
efc497cf23 | ||
|
|
f4e78c9a24 | ||
|
|
7b5865c69c | ||
|
|
9f1ad521e1 | ||
|
|
26bbef8efd | ||
|
|
6e71f84867 | ||
|
|
ab9b48f67a | ||
|
|
cd00ed72d8 | ||
|
|
05a3e04f2d | ||
|
|
66f7294120 | ||
|
|
7f6ac75617 | ||
|
|
4150f0383c | ||
|
|
25123b370a | ||
|
|
f90ed41802 | ||
|
|
8c4c158d3a | ||
|
|
2d2951875d | ||
|
|
9bfca63574 | ||
|
|
1ba444ae7f | ||
|
|
f96d9b6e51 | ||
|
|
04077c1a55 | ||
|
|
d94079d762 | ||
|
|
92ec07a1be | ||
|
|
664db62588 | ||
|
|
03a436d918 | ||
|
|
7474048295 | ||
|
|
1ee660529e | ||
|
|
311dfa1c23 | ||
|
|
f27cd2c567 | ||
|
|
f34fdc297c | ||
|
|
65fa63883d | ||
|
|
d8c683fb4c | ||
|
|
fd53af304b | ||
|
|
2a3f0ec923 | ||
|
|
00f1f7ba30 | ||
|
|
49f05e4e47 | ||
|
|
1f51b9c594 | ||
|
|
88a548a8ef | ||
|
|
db302a0f78 | ||
|
|
383d9ec2e7 | ||
|
|
52671bfc99 | ||
|
|
8307fca3b9 | ||
|
|
6526621c16 | ||
|
|
2a9b1c9c22 | ||
|
|
54ca21b604 | ||
|
|
462db6004c | ||
|
|
cfca708aae | ||
|
|
5f70e5259c | ||
|
|
8697c5d821 | ||
|
|
9436e5868e | ||
|
|
c6fa973cf6 | ||
|
|
939e03714c | ||
|
|
969f98f57e | ||
|
|
435deb0e78 | ||
|
|
b80352e512 | ||
|
|
57c46c61fc | ||
|
|
37ff13df50 | ||
|
|
1b363b7eac | ||
|
|
9562b457cf | ||
|
|
724633ceb5 | ||
|
|
152d82e798 | ||
|
|
0bb31ce7ce | ||
|
|
4cb3de0497 | ||
|
|
c6b315412d | ||
|
|
72395bec75 | ||
|
|
8ed4d86f0f | ||
|
|
419fd16b9a | ||
|
|
a8097cd9a6 | ||
|
|
02a0552325 | ||
|
|
3698193b0a | ||
|
|
de43ca2385 | ||
|
|
8c747a1916 | ||
|
|
cea110f29a | ||
|
|
3ca056a94b | ||
|
|
705d8400db | ||
|
|
655b751698 | ||
|
|
f324081277 | ||
|
|
24a284180a | ||
|
|
6f003cc983 | ||
|
|
3a58020388 | ||
|
|
829441b52e | ||
|
|
3a055663cc | ||
|
|
985a194bdc | ||
|
|
869f366d8a | ||
|
|
03936aa928 | ||
|
|
6d180307ad | ||
|
|
f2ca499c97 | ||
|
|
bd68364f25 | ||
|
|
42a6407815 | ||
|
|
a387c853ab | ||
|
|
9311e567d3 | ||
|
|
c26582bdf9 | ||
|
|
417b999c7f | ||
|
|
0205be4500 | ||
|
|
89274b5387 | ||
|
|
b65d9faf12 | ||
|
|
aa1a7e5320 | ||
|
|
6f0f17aad9 | ||
|
|
407bfa1467 | ||
|
|
f0dfcf6b81 | ||
|
|
503d2ebf98 | ||
|
|
e52bc51384 | ||
|
|
91860db578 | ||
|
|
0b317a8e7a | ||
|
|
dbd230b695 | ||
|
|
30cefcba85 | ||
|
|
94edb5759d | ||
|
|
ce57b6a3a0 | ||
|
|
fca5cad470 | ||
|
|
bb77c2090b | ||
|
|
90a94294e4 | ||
|
|
c2209b4472 | ||
|
|
8fcb2ed336 | ||
|
|
fd1567d1ba | ||
|
|
d32f34d3bf | ||
|
|
c491c5c82f | ||
|
|
74817765ae | ||
|
|
fc23fa8535 | ||
|
|
34c0f17b6b | ||
|
|
765ad6a278 | ||
|
|
f623ca89b9 | ||
|
|
e4865f09f9 | ||
|
|
4c182e4738 | ||
|
|
d0c869c8a6 | ||
|
|
cac5efcd3c | ||
|
|
514e60b71c | ||
|
|
2a34e32e05 | ||
|
|
b969024a25 | ||
|
|
f30b9a4c3a | ||
|
|
0e019fec4e | ||
|
|
7e0c72fd22 | ||
|
|
07d741cdd7 | ||
|
|
b99c38c09d | ||
|
|
64e50209ff | ||
|
|
b1ce2103ad | ||
|
|
50c4cf1df3 | ||
|
|
6fc14f398d | ||
|
|
592a8600c7 | ||
|
|
e71768700a | ||
|
|
e598e405bd | ||
|
|
8af3ce2f5b | ||
|
|
b67cb78b97 | ||
|
|
8cfee6c8a3 | ||
|
|
8673599d2b | ||
|
|
0b1b82282e | ||
|
|
d4c5a7e8ab | ||
|
|
82837864fa | ||
|
|
e1caee6459 | ||
|
|
3206b4a4e1 | ||
|
|
ec65e622aa | ||
|
|
65837f49e1 | ||
|
|
0c2e09050e | ||
|
|
83922d5c20 | ||
|
|
6bae42ff01 | ||
|
|
35e86d926e | ||
|
|
9c4ee9315d | ||
|
|
0f17cf02aa | ||
|
|
7753dc3cbe | ||
|
|
cc7f3c59ae | ||
|
|
e5b21f026e | ||
|
|
e8c1b25ab4 | ||
|
|
b9dd854595 | ||
|
|
3bead8dcb6 | ||
|
|
908a78a1d9 | ||
|
|
a9e3dc41d4 | ||
|
|
02990eb4ee | ||
|
|
ce76632322 | ||
|
|
9eac54d690 | ||
|
|
24e4ac16ad | ||
|
|
94ce15d233 | ||
|
|
8f331a538e | ||
|
|
7425ab0a39 | ||
|
|
c5292bfe0d | ||
|
|
79b2f9f410 | ||
|
|
e8358a82b1 | ||
|
|
d850e740e1 | ||
|
|
61a166bcb0 | ||
|
|
41a41ec625 | ||
|
|
bc98c589b7 | ||
|
|
4f009e4698 | ||
|
|
b6811a6f59 | ||
|
|
ae88fd3d24 | ||
|
|
db3ed0c2eb | ||
|
|
960808b172 | ||
|
|
a9dffd38ff | ||
|
|
382e6fa673 | ||
|
|
2905b0509c | ||
|
|
4911c1bf52 | ||
|
|
1744d21410 | ||
|
|
34ff53f65d | ||
|
|
893f8d5a10 | ||
|
|
3e5389d652 | ||
|
|
c44dea3acf | ||
|
|
a6dd54fa48 | ||
|
|
28bd0a22d3 | ||
|
|
960fffcf82 | ||
|
|
e7867c07a1 | ||
|
|
a828e8a44d | ||
|
|
bb33e7cf64 | ||
|
|
7e8e0654cd | ||
|
|
38af0626e0 | ||
|
|
8500e86f57 | ||
|
|
1fc4fd9bfd | ||
|
|
e4875e5398 | ||
|
|
5b1b142be0 | ||
|
|
5ba832204a | ||
|
|
1257b3a65c | ||
|
|
6013ed2cb6 | ||
|
|
034010716e | ||
|
|
b28793b0fa | ||
|
|
4bce392c31 | ||
|
|
244a28b981 | ||
|
|
f2838351c9 | ||
|
|
dae082d6a5 | ||
|
|
619a4a68f7 | ||
|
|
4a6db8bb05 | ||
|
|
c86479bc58 | ||
|
|
dc6a2dc6ff | ||
|
|
c01b9a657b | ||
|
|
652b181b5d | ||
|
|
8329d78f32 | ||
|
|
bf4579c1d1 | ||
|
|
73e099eb23 | ||
|
|
2e311b4259 | ||
|
|
7c8e940091 | ||
|
|
9b90c50789 | ||
|
|
a18e2cb2c6 | ||
|
|
be5f425122 | ||
|
|
fc6f4762da |
2
.github/workflows/check-genesis-hooks.yml
vendored
2
.github/workflows/check-genesis-hooks.yml
vendored
@@ -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
|
||||
|
||||
|
||||
127
.github/workflows/formal-verification.yml
vendored
Normal file
127
.github/workflows/formal-verification.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
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
7
.gitignore
vendored
@@ -127,5 +127,12 @@ 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
Normal file
4
.testnet/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
output/
|
||||
__pycache__/
|
||||
scenarios/odd-cases/
|
||||
scenarios/suite-experiments.yml
|
||||
29
.testnet/scenarios/entropy/consensus_entropy_crash.py
Normal file
29
.testnet/scenarios/entropy/consensus_entropy_crash.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""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")
|
||||
52
.testnet/scenarios/entropy/entropy_with_transactions.py
Normal file
52
.testnet/scenarios/entropy/entropy_with_transactions.py
Normal file
@@ -0,0 +1,52 @@
|
||||
""":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")
|
||||
28
.testnet/scenarios/entropy/fallback_without_unl_report.py
Normal file
28
.testnet/scenarios/entropy/fallback_without_unl_report.py
Normal file
@@ -0,0 +1,28 @@
|
||||
""":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")
|
||||
160
.testnet/scenarios/entropy/participant_aligned_smoke.py
Normal file
160
.testnet/scenarios/entropy/participant_aligned_smoke.py
Normal file
@@ -0,0 +1,160 @@
|
||||
""":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")
|
||||
148
.testnet/scenarios/entropy/quorum_degradation_smoke.py
Normal file
148
.testnet/scenarios/entropy/quorum_degradation_smoke.py
Normal file
@@ -0,0 +1,148 @@
|
||||
""":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")
|
||||
44
.testnet/scenarios/entropy/quorum_recovery_smoke.py
Normal file
44
.testnet/scenarios/entropy/quorum_recovery_smoke.py
Normal file
@@ -0,0 +1,44 @@
|
||||
""":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")
|
||||
27
.testnet/scenarios/entropy/steady_state_entropy.py
Normal file
27
.testnet/scenarios/entropy/steady_state_entropy.py
Normal file
@@ -0,0 +1,27 @@
|
||||
""":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")
|
||||
104
.testnet/scenarios/export-suite.yml
Normal file
104
.testnet/scenarios/export-suite.yml
Normal file
@@ -0,0 +1,104 @@
|
||||
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
|
||||
123
.testnet/scenarios/export/export_degradation.py
Normal file
123
.testnet/scenarios/export/export_degradation.py
Normal file
@@ -0,0 +1,123 @@
|
||||
""":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")
|
||||
181
.testnet/scenarios/export/export_helpers.py
Normal file
181
.testnet/scenarios/export/export_helpers.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""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
|
||||
@@ -0,0 +1,87 @@
|
||||
""":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")
|
||||
117
.testnet/scenarios/export/export_quorum.py
Normal file
117
.testnet/scenarios/export/export_quorum.py
Normal file
@@ -0,0 +1,117 @@
|
||||
""":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")
|
||||
92
.testnet/scenarios/export/export_without_unl_report.py
Normal file
92
.testnet/scenarios/export/export_without_unl_report.py
Normal file
@@ -0,0 +1,92 @@
|
||||
""":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")
|
||||
94
.testnet/scenarios/export/retriable_export.py
Normal file
94
.testnet/scenarios/export/retriable_export.py
Normal file
@@ -0,0 +1,94 @@
|
||||
""":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")
|
||||
211
.testnet/scenarios/export/steady_state_export.py
Normal file
211
.testnet/scenarios/export/steady_state_export.py
Normal file
@@ -0,0 +1,211 @@
|
||||
""":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"
|
||||
)
|
||||
180
.testnet/scenarios/helpers.py
Normal file
180
.testnet/scenarios/helpers.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""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
|
||||
92
.testnet/scenarios/latency-suite.yml
Normal file
92
.testnet/scenarios/latency-suite.yml
Normal file
@@ -0,0 +1,92 @@
|
||||
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
|
||||
196
.testnet/scenarios/perf/ce_export_latency_probe.py
Normal file
196
.testnet/scenarios/perf/ce_export_latency_probe.py
Normal file
@@ -0,0 +1,196 @@
|
||||
""":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")
|
||||
62
.testnet/scenarios/suite.yml
Normal file
62
.testnet/scenarios/suite.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -21,6 +22,7 @@ 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
|
||||
@@ -43,6 +45,7 @@ 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
|
||||
@@ -56,6 +59,8 @@ 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
|
||||
|
||||
@@ -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 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 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/)
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -95,8 +95,16 @@ if [[ "$4" == "" ]]; then
|
||||
echo "Non GH, local building, no Action runner magic"
|
||||
else
|
||||
# GH Action, runner
|
||||
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
|
||||
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
|
||||
echo "Published build to: http://build.xahau.tech/"
|
||||
echo $(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
|
||||
fi
|
||||
|
||||
@@ -160,11 +160,18 @@ 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>
|
||||
@@ -180,6 +187,21 @@ 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
|
||||
@@ -193,6 +215,7 @@ 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
|
||||
|
||||
@@ -22,6 +22,9 @@ 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
|
||||
|
||||
@@ -12,6 +12,21 @@ 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)
|
||||
|
||||
65
cmake/XahaudFormalVerification.cmake
Normal file
65
cmake/XahaudFormalVerification.cmake
Normal file
@@ -0,0 +1,65 @@
|
||||
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}")
|
||||
113
cmake/XahaudLean.cmake
Normal file
113
cmake/XahaudLean.cmake
Normal file
@@ -0,0 +1,113 @@
|
||||
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()
|
||||
29
conanfile.py
29
conanfile.py
@@ -1,4 +1,5 @@
|
||||
from conan import ConanFile
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
|
||||
import re
|
||||
|
||||
@@ -14,6 +15,7 @@ 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],
|
||||
@@ -45,6 +47,7 @@ class Xrpl(ConanFile):
|
||||
'assertions': False,
|
||||
'coverage': False,
|
||||
'fPIC': True,
|
||||
'formal_verification': False,
|
||||
'jemalloc': False,
|
||||
'rocksdb': True,
|
||||
'shared': False,
|
||||
@@ -110,6 +113,14 @@ 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)
|
||||
@@ -132,6 +143,18 @@ 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/*',
|
||||
)
|
||||
@@ -148,6 +171,7 @@ 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
|
||||
@@ -163,6 +187,11 @@ 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()
|
||||
|
||||
3213
docs/formal-proofs.md
Normal file
3213
docs/formal-proofs.md
Normal file
File diff suppressed because it is too large
Load Diff
1
formal_verification/.gitignore
vendored
Normal file
1
formal_verification/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.lake
|
||||
166
formal_verification/README.md
Normal file
166
formal_verification/README.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# 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.
|
||||
32
formal_verification/ROADMAP.md
Normal file
32
formal_verification/ROADMAP.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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.
|
||||
17
formal_verification/XahauConsensus.lean
Normal file
17
formal_verification/XahauConsensus.lean
Normal file
@@ -0,0 +1,17 @@
|
||||
-- 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
|
||||
74
formal_verification/XahauConsensus/EntropySelector.lean
Normal file
74
formal_verification/XahauConsensus/EntropySelector.lean
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
139
formal_verification/XahauConsensus/ExportGate.lean
Normal file
139
formal_verification/XahauConsensus/ExportGate.lean
Normal file
@@ -0,0 +1,139 @@
|
||||
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
|
||||
254
formal_verification/XahauConsensus/ExportQuorum.lean
Normal file
254
formal_verification/XahauConsensus/ExportQuorum.lean
Normal file
@@ -0,0 +1,254 @@
|
||||
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
|
||||
188
formal_verification/XahauConsensus/FFI.lean
Normal file
188
formal_verification/XahauConsensus/FFI.lean
Normal file
@@ -0,0 +1,188 @@
|
||||
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
|
||||
88
formal_verification/XahauConsensus/FinsetIntersection.lean
Normal file
88
formal_verification/XahauConsensus/FinsetIntersection.lean
Normal file
@@ -0,0 +1,88 @@
|
||||
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
|
||||
70
formal_verification/XahauConsensus/HonestOverlap.lean
Normal file
70
formal_verification/XahauConsensus/HonestOverlap.lean
Normal file
@@ -0,0 +1,70 @@
|
||||
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
|
||||
96
formal_verification/XahauConsensus/Intersection.lean
Normal file
96
formal_verification/XahauConsensus/Intersection.lean
Normal file
@@ -0,0 +1,96 @@
|
||||
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
|
||||
112
formal_verification/XahauConsensus/Invariants.lean
Normal file
112
formal_verification/XahauConsensus/Invariants.lean
Normal file
@@ -0,0 +1,112 @@
|
||||
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
|
||||
147
formal_verification/XahauConsensus/NunlCap.lean
Normal file
147
formal_verification/XahauConsensus/NunlCap.lean
Normal file
@@ -0,0 +1,147 @@
|
||||
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
|
||||
64
formal_verification/XahauConsensus/SelectorDeterminism.lean
Normal file
64
formal_verification/XahauConsensus/SelectorDeterminism.lean
Normal file
@@ -0,0 +1,64 @@
|
||||
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
|
||||
241
formal_verification/XahauConsensus/SidecarAlignment.lean
Normal file
241
formal_verification/XahauConsensus/SidecarAlignment.lean
Normal file
@@ -0,0 +1,241 @@
|
||||
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
|
||||
56
formal_verification/XahauConsensus/SixtyPercent.lean
Normal file
56
formal_verification/XahauConsensus/SixtyPercent.lean
Normal file
@@ -0,0 +1,56 @@
|
||||
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
|
||||
124
formal_verification/XahauConsensus/Threshold.lean
Normal file
124
formal_verification/XahauConsensus/Threshold.lean
Normal file
@@ -0,0 +1,124 @@
|
||||
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
|
||||
223
formal_verification/XahauConsensus/ThresholdFacts.lean
Normal file
223
formal_verification/XahauConsensus/ThresholdFacts.lean
Normal file
@@ -0,0 +1,223 @@
|
||||
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
|
||||
201
formal_verification/XahauConsensus/ViewUniverse.lean
Normal file
201
formal_verification/XahauConsensus/ViewUniverse.lean
Normal file
@@ -0,0 +1,201 @@
|
||||
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
|
||||
96
formal_verification/lake-manifest.json
Normal file
96
formal_verification/lake-manifest.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{"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}
|
||||
11
formal_verification/lakefile.toml
Normal file
11
formal_verification/lakefile.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
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"
|
||||
1
formal_verification/lean-toolchain
Normal file
1
formal_verification/lean-toolchain
Normal file
@@ -0,0 +1 @@
|
||||
leanprover/lean4:v4.31.0
|
||||
@@ -47,5 +47,8 @@
|
||||
#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
|
||||
|
||||
@@ -339,6 +339,41 @@ 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
|
||||
|
||||
@@ -41,6 +41,21 @@ 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");
|
||||
|
||||
|
||||
@@ -607,31 +607,37 @@ 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
|
||||
@@ -639,33 +645,35 @@ 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
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#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)
|
||||
@@ -22,6 +24,8 @@
|
||||
#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)
|
||||
@@ -80,6 +84,7 @@
|
||||
#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)
|
||||
@@ -159,6 +164,7 @@
|
||||
#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)
|
||||
@@ -288,6 +294,7 @@
|
||||
#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)
|
||||
@@ -297,6 +304,7 @@
|
||||
#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)
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#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
|
||||
@@ -74,3 +75,4 @@
|
||||
#define ttUNL_MODIFY 102
|
||||
#define ttEMIT_FAILURE 103
|
||||
#define ttUNL_REPORT 104
|
||||
#define ttCONSENSUS_ENTROPY 105
|
||||
|
||||
@@ -115,3 +115,8 @@ 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
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#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 {
|
||||
@@ -384,7 +386,10 @@ 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
|
||||
TOO_MANY_NAMESPACES = -45,
|
||||
EXPORT_FAILURE = -46,
|
||||
TOO_MANY_EXPORTED_TXN = -47,
|
||||
TOO_LITTLE_ENTROPY = -48,
|
||||
};
|
||||
|
||||
enum class ExitType : uint8_t {
|
||||
@@ -398,6 +403,7 @@ 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;
|
||||
|
||||
@@ -438,10 +444,6 @@ getImportWhitelist(Rules const& rules)
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef I32
|
||||
#undef I64
|
||||
|
||||
enum GuardRulesVersion : uint64_t {
|
||||
GuardRuleFix20250131 = 0x00000001,
|
||||
GuardRuleDepth32 = 0x00000002,
|
||||
|
||||
@@ -372,3 +372,28 @@ 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)
|
||||
|
||||
2
include/xrpl/proto/.clang-format
Normal file
2
include/xrpl/proto/.clang-format
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
DisableFormat: true
|
||||
@@ -153,7 +153,11 @@ message TMStatusChange
|
||||
message TMProposeSet
|
||||
{
|
||||
required uint32 proposeSeq = 1;
|
||||
required bytes currentTxHash = 2; // the hash of the ledger we are proposing
|
||||
// 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 nodePubKey = 3;
|
||||
required uint32 closeTime = 4;
|
||||
required bytes signature = 5; // signature of above fields
|
||||
@@ -166,6 +170,14 @@ 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
|
||||
@@ -384,4 +396,3 @@ message TMHaveTransactions
|
||||
{
|
||||
repeated bytes hashes = 1;
|
||||
}
|
||||
|
||||
|
||||
42
include/xrpl/protocol/EntropyTier.h
Normal file
42
include/xrpl/protocol/EntropyTier.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#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
|
||||
33
include/xrpl/protocol/ExportLimits.h
Normal file
33
include/xrpl/protocol/ExportLimits.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#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
|
||||
@@ -96,6 +96,15 @@ 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>
|
||||
|
||||
@@ -62,6 +62,9 @@ emittedDir() noexcept;
|
||||
Keylet
|
||||
emittedTxn(uint256 const& id) noexcept;
|
||||
|
||||
Keylet
|
||||
shadowTicket(AccountID const& account, std::uint32_t ticketSeq) noexcept;
|
||||
|
||||
Keylet
|
||||
hookDefinition(uint256 const& hash) noexcept;
|
||||
|
||||
@@ -118,6 +121,10 @@ 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
|
||||
{
|
||||
|
||||
21
include/xrpl/protocol/SidecarType.h
Normal file
21
include/xrpl/protocol/SidecarType.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#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
|
||||
@@ -68,6 +68,7 @@ enum TELcodes : TERUnderlyingType {
|
||||
telNON_LOCAL_EMITTED_TXN,
|
||||
telIMPORT_VL_KEY_NOT_RECOGNISED,
|
||||
telCAN_NOT_QUEUE_IMPORT,
|
||||
telSHADOW_TICKET_REQUIRED,
|
||||
telENV_RPC_FAILED,
|
||||
};
|
||||
|
||||
@@ -234,8 +235,10 @@ 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.
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -363,6 +366,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecARRAY_TOO_LARGE = 197,
|
||||
tecLOCKED = 198,
|
||||
tecBAD_CREDENTIALS = 199,
|
||||
tecEXPORT_EXPIRED = 200,
|
||||
tecLAST_POSSIBLE_ENTRY = 255,
|
||||
};
|
||||
|
||||
|
||||
@@ -274,6 +274,13 @@ 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
|
||||
|
||||
@@ -140,6 +140,12 @@ public:
|
||||
mHookEmissions = hookEmissions;
|
||||
}
|
||||
|
||||
void
|
||||
setExportResult(STObject const& exportResult)
|
||||
{
|
||||
mExportResult = exportResult;
|
||||
}
|
||||
|
||||
bool
|
||||
hasHookExecutions() const
|
||||
{
|
||||
@@ -152,6 +158,12 @@ public:
|
||||
return static_cast<bool>(mHookEmissions);
|
||||
}
|
||||
|
||||
bool
|
||||
hasExportResult() const
|
||||
{
|
||||
return static_cast<bool>(mExportResult);
|
||||
}
|
||||
|
||||
STAmount
|
||||
getDeliveredAmount() const
|
||||
{
|
||||
@@ -176,6 +188,7 @@ private:
|
||||
std::optional<STAmount> mDelivered;
|
||||
std::optional<STArray> mHookExecutions;
|
||||
std::optional<STArray> mHookEmissions;
|
||||
std::optional<STObject> mExportResult;
|
||||
|
||||
STArray mNodes;
|
||||
};
|
||||
|
||||
@@ -65,6 +65,8 @@ 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)
|
||||
|
||||
@@ -223,6 +223,21 @@ 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
|
||||
@@ -592,6 +607,22 @@ 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
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ 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)
|
||||
@@ -59,6 +61,8 @@ 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)
|
||||
@@ -123,6 +127,7 @@ 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)
|
||||
@@ -217,6 +222,7 @@ 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)
|
||||
@@ -382,6 +388,7 @@ 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)
|
||||
@@ -391,6 +398,7 @@ 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
|
||||
|
||||
@@ -500,6 +500,17 @@ 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},
|
||||
@@ -606,3 +617,11 @@ TRANSACTION(ttUNL_REPORT, 104, UNLReport, ({
|
||||
{sfActiveValidator, soeOPTIONAL},
|
||||
{sfImportVLKey, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
TRANSACTION(ttCONSENSUS_ENTROPY, 105, ConsensusEntropy, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfDigest, soeREQUIRED},
|
||||
{sfEntropyCount, soeREQUIRED},
|
||||
{sfEntropyTier, soeREQUIRED},
|
||||
{sfBlob, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
@@ -109,14 +109,22 @@ 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, address.at_port(0)), // Key
|
||||
std::make_tuple(m_clock.now())); // Entry
|
||||
std::make_tuple(kindInbound, key),
|
||||
std::make_tuple(m_clock.now()));
|
||||
|
||||
entry = &resultIt->second;
|
||||
entry->key = &resultIt->first;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
@@ -351,9 +352,18 @@ Logs::format(
|
||||
|
||||
if (useLocalTime)
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto local = date::make_zoned(date::current_zone(), now);
|
||||
output = date::format(fmt, local);
|
||||
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());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -74,6 +74,7 @@ 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',
|
||||
@@ -81,6 +82,7 @@ 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',
|
||||
@@ -188,6 +190,15 @@ 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
|
||||
{
|
||||
@@ -546,6 +557,14 @@ 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
|
||||
{
|
||||
|
||||
@@ -78,6 +78,7 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfHookExecutionIndex, soeREQUIRED},
|
||||
{sfHookStateChangeCount, soeREQUIRED},
|
||||
{sfHookEmitCount, soeREQUIRED},
|
||||
{sfHookExportCount, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL}});
|
||||
|
||||
add(sfHookEmission.jsonName,
|
||||
|
||||
@@ -684,7 +684,8 @@ 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 == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
|
||||
tt == ttCONSENSUS_ENTROPY;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -124,6 +124,7 @@ 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."),
|
||||
@@ -171,6 +172,7 @@ 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."),
|
||||
@@ -238,6 +240,7 @@ 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."),
|
||||
|
||||
@@ -49,6 +49,11 @@ 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)
|
||||
@@ -75,6 +80,11 @@ 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)
|
||||
@@ -245,6 +255,14 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
589
src/test/app/ConsensusEntropy_test.cpp
Normal file
589
src/test/app/ConsensusEntropy_test.cpp
Normal file
@@ -0,0 +1,589 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
326
src/test/app/ConsensusEntropy_test_hooks.h
Normal file
326
src/test/app/ConsensusEntropy_test_hooks.h
Normal file
@@ -0,0 +1,326 @@
|
||||
|
||||
// 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
|
||||
221
src/test/app/ExportResultBuilder_test.cpp
Normal file
221
src/test/app/ExportResultBuilder_test.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
299
src/test/app/ExportSigCollector_test.cpp
Normal file
299
src/test/app/ExportSigCollector_test.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
400
src/test/app/ExportSignatureHarvester_test.cpp
Normal file
400
src/test/app/ExportSignatureHarvester_test.cpp
Normal file
@@ -0,0 +1,400 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
210
src/test/app/ExportSignatureUpgrader_test.cpp
Normal file
210
src/test/app/ExportSignatureUpgrader_test.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
1540
src/test/app/Export_test.cpp
Normal file
1540
src/test/app/Export_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
483
src/test/app/Export_test_hooks.h
Normal file
483
src/test/app/Export_test_hooks.h
Normal file
@@ -0,0 +1,483 @@
|
||||
|
||||
// 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
|
||||
@@ -5766,6 +5766,16 @@ 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)
|
||||
{
|
||||
@@ -6206,6 +6216,15 @@ 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
|
||||
|
||||
456
src/test/app/XPOP_test.cpp
Normal file
456
src/test/app/XPOP_test.cpp
Normal file
@@ -0,0 +1,456 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
446
src/test/app/XportWrapperBuilder_test.cpp
Normal file
446
src/test/app/XportWrapperBuilder_test.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
3533
src/test/consensus/ConsensusExtensions_test.cpp
Normal file
3533
src/test/consensus/ConsensusExtensions_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1440
src/test/consensus/ConsensusRng_test.cpp
Normal file
1440
src/test/consensus/ConsensusRng_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,8 @@
|
||||
#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 {
|
||||
@@ -36,10 +38,18 @@ 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{};
|
||||
@@ -78,46 +88,102 @@ 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, p, true, journal_));
|
||||
checkConsensus(10, 2, 2, 0, 3s, 2s, false, 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, p, true, journal_));
|
||||
checkConsensus(10, 2, 2, 0, 3s, 4s, false, p, true, journal_));
|
||||
|
||||
// Enough time has elapsed and we all agree
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(10, 2, 2, 0, 3s, 10s, p, true, journal_));
|
||||
checkConsensus(10, 2, 2, 0, 3s, 10s, false, p, true, journal_));
|
||||
|
||||
// Enough time has elapsed and we don't yet agree
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(10, 2, 1, 0, 3s, 10s, p, true, journal_));
|
||||
checkConsensus(10, 2, 1, 0, 3s, 10s, false, 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, p, true, journal_));
|
||||
checkConsensus(10, 2, 1, 8, 3s, 10s, false, p, true, journal_));
|
||||
|
||||
// If no peers, don't agree until time has passed.
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(0, 0, 0, 0, 3s, 10s, p, true, journal_));
|
||||
checkConsensus(0, 0, 0, 0, 3s, 10s, false, p, true, journal_));
|
||||
|
||||
// Agree if no peers and enough time has passed.
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(0, 0, 0, 0, 3s, 16s, p, true, journal_));
|
||||
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_));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -125,6 +191,7 @@ public:
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
using namespace csf;
|
||||
testcase("standalone");
|
||||
|
||||
Sim s;
|
||||
PeerGroup peers = s.createGroup(1);
|
||||
@@ -149,9 +216,12 @@ 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
|
||||
@@ -179,6 +249,7 @@ public:
|
||||
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
|
||||
}
|
||||
}
|
||||
//@@end peers-agree
|
||||
}
|
||||
|
||||
void
|
||||
@@ -186,15 +257,18 @@ 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;
|
||||
@@ -229,16 +303,18 @@ 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 didn't make it
|
||||
// Tx 0 is still in the open transaction set for next round
|
||||
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)
|
||||
{
|
||||
@@ -251,6 +327,7 @@ public:
|
||||
ConsensusParms const parms{};
|
||||
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
PeerGroup slow = sim.createGroup(2);
|
||||
PeerGroup fast = sim.createGroup(4);
|
||||
PeerGroup network = fast + slow;
|
||||
@@ -351,6 +428,7 @@ 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
|
||||
@@ -379,6 +457,7 @@ public:
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
|
||||
PeerGroup groupA = sim.createGroup(2);
|
||||
PeerGroup groupB = sim.createGroup(2);
|
||||
@@ -412,11 +491,59 @@ 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.
|
||||
|
||||
@@ -426,6 +553,7 @@ 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
|
||||
@@ -446,8 +574,10 @@ 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);
|
||||
@@ -552,6 +682,7 @@ 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);
|
||||
@@ -589,6 +720,7 @@ 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
|
||||
@@ -596,6 +728,7 @@ 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
|
||||
@@ -604,9 +737,6 @@ public:
|
||||
PeerGroup fast = sim.createGroup(4);
|
||||
PeerGroup network = fast + slow;
|
||||
|
||||
for (Peer* peer : network)
|
||||
peer->consensusParms = parms;
|
||||
|
||||
// Connected trust graph
|
||||
network.trust(network);
|
||||
|
||||
@@ -692,6 +822,7 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("fork");
|
||||
|
||||
std::uint32_t numPeers = 10;
|
||||
// Vary overlap between two UNLs
|
||||
@@ -699,6 +830,7 @@ public:
|
||||
{
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
|
||||
std::uint32_t numA = (numPeers - overlap) / 2;
|
||||
std::uint32_t numB = numPeers - numA - overlap;
|
||||
@@ -729,9 +861,13 @@ public:
|
||||
}
|
||||
sim.run(1);
|
||||
|
||||
// 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
|
||||
//@@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.
|
||||
if (overlap > 0.4 * numPeers)
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
else
|
||||
@@ -740,6 +876,7 @@ public:
|
||||
// One for cliqueA, one for cliqueB and one for nodes in both
|
||||
BEAST_EXPECT(sim.branches() <= 3);
|
||||
}
|
||||
//@@end fork-threshold
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,12 +885,14 @@ 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);
|
||||
@@ -835,6 +974,7 @@ 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
|
||||
@@ -868,6 +1008,7 @@ public:
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
|
||||
// Goes A->B->D
|
||||
PeerGroup groupABD = sim.createGroup(2);
|
||||
@@ -967,6 +1108,7 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("pause for laggards");
|
||||
|
||||
// Test that validators that jump ahead of the network slow
|
||||
// down.
|
||||
@@ -990,6 +1132,7 @@ public:
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
SimDuration delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
|
||||
|
||||
PeerGroup behind = sim.createGroup(3);
|
||||
@@ -1052,6 +1195,410 @@ 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
|
||||
{
|
||||
@@ -1062,12 +1609,15 @@ 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
Reference in New Issue
Block a user