mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 23:30:54 +00:00
Compare commits
416 Commits
ximinez/ac
...
Wasm-vm-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e91a30d004 | ||
|
|
694fbb7ce3 | ||
|
|
b67a896bff | ||
|
|
9288432da5 | ||
|
|
952450255f | ||
|
|
00dd93e77d | ||
|
|
4526a97c54 | ||
|
|
ecc2f07ea3 | ||
|
|
c5d25e3055 | ||
|
|
0749043d09 | ||
|
|
95526371b0 | ||
|
|
97f32869df | ||
|
|
454c651c44 | ||
|
|
91a23fc92c | ||
|
|
7d52867e3c | ||
|
|
98cf3a0532 | ||
|
|
98abdef208 | ||
|
|
7e7014c7ca | ||
|
|
d6a66d7249 | ||
|
|
6eab6c7c28 | ||
|
|
ca560ed6b0 | ||
|
|
58e47b0101 | ||
|
|
5f6f367b23 | ||
|
|
f52eb08d8a | ||
|
|
2821cc3e8e | ||
|
|
c1008c473c | ||
|
|
50623665c7 | ||
|
|
78c8128c98 | ||
|
|
c5605b6bcd | ||
|
|
a321a5dbfb | ||
|
|
3e113db4f5 | ||
|
|
9c423d2743 | ||
|
|
e60029d5a0 | ||
|
|
13196b839e | ||
|
|
17fb37871c | ||
|
|
3caaecff07 | ||
|
|
229377abd9 | ||
|
|
e23f8e266a | ||
|
|
1d9485c926 | ||
|
|
930ba88921 | ||
|
|
c619ae0263 | ||
|
|
93db40a25e | ||
|
|
8cae773691 | ||
|
|
fe325ea96a | ||
|
|
881d040a22 | ||
|
|
ce5e724b93 | ||
|
|
1656a19fe6 | ||
|
|
3a2cf64a69 | ||
|
|
accd0cac6c | ||
|
|
9a6efde771 | ||
|
|
6abd492ebb | ||
|
|
8da36db515 | ||
|
|
152406b698 | ||
|
|
b5a90e76ee | ||
|
|
abfa572370 | ||
|
|
d8e44687bf | ||
|
|
8a3363752f | ||
|
|
226ec160bb | ||
|
|
94bccb3a5a | ||
|
|
0fb92c3194 | ||
|
|
798e889ec4 | ||
|
|
abf5511d07 | ||
|
|
a0e78b286f | ||
|
|
6bc7a9a858 | ||
|
|
ba7bf92725 | ||
|
|
8f0eff4dc4 | ||
|
|
9859e5ceda | ||
|
|
7f9ece3891 | ||
|
|
00a178fb92 | ||
|
|
cb425647a4 | ||
|
|
5bf24c4045 | ||
|
|
8d7524f03b | ||
|
|
39c8c293b3 | ||
|
|
54cfdda00b | ||
|
|
15d08770bf | ||
|
|
ffb2d37d17 | ||
|
|
6c02c45cbe | ||
|
|
eb946d23af | ||
|
|
994967521e | ||
|
|
52d2008797 | ||
|
|
4d685d4bad | ||
|
|
3f159d624b | ||
|
|
f8f8a6e059 | ||
|
|
5b2fc952d3 | ||
|
|
8ecb77dcfb | ||
|
|
f2271ecc03 | ||
|
|
8461ded0d8 | ||
|
|
0bf4739efa | ||
|
|
0df034a685 | ||
|
|
047a3f5cb8 | ||
|
|
e484a2902c | ||
|
|
d4c1359921 | ||
|
|
a88ba66fce | ||
|
|
4bd1d1ca2f | ||
|
|
0cce5a06d9 | ||
|
|
32a9cc4038 | ||
|
|
587505ef18 | ||
|
|
ef0b5dd1ac | ||
|
|
041869ff3d | ||
|
|
4eece4003d | ||
|
|
3ad6ce236e | ||
|
|
25afc04420 | ||
|
|
ed6f0f3019 | ||
|
|
b61b18a92c | ||
|
|
011235f9a7 | ||
|
|
ccb9db0bc7 | ||
|
|
24b6dad287 | ||
|
|
be7532e3f7 | ||
|
|
14e7dea7ed | ||
|
|
9e28519e56 | ||
|
|
2cc8b87c87 | ||
|
|
d8d1ec46dc | ||
|
|
e290005db5 | ||
|
|
641ecb47bd | ||
|
|
94ed8e2f49 | ||
|
|
c4ce52c810 | ||
|
|
be3d98e8ff | ||
|
|
7915202cbc | ||
|
|
7ebd92ad3e | ||
|
|
f458c0f981 | ||
|
|
abcdeab11e | ||
|
|
55911cf526 | ||
|
|
6668b7e8d0 | ||
|
|
a5cc339d7b | ||
|
|
b7059deb9f | ||
|
|
11fd30b02a | ||
|
|
1b52b6519a | ||
|
|
45e3d3f73e | ||
|
|
fcf376d3a9 | ||
|
|
ab97f4be20 | ||
|
|
1653f0c80f | ||
|
|
c50edf507c | ||
|
|
9cd531659a | ||
|
|
974b17f427 | ||
|
|
91de196312 | ||
|
|
ac71553abe | ||
|
|
9a8ec56d42 | ||
|
|
7a3bd8ace2 | ||
|
|
012f894b0a | ||
|
|
cb6dad0bc4 | ||
|
|
d54ca4cd3b | ||
|
|
5a0269aca7 | ||
|
|
0774ca32a9 | ||
|
|
8feb75011e | ||
|
|
088c5b3b03 | ||
|
|
1c4abe8c5e | ||
|
|
91a5747bf9 | ||
|
|
bc892f0bd3 | ||
|
|
8950530b04 | ||
|
|
5cbebf3c58 | ||
|
|
c634c75aee | ||
|
|
9801c23bad | ||
|
|
f6472347e9 | ||
|
|
aeee77f6ec | ||
|
|
35583fcf97 | ||
|
|
982bf36dd8 | ||
|
|
faca302adf | ||
|
|
6b3eaf091b | ||
|
|
68a765d929 | ||
|
|
033dca2f0e | ||
|
|
bf65e5fa7b | ||
|
|
90b2a68da8 | ||
|
|
06a9b1b617 | ||
|
|
7d3611df2a | ||
|
|
7877ee42a0 | ||
|
|
1dcaf4b54e | ||
|
|
5ab95748d4 | ||
|
|
4a9ee54c88 | ||
|
|
981c256933 | ||
|
|
a5af6b4e4a | ||
|
|
e1dae5f775 | ||
|
|
6c793edf72 | ||
|
|
d60955e2fc | ||
|
|
846369cbe7 | ||
|
|
d569f7db9e | ||
|
|
a12ab0496c | ||
|
|
4277c806e4 | ||
|
|
9e427ae3ba | ||
|
|
8022fc33cf | ||
|
|
465fa8ec4b | ||
|
|
b5990be624 | ||
|
|
9767d86de4 | ||
|
|
ca2d999618 | ||
|
|
5ee903befc | ||
|
|
d582ae7990 | ||
|
|
0dbe51c740 | ||
|
|
63fff4b518 | ||
|
|
d85bf722ea | ||
|
|
b664989cfb | ||
|
|
e77934302a | ||
|
|
ef7aeca6bf | ||
|
|
eec1d29b92 | ||
|
|
971ba2281e | ||
|
|
90357eeae1 | ||
|
|
1dce793acc | ||
|
|
597202a6f0 | ||
|
|
1600b3e7f3 | ||
|
|
5be406e2df | ||
|
|
bc483b2a1d | ||
|
|
175259df28 | ||
|
|
8bb8c3b24b | ||
|
|
abb2ef3bec | ||
|
|
d009ef221f | ||
|
|
5fdedd7e99 | ||
|
|
ffc55797b2 | ||
|
|
ecee732187 | ||
|
|
ce2586c039 | ||
|
|
8cc2169939 | ||
|
|
826f613ad8 | ||
|
|
1259c1d5ca | ||
|
|
d2641d85bd | ||
|
|
75f66bd9fe | ||
|
|
7cd71cb659 | ||
|
|
9917f96166 | ||
|
|
e1cc82587b | ||
|
|
2cc9439fde | ||
|
|
52af9582e2 | ||
|
|
46e88dc732 | ||
|
|
bc24f2e211 | ||
|
|
7a7c993b15 | ||
|
|
9733ca8f91 | ||
|
|
18d5e3e226 | ||
|
|
b30b4e1d65 | ||
|
|
d435893602 | ||
|
|
00b0cf50f6 | ||
|
|
7ef256499c | ||
|
|
1338062be7 | ||
|
|
4fc1778ec8 | ||
|
|
65322d9e78 | ||
|
|
c5598a4284 | ||
|
|
0deb6bcadf | ||
|
|
9b013b559b | ||
|
|
1d4a3c00b8 | ||
|
|
4b34102e8e | ||
|
|
d006433579 | ||
|
|
a7ab8ee923 | ||
|
|
e0073a4402 | ||
|
|
2930ef217f | ||
|
|
9dbb301699 | ||
|
|
531e8b6ebd | ||
|
|
90397e1a52 | ||
|
|
888ca2e6d9 | ||
|
|
b6514b680f | ||
|
|
913e4b919e | ||
|
|
196e6a1b27 | ||
|
|
27468ddbcf | ||
|
|
bce5d91e45 | ||
|
|
654338fa66 | ||
|
|
9c25d18851 | ||
|
|
3a825a41e1 | ||
|
|
a9ebf786c6 | ||
|
|
5afe8cc321 | ||
|
|
bc5ec3c962 | ||
|
|
1775251e90 | ||
|
|
61bcb7621f | ||
|
|
a3f71b1774 | ||
|
|
4df7d1a4bb | ||
|
|
125df7a425 | ||
|
|
b08bcf5d21 | ||
|
|
dc413aef0c | ||
|
|
77dfd56ace | ||
|
|
953b9a3500 | ||
|
|
1d9ec84350 | ||
|
|
0392846a17 | ||
|
|
1b4a564369 | ||
|
|
fd524c4be9 | ||
|
|
495dda7f58 | ||
|
|
9c3c0280b1 | ||
|
|
f73d8a6cf2 | ||
|
|
6728ab52b7 | ||
|
|
77673663ca | ||
|
|
c1381f8ddd | ||
|
|
bd16f7989d | ||
|
|
65f9cf80c0 | ||
|
|
de55a5ebfc | ||
|
|
2ec4a1114e | ||
|
|
ba03a8a9d2 | ||
|
|
7c8279ec83 | ||
|
|
0418ffb26a | ||
|
|
b2627039f6 | ||
|
|
8f97ec3bde | ||
|
|
e85e7b1b1a | ||
|
|
803a344c65 | ||
|
|
4eb34f381a | ||
|
|
72fffb6e51 | ||
|
|
f7ee580f01 | ||
|
|
122d405750 | ||
|
|
c1c1b4ea67 | ||
|
|
977caea0a5 | ||
|
|
d7ed6d6512 | ||
|
|
f1f2e2629f | ||
|
|
917c610f96 | ||
|
|
317e533d81 | ||
|
|
4160677878 | ||
|
|
df98db1452 | ||
|
|
673476ef1b | ||
|
|
8bc6f9cd70 | ||
|
|
ba5debfecd | ||
|
|
f4a27c9b6d | ||
|
|
fd1cb318e3 | ||
|
|
8c3544a58c | ||
|
|
ed5139d4e3 | ||
|
|
42494dd4cf | ||
|
|
ce84cc8b44 | ||
|
|
9a9a7aab01 | ||
|
|
209a1a6ffa | ||
|
|
fc35a9f9c8 | ||
|
|
c5e50aa221 | ||
|
|
074b1f00d5 | ||
|
|
7a9d245950 | ||
|
|
1809fe07f2 | ||
|
|
409c67494a | ||
|
|
c626b6403a | ||
|
|
81cbc91927 | ||
|
|
1c812a6c4d | ||
|
|
0724927799 | ||
|
|
d83ec96848 | ||
|
|
375dd50b35 | ||
|
|
419d53ec4c | ||
|
|
d4d70d5675 | ||
|
|
6ab15f8377 | ||
|
|
91f3d51f3d | ||
|
|
9ed60b45f8 | ||
|
|
d5c53dcfd2 | ||
|
|
e94321fb41 | ||
|
|
bbc28b3b1c | ||
|
|
843e981c8a | ||
|
|
5aab274b7a | ||
|
|
2c30e41191 | ||
|
|
8ea5106b0b | ||
|
|
f57f67a8ae | ||
|
|
a98269f049 | ||
|
|
b66bc47ca9 | ||
|
|
0e9c7458bb | ||
|
|
1d89940653 | ||
|
|
1a1a6806ec | ||
|
|
1977df9c2e | ||
|
|
6c95548df5 | ||
|
|
69ab39d658 | ||
|
|
b9eb66eecc | ||
|
|
881087dd3d | ||
|
|
90e0bbd0fc | ||
|
|
b57df290de | ||
|
|
8a403f1241 | ||
|
|
6d2640871d | ||
|
|
c145598ff9 | ||
|
|
50e5608d86 | ||
|
|
7a7b96107c | ||
|
|
500bb68831 | ||
|
|
53eb0f60bc | ||
|
|
41205ae928 | ||
|
|
c33b0ae463 | ||
|
|
16087c9680 | ||
|
|
56bc6d58f6 | ||
|
|
ef5d335e09 | ||
|
|
25c3060fef | ||
|
|
ce9f0b38a4 | ||
|
|
35f7cbf772 | ||
|
|
0db564d261 | ||
|
|
427b7ea104 | ||
|
|
7bf6878b4b | ||
|
|
0bc1a115ff | ||
|
|
334bcfa5ef | ||
|
|
106dea4559 | ||
|
|
3ffdcf8114 | ||
|
|
4021a7eb28 | ||
|
|
0690fda0f1 | ||
|
|
d0cc48c6d3 | ||
|
|
d66e3c949e | ||
|
|
0c65a386b5 | ||
|
|
29f5430881 | ||
|
|
101f285bcd | ||
|
|
286dc6322b | ||
|
|
c9346cd40d | ||
|
|
1c5683ec78 | ||
|
|
9bee155d59 | ||
|
|
f34b05f4de | ||
|
|
97ce25f4ce | ||
|
|
9e14c14a26 | ||
|
|
c507880d8f | ||
|
|
3f8328bbf8 | ||
|
|
c10a5f9ef6 | ||
|
|
3c141de695 | ||
|
|
da2b9455f2 | ||
|
|
cb622488c0 | ||
|
|
32f971fec6 | ||
|
|
8dea76baa4 | ||
|
|
299fbe04c4 | ||
|
|
57fc1df7d7 | ||
|
|
eaba76f9e6 | ||
|
|
cb702cc238 | ||
|
|
b69b4a0a4a | ||
|
|
50d6072a73 | ||
|
|
d24cd50e61 | ||
|
|
9f5875158c | ||
|
|
c3dc33c861 | ||
|
|
6be8f2124c | ||
|
|
edfed06001 | ||
|
|
1c646dba91 | ||
|
|
6781068058 | ||
|
|
cfe57c1dfe | ||
|
|
c34d09a971 | ||
|
|
ebd90c4742 | ||
|
|
ba52d34828 | ||
|
|
1b6312afb3 | ||
|
|
bf32dc2e72 | ||
|
|
a15d65f7a2 | ||
|
|
2de8488855 | ||
|
|
129aa4bfaa | ||
|
|
b1d70db63b | ||
|
|
f03c3aafe4 | ||
|
|
51a9f106d1 | ||
|
|
bfc048e3fe | ||
|
|
83418644f7 | ||
|
|
dbc9dd5bfc | ||
|
|
45ab15d4b5 |
24
.codecov.yml
24
.codecov.yml
@@ -1,10 +1,32 @@
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
# The C++ and Rust uploads land minutes apart; without this gate Codecov
|
||||
# publishes a near-zero total from whichever one arrives first.
|
||||
notify:
|
||||
after_n_builds: 2
|
||||
wait_for_ci: true
|
||||
|
||||
comment:
|
||||
behavior: default
|
||||
layout: reach,diff,flags,tree,reach
|
||||
show_carryforward_flags: false
|
||||
show_carryforward_flags: true
|
||||
after_n_builds: 2
|
||||
|
||||
# C++ and Rust coverage upload from independent workflows under the `cpp` and
|
||||
# `rust` flags; carryforward keeps one language's total when only the other reran.
|
||||
flag_management:
|
||||
default_rules:
|
||||
carryforward: true
|
||||
individual_flags:
|
||||
- name: cpp
|
||||
carryforward: true
|
||||
paths:
|
||||
- include/
|
||||
- src/
|
||||
- name: rust
|
||||
carryforward: true
|
||||
paths:
|
||||
- crates/
|
||||
|
||||
coverage:
|
||||
range: "70..85"
|
||||
|
||||
@@ -67,6 +67,7 @@ words:
|
||||
- Btrfs
|
||||
- Buildx
|
||||
- canonicality
|
||||
- cdylib
|
||||
- canonicalised
|
||||
- changespq
|
||||
- checkme
|
||||
@@ -103,6 +104,7 @@ words:
|
||||
- deleteme
|
||||
- demultiplexer
|
||||
- deserializaton
|
||||
- desugars
|
||||
- desync
|
||||
- desynced
|
||||
- determ
|
||||
@@ -128,15 +130,19 @@ words:
|
||||
- gcov
|
||||
- gcovr
|
||||
- ghead
|
||||
- gmock
|
||||
- Gnutella
|
||||
- godexsoft
|
||||
- gpgcheck
|
||||
- gpgkey
|
||||
- Hinnant
|
||||
- hotwallet
|
||||
- hwaddress
|
||||
- hwrap
|
||||
- ifndef
|
||||
- impls
|
||||
- inequation
|
||||
- initialiser
|
||||
- insuf
|
||||
- insuff
|
||||
- invasively
|
||||
@@ -165,6 +171,7 @@ words:
|
||||
- llection
|
||||
- LOCALGOOD
|
||||
- logwstream
|
||||
- Lombrozo
|
||||
- lseq
|
||||
- lsmf
|
||||
- ltype
|
||||
@@ -201,6 +208,7 @@ words:
|
||||
- nftokens
|
||||
- nftpage
|
||||
- nikb
|
||||
- Nikolaos
|
||||
- nixfmt
|
||||
- nixos
|
||||
- nixpkgs
|
||||
@@ -242,6 +250,7 @@ words:
|
||||
- pyparsing
|
||||
- qalloc
|
||||
- qbsprofile
|
||||
- qself
|
||||
- queuable
|
||||
- Raphson
|
||||
- rcflags
|
||||
@@ -294,6 +303,7 @@ words:
|
||||
- STATSDCOLLECTOR
|
||||
- stissue
|
||||
- stnum
|
||||
- stnumber
|
||||
- stobj
|
||||
- stobject
|
||||
- stpath
|
||||
@@ -305,6 +315,7 @@ words:
|
||||
- summands
|
||||
- superpeer
|
||||
- superpeers
|
||||
- Swatinem
|
||||
- takergets
|
||||
- takerpays
|
||||
- ters
|
||||
@@ -333,6 +344,7 @@ words:
|
||||
- unflatten
|
||||
- unfund
|
||||
- unimpair
|
||||
- unmetered
|
||||
- unroutable
|
||||
- unscalable
|
||||
- unserviced
|
||||
@@ -353,6 +365,7 @@ words:
|
||||
- vfalco
|
||||
- vinnie
|
||||
- wasmi
|
||||
- Werror
|
||||
- wextra
|
||||
- wptr
|
||||
- writeme
|
||||
@@ -360,6 +373,7 @@ words:
|
||||
- wthread
|
||||
- xbridge
|
||||
- xchain
|
||||
- xfloat
|
||||
- ximinez
|
||||
- XMACRO
|
||||
- xrpkuwait
|
||||
|
||||
17
.github/dependabot.yml
vendored
17
.github/dependabot.yml
vendored
@@ -19,3 +19,20 @@ updates:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: cargo
|
||||
directory: /crates
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
time: "04:00"
|
||||
timezone: Etc/GMT
|
||||
commit-message:
|
||||
prefix: "ci: [DEPENDABOT] "
|
||||
target-branch: develop
|
||||
open-pull-requests-limit: 10
|
||||
# Bundle all Rust dependency bumps into a single PR per run to reduce noise.
|
||||
groups:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
19
.github/scripts/strategy-matrix/generate.py
vendored
19
.github/scripts/strategy-matrix/generate.py
vendored
@@ -33,6 +33,10 @@ def get_cmake_args(build_type: str, extra_args: str) -> str:
|
||||
# Every config must declare 'minimal'. Minimal configs form the reduced matrix
|
||||
# built for pull requests by default; the full matrix adds the rest. Packaging
|
||||
# configs declare it too, but packaging is gated in the workflow, not by it.
|
||||
#
|
||||
# Configs may also opt into 'benchmark' to smoke-run the benchmarks. Note that
|
||||
# the flag applies to every entry a config expands into, so only set it on
|
||||
# configs that expand to a single combination.
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -43,6 +47,7 @@ class LinuxConfig:
|
||||
build_type: list[str]
|
||||
arch: list[str]
|
||||
minimal: bool
|
||||
benchmark: bool = False # if true, smoke-run the benchmarks after testing
|
||||
sanitizers: list[str] = dataclasses.field(default_factory=list)
|
||||
suffix: str = ""
|
||||
extra_cmake_args: str = ""
|
||||
@@ -81,6 +86,7 @@ class PlatformConfig:
|
||||
build_type: list[str]
|
||||
minimal: bool
|
||||
build_only: bool = False # if true, skip tests (e.g. macos/Windows Debug)
|
||||
benchmark: bool = False # if true, smoke-run the benchmarks after testing
|
||||
extra_cmake_args: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -125,6 +131,7 @@ class MatrixEntry:
|
||||
cmake_args: str
|
||||
cmake_target: str
|
||||
build_only: bool
|
||||
benchmark: bool
|
||||
build_type: str
|
||||
architecture: Architecture
|
||||
sanitizers: str
|
||||
@@ -136,7 +143,8 @@ class MatrixEntry:
|
||||
class PackagingEntry:
|
||||
"""One entry in the generated packaging strategy matrix."""
|
||||
|
||||
artifact_name: str
|
||||
xrpld_artifact_name: str
|
||||
validator_keys_artifact_name: str
|
||||
image: str
|
||||
distro: str # e.g. "debian" or "rhel"; drives package-format-specific steps
|
||||
|
||||
@@ -193,6 +201,7 @@ def expand_linux_matrix(linux: LinuxFile, minimal: bool) -> list[MatrixEntry]:
|
||||
cmake_args=get_cmake_args(build_type, cfg.extra_cmake_args),
|
||||
cmake_target="all",
|
||||
build_only=False,
|
||||
benchmark=cfg.benchmark,
|
||||
build_type=build_type,
|
||||
architecture=arch_info,
|
||||
sanitizers=sanitizer,
|
||||
@@ -210,14 +219,19 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]:
|
||||
the nix-based build images, because deb/rpm tooling (debhelper, rpm-build)
|
||||
is taken from the distro's archive rather than from nixpkgs. Each config
|
||||
entry carries its own 'image'.
|
||||
|
||||
The artifact names must match what the build job uploads: one artifact per
|
||||
binary, each named after the build config.
|
||||
"""
|
||||
entries = []
|
||||
for distro, configs in linux.package_configs.items():
|
||||
for cfg in configs:
|
||||
for compiler, build_type in itertools.product(cfg.compiler, cfg.build_type):
|
||||
config_name = f"{distro}-{compiler}-{build_type.lower()}-amd64"
|
||||
entries.append(
|
||||
PackagingEntry(
|
||||
artifact_name=f"xrpld-{distro}-{compiler}-{build_type.lower()}-amd64",
|
||||
xrpld_artifact_name=f"xrpld-{config_name}",
|
||||
validator_keys_artifact_name=f"validator-keys-{config_name}",
|
||||
image=cfg.image,
|
||||
distro=distro,
|
||||
)
|
||||
@@ -245,6 +259,7 @@ def expand_platform_matrix(pf: PlatformFile, minimal: bool) -> list[MatrixEntry]
|
||||
cmake_args=get_cmake_args(build_type, cfg.extra_cmake_args),
|
||||
cmake_target="install" if is_windows else "all",
|
||||
build_only=cfg.build_only,
|
||||
benchmark=cfg.benchmark,
|
||||
build_type=build_type,
|
||||
architecture=Architecture(platform=pf.platform, runner=pf.runner),
|
||||
sanitizers="",
|
||||
|
||||
9
.github/scripts/strategy-matrix/linux.json
vendored
9
.github/scripts/strategy-matrix/linux.json
vendored
@@ -14,7 +14,8 @@
|
||||
"compiler": ["clang"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"minimal": true
|
||||
"minimal": true,
|
||||
"benchmark": true
|
||||
},
|
||||
|
||||
{
|
||||
@@ -69,7 +70,8 @@
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"minimal": false
|
||||
"minimal": false,
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -78,7 +80,8 @@
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"minimal": false
|
||||
"minimal": false,
|
||||
"extra_cmake_args": "-Dvalidator_keys=ON"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
84
.github/workflows/cargo-audit.yml
vendored
Normal file
84
.github/workflows/cargo-audit.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
# This workflow audits the Rust dependencies in crates/ for known security
|
||||
# advisories using cargo-audit. It runs on a weekly schedule, whenever the
|
||||
# dependency graph changes (Cargo.lock / Cargo.toml), and on demand. On a
|
||||
# scheduled run, a failure opens a tracking issue (matching the clang-tidy
|
||||
# workflow's behavior); on push/PR it simply fails the check.
|
||||
name: Cargo audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 06:32 UTC every Monday.
|
||||
- cron: "32 6 * * 1"
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release*"
|
||||
paths:
|
||||
- "crates/**/Cargo.toml"
|
||||
- "crates/Cargo.lock"
|
||||
- ".github/workflows/cargo-audit.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "crates/**/Cargo.toml"
|
||||
- "crates/Cargo.lock"
|
||||
- ".github/workflows/cargo-audit.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: crates
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-debian:sha-2e25435
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to open an issue on scheduled failures.
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Run cargo audit
|
||||
id: audit
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -o pipefail
|
||||
cargo audit | tee /tmp/cargo-audit.txt
|
||||
|
||||
- name: Prepare issue body
|
||||
if: ${{ steps.audit.outcome != 'success' && github.event_name == 'schedule' }}
|
||||
run: |
|
||||
{
|
||||
echo "## \`cargo audit\` found advisories"
|
||||
echo
|
||||
echo '```'
|
||||
cat /tmp/cargo-audit.txt
|
||||
echo '```'
|
||||
echo
|
||||
echo "---"
|
||||
echo "*This issue was automatically created by the cargo-audit workflow.*"
|
||||
} >/tmp/cargo-audit-issue.md
|
||||
|
||||
- name: Create issue
|
||||
if: ${{ steps.audit.outcome != 'success' && github.event_name == 'schedule' }}
|
||||
uses: XRPLF/actions/create-issue@2b8bc36af85b88bca0dd7bfac2e2dc05f94ad712
|
||||
with:
|
||||
title: "cargo audit found vulnerabilities"
|
||||
body_file: /tmp/cargo-audit-issue.md
|
||||
labels: "Bug,Security"
|
||||
|
||||
- name: Fail if advisories were found
|
||||
if: ${{ steps.audit.outcome != 'success' }}
|
||||
run: |
|
||||
echo "cargo audit found advisories!"
|
||||
exit 1
|
||||
17
.github/workflows/on-pr.yml
vendored
17
.github/workflows/on-pr.yml
vendored
@@ -82,8 +82,10 @@ jobs:
|
||||
.github/scripts/strategy-matrix/**
|
||||
.github/workflows/reusable-build-test-config.yml
|
||||
.github/workflows/reusable-build-test.yml
|
||||
.github/workflows/reusable-check-autogen.yml
|
||||
.github/workflows/reusable-clang-tidy.yml
|
||||
.github/workflows/reusable-package.yml
|
||||
.github/workflows/reusable-rust.yml
|
||||
.github/workflows/reusable-strategy-matrix.yml
|
||||
.github/workflows/reusable-test.yml
|
||||
.github/workflows/reusable-upload-recipe.yml
|
||||
@@ -94,6 +96,7 @@ jobs:
|
||||
cfg/**
|
||||
cmake/**
|
||||
conan/**
|
||||
crates/**
|
||||
external/**
|
||||
include/**
|
||||
src/**
|
||||
@@ -126,6 +129,11 @@ jobs:
|
||||
outputs:
|
||||
go: ${{ steps.go.outputs.go == 'true' }}
|
||||
|
||||
check-autogen:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-check-autogen.yml
|
||||
|
||||
check-levelization:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
@@ -162,6 +170,13 @@ jobs:
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
rust:
|
||||
needs: should-run
|
||||
if: ${{ needs.should-run.outputs.go == 'true' }}
|
||||
uses: ./.github/workflows/reusable-rust.yml
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
package:
|
||||
needs: [should-run, build-test]
|
||||
# Packaging consumes the debian/rhel release binaries, which are only built
|
||||
@@ -200,10 +215,12 @@ jobs:
|
||||
passed:
|
||||
if: failure() || cancelled()
|
||||
needs:
|
||||
- check-autogen
|
||||
- check-levelization
|
||||
- check-rename
|
||||
- clang-tidy
|
||||
- build-test
|
||||
- rust
|
||||
- package
|
||||
- upload-recipe
|
||||
- notify-clio
|
||||
|
||||
11
.github/workflows/on-trigger.yml
vendored
11
.github/workflows/on-trigger.yml
vendored
@@ -20,8 +20,10 @@ on:
|
||||
- ".github/scripts/strategy-matrix/**"
|
||||
- ".github/workflows/reusable-build-test-config.yml"
|
||||
- ".github/workflows/reusable-build-test.yml"
|
||||
- ".github/workflows/reusable-check-autogen.yml"
|
||||
- ".github/workflows/reusable-clang-tidy.yml"
|
||||
- ".github/workflows/reusable-package.yml"
|
||||
- ".github/workflows/reusable-rust.yml"
|
||||
- ".github/workflows/reusable-strategy-matrix.yml"
|
||||
- ".github/workflows/reusable-test.yml"
|
||||
- ".github/workflows/reusable-upload-recipe.yml"
|
||||
@@ -32,6 +34,7 @@ on:
|
||||
- "cfg/**"
|
||||
- "cmake/**"
|
||||
- "conan/**"
|
||||
- "crates/**"
|
||||
- "external/**"
|
||||
- "include/**"
|
||||
- "src/**"
|
||||
@@ -67,6 +70,9 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
check-autogen:
|
||||
uses: ./.github/workflows/reusable-check-autogen.yml
|
||||
|
||||
clang-tidy:
|
||||
uses: ./.github/workflows/reusable-clang-tidy.yml
|
||||
permissions:
|
||||
@@ -92,6 +98,11 @@ jobs:
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
rust:
|
||||
uses: ./.github/workflows/reusable-rust.yml
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
upload-recipe:
|
||||
needs: build-test
|
||||
# Only run when pushing to the develop branch.
|
||||
|
||||
81
.github/workflows/reusable-build-test-config.yml
vendored
81
.github/workflows/reusable-build-test-config.yml
vendored
@@ -3,6 +3,12 @@ name: Build and test configuration
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
benchmark:
|
||||
description: "Whether to smoke-run the benchmarks after testing."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
build_only:
|
||||
description: 'Whether to only build or to build and test the code ("true", "false").'
|
||||
required: true
|
||||
@@ -100,9 +106,10 @@ jobs:
|
||||
# header files are copied into separate directories by CMake, which will
|
||||
# otherwise result in cache misses.
|
||||
CCACHE_SLOPPINESS: include_file_ctime,include_file_mtime
|
||||
# Determine if coverage and voidstar should be enabled.
|
||||
# Determine if coverage, voidstar and validator-keys should be enabled.
|
||||
COVERAGE_ENABLED: ${{ contains(inputs.cmake_args, '-Dcoverage=ON') }}
|
||||
VOIDSTAR_ENABLED: ${{ contains(inputs.cmake_args, '-Dvoidstar=ON') }}
|
||||
VALIDATOR_KEYS_ENABLED: ${{ contains(inputs.cmake_args, '-Dvalidator_keys=ON') }}
|
||||
SANITIZERS_ENABLED: ${{ inputs.sanitizers != '' }}
|
||||
steps:
|
||||
- name: Cleanup workspace (macOS and Windows)
|
||||
@@ -170,9 +177,9 @@ jobs:
|
||||
..
|
||||
|
||||
# Export the sanitizer options before any instrumented binary runs. The
|
||||
# protocol code-gen and build steps below invoke instrumented dependency
|
||||
# tools (protoc, grpc), so setting UBSAN_OPTIONS here lets the UBSan
|
||||
# suppression list silence their diagnostics too, not just at test time.
|
||||
# build step below invokes instrumented dependency tools (protoc, grpc),
|
||||
# so setting UBSAN_OPTIONS here lets the UBSan suppression list silence
|
||||
# their diagnostics too, not just at test time.
|
||||
# GITHUB_WORKSPACE (not the github.workspace context) is used so the path
|
||||
# resolves correctly inside the container job.
|
||||
- name: Set sanitizer options
|
||||
@@ -190,32 +197,6 @@ jobs:
|
||||
echo "UBSAN_OPTIONS=include=${SUPP}/runtime-ubsan-options.txt:suppressions=${SUPP}/ubsan.supp" >>${GITHUB_ENV}
|
||||
echo "LSAN_OPTIONS=include=${SUPP}/runtime-lsan-options.txt:suppressions=${SUPP}/lsan.supp" >>${GITHUB_ENV}
|
||||
|
||||
- name: Check protocol autogen files are up-to-date
|
||||
working-directory: ${{ env.BUILD_DIR }}
|
||||
env:
|
||||
MESSAGE: |
|
||||
|
||||
The generated protocol wrapper classes are out of date.
|
||||
|
||||
This typically happens when the macro files or generator scripts
|
||||
have changed but the generated files were not regenerated.
|
||||
|
||||
To fix this:
|
||||
1. Run: cmake --build . --target setup_code_gen
|
||||
2. Run: cmake --build . --target code_gen
|
||||
3. Commit and push the regenerated files
|
||||
run: |
|
||||
set -e
|
||||
cmake --build . --target setup_code_gen
|
||||
cmake --build . --target code_gen
|
||||
DIFF=$(git -C .. status --porcelain -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen)
|
||||
if [ -n "${DIFF}" ]; then
|
||||
echo "::error::Generated protocol files are out of date"
|
||||
git -C .. diff -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen
|
||||
echo "${MESSAGE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build the binary
|
||||
working-directory: ${{ env.BUILD_DIR }}
|
||||
env:
|
||||
@@ -249,6 +230,22 @@ jobs:
|
||||
retention-days: 3
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Run the validator-keys tests
|
||||
if: ${{ env.VALIDATOR_KEYS_ENABLED == 'true' }}
|
||||
working-directory: ${{ env.BUILD_DIR }}
|
||||
run: ./validator-keys --unittest
|
||||
|
||||
- name: Upload the validator-keys binary
|
||||
if: ${{ github.event.repository.visibility == 'public' && env.VALIDATOR_KEYS_ENABLED == 'true' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: validator-keys-${{ inputs.config_name }}
|
||||
path: |
|
||||
${{ env.BUILD_DIR }}/validator-keys
|
||||
${{ env.BUILD_DIR }}/validator-keys-LICENSE
|
||||
retention-days: 3
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload the test binary (Linux)
|
||||
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
@@ -279,7 +276,7 @@ jobs:
|
||||
working-directory: ${{ env.BUILD_DIR }}
|
||||
run: |
|
||||
ldd ./xrpld
|
||||
if [ "$(ldd ./xrpld | grep -E '(libstdc\+\+|libgcc)' | wc -l)" -eq 0 ]; then
|
||||
if [ "$(ldd ./xrpld | grep -E '(libstdc\+\+)' | wc -l)" -eq 0 ]; then
|
||||
echo 'The binary is statically linked.'
|
||||
else
|
||||
echo 'The binary is dynamically linked.'
|
||||
@@ -292,6 +289,14 @@ jobs:
|
||||
run: |
|
||||
./xrpld --version | grep libvoidstar
|
||||
|
||||
- name: Run Rust tests
|
||||
if: ${{ !inputs.build_only }}
|
||||
working-directory: crates
|
||||
# `xrpl-wasm-vm-ffi` is left out on Windows: its tests link as an executable, and
|
||||
# MSVC - unlike the Unix linkers - will not dead-strip the never-called cxx wrappers
|
||||
# whose C++ shims only the CMake build defines. The other runners cover these tests.
|
||||
run: cargo nextest run --workspace --all-features --locked --no-tests=warn ${{ runner.os == 'Windows' && '--exclude xrpl-wasm-vm-ffi' || '' }}
|
||||
|
||||
- name: Run the separate tests
|
||||
if: ${{ !inputs.build_only }}
|
||||
working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
|
||||
@@ -328,11 +333,14 @@ jobs:
|
||||
|
||||
# Smoke-run every benchmark module with a single repetition to confirm the
|
||||
# benchmarks still build and execute. This is a correctness check, not a
|
||||
# performance measurement, so it is skipped for instrumented builds
|
||||
# (sanitizers/coverage/voidstar), where it would be slow and meaningless,
|
||||
# and on Windows, where the `install` target does not build them.
|
||||
# performance measurement, so there is nothing to gain from repeating it
|
||||
# across configurations: it is opted into by a single config in the
|
||||
# strategy matrix (see the 'benchmark' flag in the JSON files), which
|
||||
# keeps it off instrumented builds (sanitizers/coverage/voidstar), where
|
||||
# it would be slow and meaningless, off Debug builds, where it is much
|
||||
# slower, and off Windows, where the `install` target does not build them.
|
||||
- name: Run the benchmarks
|
||||
if: ${{ !inputs.build_only && runner.os != 'Windows' && env.SANITIZERS_ENABLED == 'false' && env.COVERAGE_ENABLED != 'true' && env.VOIDSTAR_ENABLED != 'true' }}
|
||||
if: ${{ inputs.benchmark }}
|
||||
working-directory: ${{ env.BUILD_DIR }}
|
||||
run: |
|
||||
rc=0
|
||||
@@ -387,13 +395,14 @@ jobs:
|
||||
--target coverage
|
||||
|
||||
- name: Upload coverage report
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }}
|
||||
if: ${{ github.repository_owner == 'XRPLF' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
disable_search: true
|
||||
disable_telem: true
|
||||
fail_ci_if_error: true
|
||||
files: ${{ env.BUILD_DIR }}/coverage.xml
|
||||
flags: cpp
|
||||
plugins: noop
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
|
||||
1
.github/workflows/reusable-build-test.yml
vendored
1
.github/workflows/reusable-build-test.yml
vendored
@@ -40,6 +40,7 @@ jobs:
|
||||
fail-fast: ${{ github.event_name == 'merge_group' }}
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
with:
|
||||
benchmark: ${{ matrix.benchmark }}
|
||||
build_only: ${{ matrix.build_only }}
|
||||
build_type: ${{ matrix.build_type }}
|
||||
ccache_enabled: ${{ inputs.ccache_enabled }}
|
||||
|
||||
76
.github/workflows/reusable-check-autogen.yml
vendored
Normal file
76
.github/workflows/reusable-check-autogen.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# This workflow checks that the generated protocol wrapper classes are
|
||||
# up-to-date with the macro files and generator scripts they are produced from,
|
||||
# see more info in include/xrpl/protocol_autogen/README.md.
|
||||
name: Check autogen
|
||||
|
||||
# This workflow can only be triggered by other workflows.
|
||||
on: workflow_call
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-autogen
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
BUILD_DIR: build/codegen
|
||||
|
||||
jobs:
|
||||
autogen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
# Code generation is pure Python, so the standalone project below offers
|
||||
# the same targets as the main build without needing its dependencies or
|
||||
# a compiler, which keeps this job down to a few seconds.
|
||||
- name: Configure CMake
|
||||
run: cmake -S cmake/codegen -B "${BUILD_DIR}"
|
||||
|
||||
- name: Install code generation dependencies
|
||||
run: cmake --build "${BUILD_DIR}" --target setup_code_gen
|
||||
|
||||
- name: Generate code
|
||||
run: cmake --build "${BUILD_DIR}" --target code_gen
|
||||
|
||||
- name: Check for differences
|
||||
env:
|
||||
MESSAGE: |
|
||||
|
||||
The generated protocol wrapper classes are out of date.
|
||||
|
||||
This typically happens when the macro files or generator scripts
|
||||
have changed but the generated files were not regenerated.
|
||||
|
||||
Run the following from the repository root, then commit and push
|
||||
the regenerated files. This needs neither the dependencies nor a
|
||||
compiler. See include/xrpl/protocol_autogen/README.md for more info.
|
||||
|
||||
cmake -S cmake/codegen -B build/codegen
|
||||
cmake --build build/codegen --target setup_code_gen
|
||||
cmake --build build/codegen --target code_gen
|
||||
|
||||
In an already configured build directory, the 'setup_code_gen' and
|
||||
'code_gen' targets do the same thing.
|
||||
run: |
|
||||
# Record untracked files in the index without staging their contents,
|
||||
# so that classes generated for a newly added transaction or ledger
|
||||
# entry type show up in the diff below rather than silently as an
|
||||
# empty one.
|
||||
git add --intent-to-add .
|
||||
DIFF=$(git status --porcelain)
|
||||
if [ -n "${DIFF}" ]; then
|
||||
# Print the differences to give the contributor a hint about what to
|
||||
# expect when running code generation on their own machine.
|
||||
git diff
|
||||
echo "${MESSAGE}"
|
||||
exit 1
|
||||
fi
|
||||
6
.github/workflows/reusable-clang-tidy.yml
vendored
6
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -88,6 +88,12 @@ jobs:
|
||||
run: |
|
||||
ninja -j ${{ steps.nproc.outputs.nproc }} xrpl.libpb
|
||||
|
||||
# clang-tidy needs cxxbridge headers generated from Rust crates
|
||||
- name: Build xrpl_crates
|
||||
working-directory: ${{ env.BUILD_DIR }}
|
||||
run: |
|
||||
ninja -j ${{ steps.nproc.outputs.nproc }} xrpl_crates
|
||||
|
||||
- name: Run clang tidy
|
||||
id: run_clang_tidy
|
||||
continue-on-error: true
|
||||
|
||||
26
.github/workflows/reusable-package.yml
vendored
26
.github/workflows/reusable-package.yml
vendored
@@ -1,7 +1,7 @@
|
||||
# Build Linux packages (DEB and RPM) from pre-built binary artifacts.
|
||||
# Discovers which configurations to package from linux.json (configs in
|
||||
# "package_configs") and fans out one job per distro. Only linux/amd64 is
|
||||
# supported; the runner is hardcoded in the job below.
|
||||
# Build Linux packages (DEB and RPM) from pre-built binary artifacts (xrpld and
|
||||
# validator-keys). Discovers which configurations to package from linux.json
|
||||
# (configs in "package_configs") and fans out one job per distro. Only
|
||||
# linux/amd64 is supported; the runner is hardcoded in the job below.
|
||||
name: Package
|
||||
|
||||
on:
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
name: "${{ matrix.artifact_name }}"
|
||||
name: "${{ matrix.xrpld_artifact_name }}"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
@@ -56,14 +56,20 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download pre-built binary
|
||||
- name: Download pre-built xrpld binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
name: ${{ matrix.xrpld_artifact_name }}
|
||||
path: ${{ env.BUILD_DIR }}
|
||||
|
||||
- name: Make binary executable
|
||||
run: chmod +x "${BUILD_DIR}/xrpld"
|
||||
- name: Download pre-built validator-keys binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ matrix.validator_keys_artifact_name }}
|
||||
path: ${{ env.BUILD_DIR }}
|
||||
|
||||
- name: Make binaries executable
|
||||
run: chmod +x "${BUILD_DIR}/xrpld" "${BUILD_DIR}/validator-keys"
|
||||
|
||||
- name: Build package
|
||||
env:
|
||||
@@ -73,7 +79,7 @@ jobs:
|
||||
- name: Upload package artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}-pkg
|
||||
name: ${{ matrix.xrpld_artifact_name }}-pkg
|
||||
path: |
|
||||
${{ env.BUILD_DIR }}/debbuild/*.deb
|
||||
${{ env.BUILD_DIR }}/debbuild/*.ddeb
|
||||
|
||||
86
.github/workflows/reusable-rust.yml
vendored
Normal file
86
.github/workflows/reusable-rust.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
# Clippy, coverage and documentation for the Rust crates in crates/. Each runs
|
||||
# as an independent job on a GitHub-hosted runner, but inside the same container
|
||||
# image used to build the crates in the C++/Corrosion path, so the toolchain
|
||||
# (and therefore the lints, coverage instrumentation and the cargo cache) matches
|
||||
# what production builds use.
|
||||
#
|
||||
# Rust unit tests are deliberately NOT run here. They run as part of the C++
|
||||
# build (reusable-build-test-config.yml), which already compiles the crates on a
|
||||
# self-hosted runner, so there is no need to provision a toolchain again.
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: "The Codecov token to use for uploading coverage reports."
|
||||
required: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: crates
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fecfc0c
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Cache cargo artifacts
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: crates
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fecfc0c
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Cache cargo artifacts
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: crates
|
||||
|
||||
- name: Generate coverage report
|
||||
run: cargo llvm-cov nextest --workspace --all-features --locked --no-tests=warn --lcov --output-path lcov.info
|
||||
|
||||
- name: Upload coverage report
|
||||
if: ${{ github.repository == 'XRPLF/rippled' }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
disable_search: true
|
||||
disable_telem: true
|
||||
fail_ci_if_error: true
|
||||
files: crates/lcov.info
|
||||
flags: rust
|
||||
plugins: noop
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fecfc0c
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Cache cargo artifacts
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: crates
|
||||
|
||||
- name: Build documentation
|
||||
env:
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
run: cargo doc --workspace --no-deps --all-features --locked
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -89,3 +89,6 @@ target/
|
||||
|
||||
# clangd cache
|
||||
/.cache
|
||||
|
||||
# Rust build directory
|
||||
crates/target
|
||||
|
||||
@@ -62,6 +62,15 @@ repos:
|
||||
types_or: [c++, c, proto]
|
||||
exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-fmt
|
||||
name: cargo fmt
|
||||
entry: cargo fmt --manifest-path crates/Cargo.toml --all
|
||||
language: system
|
||||
types: [rust]
|
||||
pass_filenames: false # rustfmt formats the whole workspace
|
||||
|
||||
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
|
||||
rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7
|
||||
hooks:
|
||||
|
||||
@@ -42,6 +42,7 @@ This section contains changes targeting a future version.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- `get_aggregate_price`: Duplicate entries in the `oracles` request array are now ignored. [#6586](https://github.com/XRPLF/rippled/pull/6586)
|
||||
- Peer Crawler: The `port` field in `overlay.active[]` now consistently returns an integer instead of a string for outbound peers. [#6318](https://github.com/XRPLF/rippled/pull/6318)
|
||||
- `ping`: The `ip` field is no longer returned as an empty string for proxied connections without a forwarded-for header. It is now omitted, consistent with the behavior for identified connections. [#6730](https://github.com/XRPLF/rippled/pull/6730)
|
||||
- gRPC `GetLedgerDiff`: Fixed error message that incorrectly said "base ledger not validated" when the desired ledger was not validated. [#6730](https://github.com/XRPLF/rippled/pull/6730)
|
||||
|
||||
12
BUILD.md
12
BUILD.md
@@ -247,7 +247,17 @@ cmake --build . --target setup_code_gen # create venv and install dependencies
|
||||
cmake --build . --target code_gen # regenerate code
|
||||
```
|
||||
|
||||
The regenerated files should be committed alongside your changes.
|
||||
The same targets are also available as a standalone project, which does not
|
||||
need the dependencies to be configured first:
|
||||
|
||||
```
|
||||
cmake -S cmake/codegen -B build/codegen
|
||||
cmake --build build/codegen --target setup_code_gen
|
||||
cmake --build build/codegen --target code_gen
|
||||
```
|
||||
|
||||
The regenerated files should be committed alongside your changes. CI verifies
|
||||
that they are up-to-date.
|
||||
|
||||
## Coverage report
|
||||
|
||||
|
||||
@@ -158,11 +158,14 @@ if(coverage)
|
||||
include(XrplCov)
|
||||
endif()
|
||||
|
||||
add_subdirectory(crates)
|
||||
include(XrplCore)
|
||||
include(XrplProtocolAutogen)
|
||||
include(XrplInstall)
|
||||
include(XrplPackaging)
|
||||
include(XrplValidatorKeys)
|
||||
# Must come after XrplValidatorKeys: the 'package' target depends on the
|
||||
# validator-keys target existing.
|
||||
include(XrplPackaging)
|
||||
|
||||
if(tests)
|
||||
include(CTest)
|
||||
|
||||
@@ -488,6 +488,17 @@
|
||||
# Must be a number between 100 and 1000, defaults to 250
|
||||
#
|
||||
#
|
||||
# [max_subscriptions_per_connection]
|
||||
#
|
||||
# Maximum number of account, real-time account, and account-history
|
||||
# subscriptions a single client connection may hold at once. Bounds the
|
||||
# per-connection state torn down when the connection disconnects. Book
|
||||
# subscriptions are tracked separately and are not counted here.
|
||||
#
|
||||
# Defaults to 100000 if not set; large enough for legitimate power users
|
||||
# such as block explorers.
|
||||
#
|
||||
#
|
||||
# [overlay]
|
||||
#
|
||||
# Controls settings related to the peer to peer overlay.
|
||||
@@ -538,6 +549,45 @@
|
||||
# only be used for local testing and debugging. Do not disable
|
||||
# on mainnet.
|
||||
#
|
||||
# max_untrusted_count = <number>
|
||||
#
|
||||
# The number of manifests the server keeps for validators it does not
|
||||
# list, and the number it sends and processes in a single peer protocol
|
||||
# message. Once the server holds this many, a manifest for a new
|
||||
# unlisted validator is rejected, so peer gossip cannot grow the cache
|
||||
# without end.
|
||||
#
|
||||
# This option can take any value between 50 and 1000, inclusive. If
|
||||
# the option is not present the server uses its built-in value.
|
||||
#
|
||||
# The current default (which is subject to change) is 300.
|
||||
#
|
||||
# max_trusted_count = <number>
|
||||
#
|
||||
# The number of manifests for listed validators to allow for when
|
||||
# sizing peer protocol messages. Manifests for listed validators are
|
||||
# never dropped, whether sending or receiving, because doing so would
|
||||
# delay a validator key change reaching this server. Set this above the
|
||||
# number of validators the server lists.
|
||||
#
|
||||
# Together the two counts above set the largest manifest message the
|
||||
# server accepts: bigger messages are discarded without reading them,
|
||||
# and without penalising the sender. Raising either means the server
|
||||
# accepts and sends bigger messages than a peer using the defaults, and
|
||||
# those peers will discard what this server sends. Lowering either below
|
||||
# what peers send makes this server discard their manifest messages,
|
||||
# which it does without recording anything.
|
||||
#
|
||||
# This option can take any value between 50 and 1000, inclusive. If
|
||||
# the option is not present the server uses its built-in value.
|
||||
#
|
||||
# The current default (which is subject to change) is 300.
|
||||
#
|
||||
# NOTE: These two options (max_untrusted_count and max_trusted_count)
|
||||
# are transitional. They exist to bound manifest-message size and cache
|
||||
# growth during the network upgrade. They may be removed in a future
|
||||
# release once the fleet has upgraded, and should not be relied upon as
|
||||
# stable configuration.
|
||||
#
|
||||
# [transaction_queue] EXPERIMENTAL
|
||||
#
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
Patch executables to run in non-Nix environments.
|
||||
|
||||
The Nix toolchain links binaries against an ELF interpreter (loader)
|
||||
that lives in the Nix store, so the resulting binaries don't run elsewhere.
|
||||
`patch_nix_binary` adds a POST_BUILD step that resets the interpreter
|
||||
to the system default loader and drops the rpath.
|
||||
that lives in the Nix store, so the resulting binaries don't run elsewhere
|
||||
(including once installed from the .deb package). `patch_nix_binary` resets
|
||||
the interpreter to the system default loader and drops the rpath, once the
|
||||
binary has been linked.
|
||||
|
||||
This runs by default for Nix-toolchain builds (determined by whether the compiler resolves under /nix/store/).
|
||||
Those builds are where binaries get a Nix-store loader.
|
||||
@@ -52,13 +53,38 @@ function(patch_nix_binary target)
|
||||
if(NOT PATCH_NIX_BINARIES)
|
||||
return()
|
||||
endif()
|
||||
add_custom_command(
|
||||
TARGET ${target}
|
||||
POST_BUILD
|
||||
COMMAND
|
||||
"${PATCHELF_COMMAND}" --set-interpreter "${DEFAULT_LOADER_PATH}"
|
||||
--remove-rpath "$<TARGET_FILE:${target}>"
|
||||
COMMENT "Patching ${target}: set default loader, remove rpath"
|
||||
VERBATIM
|
||||
|
||||
set(patch_command
|
||||
"${PATCHELF_COMMAND}"
|
||||
--set-interpreter
|
||||
"${DEFAULT_LOADER_PATH}"
|
||||
--remove-rpath
|
||||
"$<TARGET_FILE:${target}>"
|
||||
)
|
||||
set(comment "Patching ${target}: set default loader, remove rpath")
|
||||
|
||||
# POST_BUILD is the cheap way to do this: it runs only when the binary is
|
||||
# relinked. It is also only available in the directory that defined the
|
||||
# target, so for a target from elsewhere (e.g. a FetchContent subproject)
|
||||
# fall back to a custom target that runs after the binary is linked. That
|
||||
# one runs on every build, which is harmless because patchelf is idempotent.
|
||||
get_target_property(target_source_dir ${target} SOURCE_DIR)
|
||||
if("${target_source_dir}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
add_custom_command(
|
||||
TARGET ${target}
|
||||
POST_BUILD
|
||||
COMMAND ${patch_command}
|
||||
COMMENT "${comment}"
|
||||
VERBATIM
|
||||
)
|
||||
else()
|
||||
add_custom_target(
|
||||
${target}-patch-nix
|
||||
ALL
|
||||
COMMAND ${patch_command}
|
||||
COMMENT "${comment}"
|
||||
VERBATIM
|
||||
)
|
||||
add_dependencies(${target}-patch-nix ${target})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
@@ -205,7 +205,17 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
add_module(xrpl tx)
|
||||
target_link_libraries(xrpl.libxrpl.tx PUBLIC xrpl.libxrpl.ledger)
|
||||
# The wasm engine is a Rust crate reached over cxx: the bridge target supplies the
|
||||
# generated `lib.h` and `rust/cxx.h` that `tx/wasm` compiles against, and the Rust
|
||||
# static library everything downstream links. PUBLIC because the include path travels
|
||||
# with the module's own public headers.
|
||||
target_link_libraries(
|
||||
xrpl.libxrpl.tx
|
||||
PUBLIC xrpl.libxrpl.ledger xrpl_wasm_vm_ffi_cxxbridge
|
||||
)
|
||||
# Those headers do not exist at configure time, and the header-verification target
|
||||
# compiles this module's headers on their own, so both need the crates built first.
|
||||
add_dependencies(xrpl.libxrpl.tx xrpl_crates)
|
||||
|
||||
add_module(xrpl consensus)
|
||||
target_link_libraries(
|
||||
|
||||
@@ -25,6 +25,19 @@ if(NOT (RPMBUILD_EXECUTABLE OR DPKG_BUILDPACKAGE_EXECUTABLE))
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT TARGET xrpld)
|
||||
message(STATUS "xrpld=ON is required; 'package' target not available")
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT TARGET validator-keys)
|
||||
message(
|
||||
STATUS
|
||||
"validator_keys=ON is required; 'package' target not available"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(package_env
|
||||
SRC_DIR=${CMAKE_SOURCE_DIR}
|
||||
BUILD_DIR=${CMAKE_BINARY_DIR}
|
||||
@@ -37,7 +50,7 @@ add_custom_target(
|
||||
${CMAKE_COMMAND} -E env ${package_env}
|
||||
${CMAKE_SOURCE_DIR}/package/build_pkg.sh
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS xrpld
|
||||
DEPENDS xrpld validator-keys
|
||||
COMMENT "Building Linux package (deb/rpm inferred from host tooling)"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
@@ -2,21 +2,22 @@
|
||||
Protocol Autogen - Code generation for protocol wrapper classes
|
||||
#]===================================================================]
|
||||
|
||||
# The repository root, derived from the location of this file rather than from
|
||||
# the including project, so that the targets below can also be offered on their
|
||||
# own by cmake/codegen/CMakeLists.txt.
|
||||
get_filename_component(XRPL_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
set(CODEGEN_VENV_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/.venv"
|
||||
"${XRPL_ROOT}/.venv"
|
||||
CACHE PATH
|
||||
"Path to a Python virtual environment for code generation. A venv will be created here by setup_code_gen and used to run generation scripts."
|
||||
)
|
||||
|
||||
# Directory paths
|
||||
set(MACRO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/xrpl/protocol/detail")
|
||||
set(AUTOGEN_HEADER_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include/xrpl/protocol_autogen"
|
||||
)
|
||||
set(AUTOGEN_TEST_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/tests/libxrpl/protocol_autogen"
|
||||
)
|
||||
set(SCRIPTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/scripts/codegen")
|
||||
set(MACRO_DIR "${XRPL_ROOT}/include/xrpl/protocol/detail")
|
||||
set(AUTOGEN_HEADER_DIR "${XRPL_ROOT}/include/xrpl/protocol_autogen")
|
||||
set(AUTOGEN_TEST_DIR "${XRPL_ROOT}/src/tests/libxrpl/protocol_autogen")
|
||||
set(SCRIPTS_DIR "${XRPL_ROOT}/cmake/scripts/codegen")
|
||||
|
||||
# Input macro files
|
||||
set(TRANSACTIONS_MACRO "${MACRO_DIR}/transactions.macro")
|
||||
@@ -114,14 +115,14 @@ if(CODEGEN_VENV_DIR)
|
||||
setup_code_gen
|
||||
COMMAND ${Python3_EXECUTABLE} -m venv "${CODEGEN_VENV_DIR}"
|
||||
COMMAND ${CODEGEN_PYTHON} -m pip install -r "${REQUIREMENTS_FILE}"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
WORKING_DIRECTORY "${XRPL_ROOT}"
|
||||
COMMENT "Creating venv and installing code generation dependencies..."
|
||||
)
|
||||
else()
|
||||
add_custom_target(
|
||||
setup_code_gen
|
||||
COMMAND ${Python3_EXECUTABLE} -m pip install -r "${REQUIREMENTS_FILE}"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
WORKING_DIRECTORY "${XRPL_ROOT}"
|
||||
COMMENT "Installing code generation dependencies..."
|
||||
)
|
||||
endif()
|
||||
@@ -139,8 +140,8 @@ add_custom_target(
|
||||
-DSFIELDS_MACRO=${SFIELDS_MACRO}
|
||||
-DAUTOGEN_HEADER_DIR=${AUTOGEN_HEADER_DIR}
|
||||
-DAUTOGEN_TEST_DIR=${AUTOGEN_TEST_DIR} -P
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/XrplProtocolAutogenRun.cmake"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/XrplProtocolAutogenRun.cmake"
|
||||
WORKING_DIRECTORY "${XRPL_ROOT}"
|
||||
COMMENT "Running protocol code generation..."
|
||||
SOURCES ${ALL_INPUT_FILES}
|
||||
)
|
||||
|
||||
@@ -5,22 +5,39 @@ option(
|
||||
)
|
||||
|
||||
if(validator_keys)
|
||||
git_branch(current_branch)
|
||||
# default to tracking VK master branch unless we are on release
|
||||
if(NOT (current_branch STREQUAL "release"))
|
||||
set(current_branch "master")
|
||||
endif()
|
||||
message(STATUS "Tracking ValidatorKeys branch: ${current_branch}")
|
||||
# Own the install destination below rather than relying on another module
|
||||
# having pulled this in first.
|
||||
include(GNUInstallDirs)
|
||||
|
||||
# Pinned to an exact commit, not a branch: the tool ships inside our
|
||||
# packages, so the same xrpld version must always package the same
|
||||
# validator-keys. Bump this deliberately.
|
||||
set(validator_keys_commit "4c0fb75eec9601c711645998c904507e87e910ae")
|
||||
message(STATUS "Using ValidatorKeys commit: ${validator_keys_commit}")
|
||||
|
||||
FetchContent_Declare(
|
||||
validator_keys
|
||||
GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git
|
||||
GIT_TAG "${current_branch}"
|
||||
GIT_TAG "${validator_keys_commit}"
|
||||
)
|
||||
FetchContent_MakeAvailable(validator_keys)
|
||||
# The tool's own CMakeLists excludes the target from 'all' when it is built
|
||||
# as a subproject. Undo that, so validator_keys=ON really does build it.
|
||||
set_target_properties(
|
||||
validator-keys
|
||||
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
EXCLUDE_FROM_ALL OFF
|
||||
EXCLUDE_FROM_DEFAULT_BUILD OFF
|
||||
)
|
||||
# We ship this binary, so like xrpld it must not keep the Nix store's ELF
|
||||
# loader, or it cannot run on the target distro at all.
|
||||
patch_nix_binary(validator-keys)
|
||||
|
||||
configure_file(
|
||||
"${validator_keys_SOURCE_DIR}/LICENSE"
|
||||
"${CMAKE_BINARY_DIR}/validator-keys-LICENSE"
|
||||
COPYONLY
|
||||
)
|
||||
install(TARGETS validator-keys RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
endif()
|
||||
|
||||
21
cmake/codegen/CMakeLists.txt
Normal file
21
cmake/codegen/CMakeLists.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
#[===================================================================[
|
||||
Protocol Autogen - Standalone project
|
||||
|
||||
Exposes the 'setup_code_gen' and 'code_gen' targets on their own, without
|
||||
configuring the rest of xrpl. Code generation is pure Python, so this needs
|
||||
neither the dependencies nor a compiler, which makes it usable in CI and by
|
||||
contributors who only want to regenerate the protocol wrapper classes:
|
||||
|
||||
cmake -S cmake/codegen -B build/codegen
|
||||
cmake --build build/codegen --target setup_code_gen
|
||||
cmake --build build/codegen --target code_gen
|
||||
|
||||
The targets are identical to the ones offered by the top-level build, since
|
||||
both come from cmake/XrplProtocolAutogen.cmake.
|
||||
#]===================================================================]
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(xrpl_codegen LANGUAGES NONE)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../XrplProtocolAutogen.cmake")
|
||||
@@ -12,7 +12,7 @@
|
||||
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
|
||||
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
|
||||
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
|
||||
"mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355",
|
||||
"mpt-crypto/1.0.2#b313cef0c1a493eb970ad185b2e9bab7%1784285108.866483",
|
||||
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
|
||||
"libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744",
|
||||
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
|
||||
|
||||
@@ -139,7 +139,7 @@ class Xrpl(ConanFile):
|
||||
if self.options.jemalloc:
|
||||
self.requires("jemalloc/5.3.1")
|
||||
self.requires("lz4/1.10.0", force=True)
|
||||
self.requires("mpt-crypto/0.4.0-rc4", transitive_headers=True)
|
||||
self.requires("mpt-crypto/1.0.2", transitive_headers=True)
|
||||
self.requires("protobuf/6.33.5", force=True)
|
||||
if self.options.rocksdb:
|
||||
self.requires("rocksdb/10.5.1")
|
||||
|
||||
5
crates/.cargo/config.toml
Normal file
5
crates/.cargo/config.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = ["-C", "link-args=-static-libgcc"]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
59
crates/CMakeLists.txt
Normal file
59
crates/CMakeLists.txt
Normal file
@@ -0,0 +1,59 @@
|
||||
set(CORROSION_VERSION 0.6.1)
|
||||
|
||||
find_package(Corrosion ${CORROSION_VERSION} QUIET)
|
||||
if(NOT Corrosion_FOUND)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
Corrosion
|
||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||
GIT_TAG v${CORROSION_VERSION}
|
||||
)
|
||||
FetchContent_MakeAvailable(Corrosion)
|
||||
endif()
|
||||
|
||||
corrosion_import_crate(MANIFEST_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Cargo.toml)
|
||||
|
||||
file(
|
||||
WRITE "${CMAKE_CURRENT_BINARY_DIR}/.clang-tidy"
|
||||
"# Auto-generated by crates/CMakeLists.txt. Do not edit.\n"
|
||||
"# Neutralizes clang-tidy for corrosion/cxxbridge-generated C++.\n"
|
||||
"# One check kept enabled to avoid clang-tidy's \"no checks enabled\" error.\n"
|
||||
"Checks: '-*,google-readability-todo'\n"
|
||||
"WarningsAsErrors: ''\n"
|
||||
"HeaderFilterRegex: ''\n"
|
||||
"InheritParentConfig: false\n"
|
||||
)
|
||||
|
||||
# Umbrella target that aggregates all crate-generated code (cxxbridge headers,
|
||||
# etc.). Build this before running clang-tidy so generated headers are present.
|
||||
add_custom_target(xrpl_crates)
|
||||
|
||||
# add_xrpl_crate(<name> CRATE <crate> FILES <file>...) Creates a cxxbridge
|
||||
# target <name>_cxxbridge and registers it with xrpl_crates.
|
||||
function(add_xrpl_crate name)
|
||||
cmake_parse_arguments(ARG "" "CRATE" "FILES" ${ARGN})
|
||||
corrosion_add_cxxbridge(${name}_cxxbridge CRATE ${ARG_CRATE} FILES
|
||||
${ARG_FILES}
|
||||
)
|
||||
# Generated cxxbridge headers don't exist at configure time; CMake 3.28+
|
||||
# validates INTERFACE_SOURCES on consuming targets. Clear it to skip the
|
||||
# existence check — build-time ordering is enforced by the custom commands.
|
||||
set_target_properties(${name}_cxxbridge PROPERTIES INTERFACE_SOURCES "")
|
||||
add_dependencies(xrpl_crates ${name}_cxxbridge)
|
||||
endfunction()
|
||||
|
||||
add_xrpl_crate(xrpl_wasm_vm_ffi CRATE xrpl_wasm_vm_ffi FILES lib.rs)
|
||||
|
||||
# Test-only, and deliberately not part of xrpl_wasm_vm_ffi: it carries the `wat` assembler,
|
||||
# which the engine's `wasmi default-features = false` exists to keep out of the consensus
|
||||
# path. Linked from src/tests/libxrpl only, so the shipped node cannot contain it.
|
||||
add_xrpl_crate(xrpl_wasm_testkit CRATE xrpl_wasm_testkit FILES lib.rs)
|
||||
|
||||
# The wasm bridge `include!`s a project header, so its generated translation unit needs
|
||||
# the project's include root. Deliberately only that: a header reached from here must
|
||||
# stay light enough to compile without the Boost paths this target does not get, which
|
||||
# is why `HostContext.h` forward-declares `xrpl::HostFunctions` instead of including it.
|
||||
target_include_directories(
|
||||
xrpl_wasm_vm_ffi_cxxbridge
|
||||
PRIVATE ${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
497
crates/Cargo.lock
generated
Normal file
497
crates/Cargo.lock
generated
Normal file
@@ -0,0 +1,497 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.198"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxx-build",
|
||||
"cxxbridge-cmd",
|
||||
"cxxbridge-flags",
|
||||
"cxxbridge-macro",
|
||||
"foldhash 0.2.0",
|
||||
"link-cplusplus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.198"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-cmd"
|
||||
version = "1.0.198"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codespan-reporting",
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.198"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.198"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scratch"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
|
||||
|
||||
[[package]]
|
||||
name = "string-interner"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.254.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser 0.254.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2300d0f78cba12f14e29e8dd157ea64050c0a688179aefdb2050105805594a0c"
|
||||
dependencies = [
|
||||
"spin",
|
||||
"wasmi_collections",
|
||||
"wasmi_core",
|
||||
"wasmi_ir",
|
||||
"wasmparser 0.239.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_collections"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8a8c42a2a76148d43097b1d7cc2a5bf33d5c23bd4dd69015fc887e311767884"
|
||||
dependencies = [
|
||||
"string-interner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_core"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9013136083d988725953390bf668b64b7a218fabf26f8b913bbc59546b97ee27"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_ir"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba1fa003f79156f406d62ef0e1464dc03e11ace37170e9fa7524299a75ad8f68"
|
||||
dependencies = [
|
||||
"wasmi_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.239.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.254.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wast"
|
||||
version = "254.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ed4dfc8f6b9fc38b231065e2cdfbf7359af5ab945990abf09658dcc63c3e32"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"leb128fmt",
|
||||
"memchr",
|
||||
"unicode-width",
|
||||
"wasm-encoder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wat"
|
||||
version = "1.254.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7127f7f9b8f127c879991cecd35f494e4628bae1b0874c681414d8d8831e952c"
|
||||
dependencies = [
|
||||
"wast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xrpl-host-functions"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"xrpl-host-functions-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xrpl-host-functions-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
"xrpl-host-functions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xrpl-wasm-testkit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"wat",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xrpl-wasm-vm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"wasmi",
|
||||
"wat",
|
||||
"xrpl-host-functions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xrpl-wasm-vm-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"xrpl-host-functions",
|
||||
"xrpl-wasm-vm",
|
||||
]
|
||||
15
crates/Cargo.toml
Normal file
15
crates/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[workspace]
|
||||
members = ["xrpl-wasm-vm-ffi", "xrpl-wasm-vm", "xrpl-wasm-testkit", "xrpl-host-functions", "xrpl-host-functions-macros"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.dependencies]
|
||||
cxx = { version = "1.0.198", features = ["c++20"] }
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
overflow-checks = true
|
||||
lto = true
|
||||
debug = true
|
||||
18
crates/xrpl-host-functions-macros/Cargo.toml
Normal file
18
crates/xrpl-host-functions-macros/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "xrpl-host-functions-macros"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "3", features = ["full"] }
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
|
||||
# The expansion names `::xrpl_host_functions::HostFnSpec`, so the doctest needs the
|
||||
# facade crate. Cargo allows this cycle because dev-dependencies are outside the
|
||||
# library build graph.
|
||||
[dev-dependencies]
|
||||
xrpl-host-functions.path = "../xrpl-host-functions"
|
||||
12
crates/xrpl-host-functions-macros/src/errors.rs
Normal file
12
crates/xrpl-host-functions-macros/src/errors.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
/// Folds accumulated diagnostics into the single error a macro can return.
|
||||
///
|
||||
/// `syn::Error` is itself a collection: `combine` appends, and
|
||||
/// `into_compile_error` emits one `compile_error!` per recorded span. Folding
|
||||
/// instead of returning the first error means every mistake in a
|
||||
/// `host_functions!` block surfaces in one build rather than one per rebuild.
|
||||
pub(crate) fn combine(errors: Vec<syn::Error>) -> Option<syn::Error> {
|
||||
errors.into_iter().reduce(|mut first, next| {
|
||||
first.combine(next);
|
||||
first
|
||||
})
|
||||
}
|
||||
384
crates/xrpl-host-functions-macros/src/lib.rs
Normal file
384
crates/xrpl-host-functions-macros/src/lib.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
mod errors;
|
||||
mod parsed_host_function;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
TraitItemFn,
|
||||
parse::{Parse, ParseStream},
|
||||
parse2,
|
||||
};
|
||||
|
||||
use parsed_host_function::ParsedHostFunction;
|
||||
|
||||
/// Declares the wasm host ABI once, and generates everything that follows from it.
|
||||
///
|
||||
/// The input is a block of `fn` declarations, each carrying the gas cost the host
|
||||
/// charges before the call and the name the guest imports it under. Doc comments
|
||||
/// are kept and appear on the generated items.
|
||||
///
|
||||
/// This crate is an implementation detail of `xrpl-host-functions`, which
|
||||
/// hand-writes the types the expansion refers to and holds the one declaration
|
||||
/// block. The expansion names those types by absolute path, so a call site needs
|
||||
/// `xrpl-host-functions` as a dependency but no imports from it.
|
||||
///
|
||||
/// ```
|
||||
/// use xrpl_host_functions::HostResult;
|
||||
/// use xrpl_host_functions_macros::host_functions;
|
||||
///
|
||||
/// host_functions! {
|
||||
/// /// The sequence number of the ledger being built, as 4 little-endian bytes.
|
||||
/// #[gas = 60]
|
||||
/// #[wasm_name = "ldgr_index"]
|
||||
/// fn get_ledger_sqn(&self, out: &mut [u8]) -> HostResult<usize>;
|
||||
///
|
||||
/// /// Writes `msg` to the trace log.
|
||||
/// #[gas = 500]
|
||||
/// #[wasm_name = "trace_num"]
|
||||
/// fn trace_num(&self, msg: &str, number: i64) -> HostResult<()>;
|
||||
/// }
|
||||
///
|
||||
/// // A `HostFunctions` trait, holding the declarations verbatim:
|
||||
/// struct Host;
|
||||
/// impl HostFunctions for Host {
|
||||
/// fn get_ledger_sqn(&self, out: &mut [u8]) -> HostResult<usize> {
|
||||
/// out[..4].copy_from_slice(&7u32.to_le_bytes());
|
||||
/// Ok(4)
|
||||
/// }
|
||||
/// fn trace_num(&self, _msg: &str, _number: i64) -> HostResult<()> { Ok(()) }
|
||||
/// }
|
||||
///
|
||||
/// // A `HostFunctionSpec` enum carrying the ABI metadata as a `const` table:
|
||||
/// assert_eq!(HostFunctionSpec::GetLedgerSqn.gas(), 60);
|
||||
/// assert_eq!(HostFunctionSpec::TraceNum.wasm_name(), "trace_num");
|
||||
/// assert_eq!(HostFunctionSpec::ALL.len(), 2);
|
||||
/// ```
|
||||
///
|
||||
/// A declaration must be a plain `fn` taking `&self` and returning
|
||||
/// `HostResult<T>`, with no body and no generics: it maps to exactly one wasm
|
||||
/// import signature. Two declarations may not share a `wasm_name`, nor collapse to
|
||||
/// the same PascalCase variant.
|
||||
#[proc_macro]
|
||||
pub fn host_functions(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
expand(input.into())
|
||||
.unwrap_or_else(syn::Error::into_compile_error)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn expand(input: TokenStream) -> syn::Result<TokenStream> {
|
||||
let HostFunctionsInput { functions } = parse2(input)?;
|
||||
|
||||
let mut parsed = Vec::with_capacity(functions.len());
|
||||
let mut errors = Vec::new();
|
||||
for function in functions {
|
||||
match ParsedHostFunction::parse(function) {
|
||||
Ok(function) => parsed.push(function),
|
||||
Err(error) => errors.push(error),
|
||||
}
|
||||
}
|
||||
if let Some(error) = errors::combine(errors) {
|
||||
return Err(error);
|
||||
}
|
||||
if let Some(error) = errors::combine(collisions(&parsed)) {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(generate(&parsed))
|
||||
}
|
||||
|
||||
/// Names two declarations may not share, because the generated code would then
|
||||
/// fail to compile at a span the caller cannot see.
|
||||
fn collisions(functions: &[ParsedHostFunction]) -> Vec<syn::Error> {
|
||||
let mut errors = Vec::new();
|
||||
let mut variants = HashSet::new();
|
||||
let mut wasm_names = HashSet::new();
|
||||
|
||||
for function in functions {
|
||||
if !variants.insert(function.variant.to_string()) {
|
||||
errors.push(syn::Error::new_spanned(
|
||||
&function.variant,
|
||||
format!(
|
||||
"another host function already becomes the `{}` variant",
|
||||
function.variant
|
||||
),
|
||||
));
|
||||
}
|
||||
if !wasm_names.insert(function.wasm_name.value()) {
|
||||
errors.push(syn::Error::new_spanned(
|
||||
&function.wasm_name,
|
||||
format!(
|
||||
"another host function is already imported as `{}`",
|
||||
function.wasm_name.value()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
fn generate(functions: &[ParsedHostFunction]) -> TokenStream {
|
||||
let trait_methods = functions.iter().map(ParsedHostFunction::trait_method);
|
||||
let variants = functions
|
||||
.iter()
|
||||
.map(ParsedHostFunction::variant_declaration);
|
||||
let spec_arms = functions.iter().map(ParsedHostFunction::spec_arm);
|
||||
let all = functions.iter().map(|function| &function.variant);
|
||||
|
||||
quote! {
|
||||
/// The host side of the wasm ABI: one method per function a guest may
|
||||
/// import.
|
||||
///
|
||||
/// Implement it once per execution environment — the ledger host, a test
|
||||
/// double, a benchmark fake — and a guest module cannot tell them apart.
|
||||
/// Each method is one declaration from the `host_functions!` block, as
|
||||
/// written; its `&self` receiver is not part of the ABI the guest sees,
|
||||
/// so a host that must mutate does so behind interior mutability.
|
||||
///
|
||||
/// # The output contract
|
||||
///
|
||||
/// A method handed an `out` buffer **writes into it only when the whole
|
||||
/// value fits, and returns the value's true length whether it fitted or
|
||||
/// not.**
|
||||
///
|
||||
/// The length is the value's, not the number of bytes written, because it
|
||||
/// is how a guest that asked with too small a buffer learns the size to
|
||||
/// ask for next time. The engine turns a length past the buffer into
|
||||
/// `BufferTooSmall`, and one past the field cap into `DataFieldTooLarge`,
|
||||
/// so a host needs to know neither.
|
||||
///
|
||||
/// Writing nothing unless the value fits is the half only a host can hold
|
||||
/// up. An engine can bound how many bytes are *writable* — and does, by
|
||||
/// handing over a region clamped to the field cap — but it cannot take
|
||||
/// back what a method already put there. A host that wrote a truncated
|
||||
/// prefix and then reported the larger length would leave those bytes in
|
||||
/// guest memory behind a refusal the guest is told to ignore. C++'s
|
||||
/// `setData` is the reference point: it wrote only on a value that fit.
|
||||
pub trait HostFunctions {
|
||||
#(#trait_methods)*
|
||||
}
|
||||
|
||||
/// One row of the ABI table: what [`HostFunctionSpec::wasm_name`] and
|
||||
/// [`HostFunctionSpec::gas`] read from.
|
||||
///
|
||||
/// Private, and the only reason it exists is to keep both of them fed
|
||||
/// from a single `match` over the declarations.
|
||||
struct HostFnSpec {
|
||||
name: &'static str,
|
||||
gas: u64,
|
||||
}
|
||||
|
||||
/// Identifies one host function, and is the compile-time source of its
|
||||
/// ABI metadata.
|
||||
///
|
||||
/// One variant per `host_functions!` declaration, named by converting the
|
||||
/// function name to PascalCase. [`Self::ALL`] is the whole ABI, which is
|
||||
/// what a wasm engine iterates to build its import table.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HostFunctionSpec {
|
||||
#(#variants,)*
|
||||
}
|
||||
|
||||
impl HostFunctionSpec {
|
||||
/// Every host function, in the order declared.
|
||||
///
|
||||
/// This is the complete import surface a guest may link against: a
|
||||
/// function absent here cannot be called, and one present here must
|
||||
/// be registered for a module that imports it to instantiate.
|
||||
pub const ALL: &'static [Self] = &[#(Self::#all,)*];
|
||||
|
||||
/// This function's row of the ABI table.
|
||||
const fn spec(self) -> HostFnSpec {
|
||||
match self {
|
||||
#(#spec_arms,)*
|
||||
}
|
||||
}
|
||||
|
||||
/// The name a guest imports this function under.
|
||||
///
|
||||
/// A guest's import name must match this exactly, or the module
|
||||
/// fails to instantiate. Usable in `const` context, so import lists
|
||||
/// can be built at compile time.
|
||||
pub const fn wasm_name(self) -> &'static str {
|
||||
self.spec().name
|
||||
}
|
||||
|
||||
/// Gas charged before the call runs, independent of its arguments.
|
||||
///
|
||||
/// Consensus-relevant: two nodes that disagree on this value
|
||||
/// disagree on transaction outcomes. Usable in `const` context, so
|
||||
/// gas tables can be built at compile time.
|
||||
pub const fn gas(self) -> u64 {
|
||||
self.spec().gas
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HostFunctionsInput {
|
||||
functions: Vec<TraitItemFn>,
|
||||
}
|
||||
|
||||
impl Parse for HostFunctionsInput {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut functions = Vec::new();
|
||||
while !input.is_empty() {
|
||||
functions.push(input.parse()?);
|
||||
}
|
||||
Ok(HostFunctionsInput { functions })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn accepts_an_empty_block() {
|
||||
expand(quote! {}).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_mistakes_from_every_function() {
|
||||
let error = expand(quote! {
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
|
||||
#[gas = 2000]
|
||||
fn sha512_half(&self, data: &[u8]) -> HostResult<[u8; 32]>;
|
||||
})
|
||||
.expect_err("expected parsing to fail");
|
||||
|
||||
let messages: Vec<_> = error.into_iter().map(|error| error.to_string()).collect();
|
||||
assert_eq!(messages.len(), 2, "{messages:?}");
|
||||
assert!(messages[0].contains("missing `#[gas"), "{messages:?}");
|
||||
assert!(messages[1].contains("missing `#[wasm_name"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propagates_syntax_errors() {
|
||||
let error = expand(quote! { fn missing_semicolon() }).expect_err("expected a syntax error");
|
||||
assert!(!error.to_string().is_empty());
|
||||
}
|
||||
|
||||
/// The messages of every diagnostic recorded by one failed `expand`.
|
||||
fn messages(input: TokenStream) -> Vec<String> {
|
||||
let Err(error) = expand(input) else {
|
||||
panic!("expected expansion to fail");
|
||||
};
|
||||
error.into_iter().map(|error| error.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_the_trait_the_enum_and_the_table() {
|
||||
let generated = expand(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
|
||||
#[gas = 500]
|
||||
#[wasm_name = "trace_num"]
|
||||
fn trace_num(&self, msg: &str, number: i64) -> HostResult<()>;
|
||||
})
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
for expected in [
|
||||
"pub trait HostFunctions",
|
||||
"fn get_ledger_sqn (& self) -> HostResult < [u8 ; 4] > ;",
|
||||
"fn trace_num (& self , msg : & str , number : i64) -> HostResult < () > ;",
|
||||
"pub enum HostFunctionSpec { GetLedgerSqn , TraceNum , }",
|
||||
"pub const ALL : & 'static [Self] = & [Self :: GetLedgerSqn , Self :: TraceNum ,]",
|
||||
// The table's row type is generated too, and stays private.
|
||||
"struct HostFnSpec { name : & 'static str , gas : u64 , }",
|
||||
"const fn spec (self) -> HostFnSpec",
|
||||
"Self :: GetLedgerSqn => HostFnSpec { name : \"ldgr_index\" , gas : 60u64 }",
|
||||
"pub const fn wasm_name (self) -> & 'static str",
|
||||
"pub const fn gas (self) -> u64",
|
||||
] {
|
||||
assert!(generated.contains(expected), "missing {expected:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The expansion stands alone: every name in it is either generated here or
|
||||
/// written in the declarations, so it cannot depend on the crate it lands in.
|
||||
#[test]
|
||||
fn names_no_crate_of_its_own() {
|
||||
let generated = expand(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
})
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
assert!(!generated.contains("xrpl_host_functions"), "{generated}");
|
||||
|
||||
// `Self::Variant` is the only path the expansion may build: anything else
|
||||
// would reach out of the generated code. Doc comments spell paths without
|
||||
// spaces (`Self::ALL`), so they do not match.
|
||||
for (index, _) in generated.match_indices(" :: ") {
|
||||
assert!(
|
||||
generated[..index].ends_with("Self"),
|
||||
"path out of the expansion at {index}: {generated}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `spec` is an implementation detail of the two accessors, so it must not
|
||||
/// become part of the ABI crate's public surface.
|
||||
#[test]
|
||||
fn keeps_the_table_row_private() {
|
||||
let generated = expand(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
})
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
assert!(!generated.contains("pub struct HostFnSpec"), "{generated}");
|
||||
assert!(!generated.contains("pub const fn spec"), "{generated}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_two_functions_that_share_a_wasm_name() {
|
||||
let messages = messages(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "trace"]
|
||||
fn trace(&self, msg: &str) -> HostResult<()>;
|
||||
|
||||
#[gas = 70]
|
||||
#[wasm_name = "trace"]
|
||||
fn trace_num(&self, msg: &str, number: i64) -> HostResult<()>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("already imported as `trace`"),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Names that differ only in underscores collapse to one enum variant.
|
||||
#[test]
|
||||
fn rejects_two_functions_that_share_a_variant() {
|
||||
let messages = messages(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "a"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
|
||||
#[gas = 70]
|
||||
#[wasm_name = "b"]
|
||||
fn get_ledger__sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("`GetLedgerSqn` variant"),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
871
crates/xrpl-host-functions-macros/src/parsed_host_function.rs
Normal file
871
crates/xrpl-host-functions-macros/src/parsed_host_function.rs
Normal file
@@ -0,0 +1,871 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
Attribute, Expr, ExprLit, Ident, Lit, LitStr, PathArguments, ReceiverKind, ReturnType, Safety,
|
||||
Signature, TraitItemFn, Type, TypePath,
|
||||
};
|
||||
|
||||
use crate::errors;
|
||||
|
||||
/// `#[gas = N]`: the base gas charged before the call runs.
|
||||
const GAS: &str = "gas";
|
||||
/// `#[wasm_name = "..."]`: the name the guest imports the function under.
|
||||
const WASM_NAME: &str = "wasm_name";
|
||||
/// `///` desugars to `#[doc = "..."]` before macro expansion.
|
||||
const DOC: &str = "doc";
|
||||
/// The alias every declaration returns its success type through.
|
||||
const HOST_RESULT: &str = "HostResult";
|
||||
|
||||
/// One entry of a `host_functions!` block: its ABI metadata and its signature.
|
||||
pub(crate) struct ParsedHostFunction {
|
||||
pub(crate) gas: u64,
|
||||
/// Kept as the literal the user wrote, so diagnostics and the generated
|
||||
/// string both carry that span.
|
||||
pub(crate) wasm_name: LitStr,
|
||||
/// Doc comments, in source order, to re-emit on the generated items.
|
||||
pub(crate) docs: Vec<Attribute>,
|
||||
/// The enum variant this declaration becomes, spanned at the function name.
|
||||
pub(crate) variant: Ident,
|
||||
pub(crate) signature: Signature,
|
||||
}
|
||||
|
||||
impl ParsedHostFunction {
|
||||
/// `#[doc …] fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;`
|
||||
pub(crate) fn trait_method(&self) -> TokenStream {
|
||||
let docs = &self.docs;
|
||||
// The declaration is already a trait method: emitted verbatim, so what
|
||||
// the block reads like is what the trait is.
|
||||
let signature = &self.signature;
|
||||
|
||||
quote! {
|
||||
#(#docs)*
|
||||
#signature;
|
||||
}
|
||||
}
|
||||
|
||||
/// `#[doc …] GetLedgerSqn`
|
||||
pub(crate) fn variant_declaration(&self) -> TokenStream {
|
||||
let docs = &self.docs;
|
||||
let variant = &self.variant;
|
||||
quote! {
|
||||
#(#docs)*
|
||||
#variant
|
||||
}
|
||||
}
|
||||
|
||||
/// `Self::GetLedgerSqn => HostFnSpec { name: "ldgr_index", gas: 60u64 }`
|
||||
pub(crate) fn spec_arm(&self) -> TokenStream {
|
||||
let Self {
|
||||
gas,
|
||||
wasm_name,
|
||||
variant,
|
||||
..
|
||||
} = self;
|
||||
quote! {
|
||||
Self::#variant => HostFnSpec { name: #wasm_name, gas: #gas }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(function: TraitItemFn) -> syn::Result<Self> {
|
||||
let mut gas = None;
|
||||
let mut wasm_name = None;
|
||||
let mut docs = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// Tracked separately from `gas`/`wasm_name` so a malformed attribute is
|
||||
// not also reported as a missing one.
|
||||
let mut saw_gas = false;
|
||||
let mut saw_wasm_name = false;
|
||||
|
||||
for attr in function.attrs {
|
||||
if attr.path().is_ident(GAS) {
|
||||
saw_gas = true;
|
||||
if let Err(error) = int_value(&attr).and_then(|v| set_once(&mut gas, v, &attr)) {
|
||||
errors.push(error);
|
||||
}
|
||||
} else if attr.path().is_ident(WASM_NAME) {
|
||||
saw_wasm_name = true;
|
||||
if let Err(error) =
|
||||
string_value(&attr).and_then(|v| set_once(&mut wasm_name, v, &attr))
|
||||
{
|
||||
errors.push(error);
|
||||
}
|
||||
} else if attr.path().is_ident(DOC) {
|
||||
docs.push(attr);
|
||||
} else {
|
||||
errors.push(syn::Error::new_spanned(
|
||||
&attr,
|
||||
format!("unexpected attribute `{}`", path_name(&attr)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !saw_gas {
|
||||
errors.push(syn::Error::new_spanned(
|
||||
&function.sig.ident,
|
||||
format!("missing `#[{GAS} = ...]` attribute"),
|
||||
));
|
||||
}
|
||||
if !saw_wasm_name {
|
||||
errors.push(syn::Error::new_spanned(
|
||||
&function.sig.ident,
|
||||
format!("missing `#[{WASM_NAME} = \"...\"]` attribute"),
|
||||
));
|
||||
}
|
||||
if let Some(body) = &function.default {
|
||||
errors.push(syn::Error::new_spanned(
|
||||
body,
|
||||
"a host function is implemented by the host, so it must not have a body",
|
||||
));
|
||||
}
|
||||
if !function.sig.generics.params.is_empty() || function.sig.generics.where_clause.is_some()
|
||||
{
|
||||
errors.push(syn::Error::new_spanned(
|
||||
&function.sig.ident,
|
||||
"a host function must not be generic: it maps to one wasm import signature",
|
||||
));
|
||||
}
|
||||
errors.extend(check_receiver(&function.sig).err());
|
||||
errors.extend(check_return_type(&function.sig).err());
|
||||
if let Some(name) = &wasm_name {
|
||||
errors.extend(check_wasm_name(name).err());
|
||||
}
|
||||
reject_modifiers(&function.sig, &mut errors);
|
||||
|
||||
// A name whose PascalCase form is not a legal variant is reported here
|
||||
// rather than emitted, which would either panic or fail downstream.
|
||||
let variant = match variant_ident(&function.sig.ident) {
|
||||
Ok(variant) => Some(variant),
|
||||
Err(error) => {
|
||||
errors.push(error);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(error) = errors::combine(errors) {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let (Some(gas), Some(wasm_name), Some(variant)) = (gas, wasm_name, variant) else {
|
||||
unreachable!("every absent field is reported above");
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
gas,
|
||||
wasm_name,
|
||||
docs,
|
||||
variant,
|
||||
signature: function.sig,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Every declaration carries a receiver, and it is always `&self`.
|
||||
///
|
||||
/// `&self` is the only receiver that can work: the VM reaches the host through a
|
||||
/// shared `&dyn HostFunctions` stored in the wasmi `Store`, and a host that needs
|
||||
/// to mutate does so behind interior mutability. The receiver is not part of the
|
||||
/// wasm ABI — the guest passes no `self` — so it is uniform across the block.
|
||||
fn check_receiver(signature: &Signature) -> syn::Result<()> {
|
||||
let Some(receiver) = signature.receiver() else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&signature.ident,
|
||||
format!(
|
||||
"a host function must declare its receiver: `fn {}(&self, ...)`",
|
||||
signature.ident
|
||||
),
|
||||
));
|
||||
};
|
||||
|
||||
// `&self` and nothing else: not `&mut self`, not `self`/`mut self`, not a
|
||||
// typed `self: Box<Self>`, and not a spelled-out lifetime.
|
||||
if !matches!(receiver.kind, ReceiverKind::Reference(_, None, None)) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
receiver,
|
||||
"a host function's receiver must be exactly `&self`: the VM calls the host \
|
||||
through a shared `&dyn HostFunctions`",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Every declaration returns `HostResult<T>`, including the ones that yield
|
||||
/// nothing (`HostResult<()>`).
|
||||
///
|
||||
/// One shape for every function is what lets a single dispatch adapter lower them
|
||||
/// all: lift the arguments out of guest memory, call the host, then turn `Ok(T)`
|
||||
/// into the wire's non-negative `i32` and `Err(e)` into a negative code or a trap.
|
||||
/// A function returning a bare `T` would need its own arm.
|
||||
fn check_return_type(signature: &Signature) -> syn::Result<()> {
|
||||
const SHAPE: &str = "a host function must return `HostResult<T>` — \
|
||||
`HostResult<()>` if it yields nothing";
|
||||
|
||||
let ReturnType::Type(_, returned) = &signature.output else {
|
||||
return Err(syn::Error::new_spanned(&signature.ident, SHAPE));
|
||||
};
|
||||
|
||||
let Type::Path(TypePath {
|
||||
qself: None, path, ..
|
||||
}) = &**returned
|
||||
else {
|
||||
return Err(syn::Error::new_spanned(returned, SHAPE));
|
||||
};
|
||||
// The last segment only, so `HostResult<T>` may be written qualified.
|
||||
let Some(last) = path.segments.last() else {
|
||||
return Err(syn::Error::new_spanned(returned, SHAPE));
|
||||
};
|
||||
if last.ident != HOST_RESULT {
|
||||
return Err(syn::Error::new_spanned(returned, SHAPE));
|
||||
}
|
||||
|
||||
// `HostResult` without its success type is `HostResult` the alias, which names
|
||||
// no type; rustc's own message for that is unhelpfully far from the cause.
|
||||
let PathArguments::AngleBracketed(arguments) = &last.arguments else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
returned,
|
||||
format!("`{HOST_RESULT}` needs its success type: `{HOST_RESULT}<T>`"),
|
||||
));
|
||||
};
|
||||
if arguments.args.len() != 1 {
|
||||
return Err(syn::Error::new_spanned(
|
||||
arguments,
|
||||
format!("`{HOST_RESULT}` takes exactly one type: `{HOST_RESULT}<T>`"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `const`, `async`, `unsafe`/`safe` and `extern "…"` have no meaning in the
|
||||
/// wasm ABI, and would otherwise pass silently into the generated trait.
|
||||
fn reject_modifiers(signature: &Signature, errors: &mut Vec<syn::Error>) {
|
||||
const PLAIN: &str =
|
||||
"a host function must be a plain `fn`: this modifier is not part of the wasm ABI";
|
||||
|
||||
if let Some(constness) = &signature.constness {
|
||||
errors.push(syn::Error::new_spanned(constness, PLAIN));
|
||||
}
|
||||
if let Some(asyncness) = &signature.asyncness {
|
||||
errors.push(syn::Error::new_spanned(asyncness, PLAIN));
|
||||
}
|
||||
match &signature.safety {
|
||||
Safety::Default => {}
|
||||
Safety::Safe(token) => errors.push(syn::Error::new_spanned(token, PLAIN)),
|
||||
Safety::Unsafe(token) => errors.push(syn::Error::new_spanned(token, PLAIN)),
|
||||
}
|
||||
if let Some(abi) = &signature.abi {
|
||||
errors.push(syn::Error::new_spanned(abi, PLAIN));
|
||||
}
|
||||
}
|
||||
|
||||
/// The wasm import name reaches the engine's import table verbatim, so it is
|
||||
/// held to what an import name can sanely be rather than to any string.
|
||||
fn check_wasm_name(name: &LitStr) -> syn::Result<()> {
|
||||
let value = name.value();
|
||||
if value.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
name,
|
||||
"the wasm name must not be empty",
|
||||
));
|
||||
}
|
||||
if let Some(character) = value
|
||||
.chars()
|
||||
.find(|c| !c.is_ascii_alphanumeric() && *c != '_')
|
||||
{
|
||||
return Err(syn::Error::new_spanned(
|
||||
name,
|
||||
format!(
|
||||
"a wasm name may only contain `A-Za-z0-9_`, but this one contains {character:?}"
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The enum variant a declaration becomes: `get_ledger_sqn` -> `GetLedgerSqn`.
|
||||
///
|
||||
/// The result carries `ident`'s span, so anything the compiler says about the
|
||||
/// variant points at the declaration that produced it.
|
||||
fn variant_ident(ident: &Ident) -> syn::Result<Ident> {
|
||||
// `to_string` spells raw identifiers `r#type`; the `r#` is not part of the name.
|
||||
let name = ident.to_string();
|
||||
let name = name.strip_prefix("r#").unwrap_or(&name);
|
||||
|
||||
let mut pascal = String::with_capacity(name.len());
|
||||
let mut capitalize = true;
|
||||
for character in name.chars() {
|
||||
if character == '_' {
|
||||
capitalize = true;
|
||||
} else if capitalize {
|
||||
pascal.extend(character.to_uppercase());
|
||||
capitalize = false;
|
||||
} else {
|
||||
pascal.push(character);
|
||||
}
|
||||
}
|
||||
|
||||
// A name of nothing but underscores leaves `pascal` empty; the original is
|
||||
// already a legal identifier, so keep it.
|
||||
if pascal.is_empty() {
|
||||
return Ok(ident.clone());
|
||||
}
|
||||
|
||||
// `Ident::new` panics on a leading digit (`_2fa` -> `2fa`) and silently
|
||||
// accepts keyword spellings (`self_` -> `Self`), which then fails to parse
|
||||
// where the variant is emitted. Parsing rejects both, without panicking.
|
||||
if let Err(error) = syn::parse_str::<Ident>(&pascal) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ident,
|
||||
format!(
|
||||
"this name becomes the enum variant `{pascal}`, which is not a valid \
|
||||
variant name ({error}); rename the host function"
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(format_ident!("{pascal}", span = ident.span()))
|
||||
}
|
||||
|
||||
/// Records `value`, or reports that the attribute appeared more than once.
|
||||
fn set_once<T>(slot: &mut Option<T>, value: T, attr: &Attribute) -> syn::Result<()> {
|
||||
if slot.replace(value).is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
attr,
|
||||
format!("duplicate `{}` attribute", path_name(attr)),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn int_value(attr: &Attribute) -> syn::Result<u64> {
|
||||
match &attr.meta.require_name_value()?.value {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Int(int), ..
|
||||
}) => {
|
||||
// `LitInt` keeps the sign in its digits, so `base10_parse::<u64>`
|
||||
// would report a negative value as "invalid digit found in string".
|
||||
if int.base10_digits().starts_with('-') {
|
||||
return Err(syn::Error::new_spanned(
|
||||
int,
|
||||
format!("`{}` must not be negative", path_name(attr)),
|
||||
));
|
||||
}
|
||||
int.base10_parse()
|
||||
}
|
||||
other => Err(syn::Error::new_spanned(
|
||||
other,
|
||||
format!("`{}` expects an integer literal", path_name(attr)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn string_value(attr: &Attribute) -> syn::Result<LitStr> {
|
||||
match &attr.meta.require_name_value()?.value {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(string),
|
||||
..
|
||||
}) => Ok(string.clone()),
|
||||
other => Err(syn::Error::new_spanned(
|
||||
other,
|
||||
format!("`{}` expects a string literal", path_name(attr)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The attribute's path as written, for diagnostics: `gas`, or `foo::bar`.
|
||||
fn path_name(attr: &Attribute) -> String {
|
||||
attr.path()
|
||||
.segments
|
||||
.iter()
|
||||
.map(|segment| segment.ident.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("::")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use quote::ToTokens;
|
||||
use syn::parse_quote;
|
||||
|
||||
/// The message of every diagnostic recorded by one failed `parse`.
|
||||
///
|
||||
/// `expect_err` is unavailable here: it needs `T: Debug`, and syn only
|
||||
/// implements `Debug` for its AST types under the `extra-traits` feature.
|
||||
fn messages(function: TraitItemFn) -> Vec<String> {
|
||||
let Err(error) = ParsedHostFunction::parse(function) else {
|
||||
panic!("expected parsing to fail");
|
||||
};
|
||||
error.into_iter().map(|error| error.to_string()).collect()
|
||||
}
|
||||
|
||||
fn doc_text(attr: &Attribute) -> String {
|
||||
match &attr.meta.require_name_value().unwrap().value {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(text),
|
||||
..
|
||||
}) => text.value(),
|
||||
_ => panic!("doc attribute is not a string literal"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_gas_and_wasm_name() {
|
||||
let parsed = ParsedHostFunction::parse(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(parsed.gas, 60);
|
||||
assert_eq!(parsed.wasm_name.value(), "ldgr_index");
|
||||
assert_eq!(parsed.signature.ident.to_string(), "get_ledger_sqn");
|
||||
assert_eq!(parsed.variant.to_string(), "GetLedgerSqn");
|
||||
assert!(parsed.docs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derives_variant_names_from_function_names() {
|
||||
for (function, variant) in [
|
||||
("get_ledger_sqn", "GetLedgerSqn"),
|
||||
("sha512_half", "Sha512Half"),
|
||||
("trace", "Trace"),
|
||||
("get_current_ledger_obj_field", "GetCurrentLedgerObjField"),
|
||||
("r#type", "Type"),
|
||||
("trace2", "Trace2"),
|
||||
// Pathological, but must not panic: no letters to capitalize.
|
||||
("__", "__"),
|
||||
] {
|
||||
let ident = format_ident!("{function}");
|
||||
assert_eq!(
|
||||
variant_ident(&ident).map(|v| v.to_string()).ok(),
|
||||
Some(variant.to_owned()),
|
||||
"{function}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `_2fa` would PascalCase to `2fa`; building that `Ident` panics, and a
|
||||
/// panic in a proc macro is reported with no useful span at all.
|
||||
#[test]
|
||||
fn rejects_a_name_that_becomes_a_leading_digit() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "two_factor"]
|
||||
fn _2fa(&self) -> HostResult<()>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("becomes the enum variant `2fa`"),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `self_` PascalCases to `Self`, which `Ident::new` accepts and rustc then
|
||||
/// rejects where the variant is emitted. `r#Self` is not a legal escape.
|
||||
#[test]
|
||||
fn rejects_a_name_that_becomes_a_keyword() {
|
||||
for function in ["self_", "_self"] {
|
||||
let ident = format_ident!("{function}");
|
||||
let Err(error) = variant_ident(&ident) else {
|
||||
panic!("expected `{function}` to be rejected");
|
||||
};
|
||||
assert!(
|
||||
error.to_string().contains("variant `Self`"),
|
||||
"{}",
|
||||
error.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_negative_gas() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = -5]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert_eq!(messages[0], "`gas` must not be negative");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unusable_wasm_names() {
|
||||
let empty = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = ""]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
assert_eq!(empty.len(), 1, "{empty:?}");
|
||||
assert_eq!(empty[0], "the wasm name must not be empty");
|
||||
|
||||
let spaced = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
assert_eq!(spaced.len(), 1, "{spaced:?}");
|
||||
assert!(spaced[0].contains("may only contain"), "{spaced:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_signature_modifiers() {
|
||||
for declaration in [
|
||||
quote! { unsafe fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>; },
|
||||
quote! { async fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>; },
|
||||
quote! { const fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>; },
|
||||
quote! { extern "C" fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>; },
|
||||
] {
|
||||
let function: TraitItemFn = syn::parse2(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
#declaration
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let messages = messages(function);
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("must be a plain `fn`"), "{messages:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trait_method_keeps_the_declared_receiver_and_ends_in_a_semicolon() {
|
||||
let parsed = ParsedHostFunction::parse(parse_quote! {
|
||||
/// Hashes `data`.
|
||||
#[gas = 2000]
|
||||
#[wasm_name = "sha512_half"]
|
||||
fn sha512_half(&self, data: &[u8]) -> HostResult<[u8; 32]>;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// `///` reaches the macro as `#[doc = r"..."]`: rustc's lexer spells doc
|
||||
// comments as raw string literals.
|
||||
let method = parsed.trait_method().to_string();
|
||||
assert!(
|
||||
method.starts_with("# [doc = r\" Hashes `data`.\"]"),
|
||||
"{method}"
|
||||
);
|
||||
assert!(
|
||||
method
|
||||
.contains("fn sha512_half (& self , data : & [u8]) -> HostResult < [u8 ; 32] > ;"),
|
||||
"{method}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_arm_carries_the_name_and_the_gas() {
|
||||
let parsed = ParsedHostFunction::parse(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed.spec_arm().to_string(),
|
||||
"Self :: GetLedgerSqn => HostFnSpec { name : \"ldgr_index\" , gas : 60u64 }"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_doc_comments_in_source_order() {
|
||||
let parsed = ParsedHostFunction::parse(parse_quote! {
|
||||
/// First line.
|
||||
///
|
||||
/// Third line.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let docs: Vec<_> = parsed.docs.iter().map(doc_text).collect();
|
||||
assert_eq!(docs, vec![" First line.", "", " Third line."]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_parameters_and_return_type() {
|
||||
let traced = ParsedHostFunction::parse(parse_quote! {
|
||||
#[gas = 500]
|
||||
#[wasm_name = "trace"]
|
||||
fn trace(&self, msg: &str, data: &[u8], as_hex: bool) -> HostResult<()>;
|
||||
})
|
||||
.unwrap();
|
||||
// The receiver is `inputs[0]`; the three wasm parameters follow it.
|
||||
assert_eq!(traced.signature.inputs.len(), 4);
|
||||
assert_eq!(
|
||||
traced.signature.output.to_token_stream().to_string(),
|
||||
"-> HostResult < () >"
|
||||
);
|
||||
|
||||
let hashed = ParsedHostFunction::parse(parse_quote! {
|
||||
#[gas = 2000]
|
||||
#[wasm_name = "sha512_half"]
|
||||
fn sha512_half(&self, data: &[u8]) -> HostResult<[u8; HASH_LEN]>;
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hashed.signature.output.to_token_stream().to_string(),
|
||||
"-> HostResult < [u8 ; HASH_LEN] >"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_both_missing_attributes_at_once() {
|
||||
let messages = messages(parse_quote! {
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert!(messages[0].contains("missing `#[gas"), "{messages:?}");
|
||||
assert!(messages[1].contains("missing `#[wasm_name"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_the_unexpected_attribute() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wsam_name = "typo"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
|
||||
// The typo'd attribute, plus the `wasm_name` it failed to be.
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert!(
|
||||
messages.iter().any(|m| m.contains("`wsam_name`")),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_wrong_literal_types() {
|
||||
let gas = messages(parse_quote! {
|
||||
#[gas = "60"]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
assert_eq!(gas.len(), 1, "{gas:?}");
|
||||
assert!(
|
||||
gas[0].contains("`gas` expects an integer literal"),
|
||||
"{gas:?}"
|
||||
);
|
||||
|
||||
let name = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = 7]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
assert_eq!(name.len(), 1, "{name:?}");
|
||||
assert!(
|
||||
name[0].contains("`wasm_name` expects a string literal"),
|
||||
"{name:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_gas_that_does_not_fit_in_u64() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = 99999999999999999999999]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("number too large"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_attribute_shapes_other_than_name_value() {
|
||||
let bare = messages(parse_quote! {
|
||||
#[gas]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
assert_eq!(bare.len(), 1, "{bare:?}");
|
||||
assert!(bare[0].contains("gas = ..."), "{bare:?}");
|
||||
|
||||
let list = messages(parse_quote! {
|
||||
#[gas(60)]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
assert_eq!(list.len(), 1, "{list:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_duplicate_attributes() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[gas = 70]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 2, "{messages:?}");
|
||||
assert!(messages[0].contains("duplicate `gas`"), "{messages:?}");
|
||||
assert!(
|
||||
messages[1].contains("duplicate `wasm_name`"),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A malformed attribute must not also be reported as an absent one.
|
||||
#[test]
|
||||
fn does_not_report_a_malformed_attribute_as_missing() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = "60"]
|
||||
#[wasm_name = 7]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 2, "{messages:?}");
|
||||
assert!(
|
||||
!messages.iter().any(|m| m.contains("missing")),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_body() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]> { Ok([0; 4]) }
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("must not have a body"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_generics() {
|
||||
let parameter = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn<T>(&self) -> HostResult<T>;
|
||||
});
|
||||
assert_eq!(parameter.len(), 1, "{parameter:?}");
|
||||
assert!(
|
||||
parameter[0].contains("must not be generic"),
|
||||
"{parameter:?}"
|
||||
);
|
||||
|
||||
let clause = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]> where Self: Sized;
|
||||
});
|
||||
assert_eq!(clause.len(), 1, "{clause:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_a_receiver() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn() -> HostResult<[u8; 4]>;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("must declare its receiver: `fn get_ledger_sqn(&self, ...)`"),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Anything but `&self` would need a host the VM cannot hand out: it holds
|
||||
/// one shared `&dyn HostFunctions` for the whole run.
|
||||
#[test]
|
||||
fn rejects_receivers_other_than_shared_self() {
|
||||
for receiver in [
|
||||
quote! { &mut self },
|
||||
quote! { self },
|
||||
quote! { mut self },
|
||||
quote! { self: Box<Self> },
|
||||
quote! { &'a self },
|
||||
] {
|
||||
let function: TraitItemFn = syn::parse2(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(#receiver) -> HostResult<[u8; 4]>;
|
||||
})
|
||||
.unwrap_or_else(|_| panic!("`{receiver}` should parse"));
|
||||
|
||||
let messages = messages(function);
|
||||
assert_eq!(messages.len(), 1, "`{receiver}`: {messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("must be exactly `&self`"),
|
||||
"`{receiver}`: {messages:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A bare `T` return would need its own lowering arm, so the uniform shape is
|
||||
/// required rather than inferred.
|
||||
#[test]
|
||||
fn rejects_returns_that_are_not_host_result() {
|
||||
for output in [
|
||||
quote! {},
|
||||
quote! { -> () },
|
||||
quote! { -> [u8; 4] },
|
||||
quote! { -> i32 },
|
||||
quote! { -> Result<[u8; 4], HostError> },
|
||||
quote! { -> impl Iterator<Item = u8> },
|
||||
] {
|
||||
let function: TraitItemFn = syn::parse2(quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) #output;
|
||||
})
|
||||
.unwrap_or_else(|_| panic!("`{output}` should parse"));
|
||||
|
||||
let messages = messages(function);
|
||||
assert_eq!(messages.len(), 1, "`{output}`: {messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("must return `HostResult<T>`"),
|
||||
"`{output}`: {messages:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `HostResult` may be written qualified, since the trait method keeps whatever
|
||||
/// path resolves where the block is written.
|
||||
#[test]
|
||||
fn accepts_a_qualified_host_result() {
|
||||
let parsed = ParsedHostFunction::parse(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> xrpl_host_functions::HostResult<[u8; 4]>;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
parsed
|
||||
.trait_method()
|
||||
.to_string()
|
||||
.contains("xrpl_host_functions :: HostResult < [u8 ; 4] >"),
|
||||
"{}",
|
||||
parsed.trait_method()
|
||||
);
|
||||
}
|
||||
|
||||
/// `HostResult` with no success type names no type at all; rustc's own error
|
||||
/// for that lands on the generated trait, far from the declaration.
|
||||
#[test]
|
||||
fn rejects_host_result_without_a_success_type() {
|
||||
let messages = messages(parse_quote! {
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self) -> HostResult;
|
||||
});
|
||||
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(
|
||||
messages[0].contains("needs its success type"),
|
||||
"{messages:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
7
crates/xrpl-host-functions/Cargo.toml
Normal file
7
crates/xrpl-host-functions/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "xrpl-host-functions"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xrpl-host-functions-macros.path = "../xrpl-host-functions-macros"
|
||||
539
crates/xrpl-host-functions/src/lib.rs
Normal file
539
crates/xrpl-host-functions/src/lib.rs
Normal file
@@ -0,0 +1,539 @@
|
||||
//! The wasm host ABI: the one place it is declared.
|
||||
//!
|
||||
//! `host_functions!` turns the declaration block at the bottom of this file into the
|
||||
//! [`HostFunctions`] trait a host implements and the [`HostFunctionSpec`] table a
|
||||
//! wasm engine registers from.
|
||||
//!
|
||||
//! The split: hand-written here is the vocabulary the declarations are written in —
|
||||
//! [`HostError`], [`TraceDataType`], [`HostResult`], [`HASH_LEN`] — and everything
|
||||
//! derived from the declarations is generated. The expansion names nothing this file
|
||||
//! does not, so the two sides meet only in the block below.
|
||||
//!
|
||||
//! So this file is lists — error codes, trace data types, functions. The `macro_rules!`
|
||||
//! that expand the first two into enums live in `macros.rs`.
|
||||
|
||||
#![no_std]
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
// Not re-exported: the ABI is declared once, here, and this is the only call site.
|
||||
use xrpl_host_functions_macros::host_functions;
|
||||
|
||||
host_errors! {
|
||||
Unimplemented = -1,
|
||||
FieldNotFound = -2,
|
||||
BufferTooSmall = -3,
|
||||
NoArray = -4,
|
||||
NotLeafField = -5,
|
||||
LocatorMalformed = -6,
|
||||
SlotOutRange = -7,
|
||||
SlotsFull = -8,
|
||||
EmptySlot = -9,
|
||||
LedgerObjNotFound = -10,
|
||||
OutOfTransferLimit = -11,
|
||||
DataFieldTooLarge = -12,
|
||||
PointerOutOfBounds = -13,
|
||||
NoMemExported = -14,
|
||||
InvalidParams = -15,
|
||||
InvalidAccount = -16,
|
||||
InvalidField = -17,
|
||||
IndexOutOfBounds = -18,
|
||||
FloatInputMalformed = -19,
|
||||
FloatComputationError = -20,
|
||||
/// Internal fatal error.
|
||||
/// User code will never see this error but keep it reserved to not rely on the value.
|
||||
InternalFatal = -2147483648,
|
||||
}
|
||||
|
||||
/// Convenience alias for the trait's fallible returns.
|
||||
pub type HostResult<T> = Result<T, HostError>;
|
||||
|
||||
/// A `sha512Half` digest: the first 32 bytes of a SHA-512, as XRPL uses it.
|
||||
pub const HASH_LEN: usize = 32;
|
||||
|
||||
trace_data_types! {
|
||||
/// 8 little-endian bytes, rendered as a signed decimal.
|
||||
Int64 = 1,
|
||||
/// 8 little-endian bytes, rendered as an unsigned decimal.
|
||||
Uint64 = 2,
|
||||
/// A serialized XRPL float: 12 bytes, mantissa then exponent.
|
||||
Xfloat = 3,
|
||||
/// A 20-byte account ID, rendered as base58.
|
||||
Account = 4,
|
||||
/// A serialized `STAmount`.
|
||||
Amount = 5,
|
||||
/// Raw bytes, hex-encoded.
|
||||
AsHex = 6,
|
||||
/// Bytes rendered verbatim as text.
|
||||
AsText = 7,
|
||||
}
|
||||
|
||||
host_functions! {
|
||||
/// The sequence number of the ledger being built, as 4 little-endian bytes.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "ldgr_index"]
|
||||
fn get_ledger_sqn(&self, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The close time of the parent (last-closed) ledger, as 4 little-endian bytes.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "parent_ldgr_time"]
|
||||
fn get_parent_ledger_time(&self, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The hash of the parent (last-closed) ledger, as 32 bytes.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "parent_ldgr_hash"]
|
||||
fn get_parent_ledger_hash(&self, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The base fee of the ledger being built, in drops, as 4 little-endian bytes.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "base_fee"]
|
||||
fn get_base_fee(&self, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// Whether an amendment is enabled. The input is either its 32-byte id or its
|
||||
/// name; the answer is `1` if enabled and `0` if not. Unlike the getters, this
|
||||
/// reads an input region and returns the flag directly rather than writing bytes.
|
||||
#[gas = 100]
|
||||
#[wasm_name = "amendment_enabled"]
|
||||
fn is_amendment_enabled(&self, amendment: &[u8]) -> HostResult<i32>;
|
||||
|
||||
/// Load the ledger object with the given 32-byte id into a cache slot, so later
|
||||
/// calls can read its fields. `cache_idx` selects the slot (1-based); `0` asks the
|
||||
/// host to assign a free one. Returns the slot used, or a negative error.
|
||||
#[gas = 5000]
|
||||
#[wasm_name = "cache_le"]
|
||||
fn cache_ledger_obj(&self, obj_id: &[u8], cache_idx: i32) -> HostResult<i32>;
|
||||
|
||||
/// The serialized bytes of one field of the transaction being executed, selected
|
||||
/// by its `SField` code.
|
||||
#[gas = 70]
|
||||
#[wasm_name = "tx_field"]
|
||||
fn get_tx_field(&self, field: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The serialized bytes of one field of the current (escrow) ledger object.
|
||||
#[gas = 70]
|
||||
#[wasm_name = "home_le_field"]
|
||||
fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The serialized bytes of one field of a previously cached ledger object,
|
||||
/// selected by its cache slot and the field's `SField` code.
|
||||
#[gas = 70]
|
||||
#[wasm_name = "le_field"]
|
||||
fn get_ledger_obj_field(&self, cache_idx: i32, field: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The serialized bytes of a nested field of the transaction, reached by a
|
||||
/// `locator`: a path of little-endian `i32` steps (so its byte length is a
|
||||
/// non-zero multiple of 4). Reads the locator region and writes the field bytes.
|
||||
#[gas = 110]
|
||||
#[wasm_name = "tx_inner"]
|
||||
fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The serialized bytes of a nested field of the current (escrow) ledger object,
|
||||
/// reached by a `locator`, as with [`Self::get_tx_nested_field`].
|
||||
#[gas = 110]
|
||||
#[wasm_name = "home_le_inner"]
|
||||
fn get_current_ledger_obj_nested_field(
|
||||
&self,
|
||||
locator: &[u8],
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The serialized bytes of a nested field of a previously cached ledger object,
|
||||
/// selected by its cache slot and reached by a `locator`.
|
||||
#[gas = 110]
|
||||
#[wasm_name = "le_inner"]
|
||||
fn get_ledger_obj_nested_field(
|
||||
&self,
|
||||
cache_idx: i32,
|
||||
locator: &[u8],
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The number of elements in an array field of the transaction, selected by its
|
||||
/// `SField` code. Answers the count directly, or a negative error (`NoArray` if
|
||||
/// the field is not an array). Reads and writes no memory.
|
||||
#[gas = 40]
|
||||
#[wasm_name = "tx_arr_len"]
|
||||
fn get_tx_array_len(&self, field: i32) -> HostResult<i32>;
|
||||
|
||||
/// The number of elements in an array field of the current (escrow) ledger
|
||||
/// object, as with [`Self::get_tx_array_len`].
|
||||
#[gas = 40]
|
||||
#[wasm_name = "home_le_arr_len"]
|
||||
fn get_current_ledger_obj_array_len(&self, field: i32) -> HostResult<i32>;
|
||||
|
||||
/// The number of elements in an array field of a previously cached ledger object,
|
||||
/// selected by its cache slot and `SField` code.
|
||||
#[gas = 40]
|
||||
#[wasm_name = "le_arr_len"]
|
||||
fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult<i32>;
|
||||
|
||||
/// The number of elements in a nested array field of the transaction, reached by a
|
||||
/// `locator`. Reads the locator region and answers the count directly.
|
||||
#[gas = 70]
|
||||
#[wasm_name = "tx_inner_arr_len"]
|
||||
fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult<i32>;
|
||||
|
||||
/// The number of elements in a nested array field of the current (escrow) ledger
|
||||
/// object, reached by a `locator`, as with [`Self::get_tx_nested_array_len`].
|
||||
#[gas = 70]
|
||||
#[wasm_name = "home_le_inner_arr_len"]
|
||||
fn get_current_ledger_obj_nested_array_len(&self, locator: &[u8]) -> HostResult<i32>;
|
||||
|
||||
/// The number of elements in a nested array field of a previously cached ledger
|
||||
/// object, selected by its cache slot and reached by a `locator`.
|
||||
#[gas = 70]
|
||||
#[wasm_name = "le_inner_arr_len"]
|
||||
fn get_ledger_obj_nested_array_len(&self, cache_idx: i32, locator: &[u8]) -> HostResult<i32>;
|
||||
|
||||
/// Verify `signature` over `message` under `pubkey`. Reads the three regions and
|
||||
/// answers `1` if the signature is valid, `0` if not, or a negative error.
|
||||
///
|
||||
/// GAS DISCREPANCY: this 300 is the value the C-ABI fork registered
|
||||
/// (`rippled-wasm-host-functions`, WasmVM.cpp), which this port follows. The
|
||||
/// prior C++ integration in this tree charged 35000 for the same call — 100x
|
||||
/// more, and closer to the real cost of signature verification. The value is
|
||||
/// consensus-critical, so confirm which is intended before this ships.
|
||||
#[gas = 300]
|
||||
#[wasm_name = "check_sig"]
|
||||
fn check_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
signature: &[u8],
|
||||
pubkey: &[u8],
|
||||
) -> HostResult<i32>;
|
||||
|
||||
/// The 32-byte ledger key (keylet) of an account's `AccountRoot`, computed from a
|
||||
/// 20-byte account id. Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "accountroot_id"]
|
||||
fn account_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of an AMM, computed from its two assets. Each asset is a
|
||||
/// byte slice whose length selects its kind (24 = MPT, 20 = XRP, 40 = issued
|
||||
/// currency + issuer). Reads both asset regions and writes the keylet.
|
||||
#[gas = 450]
|
||||
#[wasm_name = "amm_id"]
|
||||
fn amm_keylet(&self, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `Check`, computed from a 20-byte account id and its
|
||||
/// sequence number. `seq` is the guest's `u32` carried as its `i32` bit pattern.
|
||||
/// Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "check_id"]
|
||||
fn check_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `Credential`, computed from the 20-byte subject and
|
||||
/// issuer account ids and a credential-type byte string. Reads all three regions
|
||||
/// and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "credential_id"]
|
||||
fn credential_keylet(
|
||||
&self,
|
||||
subject: &[u8],
|
||||
issuer: &[u8],
|
||||
credential_type: &[u8],
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `Delegate` object, computed from the 20-byte account
|
||||
/// and the account it authorizes. Reads both account regions and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "delegate_id"]
|
||||
fn delegate_keylet(
|
||||
&self,
|
||||
account: &[u8],
|
||||
authorize: &[u8],
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `DepositPreauth`, computed from the 20-byte account and
|
||||
/// the account it authorizes to deposit. Reads both account regions and writes the
|
||||
/// keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "deposit_preauth_id"]
|
||||
fn deposit_preauth_keylet(
|
||||
&self,
|
||||
account: &[u8],
|
||||
authorize: &[u8],
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of an account's `DID`, computed from its 20-byte account id.
|
||||
/// Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "did_id"]
|
||||
fn did_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of an `Escrow`, computed from the 20-byte owner account and
|
||||
/// its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
|
||||
/// pattern. Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "escrow_id"]
|
||||
fn escrow_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `RippleState` (trust line), computed from two 20-byte
|
||||
/// account ids and a 20-byte currency. Reads all three regions and writes the
|
||||
/// keylet.
|
||||
#[gas = 400]
|
||||
#[wasm_name = "trustline_id"]
|
||||
fn trust_line_keylet(
|
||||
&self,
|
||||
account1: &[u8],
|
||||
account2: &[u8],
|
||||
currency: &[u8],
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of an `MPTokenIssuance`, computed from the 20-byte issuer
|
||||
/// account and its sequence number. `seq` is the guest's `u32` carried as its
|
||||
/// `i32` bit pattern. Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "mpt_issuance_id"]
|
||||
fn mptoken_issuance_keylet(
|
||||
&self,
|
||||
issuer: &[u8],
|
||||
seq: i32,
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of an `MPToken`, computed from a 24-byte MPT issuance id and
|
||||
/// the 20-byte holder account. Reads both regions and writes the keylet.
|
||||
#[gas = 500]
|
||||
#[wasm_name = "mptoken_id"]
|
||||
fn mptoken_keylet(&self, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of an `NFTokenOffer`, computed from the 20-byte owner account
|
||||
/// and its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
|
||||
/// pattern. Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "nft_offer_id"]
|
||||
fn nftoken_offer_keylet(
|
||||
&self,
|
||||
account: &[u8],
|
||||
seq: i32,
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of an `Offer`, computed from the 20-byte owner account and
|
||||
/// its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
|
||||
/// pattern. Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "offer_id"]
|
||||
fn offer_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of an `Oracle`, computed from the 20-byte owner account and
|
||||
/// its document id. `doc_id` is the guest's `u32` carried as its `i32` bit pattern.
|
||||
/// Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "oracle_id"]
|
||||
fn oracle_keylet(&self, account: &[u8], doc_id: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `PayChannel`, computed from the 20-byte source account,
|
||||
/// the 20-byte destination account, and the channel's sequence number. `seq` is the
|
||||
/// guest's `u32` carried as its `i32` bit pattern. Reads both account regions and
|
||||
/// writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "paychan_id"]
|
||||
fn paychannel_keylet(
|
||||
&self,
|
||||
account: &[u8],
|
||||
destination: &[u8],
|
||||
seq: i32,
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `PermissionedDomain`, computed from the 20-byte owner
|
||||
/// account and its sequence number. `seq` is the guest's `u32` carried as its `i32`
|
||||
/// bit pattern. Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "permissioned_domain_id"]
|
||||
fn permissioned_domain_keylet(
|
||||
&self,
|
||||
account: &[u8],
|
||||
seq: i32,
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `SignerList`, computed from its 20-byte owner account.
|
||||
/// Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "signers_id"]
|
||||
fn signer_list_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `Ticket`, computed from the 20-byte owner account and its
|
||||
/// ticket sequence number. `seq` is the guest's `u32` carried as its `i32` bit
|
||||
/// pattern. Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "ticket_id"]
|
||||
fn ticket_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 32-byte keylet of a `Vault`, computed from the 20-byte owner account and its
|
||||
/// sequence number. `seq` is the guest's `u32` carried as its `i32` bit pattern.
|
||||
/// Reads the account region and writes the keylet.
|
||||
#[gas = 350]
|
||||
#[wasm_name = "vault_id"]
|
||||
fn vault_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512.
|
||||
#[gas = 2000]
|
||||
#[wasm_name = "sha512_half"]
|
||||
fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// Writes `msg` to the trace log, followed by `data` rendered as `data_type` says.
|
||||
///
|
||||
/// The one declaration whose wasm function has **no result**: this node's own log
|
||||
/// is its only effect, so a guest is told nothing. An `Err` from a host therefore
|
||||
/// reaches it in no form, and only the host-fatal ones do anything at all.
|
||||
///
|
||||
/// It is also the one declaration that is **not** the wasm parameter order.
|
||||
/// `data_type` is the third wasm parameter, between the two regions, because that
|
||||
/// is where xrpld's `trace_proto` and the guest stdlib put it; `register.rs` takes
|
||||
/// the arguments in wasm order and calls this in declaration order.
|
||||
#[gas = 30]
|
||||
#[wasm_name = "trace"]
|
||||
fn trace(&self, msg: &str, data: &[u8], data_type: TraceDataType) -> HostResult<()>;
|
||||
|
||||
/// Stores `data` as the current object's data field, replacing whatever was there,
|
||||
/// and returns the number of bytes stored. Reads the data region; `DataFieldTooLarge`
|
||||
/// if it exceeds the host's limit.
|
||||
#[gas = 1000]
|
||||
#[wasm_name = "set_data"]
|
||||
fn update_data(&self, data: &[u8]) -> HostResult<i32>;
|
||||
|
||||
/// The URI of the `NFToken` with id `nft_id` (32 bytes) held by the 20-byte
|
||||
/// `account`. Reads both regions and writes the URI bytes.
|
||||
#[gas = 5000]
|
||||
#[wasm_name = "nft_uri"]
|
||||
fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The 20-byte issuer account encoded in the `NFToken` id `nft_id` (32 bytes).
|
||||
/// Reads the id region and writes the issuer bytes.
|
||||
#[gas = 70]
|
||||
#[wasm_name = "nft_issuer"]
|
||||
fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The taxon encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id region
|
||||
/// and writes the taxon as its four little-endian bytes.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "nft_taxon"]
|
||||
fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The flags encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id region
|
||||
/// and returns the flags as the call's scalar result.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "nft_flags"]
|
||||
fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult<i32>;
|
||||
|
||||
/// The transfer fee encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id
|
||||
/// region and returns the fee as the call's scalar result.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "nft_xfer_fee"]
|
||||
fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult<i32>;
|
||||
|
||||
/// The sequence number encoded in the `NFToken` id `nft_id` (32 bytes). Reads the
|
||||
/// id region and writes the sequence as its four little-endian bytes.
|
||||
#[gas = 60]
|
||||
#[wasm_name = "nft_serial"]
|
||||
fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
// A "float" here is an XRPL `Number` in its serialized form: a byte blob the guest
|
||||
// holds opaquely and hands back to these functions. Inputs and outputs that are
|
||||
// floats are byte regions; `mode` is the rounding mode, a scalar the guest chooses.
|
||||
|
||||
/// A float built from the signed integer `x` under rounding `mode`. Writes the
|
||||
/// float bytes; no input region.
|
||||
#[gas = 100]
|
||||
#[wasm_name = "float_from_int"]
|
||||
fn float_from_int(&self, x: i64, mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// A float built from the unsigned integer in the 8-byte region `x` under rounding
|
||||
/// `mode`. Reads the integer region and writes the float bytes.
|
||||
#[gas = 130]
|
||||
#[wasm_name = "float_from_uint"]
|
||||
fn float_from_uint(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// A float built from the serialized `STAmount` in `amount` under rounding `mode`.
|
||||
/// Reads the amount region and writes the float bytes.
|
||||
#[gas = 150]
|
||||
#[wasm_name = "float_from_stamount"]
|
||||
fn float_from_stamount(&self, amount: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// A float built from the serialized `STNumber` in `number` under rounding `mode`.
|
||||
/// Reads the number region and writes the float bytes.
|
||||
#[gas = 150]
|
||||
#[wasm_name = "float_from_stnumber"]
|
||||
fn float_from_stnumber(&self, number: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The float `x` rounded to a signed integer under rounding `mode`. Reads the float
|
||||
/// region and writes the integer as its eight little-endian bytes.
|
||||
#[gas = 130]
|
||||
#[wasm_name = "float_to_int"]
|
||||
fn float_to_int(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The float `x` split into its mantissa and exponent. Reads the float region and
|
||||
/// writes the mantissa (eight little-endian bytes) and the exponent (four little-
|
||||
/// endian bytes) to two separate output regions.
|
||||
#[gas = 130]
|
||||
#[wasm_name = "float_to_mant_exp"]
|
||||
fn float_to_mant_exp(
|
||||
&self,
|
||||
x: &[u8],
|
||||
mantissa_out: &mut [u8],
|
||||
exponent_out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// A float built from `mantissa` and `exponent` under rounding `mode`. Writes the
|
||||
/// float bytes; no input region.
|
||||
#[gas = 100]
|
||||
#[wasm_name = "float_from_mant_exp"]
|
||||
fn float_from_mant_exp(
|
||||
&self,
|
||||
mantissa: i64,
|
||||
exponent: i32,
|
||||
mode: i32,
|
||||
out: &mut [u8],
|
||||
) -> HostResult<usize>;
|
||||
|
||||
/// Compares floats `x` and `y`, returning a negative, zero, or positive scalar as
|
||||
/// `x` is less than, equal to, or greater than `y`. Reads both float regions.
|
||||
#[gas = 80]
|
||||
#[wasm_name = "float_cmp"]
|
||||
fn float_compare(&self, x: &[u8], y: &[u8]) -> HostResult<i32>;
|
||||
|
||||
/// The float sum `x + y` under rounding `mode`. Reads both float regions and writes
|
||||
/// the result bytes.
|
||||
#[gas = 160]
|
||||
#[wasm_name = "float_add"]
|
||||
fn float_add(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The float difference `x - y` under rounding `mode`. Reads both float regions and
|
||||
/// writes the result bytes.
|
||||
#[gas = 160]
|
||||
#[wasm_name = "float_sub"]
|
||||
fn float_subtract(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The float product `x * y` under rounding `mode`. Reads both float regions and
|
||||
/// writes the result bytes.
|
||||
#[gas = 300]
|
||||
#[wasm_name = "float_mult"]
|
||||
fn float_multiply(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The float quotient `x / y` under rounding `mode`. Reads both float regions and
|
||||
/// writes the result bytes.
|
||||
#[gas = 300]
|
||||
#[wasm_name = "float_div"]
|
||||
fn float_divide(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The `n`-th root of the float `x` under rounding `mode`. Reads the float region
|
||||
/// and writes the result bytes.
|
||||
#[gas = 5500]
|
||||
#[wasm_name = "float_root"]
|
||||
fn float_root(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
|
||||
/// The float `x` raised to the power `n` under rounding `mode`. Reads the float
|
||||
/// region and writes the result bytes.
|
||||
#[gas = 5500]
|
||||
#[wasm_name = "float_pow"]
|
||||
fn float_power(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult<usize>;
|
||||
}
|
||||
102
crates/xrpl-host-functions/src/macros.rs
Normal file
102
crates/xrpl-host-functions/src/macros.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
//! The `macro_rules!` behind the two hand-listed enums, [`crate::HostError`] and
|
||||
//! [`crate::TraceDataType`].
|
||||
//!
|
||||
//! Each takes one list of `Variant = code,` and expands the enum together with the
|
||||
//! `ALL`/`code`/`from_code` set that must not fall behind it. The lists themselves stay
|
||||
//! in `lib.rs`, beside the `host_functions!` block.
|
||||
|
||||
/// Declares [`crate::HostError`] from one list: the variants, `HostError::ALL` and
|
||||
/// `HostError::from_code`'s table all expand from the codes given.
|
||||
///
|
||||
/// One list is what makes `ALL` complete. Rust cannot enumerate an enum's
|
||||
/// variants — an exhaustive `match` forces an arm per variant but gives nothing to
|
||||
/// iterate — so a hand-written `ALL` beside a hand-written enum could only be kept
|
||||
/// in step by review, and `ALL`'s whole purpose is to be the set a test can trust.
|
||||
/// A code added to the list gains its `ALL` entry and its `from_code` arm by
|
||||
/// construction. `HostFunctionSpec::ALL` is complete the same way, from the
|
||||
/// `host_functions!` block.
|
||||
macro_rules! host_errors {
|
||||
($($(#[$doc:meta])* $variant:ident = $code:literal,)+) => {
|
||||
/// Error codes a host function may return.
|
||||
///
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum HostError {
|
||||
$($(#[$doc])* $variant = $code,)+
|
||||
}
|
||||
|
||||
impl HostError {
|
||||
/// Every error a host function may return, in code order.
|
||||
///
|
||||
/// The complete set, and complete by construction: a wasm engine's
|
||||
/// split between the codes it hands the guest and the conditions it
|
||||
/// traps on is a decision per variant, so the test that checks the
|
||||
/// split iterates this and a code added to the ABI cannot slip past it.
|
||||
pub const ALL: &'static [HostError] = &[$(HostError::$variant,)+];
|
||||
|
||||
/// The negative wire value a failed call returns. Every code but
|
||||
/// `InternalFatal` is one a guest reads off that value.
|
||||
#[inline]
|
||||
pub const fn code(self) -> i32 {
|
||||
self as i32
|
||||
}
|
||||
|
||||
/// Reconstruct a `HostError` from its wire code.
|
||||
///
|
||||
/// A code this ABI does not define is `InternalFatal`: an answer the
|
||||
/// caller cannot act on is the call not having been served, and that is
|
||||
/// the variant which says so. Positive values are not errors at all and go
|
||||
/// the same way, since this is reached only once a negative return has
|
||||
/// been read as a failure.
|
||||
pub const fn from_code(code: i32) -> HostError {
|
||||
match code {
|
||||
$($code => HostError::$variant,)+
|
||||
_ => HostError::InternalFatal,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Declares [`crate::TraceDataType`] from one list, so `TraceDataType::ALL`,
|
||||
/// `TraceDataType::code` and `TraceDataType::from_code` cannot fall behind the
|
||||
/// variants — the reason `host_errors!` above is written this way.
|
||||
macro_rules! trace_data_types {
|
||||
($($(#[$doc:meta])* $variant:ident = $code:literal,)+) => {
|
||||
/// How [`HostFunctions::trace`] is to read its data buffer.
|
||||
///
|
||||
/// The discriminants are wire values shared with the guest stdlib: append only,
|
||||
/// never renumber. They start at 1, so a zeroed argument names no type rather
|
||||
/// than the first one.
|
||||
///
|
||||
/// This is the declaration a guest and a host both compile against. The host
|
||||
/// side needs a second one — `cxx` cannot be a dependency here, since this
|
||||
/// crate also links into the guest — so `xrpl-wasm-vm-ffi` declares a shared
|
||||
/// enum for C++ and converts, exhaustively, from this.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum TraceDataType {
|
||||
$($(#[$doc])* $variant = $code,)+
|
||||
}
|
||||
|
||||
impl TraceDataType {
|
||||
/// Every data type a guest may name, in code order.
|
||||
pub const ALL: &'static [TraceDataType] = &[$(TraceDataType::$variant,)+];
|
||||
|
||||
/// The wire value a guest passes to name this type.
|
||||
#[inline]
|
||||
pub const fn code(self) -> i32 {
|
||||
self as i32
|
||||
}
|
||||
|
||||
/// The type `code` names, or `None`: the engine drops a call it cannot
|
||||
/// read rather than guessing at a rendering the guest did not ask for.
|
||||
pub const fn from_code(code: i32) -> Option<TraceDataType> {
|
||||
match code {
|
||||
$($code => Some(TraceDataType::$variant),)+
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
34
crates/xrpl-host-functions/tests/expansion_hygiene.rs
Normal file
34
crates/xrpl-host-functions/tests/expansion_hygiene.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
//! `host_functions!` must work outside the crate that declares the ABI: the only
|
||||
//! names its expansion needs are the ones the declarations themselves spell.
|
||||
|
||||
use xrpl_host_functions::HostResult;
|
||||
use xrpl_host_functions_macros::host_functions;
|
||||
|
||||
host_functions! {
|
||||
/// Answers with the number it was given.
|
||||
#[gas = 7]
|
||||
#[wasm_name = "ping"]
|
||||
fn ping(&self, number: i32) -> HostResult<i32>;
|
||||
}
|
||||
|
||||
struct Host;
|
||||
|
||||
impl HostFunctions for Host {
|
||||
fn ping(&self, number: i32) -> HostResult<i32> {
|
||||
Ok(number)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_generated_table_stands_on_its_own() {
|
||||
assert_eq!(HostFunctionSpec::ALL.len(), 1);
|
||||
assert_eq!(HostFunctionSpec::Ping.wasm_name(), "ping");
|
||||
assert_eq!(HostFunctionSpec::Ping.gas(), 7);
|
||||
}
|
||||
|
||||
/// The generated trait is implementable from another crate, which is the point of
|
||||
/// declaring the ABI in a library at all.
|
||||
#[test]
|
||||
fn the_generated_trait_is_implementable_here() {
|
||||
assert_eq!(Host.ping(3), Ok(3));
|
||||
}
|
||||
1003
crates/xrpl-host-functions/tests/generated_abi.rs
Normal file
1003
crates/xrpl-host-functions/tests/generated_abi.rs
Normal file
File diff suppressed because it is too large
Load Diff
102
crates/xrpl-host-functions/tests/host_errors.rs
Normal file
102
crates/xrpl-host-functions/tests/host_errors.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
//! Exercises what `host_errors!` generates: the wire codes, the set
|
||||
//! [`HostError::ALL`] names, and the round trip between them.
|
||||
//!
|
||||
//! The codes are consensus input — they are what a guest reads off a failed host
|
||||
//! call — so they are pinned here as literals and derived everywhere else.
|
||||
|
||||
use xrpl_host_functions::HostError;
|
||||
|
||||
/// The whole set, written out in the order `ALL` gives it: the one place the wire
|
||||
/// codes appear as literals, and a deliberate change-detector, since a code that
|
||||
/// moves changes what every deployed guest is told.
|
||||
#[test]
|
||||
fn the_error_table_matches_the_declarations() {
|
||||
let table: Vec<(HostError, i32)> = HostError::ALL
|
||||
.iter()
|
||||
.map(|&error| (error, error.code()))
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
table,
|
||||
[
|
||||
(HostError::Unimplemented, -1),
|
||||
(HostError::FieldNotFound, -2),
|
||||
(HostError::BufferTooSmall, -3),
|
||||
(HostError::NoArray, -4),
|
||||
(HostError::NotLeafField, -5),
|
||||
(HostError::LocatorMalformed, -6),
|
||||
(HostError::SlotOutRange, -7),
|
||||
(HostError::SlotsFull, -8),
|
||||
(HostError::EmptySlot, -9),
|
||||
(HostError::LedgerObjNotFound, -10),
|
||||
(HostError::OutOfTransferLimit, -11),
|
||||
(HostError::DataFieldTooLarge, -12),
|
||||
(HostError::PointerOutOfBounds, -13),
|
||||
(HostError::NoMemExported, -14),
|
||||
(HostError::InvalidParams, -15),
|
||||
(HostError::InvalidAccount, -16),
|
||||
(HostError::InvalidField, -17),
|
||||
(HostError::IndexOutOfBounds, -18),
|
||||
(HostError::FloatInputMalformed, -19),
|
||||
(HostError::FloatComputationError, -20),
|
||||
(HostError::InternalFatal, i32::MIN),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// The guest-facing set is `-1 ..= -20` and nothing else: those entries are xrpld's
|
||||
/// `HostFunctionError`, and each is a code some contract may read.
|
||||
///
|
||||
/// `InternalFatal` is the one deliberate exception, exempted by name rather than by
|
||||
/// widening the range: a condition with no number a contract can act on needs no number
|
||||
/// in the range a contract reads, and holding it at `i32::MIN` is what keeps it from
|
||||
/// ever colliding with a code appended to xrpld's list.
|
||||
#[test]
|
||||
fn every_code_but_the_sentinel_is_in_the_shared_range() {
|
||||
let shared: Vec<HostError> = HostError::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&error| error != HostError::InternalFatal)
|
||||
.collect();
|
||||
|
||||
let outside: Vec<HostError> = shared
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|error| !(-20..=-1).contains(&error.code()))
|
||||
.collect();
|
||||
|
||||
assert!(outside.is_empty(), "outside -1..=-20: {outside:?}");
|
||||
assert_eq!(shared.len(), 20);
|
||||
assert_eq!(HostError::InternalFatal.code(), i32::MIN);
|
||||
assert_eq!(HostError::ALL.len(), 21);
|
||||
}
|
||||
|
||||
/// Every code a guest can be handed comes back as the error that produced it, so a
|
||||
/// caller reading a negative return value recovers the condition and not a
|
||||
/// neighbouring one. The table above pins the numbers; this adds only the round
|
||||
/// trip.
|
||||
#[test]
|
||||
fn every_wire_code_round_trips_back_to_its_error() {
|
||||
for &error in HostError::ALL {
|
||||
assert_eq!(HostError::from_code(error.code()), error, "{error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A code from outside the set is `InternalFatal`: a host answering something this ABI
|
||||
/// does not define has not served the call, whatever it meant by it, and success is not
|
||||
/// an error at all.
|
||||
///
|
||||
/// `-21` is the code xrpld would append next, so it is the one that decides whether a
|
||||
/// list this crate has not caught up with reaches a guest or stops the run. `i32::MIN +
|
||||
/// 1` is next to the sentinel and unassigned, which is what makes the sentinel a value
|
||||
/// rather than a range.
|
||||
#[test]
|
||||
fn a_code_outside_the_set_is_internal_fatal() {
|
||||
for code in [-21, i32::MIN + 1, 0, 1, i32::MAX] {
|
||||
assert_eq!(
|
||||
HostError::from_code(code),
|
||||
HostError::InternalFatal,
|
||||
"{code}"
|
||||
);
|
||||
}
|
||||
}
|
||||
11
crates/xrpl-wasm-testkit/Cargo.toml
Normal file
11
crates/xrpl-wasm-testkit/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "xrpl-wasm-testkit"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
cxx.workspace = true
|
||||
wat = "1"
|
||||
49
crates/xrpl-wasm-testkit/src/lib.rs
Normal file
49
crates/xrpl-wasm-testkit/src/lib.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Assembles WebAssembly text for the C++ test suite. **Test-only.**
|
||||
//!
|
||||
//! A crate of its own rather than an entry on `xrpl-wasm-vm-ffi`, and the separation is the
|
||||
//! point. The engine pins `wasmi = { default-features = false }` precisely so a text
|
||||
//! assembler cannot reach the consensus path — wasmi's `wat` feature is on by default and
|
||||
//! makes `Module::new` accept text as readily as binary, which would make a transaction's
|
||||
//! validity a build flag (review finding A5). Putting `compile_wat` on the production bridge
|
||||
//! would link `wat` into xrpld even if nothing called it.
|
||||
//!
|
||||
//! Linked only into `xrpl_tests`, never into `libxrpl` or `xrpld`, so "no assembler in the
|
||||
//! shipped node" is a property of the link graph rather than a flag someone can flip.
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
|
||||
#[cxx::bridge(namespace = "rs::wasm_testkit")]
|
||||
mod ffi {
|
||||
extern "Rust" {
|
||||
/// Assemble `wat` to a wasm module.
|
||||
///
|
||||
/// Throws `rust::Error` on invalid input, which is what a test wants: a typo in a
|
||||
/// fixture should fail the test that holds it, at the line that holds it.
|
||||
fn compile_wat(wat: &str) -> Result<Vec<u8>>;
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_wat(wat: &str) -> Result<Vec<u8>, wat::Error> {
|
||||
wat::parse_str(wat)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::compile_wat;
|
||||
|
||||
#[test]
|
||||
fn a_module_assembles_to_something_beginning_with_the_wasm_magic() {
|
||||
let wasm = compile_wat("(module)").expect("assembles");
|
||||
|
||||
assert_eq!(&wasm[..4], b"\0asm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_typo_is_an_error_rather_than_a_module() {
|
||||
let error = compile_wat("(module (func (export").expect_err("must not assemble");
|
||||
|
||||
assert!(
|
||||
!error.to_string().is_empty(),
|
||||
"the error has to say something"
|
||||
);
|
||||
}
|
||||
}
|
||||
12
crates/xrpl-wasm-vm-ffi/Cargo.toml
Normal file
12
crates/xrpl-wasm-vm-ffi/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "xrpl-wasm-vm-ffi"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
cxx.workspace = true
|
||||
xrpl-host-functions = { path = "../xrpl-host-functions" }
|
||||
xrpl-wasm-vm = { path = "../xrpl-wasm-vm" }
|
||||
1287
crates/xrpl-wasm-vm-ffi/src/lib.rs
Normal file
1287
crates/xrpl-wasm-vm-ffi/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
11
crates/xrpl-wasm-vm/Cargo.toml
Normal file
11
crates/xrpl-wasm-vm/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "xrpl-wasm-vm"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wasmi = { version = "1.1.0", default-features = false, features = ["std"] }
|
||||
xrpl-host-functions = { path = "../xrpl-host-functions" }
|
||||
|
||||
[dev-dependencies]
|
||||
wat = "1"
|
||||
810
crates/xrpl-wasm-vm/src/abi.rs
Normal file
810
crates/xrpl-wasm-vm/src/abi.rs
Normal file
@@ -0,0 +1,810 @@
|
||||
use crate::region::Region;
|
||||
use crate::vm::{MAX_FIELD_BYTES, VmState};
|
||||
use wasmi::{Caller, Memory};
|
||||
use xrpl_host_functions::{HostError, HostFunctionSpec, HostFunctions, HostResult};
|
||||
|
||||
/// A condition that stops the run. It is a property of the run rather than an answer
|
||||
/// to a call, so it reaches no guest and carries no wire code — which is why it is
|
||||
/// not a [`HostError`]: no host can report one and no contract can read one.
|
||||
///
|
||||
/// The three are the outcomes a host call can end a run with, and
|
||||
/// `From<Fault> for RunError` in `vm.rs` is where each gets its name.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Fault {
|
||||
/// This call's charge would take the meter below zero. The guest exhausting the
|
||||
/// meter with its own instructions reaches [`crate::vm::RunError::OutOfGas`] by
|
||||
/// wasmi's `OutOfFuel` trap instead, never through here.
|
||||
OutOfGas,
|
||||
/// The call could not be served: either the host said so, or this engine's own
|
||||
/// fuel meter did not answer.
|
||||
Internal,
|
||||
/// There is no linear memory to work in — the module exports none, or the call
|
||||
/// came from a start section, which runs before there is an instance.
|
||||
NoMemory,
|
||||
}
|
||||
|
||||
/// How a host call fails: with a code the guest reads off the return value, or with a
|
||||
/// [`Fault`] that stops the run.
|
||||
///
|
||||
/// **The variant picks the channel.** [`to_wire`] reads it rather than asking a
|
||||
/// predicate, so the two cannot disagree, and a [`FatalHostError`] cannot be built
|
||||
/// around something a guest was supposed to see.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CallError {
|
||||
Code(HostError),
|
||||
Fatal(Fault),
|
||||
}
|
||||
|
||||
/// A host call's result inside the engine: [`HostResult`] plus the faults only the
|
||||
/// engine can raise.
|
||||
pub(crate) type CallResult<T> = Result<T, CallError>;
|
||||
|
||||
/// Which channel a host's answer takes, decided once, here.
|
||||
///
|
||||
/// Three codes stop the run instead of reaching the contract that asked. Each says the
|
||||
/// call was not served at all — the host could not do it, it has not been wired, or
|
||||
/// there is nowhere to put the answer — and a contract has no business interpreting
|
||||
/// any of them, so it is told nothing and the run ends. Every other code is the
|
||||
/// contract's to read.
|
||||
impl From<HostError> for CallError {
|
||||
fn from(error: HostError) -> CallError {
|
||||
match error {
|
||||
HostError::InternalFatal => CallError::Fatal(Fault::Internal),
|
||||
HostError::Unimplemented => CallError::Fatal(Fault::Internal),
|
||||
HostError::NoMemExported => CallError::Fatal(Fault::NoMemory),
|
||||
code => CallError::Code(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The payload a trap carries so [`crate::vm::run`] can name the outcome without
|
||||
/// parsing a message. Holds a [`Fault`], so by construction no guest-visible code can
|
||||
/// leave through this channel.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct FatalHostError(pub(crate) Fault);
|
||||
|
||||
impl wasmi::errors::HostError for FatalHostError {}
|
||||
|
||||
impl core::fmt::Display for FatalHostError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "host call refused: {:?}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge the call's gas, run its body, put the result on the wire. The one path
|
||||
/// every registered closure takes, so gas cannot be forgotten.
|
||||
pub(crate) fn charged(
|
||||
caller: &mut Caller<'_, VmState<'_>>,
|
||||
op: HostFunctionSpec,
|
||||
body: impl FnOnce(&mut Caller<'_, VmState<'_>>) -> CallResult<i32>,
|
||||
) -> Result<i32, wasmi::Error> {
|
||||
to_wire(charge(caller, op.gas()).and_then(|()| body(caller)))
|
||||
}
|
||||
|
||||
/// [`charged`] for a call the guest gets no answer from: its wasm function has no
|
||||
/// result, so a soft error has nowhere to go and is dropped. The gas is charged first
|
||||
/// and charged whatever happens after, so the cost is all such a call leaves behind.
|
||||
///
|
||||
/// Only `trace` takes this path.
|
||||
pub(crate) fn charged_unreported(
|
||||
caller: &mut Caller<'_, VmState<'_>>,
|
||||
op: HostFunctionSpec,
|
||||
body: impl FnOnce(&mut Caller<'_, VmState<'_>>) -> CallResult<()>,
|
||||
) -> Result<(), wasmi::Error> {
|
||||
dropped(charge(caller, op.gas()).and_then(|()| body(caller)))
|
||||
}
|
||||
|
||||
/// [`to_wire`] for a call with no result: there is no return value to encode a code
|
||||
/// in, so it is dropped. A [`Fault`] still stops the run — that is a property of the
|
||||
/// run, not an answer to the call.
|
||||
fn dropped(result: CallResult<()>) -> Result<(), wasmi::Error> {
|
||||
match result {
|
||||
Err(CallError::Fatal(fault)) => Err(wasmi::Error::host(FatalHostError(fault))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_wire(result: CallResult<i32>) -> Result<i32, wasmi::Error> {
|
||||
match result {
|
||||
Ok(value) => Ok(value),
|
||||
Err(CallError::Code(error)) => Ok(error.code()),
|
||||
Err(CallError::Fatal(fault)) => Err(wasmi::Error::host(FatalHostError(fault))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deduct `cost` fuel; [`Fault::OutOfGas`] if it would go negative.
|
||||
///
|
||||
/// A meter that will not answer is this crate's own defect, not the contract's, so it
|
||||
/// is [`Fault::Internal`] rather than a number a guest could act on.
|
||||
fn charge<T>(caller: &mut Caller<'_, T>, cost: u64) -> CallResult<()> {
|
||||
let remaining = caller
|
||||
.get_fuel()
|
||||
.map_err(|_| CallError::Fatal(Fault::Internal))?;
|
||||
match remaining.checked_sub(cost) {
|
||||
Some(left) => caller
|
||||
.set_fuel(left)
|
||||
.map_err(|_| CallError::Fatal(Fault::Internal)),
|
||||
None => {
|
||||
let _ = caller.set_fuel(0);
|
||||
Err(CallError::Fatal(Fault::OutOfGas))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn charge_transfer(state: &VmState<'_>, n: usize) -> Result<(), HostError> {
|
||||
let n = n as u64;
|
||||
let remaining = state.transfer_budget.get();
|
||||
match remaining.checked_sub(n) {
|
||||
Some(left) => {
|
||||
state.transfer_budget.set(left);
|
||||
Ok(())
|
||||
}
|
||||
None => Err(HostError::OutOfTransferLimit),
|
||||
}
|
||||
}
|
||||
|
||||
fn memory(caller: &Caller<'_, VmState<'_>>) -> CallResult<Memory> {
|
||||
caller
|
||||
.data()
|
||||
.memory
|
||||
.ok_or(CallError::Fatal(Fault::NoMemory))
|
||||
}
|
||||
|
||||
/// [`Region::read`] of the guest's memory, for a call that reads and writes nothing
|
||||
/// back (`trace`).
|
||||
pub(crate) fn read_borrowed<'a>(
|
||||
caller: &'a Caller<'_, VmState<'_>>,
|
||||
input: Region,
|
||||
) -> CallResult<&'a [u8]> {
|
||||
let mem = memory(caller)?;
|
||||
Ok(input.read(mem.data(caller))?)
|
||||
}
|
||||
|
||||
/// Decode a guest `u32` argument — a keylet's sequence number or document id — from
|
||||
/// its four little-endian bytes, carried on to the host as its `i32` bit pattern.
|
||||
///
|
||||
/// The ABI transports these as a 4-byte region rather than a wasm scalar (the guest
|
||||
/// SDK passes `seq.to_le_bytes()`), so the region must be exactly four bytes;
|
||||
/// `InvalidParams` otherwise, matching the C-ABI wrapper's `getDataUInt32`.
|
||||
pub(crate) fn read_u32_arg(bytes: &[u8]) -> HostResult<i32> {
|
||||
let arr: [u8; 4] = bytes.try_into().map_err(|_| HostError::InvalidParams)?;
|
||||
Ok(i32::from_le_bytes(arr))
|
||||
}
|
||||
|
||||
/// Service a call whose answer is bytes, written straight into the guest's output
|
||||
/// region.
|
||||
///
|
||||
/// **`fill` returns the value's true length, not what it wrote**: a host holding 64
|
||||
/// bytes and offered room for 4 writes nothing and answers `64`, which is how the
|
||||
/// guest learns the size to ask for. So `n` is bounded by neither the region nor the
|
||||
/// cap, and both checks below are reachable.
|
||||
pub(crate) fn write_into(
|
||||
caller: &mut Caller<'_, VmState<'_>>,
|
||||
out: Region,
|
||||
fill: impl FnOnce(&dyn HostFunctions, &mut [u8]) -> HostResult<usize>,
|
||||
) -> CallResult<i32> {
|
||||
let range = out.range()?;
|
||||
let cap = range.len();
|
||||
let mem = memory(caller)?;
|
||||
let host: &dyn HostFunctions = caller.data().host;
|
||||
// Bounds-checked over the guest's whole declared region, so a buffer running
|
||||
// past memory is a wrong pointer rather than a truncated prefix being served…
|
||||
let buf = mem
|
||||
.data_mut(&mut *caller)
|
||||
.get_mut(range)
|
||||
.ok_or(HostError::PointerOutOfBounds)?;
|
||||
// …of which only the field cap is writable, so no call can exceed it whatever
|
||||
// the guest declared.
|
||||
let buf = &mut buf[..cap.min(MAX_FIELD_BYTES)];
|
||||
|
||||
let n = fill(host, buf)?;
|
||||
|
||||
if n > MAX_FIELD_BYTES {
|
||||
return Err(HostError::DataFieldTooLarge.into());
|
||||
}
|
||||
if n > cap {
|
||||
return Err(HostError::BufferTooSmall.into());
|
||||
}
|
||||
charge_transfer(caller.data(), n)?;
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
reason = "`n > MAX_FIELD_BYTES` returned above, and the cap is far inside i32"
|
||||
)]
|
||||
let n = n as i32;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Service a call that reads guest memory and writes bytes back to it: the host
|
||||
/// fills the run's output buffer, which is copied to the guest once every rule has
|
||||
/// passed.
|
||||
///
|
||||
/// `call` gets the guest's whole memory, so it can borrow any number of input
|
||||
/// regions with [`Region::read`] — which a `&mut` view of that memory would forbid.
|
||||
/// That is why the answer goes through a buffer instead of straight into the guest
|
||||
/// as [`write_into`]'s does.
|
||||
///
|
||||
/// **The host is never told the guest's capacity**: it is offered the whole buffer
|
||||
/// and reports the value's true length, so the fit is decided here, with nothing yet
|
||||
/// in guest memory. A refused value therefore reaches it in no part.
|
||||
///
|
||||
/// The output is judged after the inputs, so a call with both bad reports the
|
||||
/// input's verdict. `NoMemExported` precedes both: there is no memory to validate a
|
||||
/// region against.
|
||||
pub(crate) fn write_buffered(
|
||||
caller: &mut Caller<'_, VmState<'_>>,
|
||||
out: Region,
|
||||
call: impl FnOnce(&dyn HostFunctions, &[u8], &mut [u8]) -> HostResult<usize>,
|
||||
) -> CallResult<i32> {
|
||||
let mem = memory(caller)?;
|
||||
// One borrow split in two: the guest's bytes for the inputs, the store data for
|
||||
// the output buffer. Taking them together is what keeps the inputs borrowed
|
||||
// rather than copied out.
|
||||
let (data, state) = mem.data_and_store_mut(&mut *caller);
|
||||
let host: &dyn HostFunctions = state.host;
|
||||
|
||||
let n = call(host, data, &mut state.out_buffer[..])?;
|
||||
|
||||
// `out` is checked here rather than before the call: the inputs are judged
|
||||
// first, so a call with both malformed reports the input's verdict.
|
||||
let range = out.range()?;
|
||||
let cap = range.len();
|
||||
if n > MAX_FIELD_BYTES {
|
||||
return Err(HostError::DataFieldTooLarge.into());
|
||||
}
|
||||
let buf = data.get_mut(range).ok_or(HostError::PointerOutOfBounds)?;
|
||||
if n > cap {
|
||||
return Err(HostError::BufferTooSmall.into());
|
||||
}
|
||||
charge_transfer(state, n)?;
|
||||
buf[..n].copy_from_slice(&state.out_buffer[..n]);
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
reason = "`n > MAX_FIELD_BYTES` returned above, and the cap is far inside i32"
|
||||
)]
|
||||
let n = n as i32;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// The mantissa and exponent widths `float_to_mant_exp` writes: an `i64` and an `i32`.
|
||||
/// Fixed by the ABI, not the guest, so the split is a constant rather than a reported
|
||||
/// length.
|
||||
const MANTISSA_BYTES: usize = 8;
|
||||
const EXPONENT_BYTES: usize = 4;
|
||||
|
||||
/// Service `float_to_mant_exp`, the one call that writes two output regions: the host
|
||||
/// fills the run's output buffer with the mantissa followed by the exponent, and each
|
||||
/// is copied to its own guest region once every rule has passed.
|
||||
///
|
||||
/// Like [`write_buffered`], the host reads its input from the guest's memory and writes
|
||||
/// to a scratch buffer, so the input stays borrowed rather than copied. The two output
|
||||
/// regions are judged after the input, and the mantissa's region before the exponent's,
|
||||
/// so the first fault reported is the leftmost.
|
||||
pub(crate) fn write_mant_exp(
|
||||
caller: &mut Caller<'_, VmState<'_>>,
|
||||
mantissa_out: Region,
|
||||
exponent_out: Region,
|
||||
call: impl FnOnce(&dyn HostFunctions, &[u8], &mut [u8], &mut [u8]) -> HostResult<usize>,
|
||||
) -> CallResult<i32> {
|
||||
let mem = memory(caller)?;
|
||||
let (data, state) = mem.data_and_store_mut(&mut *caller);
|
||||
let host: &dyn HostFunctions = state.host;
|
||||
|
||||
// The scratch buffer is split at the fixed mantissa width: the host fills the first
|
||||
// eight bytes with the mantissa and the next four with the exponent.
|
||||
let (mant_buf, exp_buf) = state.out_buffer.split_at_mut(MANTISSA_BYTES);
|
||||
let mant_buf = &mut mant_buf[..MANTISSA_BYTES];
|
||||
let exp_buf = &mut exp_buf[..EXPONENT_BYTES];
|
||||
|
||||
let total = call(host, data, mant_buf, exp_buf)?;
|
||||
|
||||
// Copy the mantissa, then the exponent, each only if its whole value fits its
|
||||
// region — a region too small is `BufferTooSmall`, with nothing written.
|
||||
let mant_range = mantissa_out.range()?;
|
||||
let mant_dst = data
|
||||
.get_mut(mant_range)
|
||||
.ok_or(HostError::PointerOutOfBounds)?;
|
||||
if mant_dst.len() < MANTISSA_BYTES {
|
||||
return Err(HostError::BufferTooSmall.into());
|
||||
}
|
||||
mant_dst[..MANTISSA_BYTES].copy_from_slice(&state.out_buffer[..MANTISSA_BYTES]);
|
||||
|
||||
let exp_range = exponent_out.range()?;
|
||||
let exp_dst = data
|
||||
.get_mut(exp_range)
|
||||
.ok_or(HostError::PointerOutOfBounds)?;
|
||||
if exp_dst.len() < EXPONENT_BYTES {
|
||||
return Err(HostError::BufferTooSmall.into());
|
||||
}
|
||||
exp_dst[..EXPONENT_BYTES]
|
||||
.copy_from_slice(&state.out_buffer[MANTISSA_BYTES..MANTISSA_BYTES + EXPONENT_BYTES]);
|
||||
|
||||
charge_transfer(state, MANTISSA_BYTES + EXPONENT_BYTES)?;
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
reason = "the total is 12, far inside i32"
|
||||
)]
|
||||
let total = total as i32;
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::vm::TRANSFER_LIMIT_BYTES;
|
||||
use std::cell::Cell;
|
||||
use wasmi::StoreLimitsBuilder;
|
||||
use xrpl_host_functions::TraceDataType;
|
||||
|
||||
/// `charge_transfer` takes the store data, which has to hold a host.
|
||||
struct UncalledHost;
|
||||
|
||||
impl HostFunctions for UncalledHost {
|
||||
fn get_ledger_sqn(&self, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_parent_ledger_time(&self, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_parent_ledger_hash(&self, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_base_fee(&self, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn is_amendment_enabled(&self, _amendment: &[u8]) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn cache_ledger_obj(&self, _obj_id: &[u8], _cache_idx: i32) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_tx_field(&self, _field: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_current_ledger_obj_field(&self, _field: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_ledger_obj_field(
|
||||
&self,
|
||||
_cache_idx: i32,
|
||||
_field: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_tx_nested_field(&self, _locator: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_current_ledger_obj_nested_field(
|
||||
&self,
|
||||
_locator: &[u8],
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_ledger_obj_nested_field(
|
||||
&self,
|
||||
_cache_idx: i32,
|
||||
_locator: &[u8],
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_tx_array_len(&self, _field: i32) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_current_ledger_obj_array_len(&self, _field: i32) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_ledger_obj_array_len(&self, _cache_idx: i32, _field: i32) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_tx_nested_array_len(&self, _locator: &[u8]) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_current_ledger_obj_nested_array_len(&self, _locator: &[u8]) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_ledger_obj_nested_array_len(
|
||||
&self,
|
||||
_cache_idx: i32,
|
||||
_locator: &[u8],
|
||||
) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn check_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_signature: &[u8],
|
||||
_pubkey: &[u8],
|
||||
) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn account_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn amm_keylet(&self, _asset1: &[u8], _asset2: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn check_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn credential_keylet(
|
||||
&self,
|
||||
_subject: &[u8],
|
||||
_issuer: &[u8],
|
||||
_credential_type: &[u8],
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn delegate_keylet(
|
||||
&self,
|
||||
_account: &[u8],
|
||||
_authorize: &[u8],
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn deposit_preauth_keylet(
|
||||
&self,
|
||||
_account: &[u8],
|
||||
_authorize: &[u8],
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn did_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn escrow_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn trust_line_keylet(
|
||||
&self,
|
||||
_account1: &[u8],
|
||||
_account2: &[u8],
|
||||
_currency: &[u8],
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn mptoken_issuance_keylet(
|
||||
&self,
|
||||
_issuer: &[u8],
|
||||
_seq: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn mptoken_keylet(
|
||||
&self,
|
||||
_mptid: &[u8],
|
||||
_holder: &[u8],
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn nftoken_offer_keylet(
|
||||
&self,
|
||||
_account: &[u8],
|
||||
_seq: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn offer_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn oracle_keylet(
|
||||
&self,
|
||||
_account: &[u8],
|
||||
_doc_id: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn paychannel_keylet(
|
||||
&self,
|
||||
_account: &[u8],
|
||||
_destination: &[u8],
|
||||
_seq: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn permissioned_domain_keylet(
|
||||
&self,
|
||||
_account: &[u8],
|
||||
_seq: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn signer_list_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn ticket_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn vault_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn sha512_half(&self, _data: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn trace(&self, _msg: &str, _data: &[u8], _data_type: TraceDataType) -> HostResult<()> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn update_data(&self, _data: &[u8]) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_nft(&self, _account: &[u8], _nft_id: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_nft_issuer(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_nft_taxon(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_nft_flags(&self, _nft_id: &[u8]) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_nft_transfer_fee(&self, _nft_id: &[u8]) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn get_nft_sequence(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_from_int(&self, _x: i64, _mode: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_from_uint(&self, _x: &[u8], _mode: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_from_stamount(
|
||||
&self,
|
||||
_amount: &[u8],
|
||||
_mode: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_from_stnumber(
|
||||
&self,
|
||||
_number: &[u8],
|
||||
_mode: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_to_int(&self, _x: &[u8], _mode: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_to_mant_exp(
|
||||
&self,
|
||||
_x: &[u8],
|
||||
_mantissa_out: &mut [u8],
|
||||
_exponent_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_from_mant_exp(
|
||||
&self,
|
||||
_mantissa: i64,
|
||||
_exponent: i32,
|
||||
_mode: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_compare(&self, _x: &[u8], _y: &[u8]) -> HostResult<i32> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_add(
|
||||
&self,
|
||||
_x: &[u8],
|
||||
_y: &[u8],
|
||||
_mode: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_subtract(
|
||||
&self,
|
||||
_x: &[u8],
|
||||
_y: &[u8],
|
||||
_mode: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_multiply(
|
||||
&self,
|
||||
_x: &[u8],
|
||||
_y: &[u8],
|
||||
_mode: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_divide(
|
||||
&self,
|
||||
_x: &[u8],
|
||||
_y: &[u8],
|
||||
_mode: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_root(&self, _x: &[u8], _n: i32, _mode: i32, _out: &mut [u8]) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
fn float_power(
|
||||
&self,
|
||||
_x: &[u8],
|
||||
_n: i32,
|
||||
_mode: i32,
|
||||
_out: &mut [u8],
|
||||
) -> HostResult<usize> {
|
||||
unreachable!("no unit test in this module calls the host")
|
||||
}
|
||||
}
|
||||
|
||||
fn state(budget: u64) -> VmState<'static> {
|
||||
VmState {
|
||||
host: &UncalledHost,
|
||||
mem_limits: StoreLimitsBuilder::new().build(),
|
||||
transfer_budget: Cell::new(budget),
|
||||
memory: None,
|
||||
out_buffer: [0u8; MAX_FIELD_BYTES],
|
||||
}
|
||||
}
|
||||
|
||||
/// `wasmi::Error` is not `PartialEq`, so a test expecting the guest-visible
|
||||
/// channel says so by going through here.
|
||||
fn wire(result: CallResult<i32>) -> i32 {
|
||||
to_wire(result)
|
||||
.unwrap_or_else(|trap| panic!("expected a guest-visible status, got a trap: {trap}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_success_becomes_the_value_and_an_error_becomes_its_code() {
|
||||
assert_eq!(wire(Ok(0)), 0);
|
||||
assert_eq!(wire(Ok(32)), 32);
|
||||
assert_eq!(wire(Err(HostError::BufferTooSmall.into())), -3);
|
||||
}
|
||||
|
||||
/// The codes a host may answer that a contract must not see, and the fault each
|
||||
/// becomes. Written out rather than derived from `From<HostError>`, which is what
|
||||
/// they are asserting.
|
||||
const STOPS_THE_RUN: [(HostError, Fault); 3] = [
|
||||
(HostError::InternalFatal, Fault::Internal),
|
||||
(HostError::Unimplemented, Fault::Internal),
|
||||
(HostError::NoMemExported, Fault::NoMemory),
|
||||
];
|
||||
|
||||
/// Every fault, so the two tests below are the whole set and not a sample.
|
||||
/// `From<Fault> for RunError` is what forces a fault added later to be
|
||||
/// considered; this is what forces it to be tested.
|
||||
const ALL_FAULTS: [Fault; 3] = [Fault::OutOfGas, Fault::Internal, Fault::NoMemory];
|
||||
|
||||
#[test]
|
||||
fn a_code_that_stops_the_run_converts_to_its_fault() {
|
||||
for (error, fault) in STOPS_THE_RUN {
|
||||
assert_eq!(CallError::from(error), CallError::Fatal(fault), "{error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Over `HostError::ALL`, so it is the whole ABI and not a sample: a code added
|
||||
/// to the ABI arrives already asserted to reach the guest as itself, and stopping
|
||||
/// the run on it is then a change someone has to come and make.
|
||||
///
|
||||
/// `OutOfTransferLimit` is the row worth reading twice: the one budget a
|
||||
/// contract can be expected to handle, so it is told no rather than killed.
|
||||
#[test]
|
||||
fn every_other_code_reaches_the_guest_as_itself() {
|
||||
for &error in HostError::ALL {
|
||||
if STOPS_THE_RUN.iter().any(|&(stops, _)| stops == error) {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(CallError::from(error), CallError::Code(error), "{error:?}");
|
||||
assert_eq!(wire(Err(error.into())), error.code(), "{error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The trap carries the fault, so `run` can name the outcome without parsing a
|
||||
/// message.
|
||||
#[test]
|
||||
fn a_fault_becomes_a_trap_carrying_it() {
|
||||
for fault in ALL_FAULTS {
|
||||
let trap = to_wire(Err(CallError::Fatal(fault)))
|
||||
.expect_err("a fault must not reach the guest as a code");
|
||||
let payload = trap.downcast_ref::<FatalHostError>().unwrap_or_else(|| {
|
||||
panic!("{fault:?}: expected a FatalHostError payload, got: {trap}")
|
||||
});
|
||||
assert_eq!(*payload, FatalHostError(fault));
|
||||
}
|
||||
}
|
||||
|
||||
/// The result-less path splits the same two channels differently: a fault still
|
||||
/// stops the run, and every code is dropped, since `trace` has no return value to
|
||||
/// carry it. Over `HostError::ALL` for the reason above — a code added to the ABI
|
||||
/// arrives asserted against both paths.
|
||||
#[test]
|
||||
fn a_call_with_no_result_drops_a_code_and_traps_on_a_fault() {
|
||||
assert!(dropped(Ok(())).is_ok());
|
||||
|
||||
for &error in HostError::ALL {
|
||||
if let CallError::Code(code) = CallError::from(error) {
|
||||
assert!(
|
||||
dropped(Err(CallError::Code(code))).is_ok(),
|
||||
"{error:?} has no channel to the guest and must be dropped"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for fault in ALL_FAULTS {
|
||||
let trap =
|
||||
dropped(Err(CallError::Fatal(fault))).expect_err("a fault must stop the run");
|
||||
let payload = trap.downcast_ref::<FatalHostError>().unwrap_or_else(|| {
|
||||
panic!("{fault:?}: expected a FatalHostError payload, got: {trap}")
|
||||
});
|
||||
assert_eq!(*payload, FatalHostError(fault));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_transfer_spends_the_budget() {
|
||||
let state = state(100);
|
||||
|
||||
assert_eq!(charge_transfer(&state, 30), Ok(()));
|
||||
assert_eq!(state.transfer_budget.get(), 70);
|
||||
assert_eq!(charge_transfer(&state, 70), Ok(()));
|
||||
assert_eq!(state.transfer_budget.get(), 0);
|
||||
}
|
||||
|
||||
/// The budget bounds the total, so the transfer that would overrun it is
|
||||
/// refused whole rather than partially charged.
|
||||
#[test]
|
||||
fn a_transfer_past_the_budget_is_refused_and_charges_nothing() {
|
||||
let state = state(100);
|
||||
|
||||
assert_eq!(
|
||||
charge_transfer(&state, 101),
|
||||
Err(HostError::OutOfTransferLimit)
|
||||
);
|
||||
assert_eq!(
|
||||
state.transfer_budget.get(),
|
||||
100,
|
||||
"a refusal must not charge"
|
||||
);
|
||||
assert_eq!(charge_transfer(&state, 100), Ok(()));
|
||||
assert_eq!(
|
||||
charge_transfer(&state, 1),
|
||||
Err(HostError::OutOfTransferLimit)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transferring_nothing_costs_nothing() {
|
||||
let state = state(0);
|
||||
|
||||
assert_eq!(charge_transfer(&state, 0), Ok(()));
|
||||
assert_eq!(state.transfer_budget.get(), 0);
|
||||
}
|
||||
|
||||
/// The field cap holds one call to a small share of the run's budget, so the
|
||||
/// budget bounds a run rather than a call. An inequality, not the two values:
|
||||
/// those are pinned in `vm.rs`.
|
||||
#[test]
|
||||
fn no_single_value_can_exhaust_the_run_budget() {
|
||||
assert!(
|
||||
(MAX_FIELD_BYTES as u64) * 64 <= TRANSFER_LIMIT_BYTES,
|
||||
"one {MAX_FIELD_BYTES}-byte value against a {TRANSFER_LIMIT_BYTES}-byte budget"
|
||||
);
|
||||
}
|
||||
}
|
||||
28
crates/xrpl-wasm-vm/src/lib.rs
Normal file
28
crates/xrpl-wasm-vm/src/lib.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! The escrow wasm VM: compile a contract, meter it, and serve its host calls.
|
||||
//!
|
||||
//! Every guest access goes through `abi.rs` and reaches linear memory only by
|
||||
//! wasmi's bounds-checked slice operations; `forbid(unsafe_code)` makes that a
|
||||
//! property rather than a claim. The cast lints are on for the same reason — on a
|
||||
//! consensus path a truncating or sign-losing cast changes what a contract is
|
||||
//! charged or told, so each one is argued for at its site.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![deny(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_lossless
|
||||
)]
|
||||
|
||||
mod abi;
|
||||
mod preflight;
|
||||
mod region;
|
||||
mod register;
|
||||
mod vm;
|
||||
|
||||
pub use preflight::{CheckError, check};
|
||||
pub use vm::{
|
||||
MAX_FIELD_BYTES, MAX_MEMORY_BYTES, MAX_MEMORY_PAGES, RunError, RunFailure, RunOutcome,
|
||||
TRANSFER_LIMIT_BYTES, run,
|
||||
};
|
||||
351
crates/xrpl-wasm-vm/src/preflight.rs
Normal file
351
crates/xrpl-wasm-vm/src/preflight.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
//! Screening a contract before it reaches the ledger.
|
||||
//!
|
||||
//! [`check`] answers whether [`crate::run`] would refuse a module before the
|
||||
//! guest's first instruction — the three stages a caller maps to a malformed
|
||||
//! transaction rather than to a failed one. It needs **no host, no store and no
|
||||
//! gas**: everything it reads is a property of the compiled module. That is what
|
||||
//! makes it callable from a transaction's preflight, which has no ledger to serve
|
||||
//! host calls from.
|
||||
//!
|
||||
//! Two things it deliberately does not screen. A module exporting **no** linear
|
||||
//! memory passes: a contract that makes no host call needs none, and one that
|
||||
//! does is refused at the call and charged for what it burned. A start section
|
||||
//! passes: it is guest code, and executing it is the one thing a check must not do
|
||||
//! — a trap in one is charged to the contract like any other trap.
|
||||
//!
|
||||
//! One thing it screens that a run can only discover: an exported memory larger
|
||||
//! than the engine grants. See [`check_memory`] for what stays invisible.
|
||||
|
||||
use std::fmt;
|
||||
use wasmi::{ExternType, FuncType, Module, ValType};
|
||||
use xrpl_host_functions::HostFunctionSpec;
|
||||
|
||||
use crate::register::HOST_MODULE;
|
||||
use crate::vm::{MAX_MEMORY_PAGES, compile};
|
||||
|
||||
/// Why a module cannot be run. One variant per stage, since the caller maps the
|
||||
/// stages separately.
|
||||
#[derive(Debug)]
|
||||
pub enum CheckError {
|
||||
/// `wasm` is not a valid module under this engine's configuration.
|
||||
Compile(String),
|
||||
/// An import no engine of this ABI defines: another module namespace, a name
|
||||
/// that is not a host function, or one imported as something other than a
|
||||
/// function.
|
||||
Import(String),
|
||||
/// No export named `function_name` with signature `() -> i32`.
|
||||
EntryPoint(String),
|
||||
/// The module asks for more linear memory than the engine grants.
|
||||
Memory(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for CheckError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CheckError::Compile(detail) => write!(f, "compile: {detail}"),
|
||||
CheckError::Import(detail) => write!(f, "import: {detail}"),
|
||||
// The detail says which of the entry point's failures this is, since
|
||||
// "no entry point" would be wrong for an export of the wrong type.
|
||||
CheckError::EntryPoint(detail) => write!(f, "{detail}"),
|
||||
CheckError::Memory(detail) => write!(f, "memory: {detail}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Screen `wasm`: it must compile, import only what the engine serves, export
|
||||
/// `function_name` as `() -> i32`, and ask for no more memory than it may have.
|
||||
///
|
||||
/// The stages are ordered by how much of the module each explains. An import fault
|
||||
/// is reported before a missing entry point because the imports are what the rest of
|
||||
/// the module is built on; memory comes last, being a resource request rather than a
|
||||
/// mistake about the ABI.
|
||||
pub fn check(wasm: &[u8], function_name: &str) -> Result<(), CheckError> {
|
||||
let module = compile(wasm).map_err(CheckError::Compile)?;
|
||||
check_imports(&module)?;
|
||||
check_entry_point(&module, function_name)?;
|
||||
check_memory(&module)
|
||||
}
|
||||
|
||||
/// Every import must be one the linker defines. The first that is not ends the
|
||||
/// check, so a module with several faults reports the earliest.
|
||||
fn check_imports(module: &Module) -> Result<(), CheckError> {
|
||||
for import in module.imports() {
|
||||
check_import(import.module(), import.name(), import.ty()).map_err(CheckError::Import)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the engine defines this one import.
|
||||
///
|
||||
/// The set of names is [`HostFunctionSpec::ALL`], which is also what
|
||||
/// [`crate::register::register_host_functions`] iterates — so a check and a run
|
||||
/// cannot disagree about which names exist, and adding a host function extends
|
||||
/// both at once. The one thing this does not compare is `ty`'s *signature*, which
|
||||
/// still parts a module from the engine at instantiation; the kind is compared
|
||||
/// because the engine defines these names as functions and as nothing else.
|
||||
///
|
||||
/// The rules are ordered, not merely alternatives: a guest importing `env::malloc`
|
||||
/// is told about the namespace rather than that `malloc` is not a host function,
|
||||
/// because the namespace is the one that explains every other import it has too.
|
||||
fn check_import(module: &str, name: &str, ty: &ExternType) -> Result<(), String> {
|
||||
if module != HOST_MODULE {
|
||||
return Err(format!("'{module}::{name}' is not from '{HOST_MODULE}'"));
|
||||
}
|
||||
if !HostFunctionSpec::ALL
|
||||
.iter()
|
||||
.any(|op| op.wasm_name() == name)
|
||||
{
|
||||
return Err(format!("no host function '{name}'"));
|
||||
}
|
||||
if !matches!(ty, ExternType::Func(_)) {
|
||||
return Err(format!("'{HOST_MODULE}::{name}' is not a function"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_entry_point(module: &Module, name: &str) -> Result<(), CheckError> {
|
||||
match module.get_export(name) {
|
||||
Some(ExternType::Func(ty)) if is_entry_point(&ty) => Ok(()),
|
||||
found => Err(CheckError::EntryPoint(entry_point_fault(found, name))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The entry point's type: nothing in, one `i32` out — what [`crate::run`]'s
|
||||
/// `get_typed_func::<(), i32>` accepts.
|
||||
fn is_entry_point(ty: &FuncType) -> bool {
|
||||
ty.params().is_empty() && matches!(ty.results(), [ValType::I32])
|
||||
}
|
||||
|
||||
/// A module may not declare more linear memory than the engine grants.
|
||||
///
|
||||
/// Only what it *exports* is visible here. A memory a module keeps to itself is not
|
||||
/// in its exports, and the store's limiter is what refuses that one — at
|
||||
/// instantiation, where the run is charged nothing and the caller cannot tell it
|
||||
/// from any other resource failure. Screening the exported case covers every
|
||||
/// contract built against the guest SDK, since a contract needs an exported memory
|
||||
/// to make a host call at all.
|
||||
fn check_memory(module: &Module) -> Result<(), CheckError> {
|
||||
for export in module.exports() {
|
||||
if let ExternType::Memory(ty) = export.ty() {
|
||||
check_initial_pages(ty.minimum()).map_err(CheckError::Memory)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the engine will grant a memory of this declared initial size.
|
||||
///
|
||||
/// The *minimum* only: a declared maximum past the cap is legal and simply
|
||||
/// unreachable, which `vm_limits::a_declared_maximum_past_the_cap_is_allowed_but_
|
||||
/// unreachable` pins on the run side. Refusing it here would turn a runnable
|
||||
/// contract away.
|
||||
fn check_initial_pages(pages: u64) -> Result<(), String> {
|
||||
if pages > u64::from(MAX_MEMORY_PAGES) {
|
||||
return Err(format!(
|
||||
"initial memory of {pages} pages is past the {MAX_MEMORY_PAGES}-page cap"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How an entry-point lookup failed, in the words both stages use: a check and a
|
||||
/// run describe the same module the same way, and "no entry point" would send a
|
||||
/// contract author looking for a function they already have.
|
||||
pub(crate) fn entry_point_fault(found: Option<ExternType>, name: &str) -> String {
|
||||
match found {
|
||||
Some(ExternType::Func(_)) => {
|
||||
format!("entry point '{name}' has the wrong signature, expected '() -> i32'")
|
||||
}
|
||||
Some(_) => format!("export '{name}' is not a function"),
|
||||
None => format!("no entry point '{name}'"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The rules, one by one, on inputs built directly rather than parsed out of a
|
||||
/// module. `tests/preflight.rs` runs real modules through [`check`]; what is here is
|
||||
/// what a module cannot state precisely — which rule fires, in which order, and in
|
||||
/// what words the caller logs it.
|
||||
///
|
||||
/// `wat` is a dev-dependency, so the one test here that does need a module writes it
|
||||
/// as text like every other test in the crate. What the library must not gain is a
|
||||
/// text *entry point* — `check` and `run` take binaries — and a `cfg(test)` caller
|
||||
/// cannot give it one.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasmi::{GlobalType, MemoryType, Mutability};
|
||||
|
||||
/// A host function as a guest declares it. Any function type will do: the
|
||||
/// signature is not what [`check_import`] compares.
|
||||
fn a_function() -> ExternType {
|
||||
ExternType::Func(FuncType::new([ValType::I32], [ValType::I32]))
|
||||
}
|
||||
|
||||
/// A name every one of these tests can use, taken from the ABI rather than
|
||||
/// spelled, so it stays a real host function as the ABI changes.
|
||||
fn a_host_function_name() -> &'static str {
|
||||
HostFunctionSpec::ALL[0].wasm_name()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Imports
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Every name the ABI declares is served. Derived from `ALL` rather than
|
||||
/// listed, so a host function added to the ABI is covered the day it lands.
|
||||
#[test]
|
||||
fn every_declared_host_function_is_served() {
|
||||
for op in HostFunctionSpec::ALL {
|
||||
assert_eq!(
|
||||
check_import(HOST_MODULE, op.wasm_name(), &a_function()),
|
||||
Ok(()),
|
||||
"{}",
|
||||
op.wasm_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_import_from_another_namespace_is_refused() {
|
||||
for namespace in ["env", "host", "host_lib2", ""] {
|
||||
let refusal = check_import(namespace, a_host_function_name(), &a_function())
|
||||
.expect_err(namespace);
|
||||
assert!(
|
||||
refusal.contains("is not from 'host_lib'"),
|
||||
"{namespace}: {refusal}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_name_is_refused() {
|
||||
let refusal =
|
||||
check_import(HOST_MODULE, "no_such_function", &a_function()).expect_err("unknown name");
|
||||
assert_eq!(refusal, "no host function 'no_such_function'");
|
||||
}
|
||||
|
||||
/// The engine defines these names as functions and as nothing else, so a module
|
||||
/// importing one as a global or a memory does not link either.
|
||||
#[test]
|
||||
fn a_host_function_imported_as_anything_else_is_refused() {
|
||||
for ty in [
|
||||
ExternType::Global(GlobalType::new(ValType::I32, Mutability::Const)),
|
||||
ExternType::Memory(MemoryType::new(1, None)),
|
||||
] {
|
||||
let name = a_host_function_name();
|
||||
let refusal = check_import(HOST_MODULE, name, &ty).expect_err("not a function");
|
||||
assert_eq!(refusal, format!("'host_lib::{name}' is not a function"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The rules are ordered. An import that breaks two of them is reported by the
|
||||
/// first, so the message a contract author reads is the one that explains the
|
||||
/// rest of their imports too.
|
||||
#[test]
|
||||
fn the_namespace_is_reported_before_the_name() {
|
||||
let refusal = check_import("env", "no_such_function", &a_function())
|
||||
.expect_err("neither the namespace nor the name is served");
|
||||
|
||||
assert!(refusal.contains("is not from 'host_lib'"), "{refusal}");
|
||||
assert!(
|
||||
!refusal.contains("no host function"),
|
||||
"the namespace explains it: {refusal}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Both halves of the type are load-bearing, and neither is checked anywhere
|
||||
/// a module cannot reach.
|
||||
#[test]
|
||||
fn the_entry_point_type_is_nothing_in_and_one_i32_out() {
|
||||
assert!(is_entry_point(&FuncType::new([], [ValType::I32])));
|
||||
|
||||
for wrong in [
|
||||
FuncType::new([], []),
|
||||
FuncType::new([], [ValType::I64]),
|
||||
FuncType::new([ValType::I32], [ValType::I32]),
|
||||
FuncType::new([], [ValType::I32, ValType::I32]),
|
||||
] {
|
||||
assert!(!is_entry_point(&wrong), "{wrong:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Three faults, three descriptions. A run reports these too, with wasmi's own
|
||||
/// error appended, so a swapped arm would mislead at both stages at once.
|
||||
#[test]
|
||||
fn each_entry_point_fault_is_described_as_itself() {
|
||||
assert_eq!(
|
||||
entry_point_fault(Some(a_function()), "finish"),
|
||||
"entry point 'finish' has the wrong signature, expected '() -> i32'"
|
||||
);
|
||||
assert_eq!(
|
||||
entry_point_fault(
|
||||
Some(ExternType::Global(GlobalType::new(
|
||||
ValType::I32,
|
||||
Mutability::Const
|
||||
))),
|
||||
"finish"
|
||||
),
|
||||
"export 'finish' is not a function"
|
||||
);
|
||||
assert_eq!(
|
||||
entry_point_fault(None, "finish"),
|
||||
"no entry point 'finish'",
|
||||
"an absent export must not be reported as a wrong signature"
|
||||
);
|
||||
}
|
||||
|
||||
/// The cap itself is granted; one page past it is not. The boundary is the whole
|
||||
/// rule, and it is the same boundary the store's limiter applies at
|
||||
/// instantiation.
|
||||
#[test]
|
||||
fn the_initial_memory_may_reach_the_cap_but_not_pass_it() {
|
||||
assert_eq!(check_initial_pages(0), Ok(()));
|
||||
assert_eq!(check_initial_pages(u64::from(MAX_MEMORY_PAGES)), Ok(()));
|
||||
|
||||
let past = u64::from(MAX_MEMORY_PAGES) + 1;
|
||||
let refusal = check_initial_pages(past).expect_err("one page past the cap");
|
||||
assert_eq!(
|
||||
refusal,
|
||||
format!("initial memory of {past} pages is past the {MAX_MEMORY_PAGES}-page cap")
|
||||
);
|
||||
}
|
||||
|
||||
/// The bridge logs this string and the C++ tests match on it, so the stage's
|
||||
/// prefix is part of the interface rather than a debugging aid.
|
||||
#[test]
|
||||
fn a_refusal_names_its_stage() {
|
||||
assert_eq!(
|
||||
CheckError::Compile("bad magic".to_string()).to_string(),
|
||||
"compile: bad magic"
|
||||
);
|
||||
assert_eq!(
|
||||
CheckError::Memory("initial memory of 129 pages".to_string()).to_string(),
|
||||
"memory: initial memory of 129 pages"
|
||||
);
|
||||
assert_eq!(
|
||||
CheckError::Import("no host function 'x'".to_string()).to_string(),
|
||||
"import: no host function 'x'"
|
||||
);
|
||||
// The entry point's detail already says which of its three faults it is,
|
||||
// so a prefix would only repeat it.
|
||||
assert_eq!(
|
||||
CheckError::EntryPoint("no entry point 'finish'".to_string()).to_string(),
|
||||
"no entry point 'finish'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_stages_run_in_order() {
|
||||
assert!(
|
||||
matches!(check(b"not wasm", "finish"), Err(CheckError::Compile(_))),
|
||||
"nothing is screened until the module compiles"
|
||||
);
|
||||
|
||||
// A module that compiles and imports nothing, so it reaches the entry point.
|
||||
let empty = wat::parse_str("(module)").expect("assembles");
|
||||
assert!(
|
||||
matches!(check(&empty, "finish"), Err(CheckError::EntryPoint(_))),
|
||||
"a module that compiles and imports nothing reaches the entry point"
|
||||
);
|
||||
}
|
||||
}
|
||||
50
crates/xrpl-wasm-vm/src/region.rs
Normal file
50
crates/xrpl-wasm-vm/src/region.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::vm::MAX_FIELD_BYTES;
|
||||
use core::ops::Range;
|
||||
use xrpl_host_functions::{HostError, HostResult};
|
||||
|
||||
/// A byte region as the guest declared it: the `(ptr, len)` pair off the wire, not
|
||||
/// yet checked.
|
||||
///
|
||||
/// Every byte parameter in this ABI is such a pair, so pairing them once at the wire
|
||||
/// boundary is what keeps the helpers in `abi.rs` from each taking two loose integers
|
||||
/// they could be handed in either order.
|
||||
///
|
||||
/// It lives in a module of its own so that the fields are out of reach and
|
||||
/// [`range`](Region::range) is the *only* way to indices — the check cannot be
|
||||
/// skipped, only deferred. Construction is infallible for that reason: a call whose
|
||||
/// output region is malformed is then refused in the order its own helper chooses,
|
||||
/// rather than at the moment the pair happened to be formed.
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct Region {
|
||||
ptr: i32,
|
||||
len: i32,
|
||||
}
|
||||
|
||||
impl Region {
|
||||
pub(crate) fn new(ptr: i32, len: i32) -> Region {
|
||||
Region { ptr, len }
|
||||
}
|
||||
|
||||
/// `start..end` as indices. The conversion is the negativity check — it fails on
|
||||
/// exactly the negative values — and the addition guards a 32-bit `usize`, where
|
||||
/// two `i32`s can sum past the end.
|
||||
pub(crate) fn range(self) -> HostResult<Range<usize>> {
|
||||
let (Ok(start), Ok(len)) = (usize::try_from(self.ptr), usize::try_from(self.len)) else {
|
||||
return Err(HostError::InvalidParams);
|
||||
};
|
||||
let end = start
|
||||
.checked_add(len)
|
||||
.ok_or(HostError::PointerOutOfBounds)?;
|
||||
Ok(start..end)
|
||||
}
|
||||
|
||||
/// The region's bytes, refused past the field cap. No copy: the slice aliases
|
||||
/// `data`.
|
||||
pub(crate) fn read(self, data: &[u8]) -> HostResult<&[u8]> {
|
||||
let range = self.range()?;
|
||||
if range.len() > MAX_FIELD_BYTES {
|
||||
return Err(HostError::DataFieldTooLarge);
|
||||
}
|
||||
data.get(range).ok_or(HostError::PointerOutOfBounds)
|
||||
}
|
||||
}
|
||||
1215
crates/xrpl-wasm-vm/src/register.rs
Normal file
1215
crates/xrpl-wasm-vm/src/register.rs
Normal file
File diff suppressed because it is too large
Load Diff
353
crates/xrpl-wasm-vm/src/vm.rs
Normal file
353
crates/xrpl-wasm-vm/src/vm.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
use std::cell::Cell;
|
||||
use std::fmt;
|
||||
use std::sync::LazyLock;
|
||||
use wasmi::{
|
||||
Config, Engine, Export, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder,
|
||||
TrapCode,
|
||||
};
|
||||
use xrpl_host_functions::HostFunctions;
|
||||
|
||||
use crate::abi::{FatalHostError, Fault};
|
||||
use crate::preflight::entry_point_fault;
|
||||
use crate::register::register_host_functions;
|
||||
|
||||
/// wasm linear-memory page size, fixed by the wasm spec (64 KiB).
|
||||
const WASM_PAGE_BYTES: u32 = 64 * 1024;
|
||||
|
||||
/// Linear-memory page cap.
|
||||
pub const MAX_MEMORY_PAGES: u32 = 128;
|
||||
|
||||
/// [`MAX_MEMORY_PAGES`] in bytes: 8 MiB.
|
||||
pub const MAX_MEMORY_BYTES: usize = (MAX_MEMORY_PAGES * WASM_PAGE_BYTES) as usize;
|
||||
|
||||
/// Total bytes that may cross the host/guest boundary in one [`run`], separate
|
||||
/// from gas.
|
||||
pub const TRANSFER_LIMIT_BYTES: u64 = 1 << 20;
|
||||
|
||||
/// Size cap on any single value crossing the boundary, in either direction; over
|
||||
/// it is `DataFieldTooLarge`.
|
||||
///
|
||||
/// A protocol limit: `kMaxWasmDataLength` in `include/xrpl/protocol/Protocol.h`.
|
||||
pub const MAX_FIELD_BYTES: usize = 1024;
|
||||
|
||||
/// State threaded through every host call, stored in the wasmi [`Store`].
|
||||
pub(crate) struct VmState<'h> {
|
||||
pub(crate) host: &'h dyn HostFunctions,
|
||||
/// Enforces [`MAX_MEMORY_BYTES`] via `Store::limiter`, which needs a `&mut`
|
||||
/// into it from `&mut VmState` — hence a field rather than a local.
|
||||
pub(crate) mem_limits: StoreLimits,
|
||||
/// Remaining transfer budget for this run ([`TRANSFER_LIMIT_BYTES`]).
|
||||
///
|
||||
/// A `Cell` because it is decremented from a shared `&Caller`. One thread per
|
||||
/// invocation touches the store, so the lack of `Sync` costs nothing.
|
||||
///
|
||||
/// TODO: the extra charge for an unaligned field copy has nothing to attach to
|
||||
/// until this ABI gains a `FieldLocator` host function.
|
||||
pub(crate) transfer_budget: Cell<u64>,
|
||||
/// The guest's linear memory, resolved once by [`run`] after instantiation so
|
||||
/// no host call pays for an export lookup.
|
||||
///
|
||||
/// Caching the handle is sound because a [`Memory`] is an arena index, not a
|
||||
/// pointer to the bytes: it survives `memory.grow`, and `data`/`data_mut`
|
||||
/// re-derive the slice per call.
|
||||
///
|
||||
/// The handle is scoped to one store, so this assumes **one module, one
|
||||
/// instance, one store per `run`**. Module linking or nested execution would
|
||||
/// have to resolve per instance: a cached handle would serve a call against the
|
||||
/// wrong instance's memory, which is a wrong answer rather than an error.
|
||||
pub(crate) memory: Option<Memory>,
|
||||
/// Where a host writes a value before [`crate::abi::write_buffered`] copies it
|
||||
/// to the guest. One buffer per run, so no call zero-fills one of its own.
|
||||
///
|
||||
/// Inline rather than boxed: the store's data is built once and then only
|
||||
/// borrowed, so a kilobyte in it costs a move where a `Box` costs an
|
||||
/// allocation. A local would cost neither, but `forbid(unsafe_code)` means a
|
||||
/// stack buffer is zero-filled — per call, which is the cost this removes.
|
||||
pub(crate) out_buffer: [u8; MAX_FIELD_BYTES],
|
||||
}
|
||||
|
||||
/// Outcome of running an escrow contract to completion.
|
||||
#[derive(Debug)]
|
||||
pub struct RunOutcome {
|
||||
/// The value returned by the exported entry point (`finish`): `> 0` means
|
||||
/// allow the escrow to finish.
|
||||
pub result: i32,
|
||||
/// Fuel (gas) consumed by the whole invocation — guest instructions plus
|
||||
/// the per-call host charges.
|
||||
pub fuel_used: u64,
|
||||
}
|
||||
|
||||
/// Why a run produced no result. Each variant is one outcome for the caller to
|
||||
/// map to a TER.
|
||||
#[derive(Debug)]
|
||||
pub enum RunError {
|
||||
/// `wasm` is not a valid module under this engine's configuration.
|
||||
Compile(String),
|
||||
/// The module compiled but the engine would not accept it: an import the
|
||||
/// linker does not define, or an initial memory past the page cap. Not guest
|
||||
/// code failing — a start section that traps is [`RunError::Trap`].
|
||||
Instantiate(String),
|
||||
/// No export named `function_name` with signature `() -> i32`: absent, not a
|
||||
/// function, or a function of another type — which the detail tells apart.
|
||||
EntryPoint(String),
|
||||
/// Gas exhausted — by the guest's own instructions or by a host call's
|
||||
/// charge. [`RunFailure::fuel_used`] is the whole limit.
|
||||
OutOfGas,
|
||||
/// The host could not serve a call.
|
||||
Internal,
|
||||
/// A host call had no linear memory to work in: the module exports none, or
|
||||
/// the call came from a start section, which runs before there is an instance
|
||||
/// to resolve the memory from.
|
||||
NoMemory,
|
||||
/// The guest trapped: `unreachable`, division by zero, an out-of-bounds
|
||||
/// access, or `memory.grow` past the page cap. Wherever the guest was
|
||||
/// executing, including a start section during instantiation.
|
||||
Trap(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for RunError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RunError::Compile(detail) => write!(f, "compile: {detail}"),
|
||||
RunError::Instantiate(detail) => write!(f, "instantiate: {detail}"),
|
||||
// The detail says which of the entry point's failures this is, since
|
||||
// "no entry point" would be wrong for an export of the wrong type.
|
||||
RunError::EntryPoint(detail) => write!(f, "{detail}"),
|
||||
RunError::OutOfGas => write!(f, "out of gas"),
|
||||
RunError::Internal => write!(f, "internal error"),
|
||||
RunError::NoMemory => write!(f, "no exported memory"),
|
||||
RunError::Trap(detail) => write!(f, "trap: {detail}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A failed run, with the gas it still owes: a contract that traps or exhausts
|
||||
/// its gas is charged for what it burned.
|
||||
#[derive(Debug)]
|
||||
pub struct RunFailure {
|
||||
pub error: RunError,
|
||||
/// Fuel consumed before the failure. The whole limit when gas ran out; `0`
|
||||
/// when the module never ran.
|
||||
pub fuel_used: u64,
|
||||
}
|
||||
|
||||
impl fmt::Display for RunFailure {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} (fuel used: {})", self.error, self.fuel_used)
|
||||
}
|
||||
}
|
||||
|
||||
impl RunFailure {
|
||||
/// A failure with no fuel accounted: it stopped the run at or before the guest's
|
||||
/// first instruction, or under a store with no meter to read.
|
||||
fn owing_nothing(error: RunError) -> RunFailure {
|
||||
RunFailure {
|
||||
error,
|
||||
fuel_used: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fuel spent out of `gas`: the one place a run's cost is measured, so success,
|
||||
/// trap and refusal all report it the same way.
|
||||
///
|
||||
/// `Store::get_fuel` fails only on a store without fuel metering, which
|
||||
/// [`build_wasm_engine`] rules out and `run`'s `set_fuel` would already have
|
||||
/// caught — so a failure here is a defect in this crate. It must not become a
|
||||
/// number: `0` forgives a run its whole cost, `gas` charges an untouched one for
|
||||
/// everything. [`RunError::Internal`] instead.
|
||||
fn fuel_used(store: &Store<VmState<'_>>, gas: u64) -> Result<u64, RunError> {
|
||||
store
|
||||
.get_fuel()
|
||||
.map(|remaining| gas.saturating_sub(remaining))
|
||||
.map_err(|_| RunError::Internal)
|
||||
}
|
||||
|
||||
/// Report `error` with the run's cost attached. A cost that cannot be read replaces
|
||||
/// the outcome rather than being invented — see [`fuel_used`].
|
||||
fn failed(store: &Store<VmState<'_>>, gas: u64, error: RunError) -> RunFailure {
|
||||
match fuel_used(store, gas) {
|
||||
Ok(fuel_used) => RunFailure { error, fuel_used },
|
||||
Err(unmetered) => RunFailure::owing_nothing(unmetered),
|
||||
}
|
||||
}
|
||||
|
||||
/// The outcome a `wasmi::Error` names for itself, if any, rather than leaving it to
|
||||
/// the stage that raised it.
|
||||
///
|
||||
/// Two ways a run halts mid-flight: a host call that could not be served, which
|
||||
/// carries a [`FatalHostError`] saying which condition it was, and the guest's own
|
||||
/// instructions exhausting the meter, which wasmi raises as `OutOfFuel`.
|
||||
///
|
||||
/// Both can happen anywhere the guest executes — including a start section, which
|
||||
/// is guest code running during instantiation — so every stage from there on asks
|
||||
/// this before naming a failure after itself.
|
||||
fn guest_halted(error: &wasmi::Error) -> Option<RunError> {
|
||||
if let Some(fatal) = error.downcast_ref::<FatalHostError>() {
|
||||
return Some(fatal.0.into());
|
||||
}
|
||||
(error.as_trap_code() == Some(TrapCode::OutOfFuel)).then_some(RunError::OutOfGas)
|
||||
}
|
||||
|
||||
/// Why instantiation failed, once [`guest_halted`] has ruled out the two conditions
|
||||
/// that can arise anywhere.
|
||||
///
|
||||
/// A start section is guest code, so it can trap on its own — `unreachable`, a
|
||||
/// division by zero, an out-of-bounds access — and a trap is the guest's fault
|
||||
/// wherever it happens. Naming that after the *stage* would file it beside the
|
||||
/// module faults a caller treats as its own defect, and charge nothing for
|
||||
/// instructions the contract burned. What is left for [`RunError::Instantiate`] is a
|
||||
/// module the linker or the store would not accept at all.
|
||||
fn instantiation_failure(error: &wasmi::Error) -> RunError {
|
||||
match error.as_trap_code() {
|
||||
Some(_) => RunError::Trap(error.to_string()),
|
||||
None => RunError::Instantiate(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The outcome a [`Fault`] is: the one place a stopped call becomes a stopped run.
|
||||
///
|
||||
/// Total and one arm each, because a `Fault` is only ever a condition that stops the
|
||||
/// run — the guest-visible codes cannot reach here, which is what
|
||||
/// [`crate::abi::CallError`] buys. A fault added later has no arm and does not
|
||||
/// compile.
|
||||
impl From<Fault> for RunError {
|
||||
fn from(fault: Fault) -> RunError {
|
||||
match fault {
|
||||
Fault::OutOfGas => RunError::OutOfGas,
|
||||
Fault::Internal => RunError::Internal,
|
||||
Fault::NoMemory => RunError::NoMemory,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The process-wide wasmi engine, built once on first use.
|
||||
///
|
||||
/// The configuration is consensus-fixed and identical for every invocation, and an
|
||||
/// [`Engine`] is an internally `Arc`ed `Send + Sync` handle, so one shared engine
|
||||
/// serves concurrent [`run`] calls.
|
||||
pub(crate) fn wasm_engine() -> &'static Engine {
|
||||
static ENGINE: LazyLock<Engine> = LazyLock::new(build_wasm_engine);
|
||||
&ENGINE
|
||||
}
|
||||
|
||||
/// Build the wasmi engine the escrow VM requires: deterministic, minimal
|
||||
/// features, fuel metering on.
|
||||
fn build_wasm_engine() -> Engine {
|
||||
let mut config = Config::default();
|
||||
config.consume_fuel(true);
|
||||
config.ignore_custom_sections(true);
|
||||
config.wasm_mutable_global(false);
|
||||
config.wasm_multi_value(false);
|
||||
config.wasm_sign_extension(false);
|
||||
config.wasm_saturating_float_to_int(false);
|
||||
config.wasm_bulk_memory(false);
|
||||
config.wasm_reference_types(false);
|
||||
config.wasm_tail_call(false);
|
||||
config.wasm_extended_const(false);
|
||||
config.floats(false);
|
||||
config.wasm_multi_memory(false);
|
||||
config.wasm_custom_page_sizes(false);
|
||||
config.wasm_memory64(false);
|
||||
config.wasm_wide_arithmetic(false);
|
||||
// TODO: enable option to reject wasm code containing start section after wasmi 2.0 release
|
||||
Engine::new(&config)
|
||||
}
|
||||
|
||||
/// Compile `wasm` for this engine.
|
||||
///
|
||||
/// The one path to a [`Module`]: the configuration is what decides whether a
|
||||
/// contract is valid at all, so [`run`] and [`crate::check`] must not be able to
|
||||
/// compile against different ones.
|
||||
pub(crate) fn compile(wasm: &[u8]) -> Result<Module, String> {
|
||||
Module::new(wasm_engine(), wasm).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Run a contract: compile `wasm`, give it `gas` fuel, service its host
|
||||
/// calls through `host`, and call the exported `function_name`.
|
||||
pub fn run<'h>(
|
||||
wasm: &[u8],
|
||||
gas: u64,
|
||||
host: &'h dyn HostFunctions,
|
||||
function_name: &str,
|
||||
) -> Result<RunOutcome, RunFailure> {
|
||||
let engine = wasm_engine();
|
||||
let module =
|
||||
compile(wasm).map_err(|detail| RunFailure::owing_nothing(RunError::Compile(detail)))?;
|
||||
|
||||
let mem_limits = StoreLimitsBuilder::new()
|
||||
.memory_size(MAX_MEMORY_BYTES)
|
||||
.trap_on_grow_failure(true)
|
||||
.build();
|
||||
let mut store = Store::new(
|
||||
engine,
|
||||
VmState {
|
||||
host,
|
||||
mem_limits,
|
||||
transfer_budget: Cell::new(TRANSFER_LIMIT_BYTES),
|
||||
memory: None,
|
||||
out_buffer: [0u8; MAX_FIELD_BYTES],
|
||||
},
|
||||
);
|
||||
|
||||
store
|
||||
.set_fuel(gas)
|
||||
.map_err(|_| RunFailure::owing_nothing(RunError::Internal))?;
|
||||
store.limiter(|state| &mut state.mem_limits);
|
||||
|
||||
let mut linker = Linker::<VmState<'h>>::new(engine);
|
||||
register_host_functions(&mut linker)
|
||||
.map_err(|_| RunFailure::owing_nothing(RunError::Internal))?;
|
||||
|
||||
let instance = match linker.instantiate_and_start(&mut store, &module) {
|
||||
Ok(instance) => instance,
|
||||
Err(e) => {
|
||||
let error = guest_halted(&e).unwrap_or_else(|| instantiation_failure(&e));
|
||||
return Err(failed(&store, gas, error));
|
||||
}
|
||||
};
|
||||
store.data_mut().memory = instance.exports(&store).find_map(Export::into_memory);
|
||||
|
||||
let function = match instance.get_typed_func::<(), i32>(&store, function_name) {
|
||||
Ok(function) => function,
|
||||
Err(e) => {
|
||||
let found = instance
|
||||
.get_export(&store, function_name)
|
||||
.map(|export| export.ty(&store));
|
||||
let error =
|
||||
RunError::EntryPoint(format!("{}: {e}", entry_point_fault(found, function_name)));
|
||||
return Err(failed(&store, gas, error));
|
||||
}
|
||||
};
|
||||
|
||||
let result = match function.call(&mut store, ()) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
let error = guest_halted(&e).unwrap_or_else(|| RunError::Trap(e.to_string()));
|
||||
return Err(failed(&store, gas, error));
|
||||
}
|
||||
};
|
||||
|
||||
let fuel_used = fuel_used(&store, gas).map_err(RunFailure::owing_nothing)?;
|
||||
Ok(RunOutcome { result, fuel_used })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_engine_is_one_engine() {
|
||||
assert!(Engine::same(wasm_engine(), wasm_engine()));
|
||||
}
|
||||
|
||||
/// The only place these numbers appear as literals; every other test derives
|
||||
/// them from the constants.
|
||||
#[test]
|
||||
fn the_limits_are_the_protocol_limits() {
|
||||
assert_eq!(MAX_MEMORY_PAGES, 128, "linear-memory page cap");
|
||||
assert_eq!(MAX_MEMORY_BYTES, 8 * 1024 * 1024, "page cap in bytes");
|
||||
assert_eq!(MAX_FIELD_BYTES, 1024, "kMaxWasmDataLength");
|
||||
assert_eq!(TRANSFER_LIMIT_BYTES, 1 << 20, "kWasmTransferLimit");
|
||||
}
|
||||
}
|
||||
750
crates/xrpl-wasm-vm/tests/budgets.rs
Normal file
750
crates/xrpl-wasm-vm/tests/budgets.rs
Normal file
@@ -0,0 +1,750 @@
|
||||
//! The two budgets a run spends: gas (fuel), and the transfer limit on bytes
|
||||
//! crossing the boundary. Both are consensus input, so several of these tests
|
||||
//! assert exact numbers.
|
||||
|
||||
mod support;
|
||||
|
||||
use support::{
|
||||
Answer, EMPTY_REGION, FakeHost, ONE_PAGE, PLENTY_OF_GAS, code, import, module, run,
|
||||
run_with_gas, trace_call,
|
||||
};
|
||||
use xrpl_host_functions::{HASH_LEN, HostError, HostFunctionSpec, TraceDataType};
|
||||
use xrpl_wasm_vm::{MAX_FIELD_BYTES, RunError, TRANSFER_LIMIT_BYTES};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gas
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The fuel a module of `body` burns, given gas to spare.
|
||||
fn fuel_for(body: &str, parts: &[&str], host: &FakeHost) -> u64 {
|
||||
let wat = module(parts, body);
|
||||
run(&wat, host).expect("the module should run").fuel_used
|
||||
}
|
||||
|
||||
/// The fuel a module burns doing nothing but returning a constant; every figure
|
||||
/// below builds on it. wasmi's number, pinned deliberately because wasmi's fuel
|
||||
/// table is consensus input.
|
||||
const EMPTY_MODULE_FUEL: u64 = 30;
|
||||
|
||||
/// wasmi's own fuel for a host call whose operands are all constants under 64: 14
|
||||
/// per `*.const`, plus 1 for the call. Our gas sits on top.
|
||||
///
|
||||
/// The formula holds only under 64, because wasmi widens a constant's encoding
|
||||
/// above that, each tier costing 7 more. Every call in [`call_for`] keeps its
|
||||
/// operands small for that reason; one with a larger constant fails here by a
|
||||
/// multiple of 7.
|
||||
fn wasmi_call_fuel(small_const_operands: u64) -> u64 {
|
||||
14 * small_const_operands + 1
|
||||
}
|
||||
|
||||
/// What wasmi charges on top of that for a call to a function with no result —
|
||||
/// `trace`'s shape, and nothing else in the ABI. Per call, not per module. Measured
|
||||
/// and pinned like the figures above.
|
||||
const WASMI_NO_RESULT_FUEL: u64 = 14;
|
||||
|
||||
/// wasmi's fuel for one `(drop …)`, which is how a module makes more than one call
|
||||
/// and keeps only the last result. Pinned like the two above.
|
||||
const WASMI_DROP_FUEL: u64 = 21;
|
||||
|
||||
/// The wasm a test needs in order to call one host function: the `(import …)`
|
||||
/// declaration, a call with small-constant operands, and how many it pushes.
|
||||
struct Call {
|
||||
import: &'static str,
|
||||
call: &'static str,
|
||||
operands: u64,
|
||||
/// Whether the call leaves an `i32` behind. `trace` does not, which is why
|
||||
/// [`Call::body`] ends every module with a constant instead of the call.
|
||||
yields: bool,
|
||||
}
|
||||
|
||||
impl Call {
|
||||
/// `n` calls in a row, leaving one `i32` for the module to return: the last
|
||||
/// answer where there is one, and a constant where the call has none.
|
||||
fn body(&self, n: usize) -> String {
|
||||
if self.yields {
|
||||
format!(
|
||||
"{}{}",
|
||||
format!("(drop {}) ", self.call).repeat(n - 1),
|
||||
self.call
|
||||
)
|
||||
} else {
|
||||
format!("{}(i32.const 0)", format!("{} ", self.call).repeat(n))
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`Call::body`] burns beside the calls' own gas and the module's floor:
|
||||
/// one `drop` between consecutive answers, or wasmi's own surcharge on a call
|
||||
/// that has none.
|
||||
fn overhead(&self, n: u64) -> u64 {
|
||||
if self.yields {
|
||||
(n - 1) * WASMI_DROP_FUEL
|
||||
} else {
|
||||
n * WASMI_NO_RESULT_FUEL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The test wasm for each host function. The `match` is exhaustive, so a function
|
||||
/// added to the ABI fails to compile until it has wasm here, and iterating
|
||||
/// [`HostFunctionSpec::ALL`] then covers the whole ABI.
|
||||
fn call_for(op: HostFunctionSpec) -> Call {
|
||||
let (import, call, operands) = match op {
|
||||
HostFunctionSpec::GetLedgerSqn => (
|
||||
import::LDGR_INDEX,
|
||||
"(call $ldgr_index (i32.const 0) (i32.const 4))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetParentLedgerTime => (
|
||||
import::PARENT_LDGR_TIME,
|
||||
"(call $parent_ldgr_time (i32.const 0) (i32.const 4))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetParentLedgerHash => (
|
||||
import::PARENT_LDGR_HASH,
|
||||
"(call $parent_ldgr_hash (i32.const 0) (i32.const 32))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetBaseFee => (
|
||||
import::BASE_FEE,
|
||||
"(call $base_fee (i32.const 0) (i32.const 4))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::IsAmendmentEnabled => (
|
||||
import::AMENDMENT_ENABLED,
|
||||
"(call $amendment_enabled (i32.const 0) (i32.const 32))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::CacheLedgerObj => (
|
||||
import::CACHE_LE,
|
||||
"(call $cache_le (i32.const 0) (i32.const 32) (i32.const 0))",
|
||||
3,
|
||||
),
|
||||
HostFunctionSpec::GetTxField => (
|
||||
import::TX_FIELD,
|
||||
"(call $tx_field (i32.const 1) (i32.const 0) (i32.const 4))",
|
||||
3,
|
||||
),
|
||||
HostFunctionSpec::GetCurrentLedgerObjField => (
|
||||
import::HOME_LE_FIELD,
|
||||
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 4))",
|
||||
3,
|
||||
),
|
||||
HostFunctionSpec::GetLedgerObjField => (
|
||||
import::LE_FIELD,
|
||||
"(call $le_field (i32.const 1) (i32.const 1) (i32.const 0) (i32.const 4))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::GetTxNestedField => (
|
||||
import::TX_INNER,
|
||||
"(call $tx_inner (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::GetCurrentLedgerObjNestedField => (
|
||||
import::HOME_LE_INNER,
|
||||
"(call $home_le_inner (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::GetLedgerObjNestedField => (
|
||||
import::LE_INNER,
|
||||
"(call $le_inner (i32.const 1) (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))",
|
||||
5,
|
||||
),
|
||||
HostFunctionSpec::GetTxArrayLen => {
|
||||
(import::TX_ARR_LEN, "(call $tx_arr_len (i32.const 1))", 1)
|
||||
}
|
||||
HostFunctionSpec::GetCurrentLedgerObjArrayLen => (
|
||||
import::HOME_LE_ARR_LEN,
|
||||
"(call $home_le_arr_len (i32.const 1))",
|
||||
1,
|
||||
),
|
||||
HostFunctionSpec::GetLedgerObjArrayLen => (
|
||||
import::LE_ARR_LEN,
|
||||
"(call $le_arr_len (i32.const 1) (i32.const 1))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetTxNestedArrayLen => (
|
||||
import::TX_INNER_ARR_LEN,
|
||||
"(call $tx_inner_arr_len (i32.const 0) (i32.const 4))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetCurrentLedgerObjNestedArrayLen => (
|
||||
import::HOME_LE_INNER_ARR_LEN,
|
||||
"(call $home_le_inner_arr_len (i32.const 0) (i32.const 4))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetLedgerObjNestedArrayLen => (
|
||||
import::LE_INNER_ARR_LEN,
|
||||
"(call $le_inner_arr_len (i32.const 1) (i32.const 0) (i32.const 4))",
|
||||
3,
|
||||
),
|
||||
HostFunctionSpec::CheckSignature => (
|
||||
import::CHECK_SIG,
|
||||
"(call $check_sig (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::AccountKeylet => (
|
||||
import::ACCOUNTROOT_ID,
|
||||
"(call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::AmmKeylet => (
|
||||
import::AMM_ID,
|
||||
"(call $amm_id (i32.const 0) (i32.const 20) (i32.const 24) (i32.const 40) (i32.const 0) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::CheckKeylet => (
|
||||
import::CHECK_ID,
|
||||
"(call $check_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::CredentialKeylet => (
|
||||
import::CREDENTIAL_ID,
|
||||
"(call $credential_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 4) (i32.const 44) (i32.const 20))",
|
||||
8,
|
||||
),
|
||||
HostFunctionSpec::DelegateKeylet => (
|
||||
import::DELEGATE_ID,
|
||||
"(call $delegate_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::DepositPreauthKeylet => (
|
||||
import::DEPOSIT_PREAUTH_ID,
|
||||
"(call $deposit_preauth_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::DidKeylet => (
|
||||
import::DID_ID,
|
||||
"(call $did_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::EscrowKeylet => (
|
||||
import::ESCROW_ID,
|
||||
"(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::TrustLineKeylet => (
|
||||
import::TRUSTLINE_ID,
|
||||
"(call $trustline_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 20) (i32.const 60) (i32.const 32))",
|
||||
8,
|
||||
),
|
||||
HostFunctionSpec::MptokenIssuanceKeylet => (
|
||||
import::MPT_ISSUANCE_ID,
|
||||
"(call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::MptokenKeylet => (
|
||||
import::MPTOKEN_ID,
|
||||
"(call $mptoken_id (i32.const 0) (i32.const 24) (i32.const 24) (i32.const 20) (i32.const 44) (i32.const 20))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::NftokenOfferKeylet => (
|
||||
import::NFT_OFFER_ID,
|
||||
"(call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::OfferKeylet => (
|
||||
import::OFFER_ID,
|
||||
"(call $offer_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::OracleKeylet => (
|
||||
import::ORACLE_ID,
|
||||
"(call $oracle_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::PaychannelKeylet => (
|
||||
import::PAYCHAN_ID,
|
||||
"(call $paychan_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 40) (i32.const 20))",
|
||||
8,
|
||||
),
|
||||
HostFunctionSpec::PermissionedDomainKeylet => (
|
||||
import::PERMISSIONED_DOMAIN_ID,
|
||||
"(call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::SignerListKeylet => (
|
||||
import::SIGNERS_ID,
|
||||
"(call $signers_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::TicketKeylet => (
|
||||
import::TICKET_ID,
|
||||
"(call $ticket_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::VaultKeylet => (
|
||||
import::VAULT_ID,
|
||||
"(call $vault_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::Sha512Half => (
|
||||
import::SHA512_HALF,
|
||||
"(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::Trace => (
|
||||
import::TRACE,
|
||||
"(call $trace (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0))",
|
||||
5,
|
||||
),
|
||||
HostFunctionSpec::UpdateData => (
|
||||
import::SET_DATA,
|
||||
"(call $set_data (i32.const 0) (i32.const 8))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetNft => (
|
||||
import::NFT_URI,
|
||||
"(call $nft_uri (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 32) (i32.const 52) (i32.const 12))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::GetNftIssuer => (
|
||||
import::NFT_ISSUER,
|
||||
"(call $nft_issuer (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 20))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::GetNftTaxon => (
|
||||
import::NFT_TAXON,
|
||||
"(call $nft_taxon (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::GetNftFlags => (
|
||||
import::NFT_FLAGS,
|
||||
"(call $nft_flags (i32.const 0) (i32.const 32))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetNftTransferFee => (
|
||||
import::NFT_XFER_FEE,
|
||||
"(call $nft_xfer_fee (i32.const 0) (i32.const 32))",
|
||||
2,
|
||||
),
|
||||
HostFunctionSpec::GetNftSequence => (
|
||||
import::NFT_SERIAL,
|
||||
"(call $nft_serial (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::FloatFromInt => (
|
||||
import::FLOAT_FROM_INT,
|
||||
"(call $float_from_int (i64.const 0) (i32.const 0) (i32.const 8) (i32.const 0))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::FloatFromUint => (
|
||||
import::FLOAT_FROM_UINT,
|
||||
"(call $float_from_uint (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))",
|
||||
5,
|
||||
),
|
||||
HostFunctionSpec::FloatFromStamount => (
|
||||
import::FLOAT_FROM_STAMOUNT,
|
||||
"(call $float_from_stamount (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))",
|
||||
5,
|
||||
),
|
||||
HostFunctionSpec::FloatFromStnumber => (
|
||||
import::FLOAT_FROM_STNUMBER,
|
||||
"(call $float_from_stnumber (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))",
|
||||
5,
|
||||
),
|
||||
HostFunctionSpec::FloatToInt => (
|
||||
import::FLOAT_TO_INT,
|
||||
"(call $float_to_int (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))",
|
||||
5,
|
||||
),
|
||||
HostFunctionSpec::FloatToMantExp => (
|
||||
import::FLOAT_TO_MANT_EXP,
|
||||
"(call $float_to_mant_exp (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 4))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::FloatFromMantExp => (
|
||||
import::FLOAT_FROM_MANT_EXP,
|
||||
"(call $float_from_mant_exp (i64.const 0) (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 0))",
|
||||
5,
|
||||
),
|
||||
HostFunctionSpec::FloatCompare => (
|
||||
import::FLOAT_CMP,
|
||||
"(call $float_cmp (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8))",
|
||||
4,
|
||||
),
|
||||
HostFunctionSpec::FloatAdd => (
|
||||
import::FLOAT_ADD,
|
||||
"(call $float_add (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))",
|
||||
7,
|
||||
),
|
||||
HostFunctionSpec::FloatSubtract => (
|
||||
import::FLOAT_SUB,
|
||||
"(call $float_sub (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))",
|
||||
7,
|
||||
),
|
||||
HostFunctionSpec::FloatMultiply => (
|
||||
import::FLOAT_MULT,
|
||||
"(call $float_mult (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))",
|
||||
7,
|
||||
),
|
||||
HostFunctionSpec::FloatDivide => (
|
||||
import::FLOAT_DIV,
|
||||
"(call $float_div (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))",
|
||||
7,
|
||||
),
|
||||
HostFunctionSpec::FloatRoot => (
|
||||
import::FLOAT_ROOT,
|
||||
"(call $float_root (i32.const 0) (i32.const 8) (i32.const 2) (i32.const 8) (i32.const 8) (i32.const 0))",
|
||||
6,
|
||||
),
|
||||
HostFunctionSpec::FloatPower => (
|
||||
import::FLOAT_POW,
|
||||
"(call $float_pow (i32.const 0) (i32.const 8) (i32.const 2) (i32.const 8) (i32.const 8) (i32.const 0))",
|
||||
6,
|
||||
),
|
||||
};
|
||||
Call {
|
||||
import,
|
||||
call,
|
||||
operands,
|
||||
yields: !matches!(op, HostFunctionSpec::Trace),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_module_burns_a_fixed_amount_of_fuel() {
|
||||
let fuel = fuel_for("(i32.const 0)", &[ONE_PAGE], &FakeHost::new());
|
||||
assert_eq!(fuel, EMPTY_MODULE_FUEL);
|
||||
}
|
||||
|
||||
/// Calling a host function `n` times costs `n` times its gas, to the unit. Every
|
||||
/// other term is known — the module's floor, wasmi's fuel per call, one `drop` per
|
||||
/// answered call — so the total is a closed form, with the gas read from the spec
|
||||
/// table rather than restated. `n = 1` pins the charge, `n > 1` pins that it lands
|
||||
/// on every call rather than once per run.
|
||||
#[test]
|
||||
fn a_host_call_costs_its_gas_every_time_it_is_called() {
|
||||
let host = FakeHost::new().answering_field(1, Answer::bytes([0xaa]));
|
||||
|
||||
for &op in HostFunctionSpec::ALL {
|
||||
let call = call_for(op);
|
||||
let per_call = wasmi_call_fuel(call.operands) + op.gas();
|
||||
|
||||
for n in 1..=3 {
|
||||
let body = call.body(n);
|
||||
let n = n as u64;
|
||||
|
||||
assert_eq!(
|
||||
fuel_for(&body, &[call.import, ONE_PAGE], &host),
|
||||
EMPTY_MODULE_FUEL + n * per_call + call.overhead(n),
|
||||
"{n} x {}",
|
||||
call.call
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The gas charge precedes the call's body, so a failing call costs exactly what a
|
||||
/// successful one costs. Field 1 is answered and field 7 is not; the two modules
|
||||
/// are otherwise identical, so their totals are comparable.
|
||||
#[test]
|
||||
fn a_failing_host_call_costs_exactly_what_a_successful_one_costs() {
|
||||
let host = FakeHost::new().answering_field(1, Answer::bytes([0xaa]));
|
||||
let call = |field: i32| {
|
||||
module(
|
||||
&[import::HOME_LE_FIELD, ONE_PAGE],
|
||||
&format!("(call $home_le_field (i32.const {field}) (i32.const 0) (i32.const 4))"),
|
||||
)
|
||||
};
|
||||
|
||||
let answered = run(&call(1), &host).expect("the module should run");
|
||||
let refused = run(&call(7), &host).expect("the module should run");
|
||||
|
||||
assert_eq!(answered.result, 1);
|
||||
assert_eq!(refused.result, code(HostError::FieldNotFound));
|
||||
assert_eq!(refused.fuel_used, answered.fuel_used);
|
||||
}
|
||||
|
||||
/// `fuel_used` is `gas - remaining`: what the run spent, not what was left or what
|
||||
/// it was handed. The gas figures are derived from the run's cost, so the boundary
|
||||
/// — exactly enough, and one short — is among the cases.
|
||||
#[test]
|
||||
fn fuel_used_is_what_was_spent_not_what_was_supplied() {
|
||||
let host = FakeHost::new();
|
||||
let op = HostFunctionSpec::GetLedgerSqn;
|
||||
let call = call_for(op);
|
||||
let wat = module(&[call.import, ONE_PAGE], call.call);
|
||||
let cost = EMPTY_MODULE_FUEL + wasmi_call_fuel(call.operands) + op.gas();
|
||||
|
||||
// Exactly its cost is enough, and no amount above it changes the figure. The
|
||||
// result is checked too, so the figure belongs to a run that did the work
|
||||
// rather than to one that was cut short.
|
||||
for gas in [cost, cost + 1, cost * 100, PLENTY_OF_GAS] {
|
||||
let outcome = run_with_gas(&wat, gas, &host).expect("should run");
|
||||
assert_eq!(
|
||||
outcome.result, 4,
|
||||
"gas {gas}: the call should have succeeded"
|
||||
);
|
||||
assert_eq!(outcome.fuel_used, cost, "gas {gas}");
|
||||
}
|
||||
|
||||
// One fuel short: the run ends at the call it cannot pay for and still owes the
|
||||
// whole limit, because `charge` spends what is left.
|
||||
let short = run_with_gas(&wat, cost - 1, &host).expect_err("one fuel short must not complete");
|
||||
assert!(
|
||||
matches!(short.error, RunError::OutOfGas),
|
||||
"expected the run to end out of gas, got: {short}"
|
||||
);
|
||||
assert_eq!(short.fuel_used, cost - 1);
|
||||
}
|
||||
|
||||
/// Fuel is metered, so the same module burns the same fuel every time — a
|
||||
/// property consensus depends on.
|
||||
#[test]
|
||||
fn the_same_run_burns_the_same_fuel() {
|
||||
let call = call_for(HostFunctionSpec::Trace);
|
||||
let wat = module(&[call.import, ONE_PAGE], &call.body(1));
|
||||
|
||||
let first = run(&wat, &FakeHost::new()).expect("should run").fuel_used;
|
||||
for _ in 0..4 {
|
||||
assert_eq!(
|
||||
run(&wat, &FakeHost::new()).expect("should run").fuel_used,
|
||||
first
|
||||
);
|
||||
}
|
||||
assert!(first > HostFunctionSpec::Trace.gas());
|
||||
}
|
||||
|
||||
/// Too little gas to finish stops the run: the meter refuses the guest's own
|
||||
/// instructions before it ever reaches the host call.
|
||||
#[test]
|
||||
fn a_run_that_cannot_afford_itself_fails() {
|
||||
let host = FakeHost::new();
|
||||
let wat = module(
|
||||
&[import::LDGR_INDEX, ONE_PAGE],
|
||||
"(call $ldgr_index (i32.const 0) (i32.const 4))",
|
||||
);
|
||||
|
||||
for gas in [0, 1, 10] {
|
||||
let Err(failure) = run_with_gas(&wat, gas, &host) else {
|
||||
panic!("gas {gas} should not have completed");
|
||||
};
|
||||
assert!(
|
||||
matches!(failure.error, RunError::OutOfGas),
|
||||
"gas {gas}: expected the run to end out of gas, got: {failure}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A guest looping forever is stopped by gas rather than running away, and owes
|
||||
/// the gas it burned doing it.
|
||||
#[test]
|
||||
fn an_endless_loop_is_stopped_by_gas() {
|
||||
const GAS: u64 = 100_000;
|
||||
|
||||
let host = FakeHost::new();
|
||||
let wat = module(&[ONE_PAGE], "(loop $l (br $l)) (i32.const 0)");
|
||||
|
||||
let failure = run_with_gas(&wat, GAS, &host).expect_err("an endless loop must not complete");
|
||||
assert!(
|
||||
matches!(failure.error, RunError::OutOfGas),
|
||||
"expected the meter to stop it, got: {failure}"
|
||||
);
|
||||
assert_eq!(
|
||||
failure.fuel_used, GAS,
|
||||
"a runaway guest burns the whole limit"
|
||||
);
|
||||
}
|
||||
|
||||
/// A host call refused its gas stops the run: the guest never gets a chance to
|
||||
/// ignore the refusal and carry on, and it is charged the whole limit.
|
||||
///
|
||||
/// The gas range is every amount that reaches the call and cannot pay for it, so
|
||||
/// the case is the whole boundary rather than one number. `trace` is the call under
|
||||
/// it because it is the one that could not report a refusal even if it wanted to:
|
||||
/// stopping the run is the whole of what the guest sees.
|
||||
#[test]
|
||||
fn a_host_call_refused_its_gas_stops_the_run() {
|
||||
let host = FakeHost::new();
|
||||
let op = HostFunctionSpec::Trace;
|
||||
let call = call_for(op);
|
||||
let wat = module(&[call.import, ONE_PAGE], &call.body(1));
|
||||
// Measured rather than derived: the whole run's cost, less the call's own gas,
|
||||
// is the least a guest can be given and still reach the call. Below that the
|
||||
// meter stops the guest's own instructions instead, which is
|
||||
// `a_run_that_cannot_afford_itself_fails`'s case, not this one.
|
||||
let cost = run(&wat, &FakeHost::new())
|
||||
.expect("the module should run")
|
||||
.fuel_used;
|
||||
|
||||
for gas in cost - op.gas()..cost {
|
||||
let Err(failure) = run_with_gas(&wat, gas, &host) else {
|
||||
panic!("gas {gas}: the run completed, so the guest was handed the refusal");
|
||||
};
|
||||
assert!(
|
||||
matches!(failure.error, RunError::OutOfGas),
|
||||
"gas {gas}: expected the run to end out of gas, got: {failure}"
|
||||
);
|
||||
assert_eq!(
|
||||
failure.fuel_used, gas,
|
||||
"gas {gas}: a call it cannot afford burns the whole limit"
|
||||
);
|
||||
}
|
||||
assert!(host.traces().is_empty(), "the host body must not have run");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The transfer limit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A module that repeats `call` while `keep_going` holds, then returns the last
|
||||
/// status, so a budget can be run to exhaustion inside one invocation.
|
||||
fn until_refused(imports: &str, call: &str, keep_going: &str) -> String {
|
||||
module(
|
||||
&[imports, ONE_PAGE],
|
||||
&format!(
|
||||
"(local $r i32)
|
||||
(loop $l
|
||||
(local.set $r {call})
|
||||
(br_if $l {keep_going}))
|
||||
(local.get $r)"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// For a call whose success is a positive byte count.
|
||||
const WHILE_POSITIVE: &str = "(i32.gt_s (local.get $r) (i32.const 0))";
|
||||
|
||||
/// Bytes written into guest memory are charged against the run's budget, and the
|
||||
/// budget is a per-run total: 1 MiB of 1 KiB values exhausts it.
|
||||
#[test]
|
||||
fn writes_spend_the_transfer_budget() {
|
||||
let host = FakeHost::new().answering_field(1, Answer::filler(MAX_FIELD_BYTES));
|
||||
let wat = until_refused(
|
||||
import::HOME_LE_FIELD,
|
||||
&format!("(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"),
|
||||
WHILE_POSITIVE,
|
||||
);
|
||||
|
||||
let outcome = run(&wat, &host).expect("the module should run");
|
||||
assert_eq!(outcome.result, code(HostError::OutOfTransferLimit));
|
||||
assert_eq!(
|
||||
host.fields_asked.borrow().len() as u64,
|
||||
TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64 + 1,
|
||||
"one call per 1 KiB of budget, plus the one that was refused"
|
||||
);
|
||||
}
|
||||
|
||||
/// The budget is per run, not per call: a fresh run starts with a full budget.
|
||||
#[test]
|
||||
fn each_run_gets_its_own_budget() {
|
||||
let wat = until_refused(
|
||||
import::HOME_LE_FIELD,
|
||||
&format!("(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"),
|
||||
WHILE_POSITIVE,
|
||||
);
|
||||
|
||||
for _ in 0..2 {
|
||||
let host = FakeHost::new().answering_field(1, Answer::filler(MAX_FIELD_BYTES));
|
||||
let outcome = run(&wat, &host).expect("the module should run");
|
||||
assert_eq!(outcome.result, code(HostError::OutOfTransferLimit));
|
||||
assert_eq!(
|
||||
host.fields_asked.borrow().len() as u64,
|
||||
TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64 + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A run well inside the budget never sees it.
|
||||
#[test]
|
||||
fn a_modest_run_never_meets_the_budget() {
|
||||
let host = FakeHost::new().answering_field(1, Answer::filler(MAX_FIELD_BYTES));
|
||||
let wat = module(
|
||||
&[import::HOME_LE_FIELD, ONE_PAGE],
|
||||
&format!("(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"),
|
||||
);
|
||||
|
||||
let outcome = run(&wat, &host).expect("the module should run");
|
||||
assert_eq!(outcome.result, MAX_FIELD_BYTES as i32);
|
||||
}
|
||||
|
||||
/// Reads leave the budget alone: `read_borrowed` hands the host a slice *aliasing*
|
||||
/// guest memory, so there are no copied bytes to charge. What bounds how many reads
|
||||
/// a run can make is gas, which every host call pays before its body runs.
|
||||
///
|
||||
/// The observation is the write at the end, not the reads: the module reads four
|
||||
/// times the whole budget first, so a rule that charged reads would have nothing
|
||||
/// left, and the write would answer `OutOfTransferLimit` instead of a byte count.
|
||||
#[test]
|
||||
fn reads_do_not_spend_the_transfer_budget() {
|
||||
/// 1 KiB reads, four times over the budget.
|
||||
const READS: u64 = 4 * TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64;
|
||||
|
||||
let host = FakeHost::new().answering_field(1, Answer::filler(MAX_FIELD_BYTES));
|
||||
let read = trace_call(
|
||||
TraceDataType::AsHex,
|
||||
EMPTY_REGION,
|
||||
&format!("(i32.const 0) (i32.const {MAX_FIELD_BYTES})"),
|
||||
);
|
||||
let wat = module(
|
||||
&[import::TRACE, import::HOME_LE_FIELD, ONE_PAGE],
|
||||
&format!(
|
||||
"(local $i i32)
|
||||
(loop $l
|
||||
{read}
|
||||
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
||||
(br_if $l (i32.lt_u (local.get $i) (i32.const {READS}))))
|
||||
(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"
|
||||
),
|
||||
);
|
||||
|
||||
let outcome = run(&wat, &host).expect("the module should run");
|
||||
assert_eq!(
|
||||
host.traces().len() as u64,
|
||||
READS,
|
||||
"every read should have been served"
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.result, MAX_FIELD_BYTES as i32,
|
||||
"the write after {READS} reads of {MAX_FIELD_BYTES} bytes should still have its budget"
|
||||
);
|
||||
}
|
||||
|
||||
/// Only the output half of a read-write call spends the budget. `sha512_half`'s
|
||||
/// input is a borrowed read like any other, aliasing guest memory rather than
|
||||
/// crossing the boundary, so a run may hash far more bytes than the budget holds as
|
||||
/// long as the digests it writes fit inside it.
|
||||
///
|
||||
/// The two totals are asserted, so the arithmetic that makes the case is in the
|
||||
/// test rather than in a comment: the inputs alone would overrun the budget, the
|
||||
/// digests alone are a small fraction of it.
|
||||
#[test]
|
||||
fn only_the_output_half_of_a_read_write_spends_the_budget() {
|
||||
/// Enough 1 KiB inputs to overrun the budget twice over.
|
||||
const CALLS: u64 = 2 * TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64;
|
||||
|
||||
assert!(
|
||||
CALLS * MAX_FIELD_BYTES as u64 > TRANSFER_LIMIT_BYTES,
|
||||
"the inputs alone must overrun the budget"
|
||||
);
|
||||
assert!(
|
||||
CALLS * HASH_LEN as u64 <= TRANSFER_LIMIT_BYTES / 2,
|
||||
"the digests alone must stay well inside it"
|
||||
);
|
||||
|
||||
let host = FakeHost::new().answering_digest(Answer::filler(HASH_LEN));
|
||||
let wat = module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
&format!(
|
||||
"(local $i i32)
|
||||
(local $r i32)
|
||||
(loop $l
|
||||
(local.set $r (call $sha512_half (i32.const 0) (i32.const {MAX_FIELD_BYTES})
|
||||
(i32.const 0) (i32.const {HASH_LEN})))
|
||||
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
||||
(br_if $l (i32.lt_u (local.get $i) (i32.const {CALLS}))))
|
||||
(local.get $r)"
|
||||
),
|
||||
);
|
||||
|
||||
let outcome = run(&wat, &host).expect("the module should run");
|
||||
assert_eq!(
|
||||
host.digested.borrow().len() as u64,
|
||||
CALLS,
|
||||
"every call should have been served"
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.result, HASH_LEN as i32,
|
||||
"only the digests are charged, and they fit"
|
||||
);
|
||||
}
|
||||
1224
crates/xrpl-wasm-vm/tests/host_calls.rs
Normal file
1224
crates/xrpl-wasm-vm/tests/host_calls.rs
Normal file
File diff suppressed because it is too large
Load Diff
621
crates/xrpl-wasm-vm/tests/memory_policy.rs
Normal file
621
crates/xrpl-wasm-vm/tests/memory_policy.rs
Normal file
@@ -0,0 +1,621 @@
|
||||
//! The bounds, field-cap and buffer-fit rules `abi.rs` enforces on every region
|
||||
//! crossing the boundary. This is the policy the guest observes, so each rule is
|
||||
//! pinned to the code it answers with.
|
||||
|
||||
mod support;
|
||||
|
||||
use support::{
|
||||
Answer, COMPLETED, EMPTY_REGION, FakeHost, ONE_PAGE, code, failure, import, module, status,
|
||||
traced,
|
||||
};
|
||||
use xrpl_host_functions::{HASH_LEN, HostError, TraceDataType};
|
||||
use xrpl_wasm_vm::{MAX_FIELD_BYTES, RunError};
|
||||
|
||||
/// One page, so anything at or past 65536 is out of bounds.
|
||||
const PAGE: i64 = 64 * 1024;
|
||||
|
||||
/// The per-field size cap, as a wasm operand.
|
||||
const CAP: i64 = MAX_FIELD_BYTES as i64;
|
||||
/// One byte over the cap: the smallest value the engine must refuse.
|
||||
const OVER_CAP: i64 = CAP + 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output regions (`write_into`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The whole output region must be in bounds, not merely its start — the engine
|
||||
/// checks `[dst, dst + cap)` before the host is allowed to write.
|
||||
#[test]
|
||||
fn an_output_region_running_past_memory_is_refused() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for (dst, cap) in [(PAGE, 4), (PAGE - 3, 4), (PAGE + 1024, 4), (0, PAGE + 1)] {
|
||||
let wat = module(
|
||||
&[import::LDGR_INDEX, ONE_PAGE],
|
||||
&format!("(call $ldgr_index (i32.const {dst}) (i32.const {cap}))"),
|
||||
);
|
||||
assert_eq!(
|
||||
status(&wat, &host),
|
||||
code(HostError::PointerOutOfBounds),
|
||||
"dst {dst} cap {cap}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A region ending exactly at the last byte of memory is in bounds.
|
||||
#[test]
|
||||
fn an_output_region_ending_at_the_last_byte_is_allowed() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[import::LDGR_INDEX, ONE_PAGE],
|
||||
&format!("(call $ldgr_index (i32.const {}) (i32.const 4))", PAGE - 4),
|
||||
);
|
||||
assert_eq!(status(&wat, &host), 4);
|
||||
}
|
||||
|
||||
/// The wire carries `i32`, so a guest can present a negative pointer or length.
|
||||
#[test]
|
||||
fn a_negative_output_pointer_or_length_is_refused() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for (dst, cap) in [(-1, 4), (0, -1), (-1, -1), (i32::MIN, 4)] {
|
||||
let wat = module(
|
||||
&[import::LDGR_INDEX, ONE_PAGE],
|
||||
&format!("(call $ldgr_index (i32.const {dst}) (i32.const {cap}))"),
|
||||
);
|
||||
assert_eq!(
|
||||
status(&wat, &host),
|
||||
code(HostError::InvalidParams),
|
||||
"dst {dst} cap {cap}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The host reports a value's true length whether or not it fitted; a value that
|
||||
/// did not fit is the guest's error, not the host's.
|
||||
#[test]
|
||||
fn a_value_larger_than_the_buffer_is_refused() {
|
||||
let host = FakeHost::new().answering_field(1, Answer::filler(64));
|
||||
|
||||
let wat = module(
|
||||
&[import::HOME_LE_FIELD, ONE_PAGE],
|
||||
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 63))",
|
||||
);
|
||||
assert_eq!(status(&wat, &host), code(HostError::BufferTooSmall));
|
||||
|
||||
let wat = module(
|
||||
&[import::HOME_LE_FIELD, ONE_PAGE],
|
||||
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 64))",
|
||||
);
|
||||
assert_eq!(status(&wat, &host), 64, "exactly enough room is enough");
|
||||
}
|
||||
|
||||
/// A zero-length output region is in bounds and simply cannot hold anything.
|
||||
#[test]
|
||||
fn a_zero_length_output_region_is_in_bounds_but_too_small() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[import::LDGR_INDEX, ONE_PAGE],
|
||||
"(call $ldgr_index (i32.const 0) (i32.const 0))",
|
||||
);
|
||||
assert_eq!(status(&wat, &host), code(HostError::BufferTooSmall));
|
||||
}
|
||||
|
||||
/// A host that reports more than the per-field cap is refused even when the
|
||||
/// guest offered room for it: the cap is the engine's rule, not the buffer's.
|
||||
#[test]
|
||||
fn a_value_past_the_field_cap_is_refused() {
|
||||
let host = FakeHost::new()
|
||||
.answering_field(1, Answer::claiming(OVER_CAP as usize))
|
||||
.answering_field(2, Answer::claiming(MAX_FIELD_BYTES));
|
||||
|
||||
let wat = module(
|
||||
&[import::HOME_LE_FIELD, ONE_PAGE],
|
||||
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 4096))",
|
||||
);
|
||||
assert_eq!(status(&wat, &host), code(HostError::DataFieldTooLarge));
|
||||
|
||||
let wat = module(
|
||||
&[import::HOME_LE_FIELD, ONE_PAGE],
|
||||
"(call $home_le_field (i32.const 2) (i32.const 0) (i32.const 4096))",
|
||||
);
|
||||
assert_eq!(status(&wat, &host), CAP as i32, "the cap itself is allowed");
|
||||
}
|
||||
|
||||
/// A refused over-cap value leaves nothing behind. `write_into` hands the host at
|
||||
/// most [`MAX_FIELD_BYTES`] of the guest's buffer however much room the guest
|
||||
/// declared, so a value past the cap does not fit the region it is offered and no
|
||||
/// prefix of it can reach guest memory either.
|
||||
///
|
||||
/// The host answers with a real over-cap value: [`Answer::claiming`] writes
|
||||
/// nothing whatever the engine does, so it could not tell the two apart. The
|
||||
/// second module folds the *whole* declared buffer rather than one byte, so the
|
||||
/// claim is about the region and not about its first byte.
|
||||
#[test]
|
||||
fn an_over_cap_value_is_refused_without_reaching_guest_memory() {
|
||||
/// The buffer the guest declares: well over the cap, so the clamp bites.
|
||||
const BUFFER: usize = 4096;
|
||||
|
||||
let over_cap = vec![0xff; MAX_FIELD_BYTES + 1];
|
||||
let host = FakeHost::new().answering_field(1, Answer::bytes(over_cap));
|
||||
let call = format!("(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {BUFFER}))");
|
||||
|
||||
// The status the guest sees, from a module that returns it directly.
|
||||
let refusing = module(&[import::HOME_LE_FIELD, ONE_PAGE], &call);
|
||||
assert_eq!(
|
||||
status(&refusing, &host),
|
||||
code(HostError::DataFieldTooLarge),
|
||||
"the value is refused"
|
||||
);
|
||||
|
||||
// Every byte of the buffer, or-ed together: guest memory starts zero-filled,
|
||||
// so any byte the host wrote shows up here.
|
||||
let reading = module(
|
||||
&[import::HOME_LE_FIELD, ONE_PAGE],
|
||||
&format!(
|
||||
"(local $i i32)
|
||||
(local $seen i32)
|
||||
(drop {call})
|
||||
(loop $l
|
||||
(local.set $seen (i32.or (local.get $seen) (i32.load8_u (local.get $i))))
|
||||
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
||||
(br_if $l (i32.lt_u (local.get $i) (i32.const {BUFFER}))))
|
||||
(local.get $seen)"
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
status(&reading, &host),
|
||||
0,
|
||||
"and not one of its bytes is in the guest's buffer"
|
||||
);
|
||||
}
|
||||
|
||||
/// The field cap is checked before the buffer-fit rule, so a value that breaks both
|
||||
/// is reported as over-cap. The guest branches on the code, and the two rules
|
||||
/// answer different questions, so the order is worth pinning.
|
||||
#[test]
|
||||
fn the_field_cap_precedes_the_buffer_fit_check() {
|
||||
let host = FakeHost::new().answering_field(1, Answer::claiming(MAX_FIELD_BYTES + 1));
|
||||
|
||||
// A 63-byte buffer: the value is both over the cap and far too big to fit.
|
||||
let wat = module(
|
||||
&[import::HOME_LE_FIELD, ONE_PAGE],
|
||||
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 63))",
|
||||
);
|
||||
assert_eq!(status(&wat, &host), code(HostError::DataFieldTooLarge));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Input regions (`Region::read`, via `sha512_half`)
|
||||
//
|
||||
// `sha512_half`'s first pair is an input region like any other, and it is the
|
||||
// input the guest gets a status back from: `trace`, the other reader, answers
|
||||
// nothing at all. So the codes are pinned here and the silence below.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// An input region is bounds-checked the same way an output region is. Every case
|
||||
/// here stays within the field cap, which on an input is checked first.
|
||||
#[test]
|
||||
fn an_input_region_running_past_memory_is_refused() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for (ptr, len) in [(PAGE, 1), (PAGE - 3, 4), (PAGE - 1, CAP)] {
|
||||
let wat = module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
&format!(
|
||||
"(call $sha512_half (i32.const {ptr}) (i32.const {len})
|
||||
(i32.const 0) (i32.const {HASH_LEN}))"
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
status(&wat, &host),
|
||||
code(HostError::PointerOutOfBounds),
|
||||
"ptr {ptr} len {len}"
|
||||
);
|
||||
assert!(host.digested.borrow().is_empty(), "the host is not called");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negative_input_pointer_or_length_is_refused() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for (ptr, len) in [(-1, 1), (0, -1), (i32::MIN, 1)] {
|
||||
let wat = module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
&format!(
|
||||
"(call $sha512_half (i32.const {ptr}) (i32.const {len})
|
||||
(i32.const 0) (i32.const {HASH_LEN}))"
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
status(&wat, &host),
|
||||
code(HostError::InvalidParams),
|
||||
"ptr {ptr} len {len}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The field cap bounds what the guest may hand *in*, too.
|
||||
#[test]
|
||||
fn an_input_past_the_field_cap_is_refused() {
|
||||
let host = FakeHost::new();
|
||||
let digest = |len: i64| {
|
||||
module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
&format!(
|
||||
"(call $sha512_half (i32.const 0) (i32.const {len})
|
||||
(i32.const 2048) (i32.const {HASH_LEN}))"
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
status(&digest(OVER_CAP), &host),
|
||||
code(HostError::DataFieldTooLarge)
|
||||
);
|
||||
assert!(host.digested.borrow().is_empty());
|
||||
|
||||
assert_eq!(
|
||||
status(&digest(CAP), &host),
|
||||
HASH_LEN as i32,
|
||||
"the cap itself is allowed"
|
||||
);
|
||||
}
|
||||
|
||||
/// The two directions check in opposite orders: an input's length is known before
|
||||
/// the read, so the cap comes first, while an output's region has to be resolved
|
||||
/// before the host can produce a value, so bounds come first there.
|
||||
#[test]
|
||||
fn the_field_cap_precedes_the_bounds_check_on_an_input() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let reading = module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
&format!(
|
||||
"(call $sha512_half (i32.const 0) (i32.const {})
|
||||
(i32.const 0) (i32.const {HASH_LEN}))",
|
||||
PAGE + 1
|
||||
),
|
||||
);
|
||||
assert_eq!(status(&reading, &host), code(HostError::DataFieldTooLarge));
|
||||
|
||||
let writing = module(
|
||||
&[import::LDGR_INDEX, ONE_PAGE],
|
||||
&format!("(call $ldgr_index (i32.const 0) (i32.const {}))", PAGE + 1),
|
||||
);
|
||||
assert_eq!(status(&writing, &host), code(HostError::PointerOutOfBounds));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The reader with no result (`read_borrowed`, via `trace`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `trace` reads two regions and either one being bad refuses the call. The same
|
||||
/// rule as above, and the guest is told nothing: the refusal is the host not being
|
||||
/// called, and the run carries on to the constant that follows.
|
||||
#[test]
|
||||
fn both_of_traces_regions_are_checked_silently() {
|
||||
let host = FakeHost::new();
|
||||
let regions = [
|
||||
(
|
||||
format!("(i32.const {PAGE}) (i32.const 1)"),
|
||||
EMPTY_REGION.to_owned(),
|
||||
),
|
||||
(
|
||||
EMPTY_REGION.to_owned(),
|
||||
format!("(i32.const {PAGE}) (i32.const 1)"),
|
||||
),
|
||||
(
|
||||
EMPTY_REGION.to_owned(),
|
||||
format!("(i32.const 0) (i32.const {OVER_CAP})"),
|
||||
),
|
||||
(
|
||||
"(i32.const -1) (i32.const 1)".to_owned(),
|
||||
EMPTY_REGION.to_owned(),
|
||||
),
|
||||
];
|
||||
|
||||
for (msg, data) in regions {
|
||||
let wat = module(
|
||||
&[import::TRACE, ONE_PAGE],
|
||||
&traced(TraceDataType::AsHex, &msg, &data),
|
||||
);
|
||||
assert_eq!(status(&wat, &host), COMPLETED, "msg {msg} data {data}");
|
||||
assert!(
|
||||
host.traces().is_empty(),
|
||||
"msg {msg} data {data}: the host must not be called"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Both at once (`write_buffered`, via `sha512_half`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A call with an input and an output region decides everything about the input
|
||||
/// before anything about the output, so a bad input is reported however the output
|
||||
/// region is wrong — out of bounds, or a pointer that is not one at all.
|
||||
///
|
||||
/// The whole output region, params included, is judged after the host has answered.
|
||||
/// Hoisting any part of that above the call would put the output's verdict first for
|
||||
/// these cases, and there is no half of it that can be hoisted on a principle the
|
||||
/// other half shares.
|
||||
#[test]
|
||||
fn a_read_write_checks_its_input_before_its_output() {
|
||||
let host = FakeHost::new();
|
||||
let digest = |src: i64, src_len: i64, dst: i64| {
|
||||
module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
&format!(
|
||||
"(call $sha512_half (i32.const {src}) (i32.const {src_len})
|
||||
(i32.const {dst}) (i32.const {HASH_LEN}))"
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let over_cap = digest(0, OVER_CAP, 0);
|
||||
assert_eq!(status(&over_cap, &host), code(HostError::DataFieldTooLarge));
|
||||
|
||||
let out_of_bounds = digest(PAGE, 4, 0);
|
||||
assert_eq!(
|
||||
status(&out_of_bounds, &host),
|
||||
code(HostError::PointerOutOfBounds)
|
||||
);
|
||||
|
||||
// A bad input against each way the output can be wrong: the input's verdict is
|
||||
// the one reported, and the host is never asked for a value nobody can take.
|
||||
for dst in [PAGE, -1] {
|
||||
let both_bad = digest(0, OVER_CAP, dst);
|
||||
assert_eq!(
|
||||
status(&both_bad, &host),
|
||||
code(HostError::DataFieldTooLarge),
|
||||
"dst {dst}"
|
||||
);
|
||||
}
|
||||
assert!(host.digested.borrow().is_empty(), "the host is not reached");
|
||||
}
|
||||
|
||||
/// The output half of a read-write call obeys the same rules as a plain write.
|
||||
#[test]
|
||||
fn a_read_write_output_obeys_the_write_rules() {
|
||||
let host = FakeHost::new().answering_digest(Answer::filler(32));
|
||||
|
||||
let wat = module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
"(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 31))",
|
||||
);
|
||||
assert_eq!(status(&wat, &host), code(HostError::BufferTooSmall));
|
||||
|
||||
let wat = module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
&format!(
|
||||
"(call $sha512_half (i32.const 0) (i32.const 4) (i32.const {PAGE}) (i32.const 32))"
|
||||
),
|
||||
);
|
||||
assert_eq!(status(&wat, &host), code(HostError::PointerOutOfBounds));
|
||||
}
|
||||
|
||||
/// A refused value reaches guest memory in no part, however much of it the host
|
||||
/// wrote. The host answers with 32 bytes it did write and a length it did not, so
|
||||
/// the refusal happens with the value sitting in the run's output buffer — and the
|
||||
/// guest's buffer has to come back untouched.
|
||||
///
|
||||
/// Stronger than the contract asks for: a guest must not read its buffer on a
|
||||
/// negative status. It holds because the buffer is copied to the guest only after
|
||||
/// the length, the bounds, the fit and the budget have all passed, so there is no
|
||||
/// window in which a refused value is in guest memory.
|
||||
#[test]
|
||||
fn a_refused_value_leaves_nothing_in_guest_memory() {
|
||||
const MARKER: u8 = 77;
|
||||
|
||||
// The two refusals a value can meet after the host has produced it: longer
|
||||
// than the field cap, and longer than the buffer the guest offered.
|
||||
let refusals = [
|
||||
(MAX_FIELD_BYTES + 1, HASH_LEN, HostError::DataFieldTooLarge),
|
||||
(HASH_LEN, HASH_LEN - 1, HostError::BufferTooSmall),
|
||||
];
|
||||
|
||||
for (claimed, cap, expected) in refusals {
|
||||
let host =
|
||||
FakeHost::new().answering_digest(Answer::writing_but_claiming([MARKER; 32], claimed));
|
||||
let call = format!(
|
||||
"(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 64) (i32.const {cap}))"
|
||||
);
|
||||
|
||||
let refused = module(&[import::SHA512_HALF, ONE_PAGE], &call);
|
||||
assert_eq!(
|
||||
status(&refused, &host),
|
||||
code(expected),
|
||||
"claiming {claimed}"
|
||||
);
|
||||
|
||||
// The same call, reporting what is at the output region afterwards.
|
||||
let inspect = module(
|
||||
&[import::SHA512_HALF, ONE_PAGE],
|
||||
&format!("(drop {call}) (i32.load8_u (i32.const 64))"),
|
||||
);
|
||||
assert_eq!(
|
||||
status(&inspect, &host),
|
||||
0,
|
||||
"claiming {claimed}: the refused value must not have been written"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An input region may overlap the output region: the host is served the input as
|
||||
/// it stands and its answer lands afterwards, so the two cannot interfere. The
|
||||
/// marker is any byte distinct from the input's first (`a`), so `finish` returning
|
||||
/// it proves the write landed.
|
||||
#[test]
|
||||
fn an_input_may_overlap_the_output() {
|
||||
const MARKER: u8 = 99;
|
||||
|
||||
let host = FakeHost::new().answering_digest(Answer::bytes([MARKER; HASH_LEN]));
|
||||
|
||||
let wat = module(
|
||||
&[
|
||||
import::SHA512_HALF,
|
||||
ONE_PAGE,
|
||||
r#"(data (i32.const 0) "abcd")"#,
|
||||
],
|
||||
&format!(
|
||||
"(drop (call $sha512_half (i32.const 0) (i32.const 4)
|
||||
(i32.const 0) (i32.const {HASH_LEN})))
|
||||
(i32.load8_u (i32.const 0))"
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
status(&wat, &host),
|
||||
i32::from(MARKER),
|
||||
"the output overwrote the input"
|
||||
);
|
||||
assert_eq!(
|
||||
*host.digested.borrow(),
|
||||
vec![b"abcd".to_vec()],
|
||||
"the host saw the input as it was"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The memory export itself
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A host call with no memory to work in ends the run instead of answering the
|
||||
/// guest: there is no buffer for a status to describe, and nothing the guest could
|
||||
/// do about the answer — which is what puts this beside out-of-gas on the fatal
|
||||
/// channel. What the guest burned getting there is still charged.
|
||||
fn assert_no_memory(wat: &str, host: &FakeHost) {
|
||||
let failure = failure(wat, host);
|
||||
assert!(
|
||||
matches!(failure.error, RunError::NoMemory),
|
||||
"expected the run to end for want of a memory export, got: {failure}"
|
||||
);
|
||||
assert!(failure.fuel_used > 0, "{failure}");
|
||||
}
|
||||
|
||||
/// Every region is relative to the guest's exported memory, so a module without
|
||||
/// one cannot make a host call at all.
|
||||
#[test]
|
||||
fn a_module_that_exports_no_memory_cannot_call_the_host() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[import::LDGR_INDEX, "(memory 1)"],
|
||||
"(call $ldgr_index (i32.const 0) (i32.const 4))",
|
||||
);
|
||||
assert_no_memory(&wat, &host);
|
||||
}
|
||||
|
||||
/// Having no memory is answered before anything about a call's arguments, so a
|
||||
/// module without one ends the run even when its arguments would have earned a
|
||||
/// guest-visible code of their own (here an input over the field cap).
|
||||
///
|
||||
/// The order is deliberate: no memory is a fact about the instance, not about this
|
||||
/// call, and a region cannot be validated against a memory that is not there. It
|
||||
/// costs the guest nothing — every call such a module makes ends the run anyway.
|
||||
#[test]
|
||||
fn no_memory_is_answered_before_a_calls_arguments_are() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[import::SHA512_HALF, "(memory 1)"],
|
||||
&format!(
|
||||
"(call $sha512_half (i32.const 0) (i32.const {OVER_CAP})
|
||||
(i32.const 0) (i32.const {HASH_LEN}))"
|
||||
),
|
||||
);
|
||||
assert_no_memory(&wat, &host);
|
||||
}
|
||||
|
||||
/// The memory's export *name* is not part of the contract: the engine takes the
|
||||
/// module's memory whatever it is called. Nothing in the wasm spec attaches meaning
|
||||
/// to `"memory"` — it is a toolchain convention, so the kind decides.
|
||||
#[test]
|
||||
fn a_memory_exported_under_any_name_is_the_guests_memory() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for name in ["mem", "linear", "the memory"] {
|
||||
let wat = module(
|
||||
&[
|
||||
import::LDGR_INDEX,
|
||||
&format!(r#"(memory (export "{name}") 1)"#),
|
||||
],
|
||||
"(drop (call $ldgr_index (i32.const 64) (i32.const 4)))
|
||||
(i32.load (i32.const 64))",
|
||||
);
|
||||
assert_eq!(
|
||||
status(&wat, &host),
|
||||
7,
|
||||
"the host wrote into the memory exported as '{name}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One memory exported under several names is one memory. The engine resolves the
|
||||
/// first export of kind memory, and with at most one memory per module every such
|
||||
/// export is that memory, so the order the exports are walked in cannot change the
|
||||
/// answer.
|
||||
#[test]
|
||||
fn one_memory_exported_under_several_names_is_still_that_memory() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[
|
||||
import::LDGR_INDEX,
|
||||
r#"(memory (export "memory") (export "mem") (export "linear") 1)"#,
|
||||
],
|
||||
"(drop (call $ldgr_index (i32.const 64) (i32.const 4)))
|
||||
(i32.load (i32.const 64))",
|
||||
);
|
||||
assert_eq!(status(&wat, &host), 7);
|
||||
}
|
||||
|
||||
/// The export has to *be* a memory: a global named `memory` is not one, and it
|
||||
/// neither serves as the guest's memory nor hides the memory the module really
|
||||
/// exports. The kind decides, so the conventional name carries no weight on
|
||||
/// either side.
|
||||
#[test]
|
||||
fn an_export_named_memory_that_is_not_a_memory_is_not_the_guests_memory() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let call = "(call $ldgr_index (i32.const 0) (i32.const 4))";
|
||||
|
||||
let wrong_kind = module(
|
||||
&[
|
||||
import::LDGR_INDEX,
|
||||
"(memory 1)",
|
||||
r#"(global (export "memory") i32 (i32.const 0))"#,
|
||||
],
|
||||
call,
|
||||
);
|
||||
assert_no_memory(&wrong_kind, &host);
|
||||
|
||||
let shadowed = module(
|
||||
&[
|
||||
import::LDGR_INDEX,
|
||||
r#"(memory (export "mem") 1)"#,
|
||||
r#"(global (export "memory") i32 (i32.const 0))"#,
|
||||
],
|
||||
call,
|
||||
);
|
||||
assert_eq!(
|
||||
status(&shadowed, &host),
|
||||
4,
|
||||
"the real memory is found past the global that took its name"
|
||||
);
|
||||
}
|
||||
|
||||
/// Bounds follow the memory the module actually declared, not a fixed page.
|
||||
#[test]
|
||||
fn bounds_follow_the_declared_memory_size() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[import::LDGR_INDEX, r#"(memory (export "memory") 2)"#],
|
||||
&format!("(call $ldgr_index (i32.const {PAGE}) (i32.const 4))"),
|
||||
);
|
||||
assert_eq!(status(&wat, &host), 4, "the second page is in bounds");
|
||||
}
|
||||
516
crates/xrpl-wasm-vm/tests/preflight.rs
Normal file
516
crates/xrpl-wasm-vm/tests/preflight.rs
Normal file
@@ -0,0 +1,516 @@
|
||||
//! What screening refuses, and that it refuses nothing a run would have served.
|
||||
//!
|
||||
//! `check` reaches its verdict from the compiled module alone, so these tests take
|
||||
//! no host — except the ones that put the same module through `run` to compare the
|
||||
//! two.
|
||||
|
||||
mod support;
|
||||
|
||||
use support::{ENTRY, FakeHost, ONE_PAGE, PLENTY_OF_GAS, assemble, import, module};
|
||||
use xrpl_host_functions::HostFunctionSpec;
|
||||
use xrpl_wasm_vm::{CheckError, MAX_MEMORY_PAGES, RunError};
|
||||
|
||||
/// Assert which stage screening refused a module at, because the caller maps the
|
||||
/// stages separately. The error comes back out for the tests that also read its
|
||||
/// message.
|
||||
macro_rules! assert_stage {
|
||||
($refusal:expr, $stage:pat) => {{
|
||||
let refusal = $refusal;
|
||||
assert!(
|
||||
matches!(refusal, $stage),
|
||||
concat!("expected a ", stringify!($stage), " refusal, got: {}"),
|
||||
refusal
|
||||
);
|
||||
refusal
|
||||
}};
|
||||
}
|
||||
|
||||
/// Screens `wat`, which must assemble.
|
||||
fn check(wat: &str) -> Result<(), CheckError> {
|
||||
xrpl_wasm_vm::check(&assemble(wat), ENTRY)
|
||||
}
|
||||
|
||||
fn refusal(wat: &str) -> CheckError {
|
||||
check(wat).expect_err(&format!("expected this module to be refused:\n{wat}"))
|
||||
}
|
||||
|
||||
fn passes(wat: &str) {
|
||||
if let Err(refusal) = check(wat) {
|
||||
panic!("expected this module to pass, but: {refusal}\n{wat}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compiling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A contract that imports a host function, exports its memory and exports the
|
||||
/// entry point is what screening is looking for.
|
||||
#[test]
|
||||
fn a_runnable_contract_passes() {
|
||||
passes(&module(
|
||||
&[import::LDGR_INDEX, ONE_PAGE],
|
||||
"(call $ldgr_index (i32.const 0) (i32.const 4))",
|
||||
));
|
||||
}
|
||||
|
||||
/// Bytes that are not a wasm module at all.
|
||||
#[test]
|
||||
fn garbage_does_not_pass() {
|
||||
for bytes in [b"".as_slice(), b"not wasm", &[0x00, 0x61, 0x73, 0x6d]] {
|
||||
let refusal = xrpl_wasm_vm::check(bytes, ENTRY).expect_err("garbage must not pass");
|
||||
assert_stage!(refusal, CheckError::Compile(_));
|
||||
}
|
||||
}
|
||||
|
||||
/// Screening takes wasm binaries, and text is not one — the same rule the VM
|
||||
/// applies, from the same `wasmi` built without its `wat` feature. Turning that
|
||||
/// feature on would make this transaction blob valid at both ends.
|
||||
#[test]
|
||||
fn a_text_format_module_does_not_pass() {
|
||||
let text = module(&[ONE_PAGE], "(i32.const 0)");
|
||||
|
||||
let refusal =
|
||||
xrpl_wasm_vm::check(text.as_bytes(), ENTRY).expect_err("text must not pass as a module");
|
||||
assert_stage!(refusal, CheckError::Compile(_));
|
||||
|
||||
// The same module, assembled first, passes: the text is sound and only the
|
||||
// format was refused.
|
||||
passes(&text);
|
||||
}
|
||||
|
||||
/// A feature the engine disables is refused here too, because both stages compile
|
||||
/// against the one engine. `vm_limits.rs` walks every disabled feature; this pins
|
||||
/// that screening sees the same configuration.
|
||||
#[test]
|
||||
fn a_disabled_feature_does_not_pass() {
|
||||
let refusal = refusal(&module(
|
||||
&[ONE_PAGE],
|
||||
"(drop (f64.add (f64.const 1) (f64.const 2))) (i32.const 0)",
|
||||
));
|
||||
let refusal = assert_stage!(refusal, CheckError::Compile(_)).to_string();
|
||||
assert!(refusal.contains("floating-point"), "{refusal}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Imports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Every host function the ABI declares, spelled as a guest imports it. The count
|
||||
/// is asserted against the ABI so a function added to it cannot be left out here.
|
||||
const ALL_IMPORTS: [&str; 61] = [
|
||||
import::LDGR_INDEX,
|
||||
import::PARENT_LDGR_TIME,
|
||||
import::PARENT_LDGR_HASH,
|
||||
import::BASE_FEE,
|
||||
import::AMENDMENT_ENABLED,
|
||||
import::CACHE_LE,
|
||||
import::TX_FIELD,
|
||||
import::HOME_LE_FIELD,
|
||||
import::LE_FIELD,
|
||||
import::TX_INNER,
|
||||
import::HOME_LE_INNER,
|
||||
import::LE_INNER,
|
||||
import::TX_ARR_LEN,
|
||||
import::HOME_LE_ARR_LEN,
|
||||
import::LE_ARR_LEN,
|
||||
import::TX_INNER_ARR_LEN,
|
||||
import::HOME_LE_INNER_ARR_LEN,
|
||||
import::LE_INNER_ARR_LEN,
|
||||
import::CHECK_SIG,
|
||||
import::ACCOUNTROOT_ID,
|
||||
import::AMM_ID,
|
||||
import::CHECK_ID,
|
||||
import::CREDENTIAL_ID,
|
||||
import::DELEGATE_ID,
|
||||
import::DEPOSIT_PREAUTH_ID,
|
||||
import::DID_ID,
|
||||
import::ESCROW_ID,
|
||||
import::TRUSTLINE_ID,
|
||||
import::MPT_ISSUANCE_ID,
|
||||
import::MPTOKEN_ID,
|
||||
import::NFT_OFFER_ID,
|
||||
import::OFFER_ID,
|
||||
import::ORACLE_ID,
|
||||
import::PAYCHAN_ID,
|
||||
import::PERMISSIONED_DOMAIN_ID,
|
||||
import::SIGNERS_ID,
|
||||
import::TICKET_ID,
|
||||
import::VAULT_ID,
|
||||
import::SHA512_HALF,
|
||||
import::TRACE,
|
||||
import::SET_DATA,
|
||||
import::NFT_URI,
|
||||
import::NFT_ISSUER,
|
||||
import::NFT_TAXON,
|
||||
import::NFT_FLAGS,
|
||||
import::NFT_XFER_FEE,
|
||||
import::NFT_SERIAL,
|
||||
import::FLOAT_FROM_INT,
|
||||
import::FLOAT_FROM_UINT,
|
||||
import::FLOAT_FROM_STAMOUNT,
|
||||
import::FLOAT_FROM_STNUMBER,
|
||||
import::FLOAT_TO_INT,
|
||||
import::FLOAT_TO_MANT_EXP,
|
||||
import::FLOAT_FROM_MANT_EXP,
|
||||
import::FLOAT_CMP,
|
||||
import::FLOAT_ADD,
|
||||
import::FLOAT_SUB,
|
||||
import::FLOAT_MULT,
|
||||
import::FLOAT_DIV,
|
||||
import::FLOAT_ROOT,
|
||||
import::FLOAT_POW,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn every_declared_host_function_may_be_imported() {
|
||||
assert_eq!(
|
||||
ALL_IMPORTS.len(),
|
||||
HostFunctionSpec::ALL.len(),
|
||||
"the ABI gained a host function with no import declaration in this test"
|
||||
);
|
||||
|
||||
let mut parts = ALL_IMPORTS.to_vec();
|
||||
parts.push(ONE_PAGE);
|
||||
passes(&module(&parts, "(i32.const 0)"));
|
||||
}
|
||||
|
||||
/// A module may import fewer host functions than are registered, but not more.
|
||||
#[test]
|
||||
fn an_unknown_host_function_does_not_pass() {
|
||||
let refusal = refusal(&module(
|
||||
&[
|
||||
r#"(import "host_lib" "no_such_function" (func $f (param i32) (result i32)))"#,
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(call $f (i32.const 0))",
|
||||
));
|
||||
let refusal = assert_stage!(refusal, CheckError::Import(_)).to_string();
|
||||
assert!(
|
||||
refusal.contains("no host function 'no_such_function'"),
|
||||
"{refusal}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Host functions live under one module name — `host_lib` — and an import naming
|
||||
/// another is refused even when the function name is real. `env` is in the list
|
||||
/// because that is what plain clang emits.
|
||||
#[test]
|
||||
fn an_import_from_another_module_does_not_pass() {
|
||||
for module_name in ["host", "env", ""] {
|
||||
let refusal = refusal(&module(
|
||||
&[
|
||||
&format!(
|
||||
r#"(import "{module_name}" "ldgr_index" (func $f (param i32 i32) (result i32)))"#
|
||||
),
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(call $f (i32.const 0) (i32.const 4))",
|
||||
));
|
||||
let refusal = assert_stage!(refusal, CheckError::Import(_)).to_string();
|
||||
assert!(refusal.contains("is not from 'host_lib'"), "{refusal}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A host function's name imported as something other than a function. The engine
|
||||
/// defines it as a function and nothing else, so this does not link either.
|
||||
#[test]
|
||||
fn a_host_function_imported_as_a_global_does_not_pass() {
|
||||
let refusal = refusal(&module(
|
||||
&[
|
||||
r#"(import "host_lib" "ldgr_index" (global $g i32))"#,
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(global.get $g)",
|
||||
));
|
||||
let refusal = assert_stage!(refusal, CheckError::Import(_)).to_string();
|
||||
assert!(
|
||||
refusal.contains("'host_lib::ldgr_index' is not a function"),
|
||||
"{refusal}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A module faulty at two stages is refused by the earlier one — it imports what no
|
||||
/// engine serves *and* exports no entry point. The imports are what the rest of the
|
||||
/// module depends on, so that is the message worth having.
|
||||
#[test]
|
||||
fn the_earlier_stage_is_the_one_reported() {
|
||||
let refusal = refusal(
|
||||
r#"(module
|
||||
(import "host_lib" "no_such_function" (func $f (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(func (export "not_the_entry_point") (result i32) (call $f)))"#,
|
||||
);
|
||||
|
||||
assert_stage!(refusal, CheckError::Import(_));
|
||||
}
|
||||
|
||||
/// The signature is the one part of an import screening does not compare, so a
|
||||
/// module that will not link can still pass. Recorded here because it is the gap
|
||||
/// this stage leaves, not because it is wanted.
|
||||
#[test]
|
||||
fn an_import_with_the_wrong_signature_still_passes() {
|
||||
let wat = module(
|
||||
&[
|
||||
r#"(import "host_lib" "ldgr_index" (func $f (param i64 i64) (result i32)))"#,
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
passes(&wat);
|
||||
|
||||
let host = FakeHost::new();
|
||||
let failure = xrpl_wasm_vm::run(&assemble(&wat), PLENTY_OF_GAS, &host, ENTRY)
|
||||
.expect_err("a mistyped import must not link");
|
||||
assert!(
|
||||
matches!(failure.error, RunError::Instantiate(_)),
|
||||
"{failure}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn a_missing_entry_point_does_not_pass() {
|
||||
let refusal = refusal(
|
||||
r#"(module (memory (export "memory") 1)
|
||||
(func (export "other") (result i32) (i32.const 0)))"#,
|
||||
);
|
||||
let refusal = assert_stage!(refusal, CheckError::EntryPoint(_)).to_string();
|
||||
assert_eq!(refusal, "no entry point 'finish'");
|
||||
}
|
||||
|
||||
/// The entry point is looked up by the name the caller asks for, as a run looks it
|
||||
/// up: screening a contract for one entry point says nothing about another.
|
||||
#[test]
|
||||
fn the_entry_point_is_the_name_the_caller_gives() {
|
||||
let wasm = assemble(
|
||||
r#"(module (memory (export "memory") 1)
|
||||
(func (export "other") (result i32) (i32.const 0)))"#,
|
||||
);
|
||||
|
||||
assert!(xrpl_wasm_vm::check(&wasm, "other").is_ok());
|
||||
assert!(xrpl_wasm_vm::check(&wasm, ENTRY).is_err());
|
||||
}
|
||||
|
||||
/// Both halves of the entry point's type are screened: a module returning the
|
||||
/// wrong thing, or taking anything at all, would fail the run's typed lookup.
|
||||
#[test]
|
||||
fn an_entry_point_of_the_wrong_type_does_not_pass() {
|
||||
for (signature, body) in [
|
||||
("(result i64)", "(i64.const 0)"),
|
||||
("(param i32) (result i32)", "(i32.const 0)"),
|
||||
("", "(nop)"),
|
||||
] {
|
||||
let refusal = refusal(&format!(
|
||||
r#"(module (memory (export "memory") 1)
|
||||
(func (export "finish") {signature} {body}))"#
|
||||
));
|
||||
let refusal = assert_stage!(refusal, CheckError::EntryPoint(_)).to_string();
|
||||
assert_eq!(
|
||||
refusal, "entry point 'finish' has the wrong signature, expected '() -> i32'",
|
||||
"{signature}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An export of the entry point's name that is not a function at all is a third
|
||||
/// case, and named as such: nothing is missing and no signature is wrong.
|
||||
#[test]
|
||||
fn an_entry_point_that_is_not_a_function_does_not_pass() {
|
||||
let refusal = refusal(
|
||||
r#"(module (memory (export "memory") 1) (global (export "finish") i32 (i32.const 0)))"#,
|
||||
);
|
||||
let refusal = assert_stage!(refusal, CheckError::EntryPoint(_)).to_string();
|
||||
assert_eq!(refusal, "export 'finish' is not a function");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agreement with a run
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A module with no linear memory to export passes. A contract that makes no host
|
||||
/// call needs none, and one that does is refused at the call and charged — a
|
||||
/// runtime fault, not a malformed module.
|
||||
#[test]
|
||||
fn a_module_exporting_no_memory_passes() {
|
||||
let wat = r#"(module (func (export "finish") (result i32) (i32.const 0)))"#;
|
||||
passes(wat);
|
||||
|
||||
let host = FakeHost::new();
|
||||
assert_eq!(
|
||||
xrpl_wasm_vm::run(&assemble(wat), PLENTY_OF_GAS, &host, ENTRY)
|
||||
.expect("a module that calls no host function needs no memory")
|
||||
.result,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/// Modules spanning what screening decides, each also put through a run.
|
||||
fn modules() -> Vec<(&'static str, String)> {
|
||||
vec![
|
||||
(
|
||||
"a runnable contract",
|
||||
module(&[import::LDGR_INDEX, ONE_PAGE], "(i32.const 0)"),
|
||||
),
|
||||
(
|
||||
"a contract that traps",
|
||||
module(&[ONE_PAGE], "(unreachable)"),
|
||||
),
|
||||
(
|
||||
"a disabled feature",
|
||||
module(&[ONE_PAGE], "(i32.extend8_s (i32.const 1))"),
|
||||
),
|
||||
(
|
||||
"an unknown host function",
|
||||
module(
|
||||
&[
|
||||
r#"(import "host_lib" "nope" (func $f (result i32)))"#,
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(call $f)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"an import from another module",
|
||||
module(
|
||||
&[
|
||||
r#"(import "env" "ldgr_index" (func $f (param i32 i32) (result i32)))"#,
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(i32.const 0)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"a host function imported as a global",
|
||||
module(
|
||||
&[r#"(import "host_lib" "trace" (global $g i32))"#, ONE_PAGE],
|
||||
"(global.get $g)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"no entry point",
|
||||
r#"(module (memory (export "memory") 1)
|
||||
(func (export "other") (result i32) (i32.const 0)))"#
|
||||
.to_string(),
|
||||
),
|
||||
(
|
||||
"an entry point of the wrong type",
|
||||
r#"(module (memory (export "memory") 1)
|
||||
(func (export "finish") (result i64) (i64.const 0)))"#
|
||||
.to_string(),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Screening refuses a module exactly when a run would refuse it at one of the
|
||||
/// three stages screening covers — nothing it rejects would have run, and nothing
|
||||
/// it passes stops before the entry point is called. The exceptions are the ones
|
||||
/// [`what_static_screening_cannot_see`] lists.
|
||||
#[test]
|
||||
fn screening_and_a_run_agree() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for (label, wat) in modules() {
|
||||
let wasm = assemble(&wat);
|
||||
let refused_early = match xrpl_wasm_vm::run(&wasm, PLENTY_OF_GAS, &host, ENTRY) {
|
||||
Err(failure) => matches!(
|
||||
failure.error,
|
||||
RunError::Compile(_) | RunError::Instantiate(_) | RunError::EntryPoint(_)
|
||||
),
|
||||
Ok(_) => false,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
xrpl_wasm_vm::check(&wasm, ENTRY).is_err(),
|
||||
refused_early,
|
||||
"{label}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A module asking for more memory than the engine grants is refused, so the
|
||||
/// contract that could never run does not reach the ledger. The cap itself passes.
|
||||
#[test]
|
||||
fn an_exported_memory_past_the_cap_does_not_pass() {
|
||||
let wat = module(
|
||||
&[&format!(
|
||||
r#"(memory (export "memory") {})"#,
|
||||
MAX_MEMORY_PAGES + 1
|
||||
)],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
let refusal = assert_stage!(refusal(&wat), CheckError::Memory(_)).to_string();
|
||||
assert!(refusal.contains("past the 128-page cap"), "{refusal}");
|
||||
|
||||
passes(&module(
|
||||
&[&format!(r#"(memory (export "memory") {MAX_MEMORY_PAGES})"#)],
|
||||
"(i32.const 0)",
|
||||
));
|
||||
}
|
||||
|
||||
/// A declared *maximum* past the cap is legal and simply unreachable, so screening
|
||||
/// must not turn it away: `vm_limits` runs this very module to completion.
|
||||
#[test]
|
||||
fn a_declared_maximum_past_the_cap_still_passes() {
|
||||
passes(&module(
|
||||
&[&format!(
|
||||
r#"(memory (export "memory") 1 {})"#,
|
||||
MAX_MEMORY_PAGES + 1
|
||||
)],
|
||||
"(i32.const 0)",
|
||||
));
|
||||
}
|
||||
|
||||
/// The gap, listed rather than described, and now one entry long. A memory a module
|
||||
/// keeps to itself is not in its exports, so this is the one module that passes
|
||||
/// screening and then fails to *instantiate* — which is why a run's refusal at that
|
||||
/// stage cannot be read as the node's fault.
|
||||
///
|
||||
/// A contract needs an exported memory to make any host call, so a module of this
|
||||
/// shape can do nothing but compute; the SDK does not produce one.
|
||||
#[test]
|
||||
fn what_static_screening_cannot_see() {
|
||||
let host = FakeHost::new();
|
||||
let wat = format!(
|
||||
r#"(module (memory {})
|
||||
(func (export "finish") (result i32) (i32.const 0)))"#,
|
||||
MAX_MEMORY_PAGES + 1
|
||||
);
|
||||
|
||||
passes(&wat);
|
||||
|
||||
let failure = xrpl_wasm_vm::run(&assemble(&wat), PLENTY_OF_GAS, &host, ENTRY)
|
||||
.expect_err("the store's limiter must refuse the memory");
|
||||
assert!(
|
||||
matches!(failure.error, RunError::Instantiate(_)),
|
||||
"{failure}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A start section is guest code, so screening cannot see whether it traps — but it
|
||||
/// no longer has to. A trap is the guest's fault wherever it happens, so the run
|
||||
/// charges the contract for what it burned instead of reporting a module the node
|
||||
/// should have screened.
|
||||
#[test]
|
||||
fn a_start_section_screening_cannot_see_is_charged_as_a_trap() {
|
||||
let host = FakeHost::new();
|
||||
let wat = format!(
|
||||
r#"(module {ONE_PAGE}
|
||||
(func $init (unreachable))
|
||||
(start $init)
|
||||
(func (export "finish") (result i32) (i32.const 0)))"#
|
||||
);
|
||||
|
||||
passes(&wat);
|
||||
|
||||
let failure = xrpl_wasm_vm::run(&assemble(&wat), PLENTY_OF_GAS, &host, ENTRY)
|
||||
.expect_err("a start section that traps must not complete the run");
|
||||
assert!(matches!(failure.error, RunError::Trap(_)), "{failure}");
|
||||
assert!(
|
||||
failure.fuel_used > 0,
|
||||
"charged for what it burned: {failure}"
|
||||
);
|
||||
}
|
||||
1550
crates/xrpl-wasm-vm/tests/support/mod.rs
Normal file
1550
crates/xrpl-wasm-vm/tests/support/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
580
crates/xrpl-wasm-vm/tests/vm_limits.rs
Normal file
580
crates/xrpl-wasm-vm/tests/vm_limits.rs
Normal file
@@ -0,0 +1,580 @@
|
||||
//! What the engine refuses outright: modules it will not compile, will not
|
||||
//! instantiate, or cannot find an entry point in — plus the linear-memory cap.
|
||||
//!
|
||||
//! These are the sandbox's outer wall. Everything here fails the run rather than
|
||||
//! returning a code to the guest, so each test reads the failure's message.
|
||||
|
||||
mod support;
|
||||
|
||||
use support::{
|
||||
FakeHost, ONE_PAGE, PLENTY_OF_GAS, failure, import, module, run, run_entry, run_with_gas,
|
||||
};
|
||||
use xrpl_wasm_vm::{MAX_MEMORY_PAGES, RunError};
|
||||
|
||||
/// Assert which stage a run failed at, because the caller maps the stages to
|
||||
/// different outcomes. A stage is one `RunError` variant, so the expectation is a
|
||||
/// pattern; the failure comes back out for the tests that also read its message.
|
||||
macro_rules! assert_stage {
|
||||
($failure:expr, $stage:pat) => {{
|
||||
let failure = $failure;
|
||||
assert!(
|
||||
matches!(failure.error, $stage),
|
||||
concat!("expected a ", stringify!($stage), " failure, got: {}"),
|
||||
failure
|
||||
);
|
||||
failure
|
||||
}};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Linear memory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A module declaring more than the cap fails to instantiate — the limit applies
|
||||
/// to the initial memory, not only to growth.
|
||||
#[test]
|
||||
fn an_initial_memory_past_the_cap_is_refused() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[&format!(
|
||||
r#"(memory (export "memory") {})"#,
|
||||
MAX_MEMORY_PAGES + 1
|
||||
)],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
|
||||
}
|
||||
|
||||
/// The cap itself is allowed.
|
||||
#[test]
|
||||
fn an_initial_memory_at_the_cap_is_allowed() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[&format!(r#"(memory (export "memory") {MAX_MEMORY_PAGES})"#)],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_eq!(run(&wat, &host).expect("should run").result, 0);
|
||||
}
|
||||
|
||||
/// Growth up to the cap succeeds; growth past it traps rather than answering -1 as
|
||||
/// `memory.grow` otherwise would, because the engine's limiter sets
|
||||
/// `trap_on_grow_failure(true)`.
|
||||
#[test]
|
||||
fn growth_stops_at_the_cap() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[ONE_PAGE],
|
||||
&format!("(memory.grow (i32.const {}))", MAX_MEMORY_PAGES - 1),
|
||||
);
|
||||
assert_eq!(
|
||||
run(&wat, &host).expect("should run").result,
|
||||
1,
|
||||
"growing to exactly the cap answers the previous size"
|
||||
);
|
||||
|
||||
let wat = module(
|
||||
&[ONE_PAGE],
|
||||
&format!("(memory.grow (i32.const {MAX_MEMORY_PAGES}))"),
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Trap(_));
|
||||
}
|
||||
|
||||
/// A module may declare a maximum above the cap: the cap is enforced on the initial
|
||||
/// memory and on growth, not on the memory type's declared bound.
|
||||
#[test]
|
||||
fn a_declared_maximum_past_the_cap_is_allowed_but_unreachable() {
|
||||
let host = FakeHost::new();
|
||||
let memory = format!(r#"(memory (export "memory") 1 {})"#, MAX_MEMORY_PAGES + 1);
|
||||
|
||||
let wat = module(&[&memory], "(i32.const 0)");
|
||||
assert_eq!(run(&wat, &host).expect("should run").result, 0);
|
||||
|
||||
let wat = module(
|
||||
&[&memory],
|
||||
&format!("(memory.grow (i32.const {MAX_MEMORY_PAGES}))"),
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Trap(_));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Engine configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One row per feature `build_wasm_engine` turns off: the smallest module that uses
|
||||
/// it, and the fragment of wasmi's refusal that names the feature. A row declaring
|
||||
/// its own memory omits [`ONE_PAGE`], or it is refused for having two memories
|
||||
/// instead.
|
||||
fn disabled_features() -> Vec<(&'static str, Vec<&'static str>, &'static str, &'static str)> {
|
||||
vec![
|
||||
(
|
||||
"wasm_multi_value",
|
||||
vec![
|
||||
ONE_PAGE,
|
||||
"(func $two (result i32 i32) (i32.const 1) (i32.const 2))",
|
||||
],
|
||||
"(call $two) (drop) (drop) (i32.const 0)",
|
||||
"multi-value",
|
||||
),
|
||||
(
|
||||
"wasm_sign_extension",
|
||||
vec![ONE_PAGE],
|
||||
"(i32.extend8_s (i32.const 1))",
|
||||
"sign extension",
|
||||
),
|
||||
(
|
||||
"wasm_bulk_memory",
|
||||
vec![ONE_PAGE],
|
||||
"(memory.fill (i32.const 0) (i32.const 0) (i32.const 1)) (i32.const 0)",
|
||||
"bulk memory",
|
||||
),
|
||||
(
|
||||
"wasm_reference_types",
|
||||
vec![ONE_PAGE, "(table 1 externref)"],
|
||||
"(i32.const 0)",
|
||||
"reference types",
|
||||
),
|
||||
// The proposal covers mutable globals crossing the module boundary; an
|
||||
// internal one is core wasm and stays allowed — see the test below.
|
||||
(
|
||||
"wasm_mutable_global",
|
||||
vec![ONE_PAGE, r#"(global (export "g") (mut i32) (i32.const 0))"#],
|
||||
"(i32.const 0)",
|
||||
"mutable global",
|
||||
),
|
||||
(
|
||||
"wasm_tail_call",
|
||||
vec![ONE_PAGE, "(func $f (result i32) (i32.const 0))"],
|
||||
"(return_call $f)",
|
||||
"tail call",
|
||||
),
|
||||
// Arithmetic in a constant initialiser. wasmi names the operator rather
|
||||
// than the proposal here.
|
||||
(
|
||||
"wasm_extended_const",
|
||||
vec![
|
||||
ONE_PAGE,
|
||||
"(global $g i32 (i32.add (i32.const 1) (i32.const 2)))",
|
||||
],
|
||||
"(global.get $g)",
|
||||
"non-constant operator",
|
||||
),
|
||||
(
|
||||
"wasm_multi_memory",
|
||||
vec![ONE_PAGE, "(memory 1)"],
|
||||
"(i32.const 0)",
|
||||
"multiple memories",
|
||||
),
|
||||
(
|
||||
"wasm_memory64",
|
||||
vec![r#"(memory (export "memory") i64 1)"#],
|
||||
"(i32.const 0)",
|
||||
"memory64",
|
||||
),
|
||||
(
|
||||
"wasm_custom_page_sizes",
|
||||
vec![r#"(memory (export "memory") 1 (pagesize 1))"#],
|
||||
"(i32.const 0)",
|
||||
"custom page sizes",
|
||||
),
|
||||
(
|
||||
"wasm_wide_arithmetic",
|
||||
vec![ONE_PAGE],
|
||||
"(drop (i64.add128 (i64.const 1) (i64.const 2) (i64.const 3) (i64.const 4)))
|
||||
(i32.const 0)",
|
||||
"wide arithmetic",
|
||||
),
|
||||
// Determinism across nodes is the reason floats are off.
|
||||
(
|
||||
"floats",
|
||||
vec![ONE_PAGE],
|
||||
"(drop (f64.add (f64.const 1) (f64.const 2))) (i32.const 0)",
|
||||
"floating-point",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Every feature the engine disables is refused, and refused for that reason.
|
||||
///
|
||||
/// `wasm_custom_page_sizes` and `wasm_wide_arithmetic` are off by default in wasmi
|
||||
/// 1.1 (`engine/config.rs:72,74`), so their rows guard against wasmi changing that
|
||||
/// default rather than against our own config.
|
||||
#[test]
|
||||
fn every_disabled_feature_is_refused_by_name() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for (knob, parts, body, expected) in disabled_features() {
|
||||
let wat = module(&parts, body);
|
||||
let failure = assert_stage!(failure(&wat, &host), RunError::Compile(_)).to_string();
|
||||
|
||||
assert!(
|
||||
failure.contains(expected),
|
||||
"{knob}: expected a refusal mentioning {expected:?}, got: {failure}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The three knobs [`every_disabled_feature_is_refused_by_name`] cannot cover. The
|
||||
/// engine is a process-wide `LazyLock`, so a test observes the one configuration we
|
||||
/// build: a knob masked by another, or with no caller-visible effect, has no
|
||||
/// distinguishing module.
|
||||
#[test]
|
||||
fn the_knobs_without_a_module_of_their_own() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
// `wasm_saturating_float_to_int(false)`: every saturating conversion takes a
|
||||
// float operand, so `floats(false)` refuses it first, as the message shows.
|
||||
let wat = module(&[ONE_PAGE], "(i32.trunc_sat_f32_s (f32.const 1))");
|
||||
let refusal = failure(&wat, &host).to_string();
|
||||
assert!(refusal.contains("floating-point"), "{refusal}");
|
||||
assert!(!refusal.contains("saturating"), "{refusal}");
|
||||
|
||||
// `ignore_custom_sections(true)`: governs whether wasmi retains custom
|
||||
// sections, not accept/reject, so this pins only that one is harmless.
|
||||
let wat = module(
|
||||
&[ONE_PAGE, r#"(@custom "note" "ignored")"#],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_eq!(run(&wat, &host).expect("should run").result, 0);
|
||||
|
||||
// `consume_fuel(true)`: with it off, `Store::set_fuel` fails and `run` returns
|
||||
// before instantiating, so every test in the suite fails.
|
||||
let wat = module(&[ONE_PAGE], "(i32.const 0)");
|
||||
assert!(run(&wat, &host).expect("should run").fuel_used > 0);
|
||||
}
|
||||
|
||||
/// A mutable global the module keeps to itself is core wasm, so the disabled
|
||||
/// proposal does not reach it: a guest can still have mutable state.
|
||||
#[test]
|
||||
fn an_internal_mutable_global_is_still_allowed() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[ONE_PAGE, "(global $g (mut i32) (i32.const 0))"],
|
||||
"(global.set $g (i32.const 7)) (global.get $g)",
|
||||
);
|
||||
assert_eq!(run(&wat, &host).expect("should run").result, 7);
|
||||
}
|
||||
|
||||
/// Bytes that are not a wasm module at all.
|
||||
#[test]
|
||||
fn garbage_does_not_compile() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for bytes in [b"".as_slice(), b"not wasm", &[0x00, 0x61, 0x73, 0x6d]] {
|
||||
let failure = xrpl_wasm_vm::run(bytes, PLENTY_OF_GAS, &host, support::ENTRY)
|
||||
.expect_err("garbage must not compile");
|
||||
assert_stage!(failure, RunError::Compile(_));
|
||||
}
|
||||
}
|
||||
|
||||
/// The VM takes wasm binaries, and text is not one. wasmi's `wat` feature is on by
|
||||
/// default and would have `Module::new` assemble text too, so the crate builds
|
||||
/// wasmi without it; turning it back on would make this transaction blob valid.
|
||||
#[test]
|
||||
fn the_vm_refuses_a_text_format_module() {
|
||||
let host = FakeHost::new();
|
||||
let text = module(&[ONE_PAGE], "(i32.const 0)");
|
||||
|
||||
let failure = xrpl_wasm_vm::run(text.as_bytes(), PLENTY_OF_GAS, &host, support::ENTRY)
|
||||
.expect_err("text must not compile as a module");
|
||||
assert_stage!(failure, RunError::Compile(_));
|
||||
|
||||
// The same module, assembled first, runs: the text is sound and only the
|
||||
// format was refused.
|
||||
assert_eq!(run(&text, &host).expect("should run").result, 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Imports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A module may import fewer host functions than are registered, but not more:
|
||||
/// an import the linker does not define fails instantiation.
|
||||
#[test]
|
||||
fn an_unknown_import_fails_instantiation() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[
|
||||
r#"(import "host_lib" "no_such_function" (func $f (param i32) (result i32)))"#,
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(call $f (i32.const 0))",
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
|
||||
}
|
||||
|
||||
/// Host functions are registered under one module name — `host_lib`, the name the
|
||||
/// guest SDK and this repo's fixtures import from — and a guest naming a different
|
||||
/// one does not link. `env` is in the list because that is what plain clang emits.
|
||||
#[test]
|
||||
fn the_import_module_name_must_match() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for module_name in ["host", "env", ""] {
|
||||
let wat = module(
|
||||
&[
|
||||
&format!(
|
||||
r#"(import "{module_name}" "ldgr_index" (func $f (param i32 i32) (result i32)))"#
|
||||
),
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(call $f (i32.const 0) (i32.const 4))",
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
|
||||
}
|
||||
}
|
||||
|
||||
/// An import spelled with the wrong signature does not link even under the right
|
||||
/// name, which is what makes the registered signatures load-bearing.
|
||||
#[test]
|
||||
fn an_import_with_the_wrong_signature_fails_instantiation() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for signature in [
|
||||
"(param i32) (result i32)", // too few parameters
|
||||
"(param i32 i32 i32) (result i32)", // too many
|
||||
"(param i64 i64) (result i32)", // wrong parameter types
|
||||
"(param i32 i32) (result i64)", // wrong result type
|
||||
"(param i32 i32)", // no result
|
||||
] {
|
||||
let wat = module(
|
||||
&[
|
||||
&format!(r#"(import "host_lib" "ldgr_index" (func $f {signature}))"#),
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
|
||||
}
|
||||
}
|
||||
|
||||
/// A module that imports a host function it never calls still has to link.
|
||||
#[test]
|
||||
fn an_unused_import_is_still_linked() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[import::LDGR_INDEX, import::TRACE, ONE_PAGE],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_eq!(run(&wat, &host).expect("should run").result, 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The start section
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A start section runs guest code during instantiation, before the entry point
|
||||
/// is even looked up, and `set_fuel` and the memory limiter are both installed by
|
||||
/// then — so it is metered like any other guest code, and a run it stops is
|
||||
/// charged for what it burned.
|
||||
///
|
||||
/// Reported as a **trap**, not as a module that would not instantiate: a trap is the
|
||||
/// guest's fault wherever it happens, and the stage a run stopped at is not what the
|
||||
/// caller maps. Filing it under the stage would put a contract's own defect among the
|
||||
/// faults a caller treats as the node's, and charge nothing for the instructions the
|
||||
/// contract burned reaching it.
|
||||
#[test]
|
||||
fn a_trapping_start_section_is_a_guest_trap_and_is_charged() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = format!(
|
||||
r#"(module {ONE_PAGE}
|
||||
(func $init (unreachable))
|
||||
(start $init)
|
||||
(func (export "finish") (result i32) (i32.const 0)))"#
|
||||
);
|
||||
let failure = assert_stage!(
|
||||
run_with_gas(&wat, PLENTY_OF_GAS, &host)
|
||||
.expect_err("a start section that traps must not complete the run"),
|
||||
RunError::Trap(_)
|
||||
);
|
||||
assert!(
|
||||
failure.fuel_used > 0,
|
||||
"the start section's instructions are metered: {failure}"
|
||||
);
|
||||
}
|
||||
|
||||
/// What `RunError::Instantiate` is left to mean: a module the linker or the store
|
||||
/// would not accept, rather than one whose guest code failed. Its two shapes, so the
|
||||
/// variant is not left standing for nothing.
|
||||
#[test]
|
||||
fn instantiation_failure_is_a_module_the_engine_will_not_accept() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
// The linker defines no such import.
|
||||
let wat = module(
|
||||
&[
|
||||
r#"(import "host_lib" "no_such_function" (func $f (result i32)))"#,
|
||||
ONE_PAGE,
|
||||
],
|
||||
"(call $f)",
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
|
||||
|
||||
// The store's limiter will not grant the memory, and does not trap to say so.
|
||||
let wat = module(
|
||||
&[&format!("(memory {})", MAX_MEMORY_PAGES + 1)],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
|
||||
}
|
||||
|
||||
/// A start section that runs out of gas is reported as out of gas, not as a module
|
||||
/// that would not instantiate. The stage a run stopped at is not what the caller
|
||||
/// maps — the reason is — and gas exhaustion is one outcome wherever the guest
|
||||
/// reaches it.
|
||||
#[test]
|
||||
fn a_start_section_that_exhausts_gas_is_out_of_gas_not_an_instantiation_failure() {
|
||||
const GAS: u64 = 10_000;
|
||||
|
||||
let host = FakeHost::new();
|
||||
let wat = format!(
|
||||
r#"(module {ONE_PAGE}
|
||||
(func $init (loop $l (br $l)))
|
||||
(start $init)
|
||||
(func (export "finish") (result i32) (i32.const 0)))"#
|
||||
);
|
||||
|
||||
let failure = assert_stage!(
|
||||
run_with_gas(&wat, GAS, &host).expect_err("an endless start section must not instantiate"),
|
||||
RunError::OutOfGas
|
||||
);
|
||||
assert_eq!(
|
||||
failure.fuel_used, GAS,
|
||||
"a runaway start section burns the whole limit"
|
||||
);
|
||||
}
|
||||
|
||||
/// A start section cannot make a host call that needs guest memory, even in a
|
||||
/// module that exports one: the memory is resolved from the *instance's* exports,
|
||||
/// and instantiation is what produces the instance, so a call made while it is
|
||||
/// still running has no memory to work in and ends the run.
|
||||
///
|
||||
/// Not a choice: `Module::instantiate` is `pub(crate)` in wasmi, so instantiation
|
||||
/// cannot be split from the start section to resolve the memory in between.
|
||||
#[test]
|
||||
fn a_start_section_cannot_make_a_host_call() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = format!(
|
||||
r#"(module {ldgr_index} {ONE_PAGE}
|
||||
(func $init (drop (call $ldgr_index (i32.const 0) (i32.const 4))))
|
||||
(start $init)
|
||||
(func (export "finish") (result i32) (i32.const 0)))"#,
|
||||
ldgr_index = import::LDGR_INDEX
|
||||
);
|
||||
|
||||
let failure = assert_stage!(
|
||||
run_with_gas(&wat, PLENTY_OF_GAS, &host)
|
||||
.expect_err("a host call from a start section must not be served"),
|
||||
RunError::NoMemory
|
||||
);
|
||||
assert!(
|
||||
failure.fuel_used > 0,
|
||||
"the start section is metered up to the refused call: {failure}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn a_missing_entry_point_fails() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = r#"(module (memory (export "memory") 1) (func (export "other") (result i32) (i32.const 0)))"#;
|
||||
let failure = assert_stage!(
|
||||
run_with_gas(wat, PLENTY_OF_GAS, &host)
|
||||
.expect_err("a module without the entry point must not run"),
|
||||
RunError::EntryPoint(_)
|
||||
);
|
||||
assert!(
|
||||
failure.to_string().contains("no entry point 'finish'"),
|
||||
"{failure}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The entry point is looked up by the name the caller asks for.
|
||||
#[test]
|
||||
fn the_entry_point_is_the_name_the_caller_gives() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = r#"(module (memory (export "memory") 1) (func (export "other") (result i32) (i32.const 9)))"#;
|
||||
let outcome = run_entry(wat, &host, "other").expect("should run");
|
||||
assert_eq!(outcome.result, 9);
|
||||
}
|
||||
|
||||
/// The entry point must take nothing and return an `i32`. A module that exports the
|
||||
/// name with another signature is told so, rather than being told the export is
|
||||
/// missing: wasmi answers both cases with one error, and "no entry point" would send
|
||||
/// a contract author looking for a function they already have.
|
||||
#[test]
|
||||
fn an_entry_point_of_the_wrong_type_fails() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
for signature in ["(result i64)", "(param i32) (result i32)", ""] {
|
||||
let body = if signature.contains("result i64") {
|
||||
"(i64.const 0)"
|
||||
} else if signature.is_empty() {
|
||||
"(nop)"
|
||||
} else {
|
||||
"(i32.const 0)"
|
||||
};
|
||||
let wat = format!(
|
||||
r#"(module (memory (export "memory") 1) (func (export "finish") {signature} {body}))"#
|
||||
);
|
||||
let failure = assert_stage!(
|
||||
run_with_gas(&wat, PLENTY_OF_GAS, &host)
|
||||
.expect_err("a wrongly-typed entry point must not run"),
|
||||
RunError::EntryPoint(_)
|
||||
)
|
||||
.to_string();
|
||||
assert!(
|
||||
failure.contains("entry point 'finish' has the wrong signature"),
|
||||
"{signature}: {failure}"
|
||||
);
|
||||
assert!(
|
||||
!failure.contains("no entry point"),
|
||||
"a present export must not be reported as absent — {signature}: {failure}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An export of the entry point's name that is not a function at all is a third
|
||||
/// case, and named as such: nothing is missing and no signature is wrong.
|
||||
#[test]
|
||||
fn an_entry_point_that_is_not_a_function_fails() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat =
|
||||
r#"(module (memory (export "memory") 1) (global (export "finish") i32 (i32.const 0)))"#;
|
||||
let failure = assert_stage!(
|
||||
run_with_gas(wat, PLENTY_OF_GAS, &host).expect_err("a non-function export must not run"),
|
||||
RunError::EntryPoint(_)
|
||||
)
|
||||
.to_string();
|
||||
assert!(
|
||||
failure.contains("export 'finish' is not a function"),
|
||||
"{failure}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A guest that traps fails the run rather than returning a value.
|
||||
#[test]
|
||||
fn a_trapping_guest_fails_the_run() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(&[ONE_PAGE], "(unreachable)");
|
||||
assert_stage!(failure(&wat, &host), RunError::Trap(_));
|
||||
|
||||
// An out-of-bounds guest access is a trap too, caught by the engine rather
|
||||
// than anything the host is asked about.
|
||||
let wat = module(&[ONE_PAGE], "(i32.load (i32.const 100000))");
|
||||
assert_stage!(failure(&wat, &host), RunError::Trap(_));
|
||||
}
|
||||
@@ -549,8 +549,21 @@ public:
|
||||
setround(RoundingMode inMode);
|
||||
|
||||
/**
|
||||
* Returns which mantissa scale is currently in use for normalization.
|
||||
* Convert an integer to a RoundingMode, validating that it is in range.
|
||||
*
|
||||
* Returns std::nullopt if the value does not correspond to a valid
|
||||
* RoundingMode.
|
||||
*/
|
||||
static std::optional<RoundingMode>
|
||||
checkedRoundingMode(int mode) noexcept
|
||||
{
|
||||
if (mode < static_cast<int>(RoundingMode::ToNearest) ||
|
||||
mode > static_cast<int>(RoundingMode::Upward))
|
||||
return std::nullopt;
|
||||
return static_cast<RoundingMode>(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* If you think you need to call this outside of unit tests, no you don't.
|
||||
*/
|
||||
static MantissaRange::MantissaScale
|
||||
|
||||
@@ -41,6 +41,35 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace base64 {
|
||||
|
||||
/**
|
||||
* Returns the maximum number of characters needed to base64-encode @p nBytes bytes.
|
||||
*
|
||||
* @param nBytes Number of input bytes.
|
||||
* @return Size of the encoded string, including padding.
|
||||
*/
|
||||
constexpr std::size_t
|
||||
encodedSize(std::size_t const nBytes)
|
||||
{
|
||||
return 4 * ((nBytes + 2) / 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of bytes a base64 string of @p numChars characters
|
||||
* decodes to.
|
||||
*
|
||||
* @param numChars Number of base64 characters.
|
||||
* @return Upper bound on the number of decoded bytes.
|
||||
*/
|
||||
constexpr std::size_t
|
||||
decodedSize(std::size_t const numChars)
|
||||
{
|
||||
return ((numChars / 4) * 3) + 2;
|
||||
}
|
||||
|
||||
} // namespace base64
|
||||
|
||||
std::string
|
||||
base64Encode(std::uint8_t const* data, std::size_t len);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ struct Sections
|
||||
static constexpr auto kLedgerHistory = "ledger_history";
|
||||
static constexpr auto kLedgerReplay = "ledger_replay";
|
||||
static constexpr auto kLedgerTxTables = "ledger_tx_tables";
|
||||
static constexpr auto kMaxSubscriptionsPerConnection = "max_subscriptions_per_connection";
|
||||
static constexpr auto kMaxTransactions = "max_transactions";
|
||||
static constexpr auto kNetworkId = "network_id";
|
||||
static constexpr auto kNetworkQuorum = "network_quorum";
|
||||
@@ -118,7 +119,9 @@ struct Keys
|
||||
static constexpr auto kLogInterval = "log_interval";
|
||||
static constexpr auto kMaxDivergedTime = "max_diverged_time";
|
||||
static constexpr auto kMaxLedgerCountsToStore = "max_ledger_counts_to_store";
|
||||
static constexpr auto kMaxTrustedCount = "max_trusted_count";
|
||||
static constexpr auto kMaxUnknownTime = "max_unknown_time";
|
||||
static constexpr auto kMaxUntrustedCount = "max_untrusted_count";
|
||||
static constexpr auto kMaximumTxnInLedger = "maximum_txn_in_ledger";
|
||||
static constexpr auto kMaximumTxnPerAccount = "maximum_txn_per_account";
|
||||
static constexpr auto kMemoryLevel = "memory_level";
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -1579,7 +1580,13 @@ Consensus<Adaptor>::updateOurPositions(std::unique_ptr<std::stringstream> const&
|
||||
JLOG(j_.info()) << ss.str();
|
||||
CLOG(clog) << ss.str();
|
||||
|
||||
for (auto const& [t, v] : closeTimeVotes)
|
||||
// Walk the votes highest-time first so that, among close times tied
|
||||
// for the most votes, the earliest wins. The smaller value is the
|
||||
// safer choice: without close-time consensus this round, the winner
|
||||
// only updates our position for the next proposal, and a too-early
|
||||
// time is bounded below by the prior ledger's close time. Only the
|
||||
// tie-break changes; the bin with the most votes still wins.
|
||||
for (auto const& [t, v] : std::views::reverse(closeTimeVotes))
|
||||
{
|
||||
JLOG(j_.debug()) << "CCTime: seq "
|
||||
<< static_cast<std::uint32_t>(previousLedger_.seq()) + 1 << ": "
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -189,6 +191,75 @@ struct ConsensusCloseTimes
|
||||
NetClock::time_point self;
|
||||
};
|
||||
|
||||
/**
|
||||
* Offset of the network's close time relative to ours, using a weighted median.
|
||||
*
|
||||
* Treats the sample set as `{self x 1}` merged with `{t x w}` for each
|
||||
* `(t, w)` in `times.peers`, in time order, and returns `(median - self)`
|
||||
* in whole seconds. Uses the lower weighted median: the median is the
|
||||
* earliest time at which the running weight reaches half the total, so an
|
||||
* even total whose halfway point falls between two bins resolves to the
|
||||
* earlier bin.
|
||||
*
|
||||
* @param times Our own close time and the weighted close times of peers.
|
||||
* @return Weighted median of all close times minus our own, in whole seconds.
|
||||
*/
|
||||
inline std::chrono::seconds
|
||||
medianCloseOffset(ConsensusCloseTimes const& times)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
using time_point = NetClock::time_point;
|
||||
|
||||
std::int64_t totalWeight = 1;
|
||||
for (auto const& [_, w] : times.peers)
|
||||
totalWeight += w;
|
||||
|
||||
std::int64_t const halfWeight = (totalWeight + 1) / 2;
|
||||
|
||||
std::optional<time_point> median{};
|
||||
std::int64_t tally = 0;
|
||||
bool selfPlaced = false;
|
||||
|
||||
// Accumulate weight in time order; the first bin to reach halfWeight is
|
||||
// the (lower) weighted median. Returns true once that bin is found.
|
||||
auto step = [&](time_point t, std::int64_t w) {
|
||||
XRPL_ASSERT(tally < halfWeight, "xrpl::medianCloseOffset::step : median not yet found");
|
||||
tally += w;
|
||||
if (tally >= halfWeight)
|
||||
{
|
||||
median = t;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (auto const& [t, w] : times.peers)
|
||||
{
|
||||
if (!selfPlaced && times.self <= t)
|
||||
{
|
||||
selfPlaced = true;
|
||||
if (step(times.self, 1))
|
||||
break;
|
||||
}
|
||||
if (step(t, w))
|
||||
break;
|
||||
}
|
||||
if (!selfPlaced && !median)
|
||||
step(times.self, 1);
|
||||
|
||||
if (!median)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::medianCloseOffset : median not found");
|
||||
median = times.self;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return duration_cast<seconds>(
|
||||
duration<std::int64_t>{median->time_since_epoch().count()} -
|
||||
duration<std::int64_t>{times.self.time_since_epoch().count()});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we have or don't have a consensus
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
@@ -15,6 +16,7 @@
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -241,10 +243,25 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
if (ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
lockedRate >= kParityRate,
|
||||
"xrpl::escrowUnlockApplyHelper<MPTIssue> : lockedRate is at least parity");
|
||||
// MPTs are integral, so round the delivered amount down and
|
||||
// charge any fractional transfer fee to the escrowed amount.
|
||||
auto const delivered =
|
||||
mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false);
|
||||
finalAmt = STAmount(amount.asset(), delivered.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
}
|
||||
return unlockEscrowMPT(
|
||||
ctx.view,
|
||||
|
||||
@@ -301,14 +301,15 @@ message TMLedgerData {
|
||||
}
|
||||
|
||||
message TMPing {
|
||||
// Previously used - don't reuse.
|
||||
reserved 3, 4;
|
||||
|
||||
enum pingType {
|
||||
ptPING = 0; // we want a reply
|
||||
ptPONG = 1; // this is a reply
|
||||
}
|
||||
required pingType type = 1;
|
||||
optional uint32 seq = 2; // detect stale replies, ensure other side is reading
|
||||
optional uint64 pingTime = 3; // know when we think we sent the ping
|
||||
optional uint64 netTime = 4;
|
||||
optional uint32 seq = 2; // detect stale replies, ensure other side is reading
|
||||
}
|
||||
|
||||
message TMSquelch {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STXChainBridge.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <array>
|
||||
@@ -21,8 +22,6 @@
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class SeqProxy;
|
||||
/**
|
||||
* Keylet computation functions.
|
||||
*
|
||||
@@ -123,7 +122,7 @@ trustLine(AccountID const& id, Issue const& issue) noexcept
|
||||
*/
|
||||
/** @{ */
|
||||
Keylet
|
||||
offer(AccountID const& id, std::uint32_t seq) noexcept;
|
||||
offer(AccountID const& id, SeqProxy const& seq) noexcept;
|
||||
|
||||
inline Keylet
|
||||
offer(uint256 const& key) noexcept
|
||||
@@ -136,7 +135,7 @@ offer(uint256 const& key) noexcept
|
||||
* The initial directory page for a specific quality
|
||||
*/
|
||||
Keylet
|
||||
quality(Keylet const& k, std::uint64_t q) noexcept;
|
||||
quality(Keylet const& k, std::uint64_t const q) noexcept;
|
||||
|
||||
/**
|
||||
* The directory for the next lower quality
|
||||
@@ -149,10 +148,7 @@ next(Keylet const& k);
|
||||
*/
|
||||
/** @{ */
|
||||
Keylet
|
||||
ticket(AccountID const& id, std::uint32_t ticketSeq);
|
||||
|
||||
Keylet
|
||||
ticket(AccountID const& id, SeqProxy ticketSeq);
|
||||
ticket(AccountID const& id, SeqProxy const& ticketSeq);
|
||||
|
||||
inline Keylet
|
||||
ticket(uint256 const& key)
|
||||
@@ -178,7 +174,7 @@ sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept;
|
||||
*/
|
||||
/** @{ */
|
||||
Keylet
|
||||
check(AccountID const& id, std::uint32_t seq) noexcept;
|
||||
check(AccountID const& id, SeqProxy const& seq) noexcept;
|
||||
|
||||
inline Keylet
|
||||
check(uint256 const& key) noexcept
|
||||
@@ -225,10 +221,10 @@ ownerDir(AccountID const& id) noexcept;
|
||||
*/
|
||||
/** @{ */
|
||||
Keylet
|
||||
page(uint256 const& root, std::uint64_t index = 0) noexcept;
|
||||
page(uint256 const& root, std::uint64_t const index = 0) noexcept;
|
||||
|
||||
inline Keylet
|
||||
page(Keylet const& root, std::uint64_t index = 0) noexcept
|
||||
page(Keylet const& root, std::uint64_t const index = 0) noexcept
|
||||
{
|
||||
XRPL_ASSERT(root.type == ltDIR_NODE, "xrpl::keylet::page : valid root type");
|
||||
return page(root.key, index);
|
||||
@@ -239,13 +235,13 @@ page(Keylet const& root, std::uint64_t index = 0) noexcept
|
||||
* An escrow entry
|
||||
*/
|
||||
Keylet
|
||||
escrow(AccountID const& src, std::uint32_t seq) noexcept;
|
||||
escrow(AccountID const& src, SeqProxy const& seq) noexcept;
|
||||
|
||||
/**
|
||||
* A PaymentChannel
|
||||
*/
|
||||
Keylet
|
||||
payChannel(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept;
|
||||
payChannel(AccountID const& src, AccountID const& dst, SeqProxy const& seq) noexcept;
|
||||
|
||||
/**
|
||||
* NFT page keylets
|
||||
@@ -276,7 +272,7 @@ nftokenPage(Keylet const& k, uint256 const& token);
|
||||
* An offer from an account to buy or sell an NFT
|
||||
*/
|
||||
Keylet
|
||||
nftokenOffer(AccountID const& owner, std::uint32_t seq);
|
||||
nftokenOffer(AccountID const& owner, SeqProxy const& seq);
|
||||
|
||||
inline Keylet
|
||||
nftokenOffer(uint256 const& offer)
|
||||
@@ -316,17 +312,17 @@ bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType);
|
||||
|
||||
// `seq` is stored as `sfXChainClaimID` in the object
|
||||
Keylet
|
||||
xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
||||
xChainClaimID(STXChainBridge const& bridge, std::uint64_t const seq);
|
||||
|
||||
// `seq` is stored as `sfXChainAccountCreateCount` in the object
|
||||
Keylet
|
||||
xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
||||
xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t const seq);
|
||||
|
||||
Keylet
|
||||
did(AccountID const& account) noexcept;
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept;
|
||||
oracle(AccountID const& account, std::uint32_t const documentID) noexcept;
|
||||
|
||||
Keylet
|
||||
credential(AccountID const& subject, AccountID const& issuer, Slice const& credType) noexcept;
|
||||
@@ -337,9 +333,6 @@ credential(uint256 const& key) noexcept
|
||||
return {ltCREDENTIAL, key};
|
||||
}
|
||||
|
||||
Keylet
|
||||
mptokenIssuance(std::uint32_t seq, AccountID const& issuer) noexcept;
|
||||
|
||||
Keylet
|
||||
mptokenIssuance(MPTID const& issuanceID) noexcept;
|
||||
|
||||
@@ -362,7 +355,7 @@ Keylet
|
||||
mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept;
|
||||
|
||||
Keylet
|
||||
vault(AccountID const& owner, std::uint32_t seq) noexcept;
|
||||
vault(AccountID const& owner, SeqProxy const& seq) noexcept;
|
||||
|
||||
inline Keylet
|
||||
vault(uint256 const& vaultKey)
|
||||
@@ -371,7 +364,7 @@ vault(uint256 const& vaultKey)
|
||||
}
|
||||
|
||||
Keylet
|
||||
loanBroker(AccountID const& owner, std::uint32_t seq) noexcept;
|
||||
loanBroker(AccountID const& owner, SeqProxy const& seq) noexcept;
|
||||
|
||||
inline Keylet
|
||||
loanBroker(uint256 const& key)
|
||||
@@ -380,7 +373,7 @@ loanBroker(uint256 const& key)
|
||||
}
|
||||
|
||||
Keylet
|
||||
loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept;
|
||||
loan(uint256 const& loanBrokerID, SeqProxy const& loanSeq) noexcept;
|
||||
|
||||
inline Keylet
|
||||
loan(uint256 const& key)
|
||||
@@ -389,7 +382,7 @@ loan(uint256 const& key)
|
||||
}
|
||||
|
||||
Keylet
|
||||
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
|
||||
permissionedDomain(AccountID const& account, SeqProxy const& seq) noexcept;
|
||||
|
||||
Keylet
|
||||
permissionedDomain(uint256 const& domainID) noexcept;
|
||||
@@ -407,12 +400,6 @@ getQualityNext(uint256 const& uBase);
|
||||
std::uint64_t
|
||||
getQuality(uint256 const& uBase);
|
||||
|
||||
uint256
|
||||
getTicketIndex(AccountID const& account, std::uint32_t uSequence);
|
||||
|
||||
uint256
|
||||
getTicketIndex(AccountID const& account, SeqProxy ticketSeq);
|
||||
|
||||
template <class... KeyletParams>
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init)
|
||||
struct KeyletDesc
|
||||
@@ -426,6 +413,6 @@ struct KeyletDesc
|
||||
extern std::array<KeyletDesc<AccountID const&>, 6> const kDirectAccountKeylets;
|
||||
|
||||
MPTID
|
||||
makeMptID(std::uint32_t sequence, AccountID const& account);
|
||||
makeMptID(std::uint32_t const sequence, AccountID const& account);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -190,17 +190,6 @@ enum LedgerEntryType : std::uint16_t {
|
||||
LSF_FLAG(lsfMPTCanClawback, 0x00000040) \
|
||||
LSF_FLAG(lsfMPTCanHoldConfidentialBalance, 0x00000080)) \
|
||||
\
|
||||
LEDGER_OBJECT(MPTokenIssuanceMutable, \
|
||||
LSF_FLAG(lsmfMPTCanEnableCanLock, 0x00000002) \
|
||||
LSF_FLAG(lsmfMPTCanEnableRequireAuth, 0x00000004) \
|
||||
LSF_FLAG(lsmfMPTCanEnableCanEscrow, 0x00000008) \
|
||||
LSF_FLAG(lsmfMPTCanEnableCanTrade, 0x00000010) \
|
||||
LSF_FLAG(lsmfMPTCanEnableCanTransfer, 0x00000020) \
|
||||
LSF_FLAG(lsmfMPTCanEnableCanClawback, 0x00000040) \
|
||||
LSF_FLAG(lsmfMPTCannotEnableCanHoldConfidentialBalance, 0x00000080) \
|
||||
LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \
|
||||
LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \
|
||||
\
|
||||
LEDGER_OBJECT(MPToken, \
|
||||
LSF_FLAG2(lsfMPTLocked, 0x00000001) \
|
||||
LSF_FLAG(lsfMPTAuthorized, 0x00000002) \
|
||||
@@ -294,6 +283,17 @@ getAllLedgerFlags()
|
||||
#pragma pop_macro("TO_MAP")
|
||||
#pragma pop_macro("ALL_LEDGER_FLAGS")
|
||||
|
||||
// MPTokenIssuance ImmutableFlags (sfImmutableFlags)
|
||||
inline constexpr std::uint32_t lsifMPTCanLock = 0x00000002;
|
||||
inline constexpr std::uint32_t lsifMPTRequireAuth = 0x00000004;
|
||||
inline constexpr std::uint32_t lsifMPTCanEscrow = 0x00000008;
|
||||
inline constexpr std::uint32_t lsifMPTCanTrade = 0x00000010;
|
||||
inline constexpr std::uint32_t lsifMPTCanTransfer = 0x00000020;
|
||||
inline constexpr std::uint32_t lsifMPTCanClawback = 0x00000040;
|
||||
inline constexpr std::uint32_t lsifMPTCanHoldConfidentialBalance = 0x00000080;
|
||||
inline constexpr std::uint32_t lsifMPTMetadata = 0x00010000;
|
||||
inline constexpr std::uint32_t lsifMPTTransferFee = 0x00020000;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -333,6 +333,16 @@ enum class VaultVersion : uint8_t {
|
||||
*/
|
||||
constexpr std::uint8_t kMaxAssetCheckDepth = 5;
|
||||
|
||||
/**
|
||||
* Maximum length of a Data field in Escrow object that can be updated by WASM code.
|
||||
*/
|
||||
constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
|
||||
|
||||
/**
|
||||
* Maximum amount of data transfer across hostfunction<->wasm border.
|
||||
*/
|
||||
constexpr std::size_t kWasmTransferLimit = 1 << 20; // 1MB
|
||||
|
||||
/**
|
||||
* A ledger index.
|
||||
*/
|
||||
|
||||
@@ -90,7 +90,11 @@ public:
|
||||
operator=(STObject&& other);
|
||||
|
||||
STObject(SOTemplate const& type, SField const& name);
|
||||
STObject(SOTemplate const& type, SerialIter& sit, SField const& name);
|
||||
STObject(
|
||||
SOTemplate const& type,
|
||||
SerialIter& sit,
|
||||
SField const& name,
|
||||
bool requireCanonicalOrder = false);
|
||||
STObject(SerialIter& sit, SField const& name, int depth = 0);
|
||||
STObject(SerialIter&& sit, SField const& name);
|
||||
explicit STObject(SField const& name);
|
||||
@@ -123,7 +127,7 @@ public:
|
||||
set(SOTemplate const&);
|
||||
|
||||
bool
|
||||
set(SerialIter& u, int depth = 0);
|
||||
set(SerialIter& u, int depth = 0, bool requireCanonicalOrder = false);
|
||||
|
||||
[[nodiscard]] SerializedTypeID
|
||||
getSType() const override;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -108,6 +109,9 @@ public:
|
||||
[[nodiscard]] bool
|
||||
isType(Type const& pe) const;
|
||||
|
||||
[[nodiscard]] size_t
|
||||
getHash() const;
|
||||
|
||||
bool
|
||||
operator==(STPathElement const& t) const;
|
||||
|
||||
@@ -171,12 +175,23 @@ public:
|
||||
reserve(size_t s);
|
||||
};
|
||||
|
||||
template <class Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, STPath const& p) noexcept
|
||||
{
|
||||
for (auto const& e : p)
|
||||
{
|
||||
beast::hash_append(h, e.getHash());
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// A set of zero or more payment paths
|
||||
class STPathSet final : public STBase, public CountedObject<STPathSet>
|
||||
{
|
||||
std::vector<STPath> value_;
|
||||
xrpl::hardened_hash_set<STPath> seenHashes_;
|
||||
|
||||
public:
|
||||
STPathSet() = default;
|
||||
@@ -205,9 +220,6 @@ public:
|
||||
std::vector<STPath>::const_reference
|
||||
operator[](std::vector<STPath>::size_type n) const;
|
||||
|
||||
std::vector<STPath>::reference
|
||||
operator[](std::vector<STPath>::size_type n);
|
||||
|
||||
[[nodiscard]] std::vector<STPath>::const_iterator
|
||||
begin() const;
|
||||
|
||||
@@ -227,6 +239,9 @@ public:
|
||||
void
|
||||
emplaceBack(Args&&... args);
|
||||
|
||||
[[nodiscard]] bool
|
||||
contains(STPath const& path) const;
|
||||
|
||||
private:
|
||||
STBase*
|
||||
copy(std::size_t n, void* buf) const override;
|
||||
@@ -515,12 +530,6 @@ STPathSet::operator[](std::vector<STPath>::size_type n) const
|
||||
return value_[n];
|
||||
}
|
||||
|
||||
inline std::vector<STPath>::reference
|
||||
STPathSet::operator[](std::vector<STPath>::size_type n)
|
||||
{
|
||||
return value_[n];
|
||||
}
|
||||
|
||||
inline std::vector<STPath>::const_iterator
|
||||
STPathSet::begin() const
|
||||
{
|
||||
@@ -549,6 +558,7 @@ inline void
|
||||
STPathSet::pushBack(STPath const& e)
|
||||
{
|
||||
value_.push_back(e);
|
||||
seenHashes_.emplace(value_.back());
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
@@ -556,6 +566,13 @@ inline void
|
||||
STPathSet::emplaceBack(Args&&... args)
|
||||
{
|
||||
value_.emplace_back(std::forward<Args>(args)...);
|
||||
seenHashes_.emplace(value_.back());
|
||||
}
|
||||
|
||||
inline bool
|
||||
STPathSet::contains(STPath const& path) const
|
||||
{
|
||||
return seenHashes_.contains(path);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -93,12 +93,6 @@ public:
|
||||
[[nodiscard]] SeqProxy
|
||||
getSeqProxy() const;
|
||||
|
||||
/**
|
||||
* Returns the first non-zero value of (Sequence, TicketSequence).
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
getSeqValue() const;
|
||||
|
||||
[[nodiscard]] boost::container::flat_set<AccountID>
|
||||
getMentionedAccounts() const;
|
||||
|
||||
|
||||
@@ -54,6 +54,22 @@ class STValidation final : public STObject, public CountedObject<STValidation>
|
||||
NetClock::time_point seenTime_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @struct DeserializeOptions
|
||||
* @brief Options controlling deserialization of a STValidation.
|
||||
|
||||
* @var DeserializeOptions::checkSignature
|
||||
* Whether to verify the data was signed properly
|
||||
*
|
||||
* @var DeserializeOptions::requireCanonicalOrder
|
||||
* Whether to require the fields to be in canonical order
|
||||
*/
|
||||
struct DeserializeOptions
|
||||
{
|
||||
bool checkSignature;
|
||||
bool requireCanonicalOrder;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a STValidation from a peer from serialized data.
|
||||
*
|
||||
@@ -64,12 +80,12 @@ public:
|
||||
* that signed the validation. For manifest based
|
||||
* validators, this should be the NodeID of the master
|
||||
* public key.
|
||||
* @param checkSignature Whether to verify the data was signed properly
|
||||
* @param options Options controlling deserialization
|
||||
*
|
||||
* @note Throws if the object is not valid
|
||||
*/
|
||||
template <class LookupNodeID>
|
||||
STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature);
|
||||
STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, DeserializeOptions options);
|
||||
|
||||
/**
|
||||
* Construct, sign and trust a new STValidation issued by this node.
|
||||
@@ -163,8 +179,8 @@ private:
|
||||
};
|
||||
|
||||
template <class LookupNodeID>
|
||||
STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature)
|
||||
: STObject(validationFormat(), sit, sfValidation)
|
||||
STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, DeserializeOptions options)
|
||||
: STObject(validationFormat(), sit, sfValidation, options.requireCanonicalOrder)
|
||||
, signingPubKey_([this]() {
|
||||
auto const spk = getFieldVL(sfSigningPubKey);
|
||||
|
||||
@@ -175,7 +191,7 @@ STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool ch
|
||||
}())
|
||||
, nodeID_(lookupNodeID(signingPubKey_))
|
||||
{
|
||||
if (checkSignature && !isValid())
|
||||
if (options.checkSignature && !isValid())
|
||||
{
|
||||
JLOG(debugLog().error()) << "Invalid signature in validation: "
|
||||
<< getJson(JsonOptions::Values::None);
|
||||
|
||||
@@ -53,14 +53,29 @@ public:
|
||||
operator=(SeqProxy const& other) = default;
|
||||
|
||||
/**
|
||||
* Factory function to return a sequence-based SeqProxy
|
||||
* Factory function to return a sequence-based SeqProxy.
|
||||
* Outside of tests, this function should only be used for "secondary" transaction sequences,
|
||||
* e.g. `sfOfferSequence`, or sequence fields in an existing ledger object. DO NOT use this for
|
||||
* the "primary" sequence of a transaction, `sfSequence`.
|
||||
*/
|
||||
static constexpr SeqProxy
|
||||
sequence(std::uint32_t v)
|
||||
rawSequence(std::uint32_t v)
|
||||
{
|
||||
return SeqProxy{Type::Seq, v};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to return a ticket-based SeqProxy.
|
||||
* Outside of tests, this function should only be used for "secondary" transaction sequences,
|
||||
* e.g. `sfOfferSequence`, or sequence fields in an existing ledger object. DO NOT use this for
|
||||
* the "primary" ticket sequence of a transaction, `sfTicketSequence`.
|
||||
*/
|
||||
static constexpr SeqProxy
|
||||
rawTicket(std::uint32_t v)
|
||||
{
|
||||
return SeqProxy{Type::Ticket, v};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr std::uint32_t
|
||||
value() const
|
||||
{
|
||||
|
||||
@@ -129,8 +129,10 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temARRAY_TOO_LARGE,
|
||||
temBAD_TRANSFER_FEE,
|
||||
temINVALID_INNER_BATCH,
|
||||
|
||||
temBAD_MPT,
|
||||
temBAD_CIPHERTEXT,
|
||||
temBAD_WASM,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -370,6 +372,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecNO_DELEGATE_PERMISSION = 198,
|
||||
tecBAD_PROOF = 199,
|
||||
tecNO_SPONSOR_PERMISSION = 200,
|
||||
tecOUT_OF_GAS = 201,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -152,7 +152,14 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
\
|
||||
TRANSACTION(MPTokenIssuanceSet, \
|
||||
TF_FLAG(tfMPTLock, 0x00000001) \
|
||||
TF_FLAG(tfMPTUnlock, 0x00000002), \
|
||||
TF_FLAG(tfMPTUnlock, 0x00000002) \
|
||||
TF_FLAG(tfMPTSetCanLock, 0x00000004) \
|
||||
TF_FLAG(tfMPTSetRequireAuth, 0x00000008) \
|
||||
TF_FLAG(tfMPTSetCanEscrow, 0x00000010) \
|
||||
TF_FLAG(tfMPTSetCanTrade, 0x00000020) \
|
||||
TF_FLAG(tfMPTSetCanTransfer, 0x00000040) \
|
||||
TF_FLAG(tfMPTSetCanClawback, 0x00000080) \
|
||||
TF_FLAG(tfMPTSetCanHoldConfidentialBalance, 0x00000100), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(NFTokenCreateOffer, \
|
||||
@@ -356,38 +363,26 @@ inline constexpr FlagValue tfMPTPaymentMask = ~(tfUniversal | tfPartialPayment);
|
||||
inline constexpr FlagValue tfTrustSetPermissionMask =
|
||||
~(tfUniversal | tfSetfAuth | tfSetFreeze | tfClearFreeze);
|
||||
|
||||
// MPTokenIssuanceCreate MutableFlags:
|
||||
// Indicating specific fields or flags may be changed after issuance.
|
||||
inline constexpr FlagValue tmfMPTCanEnableCanLock = lsmfMPTCanEnableCanLock;
|
||||
inline constexpr FlagValue tmfMPTCanEnableRequireAuth = lsmfMPTCanEnableRequireAuth;
|
||||
inline constexpr FlagValue tmfMPTCanEnableCanEscrow = lsmfMPTCanEnableCanEscrow;
|
||||
inline constexpr FlagValue tmfMPTCanEnableCanTrade = lsmfMPTCanEnableCanTrade;
|
||||
inline constexpr FlagValue tmfMPTCanEnableCanTransfer = lsmfMPTCanEnableCanTransfer;
|
||||
inline constexpr FlagValue tmfMPTCanEnableCanClawback = lsmfMPTCanEnableCanClawback;
|
||||
inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata;
|
||||
inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee;
|
||||
inline constexpr FlagValue tmfMPTCannotEnableCanHoldConfidentialBalance =
|
||||
lsmfMPTCannotEnableCanHoldConfidentialBalance;
|
||||
inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask =
|
||||
~(tmfMPTCanEnableCanLock | tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanEscrow |
|
||||
tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanEnableCanClawback |
|
||||
tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee |
|
||||
tmfMPTCannotEnableCanHoldConfidentialBalance);
|
||||
// MPTokenIssuanceCreate / MPTokenIssuanceSet ImmutableFlags:
|
||||
// Defines the immutable fields and flags specific to MPTokenIssuance.
|
||||
inline constexpr FlagValue tifMPTCanLock = lsifMPTCanLock;
|
||||
inline constexpr FlagValue tifMPTRequireAuth = lsifMPTRequireAuth;
|
||||
inline constexpr FlagValue tifMPTCanEscrow = lsifMPTCanEscrow;
|
||||
inline constexpr FlagValue tifMPTCanTrade = lsifMPTCanTrade;
|
||||
inline constexpr FlagValue tifMPTCanTransfer = lsifMPTCanTransfer;
|
||||
inline constexpr FlagValue tifMPTCanClawback = lsifMPTCanClawback;
|
||||
inline constexpr FlagValue tifMPTMetadata = lsifMPTMetadata;
|
||||
inline constexpr FlagValue tifMPTTransferFee = lsifMPTTransferFee;
|
||||
inline constexpr FlagValue tifMPTCanHoldConfidentialBalance = lsifMPTCanHoldConfidentialBalance;
|
||||
inline constexpr FlagValue tifMPTokenIssuanceImmutableMask =
|
||||
~(tifMPTCanLock | tifMPTRequireAuth | tifMPTCanEscrow | tifMPTCanTrade | tifMPTCanTransfer |
|
||||
tifMPTCanClawback | tifMPTMetadata | tifMPTTransferFee | tifMPTCanHoldConfidentialBalance);
|
||||
|
||||
// MPTokenIssuanceSet MutableFlags:
|
||||
// Enable mutable capability flags. These flags are one-way: once enabled,
|
||||
// the corresponding capability cannot be disabled by MPTokenIssuanceSet.
|
||||
|
||||
inline constexpr FlagValue tmfMPTSetCanLock = 0x00000001;
|
||||
inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000002;
|
||||
inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000004;
|
||||
inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000008;
|
||||
inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000010;
|
||||
inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000020;
|
||||
inline constexpr FlagValue tmfMPTSetCanHoldConfidentialBalance = 0x00000040;
|
||||
inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask =
|
||||
~(tmfMPTSetCanLock | tmfMPTSetRequireAuth | tmfMPTSetCanEscrow | tmfMPTSetCanTrade |
|
||||
tmfMPTSetCanTransfer | tmfMPTSetCanClawback | tmfMPTSetCanHoldConfidentialBalance);
|
||||
// MPTokenIssuanceSet set of flags that is used to enable capabilities on an MPTokenIssuance.
|
||||
// Used as `txFlags & tfMPTokenIssuanceSetEnableFlagMask` to extract the capability-enabling bits.
|
||||
inline constexpr FlagValue tfMPTokenIssuanceSetEnableFlagMask = tfMPTSetCanLock |
|
||||
tfMPTSetRequireAuth | tfMPTSetCanEscrow | tfMPTSetCanTrade | tfMPTSetCanTransfer |
|
||||
tfMPTSetCanClawback | tfMPTSetCanHoldConfidentialBalance;
|
||||
|
||||
// Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a
|
||||
// TrustLine to be added to the issuer of that token without explicit permission from that issuer.
|
||||
|
||||
@@ -59,7 +59,6 @@ XRPL_FIX (PreviousTxnID, Supported::Yes, VoteBehavior::DefaultNo
|
||||
XRPL_FIX (XChainRewardRounding, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (EmptyDID, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PriceOracle, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (AMMOverflowOffer, Supported::Yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (FillOrKill, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(DID, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(XChainBridge, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
@@ -100,6 +99,7 @@ XRPL_RETIRE_FIX(1578)
|
||||
XRPL_RETIRE_FIX(1623)
|
||||
XRPL_RETIRE_FIX(1781)
|
||||
XRPL_RETIRE_FIX(AmendmentMajorityCalc)
|
||||
XRPL_RETIRE_FIX(AMMOverflowOffer)
|
||||
XRPL_RETIRE_FIX(CheckThreading)
|
||||
XRPL_RETIRE_FIX(DisallowIncomingV1)
|
||||
XRPL_RETIRE_FIX(InnerObjTemplate)
|
||||
|
||||
@@ -404,7 +404,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfPreviousTxnID, SoeRequired},
|
||||
{sfPreviousTxnLgrSeq, SoeRequired},
|
||||
{sfDomainID, SoeOptional},
|
||||
{sfMutableFlags, SoeDefault},
|
||||
{sfImmutableFlags, SoeDefault},
|
||||
{sfReferenceHolding, SoeOptional},
|
||||
{sfIssuerEncryptionKey, SoeOptional},
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
|
||||
@@ -98,7 +98,7 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48)
|
||||
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
||||
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
||||
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
|
||||
TYPED_SFIELD(sfMutableFlags, UINT32, 53)
|
||||
TYPED_SFIELD(sfImmutableFlags, UINT32, 53)
|
||||
TYPED_SFIELD(sfStartDate, UINT32, 54)
|
||||
TYPED_SFIELD(sfPaymentInterval, UINT32, 55)
|
||||
TYPED_SFIELD(sfGracePeriod, UINT32, 56)
|
||||
@@ -239,6 +239,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset
|
||||
|
||||
// int32
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
TYPED_SFIELD(sfRemainingOwnerCountDelta, INT32, 2)
|
||||
|
||||
// currency amount (common)
|
||||
TYPED_SFIELD(sfAmount, AMOUNT, 1)
|
||||
@@ -278,6 +279,7 @@ TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30)
|
||||
TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31)
|
||||
TYPED_SFIELD(sfFeeAmount, AMOUNT, 32)
|
||||
TYPED_SFIELD(sfMaxFee, AMOUNT, 33)
|
||||
TYPED_SFIELD(sfFeeAmountDelta, AMOUNT, 34)
|
||||
|
||||
// variable length (common)
|
||||
TYPED_SFIELD(sfPublicKey, VL, 1)
|
||||
|
||||
@@ -705,7 +705,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate,
|
||||
{sfMaximumAmount, SoeOptional},
|
||||
{sfMPTokenMetadata, SoeOptional},
|
||||
{sfDomainID, SoeOptional},
|
||||
{sfMutableFlags, SoeOptional},
|
||||
{sfImmutableFlags, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction type destroys a MPTokensIssuance instance */
|
||||
@@ -734,7 +734,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
|
||||
{sfDomainID, SoeOptional},
|
||||
{sfMPTokenMetadata, SoeOptional},
|
||||
{sfTransferFee, SoeOptional},
|
||||
{sfMutableFlags, SoeOptional},
|
||||
{sfImmutableFlags, SoeOptional},
|
||||
{sfIssuerEncryptionKey, SoeOptional},
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
}))
|
||||
@@ -1085,7 +1085,7 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay,
|
||||
# include <xrpl/tx/transactors/token/ConfidentialMPTConvert.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert,
|
||||
Delegation::Delegable,
|
||||
Delegation::NotDelegable,
|
||||
featureConfidentialTransfer,
|
||||
NoPriv,
|
||||
({
|
||||
@@ -1189,9 +1189,9 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet,
|
||||
({
|
||||
{sfCounterpartySponsor, SoeOptional},
|
||||
{sfSponsee, SoeOptional},
|
||||
{sfFeeAmount, SoeOptional},
|
||||
{sfFeeAmountDelta, SoeOptional},
|
||||
{sfMaxFee, SoeOptional},
|
||||
{sfRemainingOwnerCount, SoeOptional},
|
||||
{sfRemainingOwnerCountDelta, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
@@ -23,6 +23,16 @@ By default, `CODEGEN_VENV_DIR` points to `.venv` in the project root. The
|
||||
`setup_code_gen` target creates a venv there and installs the required packages.
|
||||
The `code_gen` target then uses the venv's Python interpreter to run generation.
|
||||
|
||||
Generation is pure Python, so the same targets are also available as a
|
||||
standalone project that needs neither the dependencies nor a compiler. This is
|
||||
what CI uses, and it is handy if you only want to regenerate these files:
|
||||
|
||||
```bash
|
||||
cmake -S cmake/codegen -B build/codegen
|
||||
cmake --build build/codegen --target setup_code_gen
|
||||
cmake --build build/codegen --target code_gen
|
||||
```
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
The code generation requires the following Python packages (installed by `setup_code_gen`):
|
||||
|
||||
@@ -256,27 +256,27 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfMutableFlags (SoeDefault)
|
||||
* @brief Get sfImmutableFlags (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getMutableFlags() const
|
||||
getImmutableFlags() const
|
||||
{
|
||||
if (hasMutableFlags())
|
||||
return this->sle_->at(sfMutableFlags);
|
||||
if (hasImmutableFlags())
|
||||
return this->sle_->at(sfImmutableFlags);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfMutableFlags is present.
|
||||
* @brief Check if sfImmutableFlags is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasMutableFlags() const
|
||||
hasImmutableFlags() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfMutableFlags);
|
||||
return this->sle_->isFieldPresent(sfImmutableFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -557,13 +557,13 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfMutableFlags (SoeDefault)
|
||||
* @brief Set sfImmutableFlags (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceBuilder&
|
||||
setMutableFlags(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
setImmutableFlags(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfMutableFlags] = value;
|
||||
object_[sfImmutableFlags] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class ConfidentialMPTConvertBuilder;
|
||||
* @brief Transaction: ConfidentialMPTConvert
|
||||
*
|
||||
* Type: ttCONFIDENTIAL_MPT_CONVERT (85)
|
||||
* Delegable: Delegation::Delegable
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureConfidentialTransfer
|
||||
* Privileges: NoPriv
|
||||
*
|
||||
|
||||
@@ -178,29 +178,29 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfMutableFlags (SoeOptional)
|
||||
* @brief Get sfImmutableFlags (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getMutableFlags() const
|
||||
getImmutableFlags() const
|
||||
{
|
||||
if (hasMutableFlags())
|
||||
if (hasImmutableFlags())
|
||||
{
|
||||
return this->tx_->at(sfMutableFlags);
|
||||
return this->tx_->at(sfImmutableFlags);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfMutableFlags is present.
|
||||
* @brief Check if sfImmutableFlags is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasMutableFlags() const
|
||||
hasImmutableFlags() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfMutableFlags);
|
||||
return this->tx_->isFieldPresent(sfImmutableFlags);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -302,13 +302,13 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfMutableFlags (SoeOptional)
|
||||
* @brief Set sfImmutableFlags (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceCreateBuilder&
|
||||
setMutableFlags(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
setImmutableFlags(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfMutableFlags] = value;
|
||||
object_[sfImmutableFlags] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -163,29 +163,29 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfMutableFlags (SoeOptional)
|
||||
* @brief Get sfImmutableFlags (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getMutableFlags() const
|
||||
getImmutableFlags() const
|
||||
{
|
||||
if (hasMutableFlags())
|
||||
if (hasImmutableFlags())
|
||||
{
|
||||
return this->tx_->at(sfMutableFlags);
|
||||
return this->tx_->at(sfImmutableFlags);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfMutableFlags is present.
|
||||
* @brief Check if sfImmutableFlags is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasMutableFlags() const
|
||||
hasImmutableFlags() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfMutableFlags);
|
||||
return this->tx_->isFieldPresent(sfImmutableFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,13 +341,13 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfMutableFlags (SoeOptional)
|
||||
* @brief Set sfImmutableFlags (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceSetBuilder&
|
||||
setMutableFlags(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
setImmutableFlags(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfMutableFlags] = value;
|
||||
object_[sfImmutableFlags] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -100,29 +100,29 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfFeeAmount (SoeOptional)
|
||||
* @brief Get sfFeeAmountDelta (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_AMOUNT::type::value_type>
|
||||
getFeeAmount() const
|
||||
getFeeAmountDelta() const
|
||||
{
|
||||
if (hasFeeAmount())
|
||||
if (hasFeeAmountDelta())
|
||||
{
|
||||
return this->tx_->at(sfFeeAmount);
|
||||
return this->tx_->at(sfFeeAmountDelta);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfFeeAmount is present.
|
||||
* @brief Check if sfFeeAmountDelta is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasFeeAmount() const
|
||||
hasFeeAmountDelta() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfFeeAmount);
|
||||
return this->tx_->isFieldPresent(sfFeeAmountDelta);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,29 +152,29 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfRemainingOwnerCount (SoeOptional)
|
||||
* @brief Get sfRemainingOwnerCountDelta (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getRemainingOwnerCount() const
|
||||
protocol_autogen::Optional<SF_INT32::type::value_type>
|
||||
getRemainingOwnerCountDelta() const
|
||||
{
|
||||
if (hasRemainingOwnerCount())
|
||||
if (hasRemainingOwnerCountDelta())
|
||||
{
|
||||
return this->tx_->at(sfRemainingOwnerCount);
|
||||
return this->tx_->at(sfRemainingOwnerCountDelta);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfRemainingOwnerCount is present.
|
||||
* @brief Check if sfRemainingOwnerCountDelta is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasRemainingOwnerCount() const
|
||||
hasRemainingOwnerCountDelta() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfRemainingOwnerCount);
|
||||
return this->tx_->isFieldPresent(sfRemainingOwnerCountDelta);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -243,13 +243,13 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfFeeAmount (SoeOptional)
|
||||
* @brief Set sfFeeAmountDelta (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
SponsorshipSetBuilder&
|
||||
setFeeAmount(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
|
||||
setFeeAmountDelta(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
|
||||
{
|
||||
object_[sfFeeAmount] = value;
|
||||
object_[sfFeeAmountDelta] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -265,13 +265,13 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfRemainingOwnerCount (SoeOptional)
|
||||
* @brief Set sfRemainingOwnerCountDelta (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
SponsorshipSetBuilder&
|
||||
setRemainingOwnerCount(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
setRemainingOwnerCountDelta(std::decay_t<typename SF_INT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfRemainingOwnerCount] = value;
|
||||
object_[sfRemainingOwnerCountDelta] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ extern Charge const kFeeRequestNoReply; // A request that we cannot satisfy.
|
||||
extern Charge const kFeeInvalidSignature; // An object whose signature we had to check that failed.
|
||||
extern Charge const kFeeUselessData; // Data we have no use for.
|
||||
extern Charge const kFeeInvalidData; // Data we have to verify before rejecting.
|
||||
extern Charge const kFeeMalformedData; // Data that no honest peer would send.
|
||||
|
||||
// RPC loads
|
||||
extern Charge const kFeeMalformedRpc; // An RPC request that we can immediately tell is invalid.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -22,6 +23,39 @@ namespace xrpl {
|
||||
// Operations that clients may wish to perform against the network
|
||||
// Master operational handler, server sequencer, network tracker
|
||||
|
||||
/**
|
||||
* Maximum number of subscriptions a single client connection may hold at once.
|
||||
*
|
||||
* Applies to the account, real-time account, and account-history subscriptions
|
||||
* tracked on one InfoSub (the sets counted by totalSubscriptionCount), bounding
|
||||
* the disconnect-time cleanup of those sets. Book subscriptions are tracked
|
||||
* separately (OrderBookDB) and are not counted here. Generous enough for
|
||||
* legitimate power users such as block explorers.
|
||||
*/
|
||||
constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000;
|
||||
|
||||
/**
|
||||
* Whether adding @p additional subscriptions to a connection already holding
|
||||
* @p current would exceed the cap.
|
||||
*
|
||||
* Pure arithmetic split out so it can be unit-tested without a live
|
||||
* connection. The first term avoids underflow in the subtraction.
|
||||
*
|
||||
* @param current Subscriptions already tracked on the connection.
|
||||
* @param additional Subscriptions a request would add.
|
||||
* @param cap The effective per-connection cap. Defaults to the
|
||||
* built-in limit; callers may pass a configured override.
|
||||
* @return true if the request must be rejected to stay within the cap.
|
||||
*/
|
||||
[[nodiscard]] constexpr bool
|
||||
exceedsSubscriptionCap(
|
||||
std::size_t current,
|
||||
std::size_t additional,
|
||||
std::size_t cap = kMaxSubscriptionsPerConnection)
|
||||
{
|
||||
return additional > cap || current > cap - additional;
|
||||
}
|
||||
|
||||
class InfoSubRequest : public CountedObject<InfoSubRequest>
|
||||
{
|
||||
public:
|
||||
@@ -44,12 +78,12 @@ public:
|
||||
* map.
|
||||
*
|
||||
* @note Lifetime contract: every `InfoSub` instance MUST be destroyed
|
||||
* before the backing `Source`. NetworkOPsImp shutdown drops all
|
||||
* subscriber strong refs before its own teardown to satisfy this.
|
||||
* before the backing `Source`. NetworkOPsImp shutdown drops all
|
||||
* subscriber strong refs before its own teardown to satisfy this.
|
||||
* @note Thread-safety: per-instance state is guarded by `lock_`. The
|
||||
* destructor reads tracking sets without taking `lock_` because
|
||||
* the strong-pointer ref-count is zero at destruction time, so
|
||||
* no other thread can be calling the public mutators.
|
||||
* destructor reads tracking sets without taking `lock_` because
|
||||
* the strong-pointer ref-count is zero at destruction time, so
|
||||
* no other thread can be calling the public mutators.
|
||||
*/
|
||||
class InfoSub : public CountedObject<InfoSub>
|
||||
{
|
||||
@@ -117,6 +151,34 @@ public:
|
||||
AccountID const& account,
|
||||
bool historyOnly) = 0;
|
||||
|
||||
/**
|
||||
* Schedule the server-side teardown of a disconnecting connection's
|
||||
* account subscriptions off the destructor thread.
|
||||
*
|
||||
* The implementation posts a low-priority JobQueue task that erases the
|
||||
* entries in bounded chunks, so `~InfoSub` returns immediately instead
|
||||
* of running the erase loop inline. The sets are taken by value so the
|
||||
* job owns its copies and never references the destroyed `InfoSub`.
|
||||
* Cleanup is keyed on `seq` (unique per connection), so deferring it
|
||||
* cannot disturb a reconnected client reusing the same accounts.
|
||||
*
|
||||
* @param seq The disconnecting connection's unique subscription id.
|
||||
* @param rtAccounts Real-time account subscriptions to remove.
|
||||
* @param normalAccounts Normal account subscriptions to remove.
|
||||
* @param historyAccounts Account-history subscriptions to remove.
|
||||
*
|
||||
* @note The implementing `Source` must outlive any job it posts. If the
|
||||
* JobQueue is already stopping (process shutdown), the job is not
|
||||
* enqueued; the cleanup is skipped because the server-side maps
|
||||
* are about to be destroyed and no publishing can run.
|
||||
*/
|
||||
virtual void
|
||||
scheduleAccountCleanup(
|
||||
std::uint64_t seq,
|
||||
hash_set<AccountID> rtAccounts,
|
||||
hash_set<AccountID> normalAccounts,
|
||||
hash_set<AccountID> historyAccounts) = 0;
|
||||
|
||||
// VFALCO TODO Document the bool return value
|
||||
virtual bool
|
||||
subLedger(ref ispListener, json::Value& jvResult) = 0;
|
||||
@@ -153,12 +215,12 @@ public:
|
||||
* @param ispListener The subscriber requesting removal.
|
||||
* @param book The order book to unsubscribe from.
|
||||
* @return true if the entry was present and removed, false if the
|
||||
* subscriber was not subscribed to @p book.
|
||||
* subscriber was not subscribed to @p book.
|
||||
*
|
||||
* @note Thread-safety: acquires subLock_ internally.
|
||||
* @note Thread-safety: acquires bookLock_ internally.
|
||||
* @note Do NOT call from ~InfoSub(). Use unsubBookInternal instead
|
||||
* to avoid a redundant write-back to bookSubscriptions_ on a
|
||||
* partially-destroyed object.
|
||||
* to avoid a redundant write-back to bookSubscriptions_ on a
|
||||
* partially-destroyed object.
|
||||
*/
|
||||
virtual bool
|
||||
unsubBook(ref ispListener, Book const&) = 0;
|
||||
@@ -173,9 +235,9 @@ public:
|
||||
* @param uListener The sequence number of the subscriber being torn down.
|
||||
* @param book The order book entry to remove.
|
||||
* @return true if the entry was present and removed, false otherwise
|
||||
* (e.g., already removed by a concurrent RPC unsubscribe).
|
||||
* (e.g., already removed by a concurrent RPC unsubscribe).
|
||||
*
|
||||
* @note Thread-safety: acquires subLock_ internally.
|
||||
* @note Thread-safety: acquires bookLock_ internally.
|
||||
*/
|
||||
virtual bool
|
||||
unsubBookInternal(std::uint64_t uListener, Book const&) = 0;
|
||||
@@ -221,8 +283,8 @@ public:
|
||||
|
||||
/**
|
||||
* Journal used by InfoSub for diagnostics that occur after the
|
||||
* owning subsystem (e.g. application-level Logs) is the only
|
||||
* surviving sink — primarily destructor-time cleanup failures.
|
||||
* owning subsystem (e.g. application-level Logs) is the only
|
||||
* surviving sink — primarily destructor-time cleanup failures.
|
||||
*/
|
||||
[[nodiscard]] virtual beast::Journal const&
|
||||
journal() const = 0;
|
||||
@@ -243,6 +305,56 @@ public:
|
||||
[[nodiscard]] std::uint64_t
|
||||
getSeq() const;
|
||||
|
||||
/**
|
||||
* Return the number of subscriptions currently tracked on this
|
||||
* connection.
|
||||
*
|
||||
* The combined size of the per-connection account, real-time account, and
|
||||
* account-history subscription sets. `doSubscribe` reads this to enforce
|
||||
* the per-connection subscription cap before admitting more.
|
||||
*
|
||||
* @return The total tracked subscription count for this connection.
|
||||
*
|
||||
* @note Thread-safe: takes `lock_` for the read; read-only.
|
||||
*/
|
||||
[[nodiscard]] std::size_t
|
||||
totalSubscriptionCount() const;
|
||||
|
||||
/**
|
||||
* Enforce the cap and reserve a request's net-new accounts, atomically.
|
||||
*
|
||||
* Under one hold of `lock_`: count the net-new entries in the two sets,
|
||||
* check the total against @p cap, and insert them only if it fits.
|
||||
* All-or-nothing. Doing check and insert together stops two concurrent
|
||||
* requests sharing an InfoSub (the admin subscribe-by-url path) from both
|
||||
* passing the check before either records its accounts. The server-side
|
||||
* maps are populated afterwards by subAccount, whose re-insert is a no-op.
|
||||
*
|
||||
* @param proposedAccounts Real-time (accounts_proposed) ids to reserve.
|
||||
* @param normalAccounts Normal (accounts) ids to reserve.
|
||||
* @param cap The effective per-connection cap.
|
||||
* @return true if reserved; false if the request must be rejected.
|
||||
* @note Thread-safe: takes `lock_`.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
tryReserveAccountSubscriptions(
|
||||
hash_set<AccountID> const& proposedAccounts,
|
||||
hash_set<AccountID> const& normalAccounts,
|
||||
std::size_t cap);
|
||||
|
||||
/**
|
||||
* Whether this connection already tracks an account-history for @p account.
|
||||
*
|
||||
* `doSubscribe` reads this to charge the cap for an account_history_tx_stream
|
||||
* only when it is net-new, matching the account branches.
|
||||
*
|
||||
* @param account The account an account_history_tx_stream would add.
|
||||
* @return true if @p account is already in the account-history set.
|
||||
* @note Thread-safe: takes `lock_`; read-only.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
hasAccountHistorySubscription(AccountID const& account) const;
|
||||
|
||||
void
|
||||
onSendEmpty();
|
||||
|
||||
@@ -302,7 +414,9 @@ public:
|
||||
getApiVersion() const noexcept;
|
||||
|
||||
protected:
|
||||
std::mutex lock_;
|
||||
// Mutable so the read-only totalSubscriptionCount() accessor can lock it
|
||||
// from a const method; locking semantics are otherwise unchanged.
|
||||
mutable std::mutex lock_;
|
||||
|
||||
private:
|
||||
Consumer consumer_;
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -43,12 +45,15 @@ namespace xrpl {
|
||||
dynamically generates the signatureless form when it needs to verify
|
||||
the signature.
|
||||
|
||||
An instance of ManifestCache stores, for each trusted validator, (a) its
|
||||
An instance of ManifestCache stores, for each known validator, (a) its
|
||||
master public key, and (b) the most senior of all valid manifests it has
|
||||
seen for that validator, if any. On startup, the [validator_token] config
|
||||
entry (which contains the manifest for this validator) is decoded and
|
||||
added to the manifest cache. Other manifests are added as "gossip"
|
||||
received from xrpld peers.
|
||||
received from xrpld peers, including ones for validators this node does not
|
||||
trust. Manifests for untrusted validators are capped (kMaxUntrustedCount)
|
||||
so peer gossip cannot grow the cache without bound; trusted validators are
|
||||
not capped. Entries are never evicted, so a stored revocation is permanent.
|
||||
|
||||
When an ephemeral key is compromised, a new signing key pair is created,
|
||||
along with a new manifest vouching for it (with a higher sequence number),
|
||||
@@ -164,6 +169,100 @@ struct Manifest
|
||||
std::string
|
||||
to_string(Manifest const& m);
|
||||
|
||||
/**
|
||||
* Largest a valid manifest can be, in decoded bytes.
|
||||
*
|
||||
* A manifest has a fixed set of fields. Each is serialized as a field header
|
||||
* (1-2 bytes), an optional length prefix (1 byte for these sizes), and the
|
||||
* field body. Taking every field at its largest gives the maximum below, so
|
||||
* anything larger cannot be a valid manifest.
|
||||
*
|
||||
* Field header + length + body = bytes
|
||||
* sfVersion (U16) 2 0 2 4
|
||||
* sfSequence (U32) 1 0 4 5
|
||||
* sfPublicKey (33) 1 1 33 35
|
||||
* sfSigningPubKey (33) 1 1 33 35
|
||||
* sfSignature (72) 1 1 72 74
|
||||
* sfMasterSignature (72) 2 1 72 75
|
||||
* sfDomain (128) 1 1 128 130
|
||||
* -----
|
||||
* 358
|
||||
*/
|
||||
constexpr std::size_t kMaxManifestBytes = 358;
|
||||
|
||||
/**
|
||||
* Largest a valid manifest can be, in base64 characters.
|
||||
*
|
||||
* base64 encodes 3 bytes as 4 characters, so this is the encoded form of
|
||||
* @ref kMaxManifestBytes. Callers that receive a base64 manifest should
|
||||
* reject anything longer than this before decoding, to avoid allocating
|
||||
* memory for an oversized input.
|
||||
*/
|
||||
constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes);
|
||||
|
||||
/**
|
||||
* Default number of untrusted manifests to store in cache and allowed
|
||||
* in one Manifest message.
|
||||
*
|
||||
* Bounds unlisted validators two ways. In the cache, a manifest for a
|
||||
* brand-new unlisted key is rejected once this many are held, so peer gossip
|
||||
* cannot grow the cache without end. In a TMManifests message, this many are
|
||||
* sent and processed, so a peer sending its whole cache cannot force unbounded
|
||||
* work.
|
||||
*
|
||||
* Operators can override this with `[overlay] max_untrusted_count`. Both users
|
||||
* read the configured value and fall back to this default.
|
||||
*/
|
||||
constexpr std::size_t kMaxUntrustedCount = 300;
|
||||
|
||||
/**
|
||||
* Default number of trusted manifests allowed in a Manifest message.
|
||||
* Not used atm while creating the message, but used to calculate the higher limit on
|
||||
* received message size. Introduced to maintain consistency. Future implementation
|
||||
* will use this limit.
|
||||
*
|
||||
* Trusted manifests are never dropped: every one this node holds is sent, and
|
||||
* every one received is processed, since dropping one would delay a validator
|
||||
* key rotation. This count only sizes the largest message accepted, so it must
|
||||
* stay above any realistic validator list. Cap can be increased in the config
|
||||
* file if messages get rejected with actual trusted manifest count crossing
|
||||
* configured(or else default) value.
|
||||
* Operators can override this with `[overlay] max_trusted_count`.
|
||||
*/
|
||||
constexpr std::size_t kMaxTrustedCount = 300;
|
||||
|
||||
/**
|
||||
* Number of untrusted manifests to store in cache and allowed
|
||||
* in one Manifest message..
|
||||
*
|
||||
* Returns the operator's override when one is configured, otherwise
|
||||
* @ref kMaxUntrustedCount. Config stores an override rather than the default
|
||||
* itself because the core module cannot depend on this module.
|
||||
*
|
||||
* @param configured The value from `[overlay] max_untrusted_count`, or
|
||||
* `std::nullopt` when the operator did not set it.
|
||||
*/
|
||||
constexpr std::size_t
|
||||
untrustedManifestCount(std::optional<std::size_t> const& configured)
|
||||
{
|
||||
return configured.value_or(kMaxUntrustedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of trusted manifests allowed in a Manifest message.
|
||||
*
|
||||
* Not a cap on how many are sent or processed; see @ref kMaxTrustedCount.
|
||||
* but used to calculate the higher limit on received message size.
|
||||
*
|
||||
* @param configured The value from `[overlay] max_trusted_count`, or
|
||||
* `std::nullopt` when the operator did not set it.
|
||||
*/
|
||||
constexpr std::size_t
|
||||
trustedManifestCount(std::optional<std::size_t> const& configured)
|
||||
{
|
||||
return configured.value_or(kMaxTrustedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs Manifest from serialized string
|
||||
*
|
||||
@@ -172,7 +271,7 @@ to_string(Manifest const& m);
|
||||
* @return `std::nullopt` if string is invalid
|
||||
*
|
||||
* @note This does not verify manifest signatures.
|
||||
* `Manifest::verify` should be called after constructing manifest.
|
||||
* `Manifest::verify` should be called after constructing manifest.
|
||||
*/
|
||||
/** @{ */
|
||||
std::optional<Manifest>
|
||||
@@ -225,30 +324,17 @@ loadValidatorToken(
|
||||
beast::Journal journal = beast::Journal(beast::Journal::getNullSink()));
|
||||
|
||||
enum class ManifestDisposition {
|
||||
/**
|
||||
* Manifest is valid
|
||||
*/
|
||||
Accepted = 0,
|
||||
Accepted = 0, ///< Manifest is valid
|
||||
|
||||
/**
|
||||
* Sequence is too old
|
||||
*/
|
||||
Stale,
|
||||
Stale, ///< Sequence is too old
|
||||
|
||||
/**
|
||||
* The master key is not acceptable to us
|
||||
*/
|
||||
BadMasterKey,
|
||||
BadMasterKey, ///< The master key is not acceptable to us
|
||||
|
||||
/**
|
||||
* The ephemeral key is not acceptable to us
|
||||
*/
|
||||
BadEphemeralKey,
|
||||
BadEphemeralKey, ///< The ephemeral key is not acceptable to us
|
||||
|
||||
/**
|
||||
* Timely, but invalid signature
|
||||
*/
|
||||
Invalid
|
||||
Invalid, ///< Timely, but invalid signature
|
||||
|
||||
UntrustedCapacity ///< Unlisted and limit reached
|
||||
};
|
||||
|
||||
inline std::string
|
||||
@@ -266,11 +352,25 @@ to_string(ManifestDisposition m)
|
||||
return "badEphemeralKey";
|
||||
case ManifestDisposition::Invalid:
|
||||
return "invalid";
|
||||
case ManifestDisposition::UntrustedCapacity:
|
||||
return "untrustedCapacity";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a manifest counts against the 'untrusted' cache cap.
|
||||
*
|
||||
* Passed to `ManifestCache::applyManifest` with no default, so every caller
|
||||
* must choose. `Capped` is the safe, flood-resistant value; only listed or
|
||||
* configured keys should use `Uncapped`.
|
||||
*/
|
||||
enum class ManifestRateLimitCapPolicy : std::uint8_t {
|
||||
Capped, ///< Subject to the untrusted cap (unlisted peer gossip)
|
||||
Uncapped ///< Bypasses the cap (listed/trusted or config manifests)
|
||||
};
|
||||
|
||||
class DatabaseCon;
|
||||
|
||||
/**
|
||||
@@ -294,8 +394,51 @@ private:
|
||||
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
/**
|
||||
* Master keys of cached manifests for validators this node does not list.
|
||||
*
|
||||
* One entry per capped key in `map_`; its size enforces the cap below.
|
||||
* A key is added when first cached under `Capped` and removed when it
|
||||
* becomes listed (see `promoteToTrusted`) or an `Uncapped` update arrives,
|
||||
* never re-added on de-listing. Uncapped keys are not tracked here.
|
||||
*/
|
||||
hash_set<PublicKey> untrustedKeys_;
|
||||
|
||||
/**
|
||||
* Maximum number of untrusted master keys kept in the cache.
|
||||
*
|
||||
* Once reached, a manifest for a brand-new unlisted key is rejected. Set
|
||||
* from the config, defaulting to @ref kMaxUntrustedCount.
|
||||
*/
|
||||
std::size_t const maxUntrustedCount_;
|
||||
|
||||
/**
|
||||
* Running count of manifests rejected because the untrusted cap was full.
|
||||
*
|
||||
* Drives throttled logging (see `kUntrustedRejectCount`). Atomic because
|
||||
* `applyManifest` may run concurrently.
|
||||
*/
|
||||
std::atomic<std::uint64_t> untrustedRejectCount_{0};
|
||||
|
||||
/**
|
||||
* Number of cap rejections between summary warnings.
|
||||
*
|
||||
* @see untrustedRejectCount_
|
||||
*/
|
||||
static constexpr std::uint64_t kUntrustedRejectCount = 10000;
|
||||
|
||||
public:
|
||||
explicit ManifestCache(beast::Journal j = beast::Journal(beast::Journal::getNullSink())) : j_(j)
|
||||
/**
|
||||
* @param j Journal for logging.
|
||||
*
|
||||
* @param maxUntrustedCount Untrusted master keys to keep. Pass the
|
||||
* configured value; defaults to @ref kMaxUntrustedCount. Taken as a
|
||||
* parameter because this module cannot depend on the config.
|
||||
*/
|
||||
explicit ManifestCache(
|
||||
beast::Journal j = beast::Journal(beast::Journal::getNullSink()),
|
||||
std::size_t maxUntrustedCount = kMaxUntrustedCount)
|
||||
: j_(j), maxUntrustedCount_(maxUntrustedCount)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -378,17 +521,44 @@ public:
|
||||
/**
|
||||
* Add manifest to cache.
|
||||
*
|
||||
* A brand-new unlisted key is rejected once the untrusted cap is full;
|
||||
* updates to a cached key and `Uncapped` manifests bypass the cap. The
|
||||
* caller decides `cap` before calling so the cache lock is not held while
|
||||
* consulting the validator list, which would risk a lock-ordering deadlock.
|
||||
*
|
||||
* @param m Manifest to add
|
||||
*
|
||||
* @return `ManifestDisposition::accepted` if successful, or
|
||||
* `stale` or `invalid` otherwise
|
||||
* @param cap `Uncapped` skips the untrusted cap; use it for keys that are
|
||||
* listed, configured, or loaded from the DB. Note `Uncapped` does not
|
||||
* assert the key is currently trusted (a DB entry may predate a
|
||||
* de-listing). Callers must state this explicitly so a manifest is
|
||||
* never left uncapped by omission.
|
||||
*
|
||||
* @return `Accepted` if stored, `Stale` if superseded, `Invalid`/
|
||||
* `BadEphemeralKey` if malformed, or `UntrustedCapacity` if the
|
||||
* untrusted cap is full.
|
||||
*
|
||||
* @par Thread Safety
|
||||
*
|
||||
* May be called concurrently
|
||||
*/
|
||||
ManifestDisposition
|
||||
applyManifest(Manifest m);
|
||||
applyManifest(Manifest m, ManifestRateLimitCapPolicy cap);
|
||||
|
||||
/**
|
||||
* Stop counting a master key against the untrusted cap.
|
||||
*
|
||||
* Called when a cached untrusted key becomes listed, freeing its slot.
|
||||
* Idempotent and a no-op for keys that were never counted.
|
||||
*
|
||||
* @param pk Master public key that is now listed/trusted
|
||||
*
|
||||
* @par Thread Safety
|
||||
*
|
||||
* May be called concurrently
|
||||
*/
|
||||
void
|
||||
promoteToTrusted(PublicKey const& pk);
|
||||
|
||||
/**
|
||||
* Populate manifest cache with manifests in database and config.
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/tx/transactors/dex/AMMContext.h>
|
||||
|
||||
#include <cstdint>
|
||||
@@ -124,17 +123,12 @@ private:
|
||||
generateFibSeqOffer(TAmounts<TIn, TOut> const& balances) const;
|
||||
|
||||
/**
|
||||
* Generate max offer.
|
||||
* If `fixAMMOverflowOffer` is active, the offer is generated as:
|
||||
* Generate max offer. The offer is generated as:
|
||||
* takerGets = 99% * balances.out takerPays = swapOut(takerGets).
|
||||
* Return nullopt if takerGets is 0 or takerGets == balances.out.
|
||||
*
|
||||
* If `fixAMMOverflowOffer` is not active, the offer is generated as:
|
||||
* takerPays = max input amount;
|
||||
* takerGets = swapIn(takerPays).
|
||||
*/
|
||||
[[nodiscard]] std::optional<AMMOffer<TIn, TOut>>
|
||||
maxOffer(TAmounts<TIn, TOut> const& balances, Rules const& rules) const;
|
||||
maxOffer(TAmounts<TIn, TOut> const& balances) const;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -16,7 +18,7 @@ namespace xrpl {
|
||||
class SponsorshipSet : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Custom;
|
||||
|
||||
explicit SponsorshipSet(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
@@ -47,6 +49,15 @@ public:
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
|
||||
private:
|
||||
TER
|
||||
createSponsorship(
|
||||
Keylet const& sponsorshipKeylet,
|
||||
AccountID const& sponsorID,
|
||||
AccountID const& sponseeID,
|
||||
SLE::ref sponsorAccSle,
|
||||
SLE::ref reserveSponsorAccSle);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -32,7 +32,7 @@ struct MPTCreateArgs
|
||||
std::optional<std::uint16_t> transferFee = std::nullopt;
|
||||
std::optional<Slice> const& metadata{};
|
||||
std::optional<uint256> domainId = std::nullopt;
|
||||
std::optional<std::uint32_t> mutableFlags = std::nullopt;
|
||||
std::optional<std::uint32_t> immutableFlags = std::nullopt;
|
||||
// Set only by callers that issue an MPT representing a wrapped asset
|
||||
// (e.g. VaultCreate's share token). The keylet must point to an
|
||||
// existing MPToken or RippleState owned by `account`. Surfaces on
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -22,6 +25,37 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
// Maps each MPTokenIssuanceSet set flag(e.g., tfMPTSetCanLock), to the issuance's
|
||||
// corresponding immutable flag (e.g., lsifMPTCanLock) and the target ledger flag (e.g.,
|
||||
// lsfMPTCanLock).
|
||||
struct FlagMapping
|
||||
{
|
||||
std::uint32_t setFlag;
|
||||
std::uint32_t immutableFlag;
|
||||
std::uint32_t ledgerFlag;
|
||||
};
|
||||
|
||||
static constexpr std::array<FlagMapping, 7> flagMapping = {
|
||||
{{.setFlag = tfMPTSetCanLock, .immutableFlag = lsifMPTCanLock, .ledgerFlag = lsfMPTCanLock},
|
||||
{.setFlag = tfMPTSetRequireAuth,
|
||||
.immutableFlag = lsifMPTRequireAuth,
|
||||
.ledgerFlag = lsfMPTRequireAuth},
|
||||
{.setFlag = tfMPTSetCanEscrow,
|
||||
.immutableFlag = lsifMPTCanEscrow,
|
||||
.ledgerFlag = lsfMPTCanEscrow},
|
||||
{.setFlag = tfMPTSetCanTrade,
|
||||
.immutableFlag = lsifMPTCanTrade,
|
||||
.ledgerFlag = lsfMPTCanTrade},
|
||||
{.setFlag = tfMPTSetCanTransfer,
|
||||
.immutableFlag = lsifMPTCanTransfer,
|
||||
.ledgerFlag = lsfMPTCanTransfer},
|
||||
{.setFlag = tfMPTSetCanClawback,
|
||||
.immutableFlag = lsifMPTCanClawback,
|
||||
.ledgerFlag = lsfMPTCanClawback},
|
||||
{.setFlag = tfMPTSetCanHoldConfidentialBalance,
|
||||
.immutableFlag = lsifMPTCanHoldConfidentialBalance,
|
||||
.ledgerFlag = lsfMPTCanHoldConfidentialBalance}}};
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
|
||||
427
include/xrpl/tx/wasm/HostContext.h
Normal file
427
include/xrpl/tx/wasm/HostContext.h
Normal file
@@ -0,0 +1,427 @@
|
||||
#pragma once
|
||||
|
||||
#include <rust/cxx.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
// `xrpl::HostFunctions` is forward-declared rather than included: this header is
|
||||
// `include!()`d by the cxxbridge-generated translation unit, whose target gets only the
|
||||
// project's `include/` directory - not the Boost paths that HostFunc.h -> Slice.h ->
|
||||
// strHex.h transitively need. A reference member and declarations alone do not require a
|
||||
// complete type; HostContext.cpp, compiled into libxrpl, includes the real header.
|
||||
class HostFunctions;
|
||||
|
||||
// Defined by the cxx bridge, which emits it into `xrpl_wasm_vm_ffi_cxxbridge/lib.h` from the
|
||||
// declaration in `crates/xrpl-wasm-vm-ffi` - so the data types and their wire values are
|
||||
// written once, in Rust, rather than kept in step with a copy here.
|
||||
//
|
||||
// Forward-declared for the reason `HostFunctions` above is: that generated header includes
|
||||
// this one, so naming its definition here would be circular. A scoped enum with a fixed
|
||||
// underlying type needs no definition to appear in a signature; `HostContext.cpp` includes
|
||||
// the generated header for the `switch`.
|
||||
enum class TraceDataType : std::int32_t;
|
||||
|
||||
// The host handed to the Rust wasm engine: one method per entry in the wasm host ABI,
|
||||
// each forwarding to `xrpl::HostFunctions` - the single source of truth for ledger
|
||||
// access - and lowering its typed `std::expected` result onto the ABI's wire form.
|
||||
//
|
||||
// Every method is `noexcept`, and every body catches everything: a C++ exception
|
||||
// unwinding into the Rust frames that called it would be undefined behaviour, so a
|
||||
// failure leaves here as -1, which the engine reads as a fatal error and reports as
|
||||
// `tecINTERNAL`.
|
||||
//
|
||||
// Not an owner: it borrows `hf` for the length of one run. Declared `struct` because the
|
||||
// Rust side only ever sees an opaque pointer.
|
||||
class HostContext
|
||||
{
|
||||
// Non-const so a host function that mutates (`cacheLedgerObj`, `updateData`) can be
|
||||
// reached from the `const` methods below: constness of the reference is not
|
||||
// constness of the referent.
|
||||
HostFunctions& hostFunctions_;
|
||||
|
||||
public:
|
||||
HostContext(HostFunctions& hostFunctions);
|
||||
|
||||
// A byte-producing call is handed `out` - a slice aliasing either guest linear
|
||||
// memory or the engine's output buffer - writes the value only if the whole of it
|
||||
// fits, and returns the value's *true* length, which may exceed `out`. That is how a
|
||||
// guest learns the size to ask for, and it is why these methods never need to know
|
||||
// the guest's capacity: the engine owns the buffer-fit, field-cap and transfer-budget
|
||||
// rules and derives all three from the length returned here.
|
||||
//
|
||||
// A negative return is a `HostFunctionError` code.
|
||||
[[nodiscard]] std::int32_t
|
||||
getLedgerSqn(rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getParentLedgerTime(rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getParentLedgerHash(rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getBaseFee(rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The amendment is either a 32-byte id or a name; a 32-byte input is tried as an
|
||||
// id first and falls back to a name lookup. Answers 1 or 0, or a negative
|
||||
// `HostFunctionError` code.
|
||||
[[nodiscard]] std::int32_t
|
||||
isAmendmentEnabled(rust::Slice<std::uint8_t const> amendment) const noexcept;
|
||||
|
||||
// The object id must be a 32-byte uint256, else `InvalidParams`. `cacheIdx` selects
|
||||
// the slot (0 = pick a free one). Answers the slot used, or a negative
|
||||
// `HostFunctionError` code.
|
||||
[[nodiscard]] std::int32_t
|
||||
cacheLedgerObj(rust::Slice<std::uint8_t const> objId, std::int32_t cacheIdx) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getTxField(std::int32_t field, rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getCurrentLedgerObjField(std::int32_t field, rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getLedgerObjField(std::int32_t cacheIdx, std::int32_t field, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
// The locator is a path of little-endian i32 steps, so its byte length must be a
|
||||
// non-zero multiple of 4, else `LocatorMalformed`.
|
||||
[[nodiscard]] std::int32_t
|
||||
getTxNestedField(rust::Slice<std::uint8_t const> locator, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getCurrentLedgerObjNestedField(
|
||||
rust::Slice<std::uint8_t const> locator,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getLedgerObjNestedField(
|
||||
std::int32_t cacheIdx,
|
||||
rust::Slice<std::uint8_t const> locator,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Answers the array's element count directly, or a negative `HostFunctionError`
|
||||
// code (`NoArray` if the field is not an array).
|
||||
[[nodiscard]] std::int32_t
|
||||
getTxArrayLen(std::int32_t field) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getTxNestedArrayLen(rust::Slice<std::uint8_t const> locator) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getCurrentLedgerObjNestedArrayLen(rust::Slice<std::uint8_t const> locator) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
getLedgerObjNestedArrayLen(std::int32_t cacheIdx, rust::Slice<std::uint8_t const> locator)
|
||||
const noexcept;
|
||||
|
||||
// Answers 1/0 for a valid/invalid signature, or a negative `HostFunctionError`.
|
||||
[[nodiscard]] std::int32_t
|
||||
checkSignature(
|
||||
rust::Slice<std::uint8_t const> message,
|
||||
rust::Slice<std::uint8_t const> signature,
|
||||
rust::Slice<std::uint8_t const> pubkey) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
accountKeylet(rust::Slice<std::uint8_t const> account, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
// Each asset is decoded by length (24 = MPT, 20 = XRP, 40 = issue), else
|
||||
// `InvalidParams`. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
ammKeylet(
|
||||
rust::Slice<std::uint8_t const> asset1,
|
||||
rust::Slice<std::uint8_t const> asset2,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
|
||||
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
checkKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Subject and issuer must each be 20 bytes, else `InvalidParams`. Writes the
|
||||
// 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
credentialKeylet(
|
||||
rust::Slice<std::uint8_t const> subject,
|
||||
rust::Slice<std::uint8_t const> issuer,
|
||||
rust::Slice<std::uint8_t const> credentialType,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
delegateKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
rust::Slice<std::uint8_t const> authorize,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
depositPreauthKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
rust::Slice<std::uint8_t const> authorize,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
didKeylet(rust::Slice<std::uint8_t const> account, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
|
||||
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
escrowKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Both accounts and the currency must each be 20 bytes, else `InvalidParams`.
|
||||
// Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
trustLineKeylet(
|
||||
rust::Slice<std::uint8_t const> account1,
|
||||
rust::Slice<std::uint8_t const> account2,
|
||||
rust::Slice<std::uint8_t const> currency,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The issuer id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
|
||||
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
mptokenIssuanceKeylet(
|
||||
rust::Slice<std::uint8_t const> issuer,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The MPT id must be 24 bytes and the holder 20, else `InvalidParams`. Writes the
|
||||
// 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
mptokenKeylet(
|
||||
rust::Slice<std::uint8_t const> mptid,
|
||||
rust::Slice<std::uint8_t const> holder,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
|
||||
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
nftokenOfferKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
|
||||
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
offerKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. `docId` carries the
|
||||
// guest's u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
oracleKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
std::int32_t docId,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Both account ids must be 20 bytes, else `InvalidParams`. `seq` carries the
|
||||
// guest's u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
paychannelKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
rust::Slice<std::uint8_t const> destination,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
|
||||
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
permissionedDomainKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
signerListKeylet(rust::Slice<std::uint8_t const> account, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
|
||||
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
ticketKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
|
||||
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
|
||||
[[nodiscard]] std::int32_t
|
||||
vaultKeylet(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
std::int32_t seq,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
sha512Half(rust::Slice<std::uint8_t const> data, rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Renders `data` as `dataType` says, and hands the text to `HostFunctions::trace`, which
|
||||
// is what puts it in this node's log.
|
||||
//
|
||||
// The one call that answers nothing: the guest's wasm function has no result, and this
|
||||
// node's own log is the only thing a trace touches, so a buffer that does not hold what
|
||||
// it claims is logged here and dropped rather than reported to a contract.
|
||||
void
|
||||
trace(rust::Str msg, rust::Slice<std::uint8_t const> data, TraceDataType dataType)
|
||||
const noexcept;
|
||||
|
||||
// Stores `data` as the current object's data field and returns the number of bytes
|
||||
// stored, or a negative `HostFunctionError` code.
|
||||
[[nodiscard]] std::int32_t
|
||||
updateData(rust::Slice<std::uint8_t const> data) const noexcept;
|
||||
|
||||
// The account id must be 20 bytes and the nft id 32 bytes, else `InvalidParams`.
|
||||
// Writes the token's URI bytes.
|
||||
[[nodiscard]] std::int32_t
|
||||
getNFT(
|
||||
rust::Slice<std::uint8_t const> account,
|
||||
rust::Slice<std::uint8_t const> nftId,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The nft id must be 32 bytes, else `InvalidParams`. Writes the 20-byte issuer
|
||||
// account encoded in the id.
|
||||
[[nodiscard]] std::int32_t
|
||||
getNFTIssuer(rust::Slice<std::uint8_t const> nftId, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
// The nft id must be 32 bytes, else `InvalidParams`. Writes the taxon as its four
|
||||
// little-endian bytes.
|
||||
[[nodiscard]] std::int32_t
|
||||
getNFTTaxon(rust::Slice<std::uint8_t const> nftId, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
// The nft id must be 32 bytes, else `InvalidParams`. Returns the flags, or a
|
||||
// negative `HostFunctionError` code.
|
||||
[[nodiscard]] std::int32_t
|
||||
getNFTFlags(rust::Slice<std::uint8_t const> nftId) const noexcept;
|
||||
|
||||
// The nft id must be 32 bytes, else `InvalidParams`. Returns the transfer fee, or a
|
||||
// negative `HostFunctionError` code.
|
||||
[[nodiscard]] std::int32_t
|
||||
getNFTTransferFee(rust::Slice<std::uint8_t const> nftId) const noexcept;
|
||||
|
||||
// The nft id must be 32 bytes, else `InvalidParams`. Writes the sequence number as
|
||||
// its four little-endian bytes.
|
||||
[[nodiscard]] std::int32_t
|
||||
getNFTSequence(rust::Slice<std::uint8_t const> nftId, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
// Float / number arithmetic. A float is an XRPL `Number` in serialized form;
|
||||
// `mode` is a rounding mode. Each writes the result float bytes unless noted.
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
floatFromInt(std::int64_t x, std::int32_t mode, rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// The integer region must be eight bytes, else `InvalidParams`.
|
||||
[[nodiscard]] std::int32_t
|
||||
floatFromUint(
|
||||
rust::Slice<std::uint8_t const> x,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// `amount` must be a serialized `STAmount`, else `InvalidParams`.
|
||||
[[nodiscard]] std::int32_t
|
||||
floatFromSTAmount(
|
||||
rust::Slice<std::uint8_t const> amount,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// `number` must be a serialized `STNumber`, else `InvalidParams`.
|
||||
[[nodiscard]] std::int32_t
|
||||
floatFromSTNumber(
|
||||
rust::Slice<std::uint8_t const> number,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Rounds the float to an integer, written as its eight little-endian bytes.
|
||||
[[nodiscard]] std::int32_t
|
||||
floatToInt(rust::Slice<std::uint8_t const> x, std::int32_t mode, rust::Slice<std::uint8_t> out)
|
||||
const noexcept;
|
||||
|
||||
// Writes the mantissa (eight little-endian bytes) and the exponent (four little-
|
||||
// endian bytes) to two output regions; returns their total size.
|
||||
[[nodiscard]] std::int32_t
|
||||
floatToMantExp(
|
||||
rust::Slice<std::uint8_t const> x,
|
||||
rust::Slice<std::uint8_t> mantissaOut,
|
||||
rust::Slice<std::uint8_t> exponentOut) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
floatFromMantExp(
|
||||
std::int64_t mantissa,
|
||||
std::int32_t exponent,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
// Returns a negative, zero, or positive scalar as `x` is less than, equal to, or
|
||||
// greater than `y`, or a negative `HostFunctionError` code on failure.
|
||||
[[nodiscard]] std::int32_t
|
||||
floatCompare(rust::Slice<std::uint8_t const> x, rust::Slice<std::uint8_t const> y)
|
||||
const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
floatAdd(
|
||||
rust::Slice<std::uint8_t const> x,
|
||||
rust::Slice<std::uint8_t const> y,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
floatSubtract(
|
||||
rust::Slice<std::uint8_t const> x,
|
||||
rust::Slice<std::uint8_t const> y,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
floatMultiply(
|
||||
rust::Slice<std::uint8_t const> x,
|
||||
rust::Slice<std::uint8_t const> y,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
floatDivide(
|
||||
rust::Slice<std::uint8_t const> x,
|
||||
rust::Slice<std::uint8_t const> y,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
floatRoot(
|
||||
rust::Slice<std::uint8_t const> x,
|
||||
std::int32_t n,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
floatPower(
|
||||
rust::Slice<std::uint8_t const> x,
|
||||
std::int32_t n,
|
||||
std::int32_t mode,
|
||||
rust::Slice<std::uint8_t> out) const noexcept;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
473
include/xrpl/tx/wasm/HostFunc.h
Normal file
473
include/xrpl/tx/wasm/HostFunc.h
Normal file
@@ -0,0 +1,473 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace wasm_float {
|
||||
|
||||
std::string
|
||||
floatToString(Slice const& data);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromIntImpl(int64_t x, int32_t mode);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromUintImpl(uint64_t x, int32_t mode);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromSTAmountImpl(STAmount const& x, int32_t mode);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromSTNumberImpl(STNumber const& x, int32_t mode);
|
||||
|
||||
std::expected<int64_t, HostFunctionError>
|
||||
floatToIntImpl(Slice const& x, int32_t mode);
|
||||
|
||||
std::expected<FloatPair, HostFunctionError>
|
||||
floatToMantExpImpl(Slice const& x);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromMantExpImpl(int64_t mantissa, int32_t exponent, int32_t mode);
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
floatCompareImpl(Slice const& x, Slice const& y);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatAddImpl(Slice const& x, Slice const& y, int32_t mode);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatSubtractImpl(Slice const& x, Slice const& y, int32_t mode);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatMultiplyImpl(Slice const& x, Slice const& y, int32_t mode);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatDivideImpl(Slice const& x, Slice const& y, int32_t mode);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatRootImpl(Slice const& x, int32_t n, int32_t mode);
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatPowerImpl(Slice const& x, int32_t n, int32_t mode);
|
||||
|
||||
} // namespace wasm_float
|
||||
|
||||
// Intended to work only through wasm runtime. Don't call them directly, except with unit tests
|
||||
class HostFunctions
|
||||
{
|
||||
protected:
|
||||
beast::Journal j_;
|
||||
|
||||
public:
|
||||
HostFunctions(beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) : j_(j)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] beast::Journal
|
||||
getJournal() const
|
||||
{
|
||||
return j_;
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
|
||||
[[nodiscard]] virtual bool
|
||||
checkSelf() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
|
||||
getLedgerSqn() const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
|
||||
getParentLedgerTime() const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Hash, HostFunctionError>
|
||||
getParentLedgerHash() const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<uint32_t, HostFunctionError>
|
||||
getBaseFee() const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
isAmendmentEnabled(uint256 const& amendmentId) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
isAmendmentEnabled(std::string_view const& amendmentName) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
virtual std::expected<int32_t, HostFunctionError>
|
||||
cacheLedgerObj(uint256 const& objId, int32_t cacheIdx)
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
getTxField(SField const& fname) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
getCurrentLedgerObjField(SField const& fname) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
getLedgerObjField(int32_t cacheIdx, SField const& fname) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
getTxNestedField(FieldLocator const& locator) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
getCurrentLedgerObjNestedField(FieldLocator const& locator) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
getTxArrayLen(SField const& fname) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
getCurrentLedgerObjArrayLen(SField const& fname) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
getTxNestedArrayLen(FieldLocator const& locator) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
virtual std::expected<int32_t, HostFunctionError>
|
||||
updateData(Slice const& data)
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
checkSignature(Slice const& message, Slice const& signature, Slice const& pubkey) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Hash, HostFunctionError>
|
||||
computeSha512HalfHash(Slice const& data) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
accountKeylet(AccountID const& account) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
ammKeylet(Asset const& issue1, Asset const& issue2) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
checkKeylet(AccountID const& account, std::uint32_t seq) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
credentialKeylet(AccountID const& subject, AccountID const& issuer, Slice const& credentialType)
|
||||
const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
didKeylet(AccountID const& account) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
delegateKeylet(AccountID const& account, AccountID const& authorize) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
depositPreauthKeylet(AccountID const& account, AccountID const& authorize) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
escrowKeylet(AccountID const& account, std::uint32_t seq) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
trustLineKeylet(AccountID const& account1, AccountID const& account2, Currency const& currency)
|
||||
const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
mptokenIssuanceKeylet(AccountID const& issuer, std::uint32_t seq) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
mptokenKeylet(MPTID const& mptid, AccountID const& holder) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
nftokenOfferKeylet(AccountID const& account, std::uint32_t seq) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
offerKeylet(AccountID const& account, std::uint32_t seq) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
oracleKeylet(AccountID const& account, std::uint32_t docId) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
paychannelKeylet(AccountID const& account, AccountID const& destination, std::uint32_t seq)
|
||||
const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
permissionedDomainKeylet(AccountID const& account, std::uint32_t seq) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
signerListKeylet(AccountID const& account) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
ticketKeylet(AccountID const& account, std::uint32_t seq) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
vaultKeylet(AccountID const& account, std::uint32_t seq) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
getNFT(AccountID const& account, uint256 const& nftId) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
getNFTIssuer(uint256 const& nftId) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
|
||||
getNFTTaxon(uint256 const& nftId) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
getNFTFlags(uint256 const& nftId) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
getNFTTransferFee(uint256 const& nftId) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
|
||||
getNFTSequence(uint256 const& nftId) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
// A no-op rather than Unimplemented: trace only writes to the local log.
|
||||
// trace_wrap has already rendered the guest's buffer into `data`.
|
||||
virtual void
|
||||
trace(std::string_view const& msg, std::string_view const& data) const
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatFromInt(int64_t x, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatFromUint(uint64_t x, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatFromSTAmount(STAmount const& x, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatFromSTNumber(STNumber const& x, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int64_t, HostFunctionError>
|
||||
floatToInt(Slice const& x, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<FloatPair, HostFunctionError>
|
||||
floatToMantExp(Slice const& x) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
|
||||
floatCompare(Slice const& x, Slice const& y) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatAdd(Slice const& x, Slice const& y, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatSubtract(Slice const& x, Slice const& y, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatMultiply(Slice const& x, Slice const& y, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatDivide(Slice const& x, Slice const& y, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatRoot(Slice const& x, int32_t n, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
|
||||
floatPower(Slice const& x, int32_t n, int32_t mode) const
|
||||
{
|
||||
return std::unexpected(HostFunctionError::Unimplemented);
|
||||
}
|
||||
|
||||
virtual ~HostFunctions() = default;
|
||||
// LCOV_EXCL_STOP
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
290
include/xrpl/tx/wasm/HostFuncImpl.h
Normal file
290
include/xrpl/tx/wasm/HostFuncImpl.h
Normal file
@@ -0,0 +1,290 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/wasm/HostFunc.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Intended to work only through wasm runtime. Don't call them directly, except with unit tests
|
||||
class WasmHostFunctionsImpl : public HostFunctions
|
||||
{
|
||||
ApplyContext& ctx_;
|
||||
|
||||
Keylet leKey_;
|
||||
mutable std::optional<std::shared_ptr<SLE const>> currentLedgerObj_;
|
||||
|
||||
static int constexpr maxCache = 256;
|
||||
std::array<std::shared_ptr<SLE const>, maxCache> cache_;
|
||||
|
||||
std::optional<Bytes> data_;
|
||||
|
||||
public:
|
||||
std::expected<std::shared_ptr<SLE const>, HostFunctionError>
|
||||
getCurrentLedgerObj() const
|
||||
{
|
||||
if (!currentLedgerObj_)
|
||||
currentLedgerObj_ = ctx_.view().read(leKey_);
|
||||
if (*currentLedgerObj_)
|
||||
return *currentLedgerObj_;
|
||||
return std::unexpected(HostFunctionError::LedgerObjNotFound);
|
||||
}
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
normalizeCacheIndex(int32_t cacheIdx) const
|
||||
{
|
||||
--cacheIdx;
|
||||
if (cacheIdx < 0 || cacheIdx >= maxCache)
|
||||
return std::unexpected(HostFunctionError::SlotOutRange);
|
||||
if (!cache_[cacheIdx])
|
||||
return std::unexpected(HostFunctionError::EmptySlot);
|
||||
return cacheIdx;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void
|
||||
log(std::string_view const& msg, F&& dataFn) const
|
||||
{
|
||||
#ifdef DEBUG_OUTPUT
|
||||
auto& j = std::cerr;
|
||||
#else
|
||||
if (!getJournal().active(beast::Severity::Trace))
|
||||
return;
|
||||
auto j = getJournal().trace();
|
||||
#endif
|
||||
j << "WasmTrace[" << toShortString(leKey_.key) << "]: " << msg << " " << dataFn();
|
||||
|
||||
#ifdef DEBUG_OUTPUT
|
||||
j << std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
WasmHostFunctionsImpl(ApplyContext& ct, Keylet const& leKey)
|
||||
: HostFunctions(ct.journal), ctx_(ct), leKey_(leKey)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
checkSelf() const override
|
||||
{
|
||||
return !currentLedgerObj_ && !data_ &&
|
||||
std::ranges::none_of(cache_, [](auto const& p) { return !!p; });
|
||||
}
|
||||
|
||||
std::optional<Bytes> const&
|
||||
getData() const
|
||||
{
|
||||
return data_;
|
||||
}
|
||||
|
||||
std::expected<std::uint32_t, HostFunctionError>
|
||||
getLedgerSqn() const override;
|
||||
|
||||
std::expected<std::uint32_t, HostFunctionError>
|
||||
getParentLedgerTime() const override;
|
||||
|
||||
std::expected<Hash, HostFunctionError>
|
||||
getParentLedgerHash() const override;
|
||||
|
||||
std::expected<std::uint32_t, HostFunctionError>
|
||||
getBaseFee() const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
isAmendmentEnabled(uint256 const& amendmentId) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
isAmendmentEnabled(std::string_view const& amendmentName) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
cacheLedgerObj(uint256 const& objId, int32_t cacheIdx) override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
getTxField(SField const& fname) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
getCurrentLedgerObjField(SField const& fname) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
getLedgerObjField(int32_t cacheIdx, SField const& fname) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
getTxNestedField(FieldLocator const& locator) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
getCurrentLedgerObjNestedField(FieldLocator const& locator) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
getTxArrayLen(SField const& fname) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
getCurrentLedgerObjArrayLen(SField const& fname) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
getTxNestedArrayLen(FieldLocator const& locator) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
updateData(Slice const& data) override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
checkSignature(Slice const& message, Slice const& signature, Slice const& pubkey)
|
||||
const override;
|
||||
|
||||
std::expected<Hash, HostFunctionError>
|
||||
computeSha512HalfHash(Slice const& data) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
accountKeylet(AccountID const& account) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
ammKeylet(Asset const& issue1, Asset const& issue2) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
checkKeylet(AccountID const& account, std::uint32_t seq) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
credentialKeylet(AccountID const& subject, AccountID const& issuer, Slice const& credentialType)
|
||||
const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
didKeylet(AccountID const& account) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
delegateKeylet(AccountID const& account, AccountID const& authorize) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
depositPreauthKeylet(AccountID const& account, AccountID const& authorize) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
escrowKeylet(AccountID const& account, std::uint32_t seq) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
trustLineKeylet(AccountID const& account1, AccountID const& account2, Currency const& currency)
|
||||
const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
mptokenIssuanceKeylet(AccountID const& issuer, std::uint32_t seq) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
mptokenKeylet(MPTID const& mptid, AccountID const& holder) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
nftokenOfferKeylet(AccountID const& account, std::uint32_t seq) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
offerKeylet(AccountID const& account, std::uint32_t seq) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
oracleKeylet(AccountID const& account, std::uint32_t docId) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
paychannelKeylet(AccountID const& account, AccountID const& destination, std::uint32_t seq)
|
||||
const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
permissionedDomainKeylet(AccountID const& account, std::uint32_t seq) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
signerListKeylet(AccountID const& account) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
ticketKeylet(AccountID const& account, std::uint32_t seq) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
vaultKeylet(AccountID const& account, std::uint32_t seq) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
getNFT(AccountID const& account, uint256 const& nftId) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
getNFTIssuer(uint256 const& nftId) const override;
|
||||
|
||||
std::expected<std::uint32_t, HostFunctionError>
|
||||
getNFTTaxon(uint256 const& nftId) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
getNFTFlags(uint256 const& nftId) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
getNFTTransferFee(uint256 const& nftId) const override;
|
||||
|
||||
std::expected<std::uint32_t, HostFunctionError>
|
||||
getNFTSequence(uint256 const& nftId) const override;
|
||||
|
||||
void
|
||||
trace(std::string_view const& msg, std::string_view const& data) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromInt(int64_t x, int32_t mode) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromUint(uint64_t x, int32_t mode) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromSTAmount(STAmount const& x, int32_t mode) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromSTNumber(STNumber const& x, int32_t mode) const override;
|
||||
|
||||
std::expected<int64_t, HostFunctionError>
|
||||
floatToInt(Slice const& x, int32_t mode) const override;
|
||||
|
||||
std::expected<FloatPair, HostFunctionError>
|
||||
floatToMantExp(Slice const& x) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const override;
|
||||
|
||||
std::expected<int32_t, HostFunctionError>
|
||||
floatCompare(Slice const& x, Slice const& y) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatAdd(Slice const& x, Slice const& y, int32_t mode) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatSubtract(Slice const& x, Slice const& y, int32_t mode) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatMultiply(Slice const& x, Slice const& y, int32_t mode) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatDivide(Slice const& x, Slice const& y, int32_t mode) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatRoot(Slice const& x, int32_t n, int32_t mode) const override;
|
||||
|
||||
std::expected<Bytes, HostFunctionError>
|
||||
floatPower(Slice const& x, int32_t n, int32_t mode) const override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
40
include/xrpl/tx/wasm/README.md
Normal file
40
include/xrpl/tx/wasm/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# WASM Module for Programmable Escrows
|
||||
|
||||
WebAssembly execution for programmable escrows. When an escrow is finished, its contract
|
||||
runs to decide whether the release conditions are met. Specification:
|
||||
[XLS-0102: WASM VM](https://xls.xrpl.org/xls/XLS-0102-wasm-vm.html).
|
||||
|
||||
The engine itself is Rust (`crates/xrpl-wasm-vm`, over wasmi), reached through a cxx
|
||||
bridge.
|
||||
|
||||
## What is in this directory
|
||||
|
||||
- **`WasmVM.h`** — the entry points xrpld calls: `runEscrowWasm` (execute a contract,
|
||||
returning a result and its gas cost, or a `WasmTER`) and `preflightEscrowWasm` (screen a
|
||||
module with no host and no execution). Both own their TER maps.
|
||||
- **`HostFunc.h`** — the `HostFunctions` interface: one virtual per host function, each
|
||||
defaulting to `Unimplemented`, returning `std::expected<T, HostFunctionError>`.
|
||||
- **`HostFuncImpl.h`** — `WasmHostFunctionsImpl`, the implementation over an
|
||||
`ApplyContext&`. Bodies are split across `HostFuncImpl*.cpp` by category.
|
||||
- **`HostContext.h`** — the bridge's C++ half: an ABI-shaped, `noexcept` view of
|
||||
`HostFunctions` that the engine calls back into. Nothing may unwind into Rust, so every
|
||||
method routes through `guarded()`.
|
||||
- **`WasmCommon.h`** — the shared vocabulary: `HostFunctionError` (the codes a contract
|
||||
sees), `Bytes`, `FieldLocator`, `WasmTER`, `adjustWasmEndianess`, which is where the
|
||||
boundary's byte order is decided, and `guarded()`, the one catch every crossing of the
|
||||
bridge's C++ half goes through.
|
||||
|
||||
## Host functions
|
||||
|
||||
Grouped by what they reach: ledger information; transaction and ledger-object field access;
|
||||
keylet construction; cryptography; float arithmetic; NFT queries; tracing.
|
||||
|
||||
The wire names and per-call gas costs are declared in `crates/xrpl-host-functions` —
|
||||
one `host_functions!` block that generates the ABI trait and the spec table. That
|
||||
declaration is the single source of truth; `HostFunc.h` is the C++ side of it.
|
||||
|
||||
## Entry point
|
||||
|
||||
A module must export `escrow_finish` (`escrowFunctionName`) taking no parameters and
|
||||
returning `int32_t`: positive means the conditions are met, zero or negative rejects the
|
||||
finish. Everything the contract needs it asks for through a host call.
|
||||
187
include/xrpl/tx/wasm/WasmCommon.h
Normal file
187
include/xrpl/tx/wasm/WasmCommon.h
Normal file
@@ -0,0 +1,187 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <source_location>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
using Bytes = std::vector<std::uint8_t>;
|
||||
using Hash = xrpl::uint256;
|
||||
using FloatPair = std::pair<int64_t, int32_t>;
|
||||
|
||||
enum class HostFunctionError : int32_t {
|
||||
Unimplemented = -1,
|
||||
FieldNotFound = -2,
|
||||
BufferTooSmall = -3,
|
||||
NoArray = -4,
|
||||
NotLeafField = -5,
|
||||
LocatorMalformed = -6,
|
||||
SlotOutRange = -7,
|
||||
SlotsFull = -8,
|
||||
EmptySlot = -9,
|
||||
LedgerObjNotFound = -10,
|
||||
OutOfTransferLimit = -11,
|
||||
DataFieldTooLarge = -12,
|
||||
PointerOutOfBounds = -13,
|
||||
NoMemExported = -14,
|
||||
InvalidParams = -15,
|
||||
InvalidAccount = -16,
|
||||
InvalidField = -17,
|
||||
IndexOutOfBounds = -18,
|
||||
FloatInputMalformed = -19,
|
||||
FloatComputationError = -20,
|
||||
|
||||
// The call was not served at all, so the engine stops the run and the transaction is
|
||||
// tecINTERNAL rather than the contract being handed a code to interpret. `guarded`
|
||||
// answers it for a host body that throws.
|
||||
//
|
||||
// Outside the -1 ..= -20 range that a contract reads, and the only entry that is: it
|
||||
// needs no number in that range, and INT32_MIN cannot collide with a code appended
|
||||
// above. Negative so that a reader treating it as an ordinary failure is still right.
|
||||
InternalFatal = std::numeric_limits<int32_t>::min(),
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct WasmResult
|
||||
{
|
||||
T result;
|
||||
int64_t cost;
|
||||
};
|
||||
using EscrowResult = WasmResult<int32_t>;
|
||||
|
||||
// Engine error when wasm does not run to completion. `cost` is the gas consumed
|
||||
// when meaningful (tecOUT_OF_GAS / tecFAILED_PROCESSING; caller writes it to tx
|
||||
// metadata); std::nullopt for tecINTERNAL and malformed input (no gas reported).
|
||||
struct WasmTER
|
||||
{
|
||||
TER ter;
|
||||
std::optional<int64_t> cost;
|
||||
};
|
||||
|
||||
class FieldLocator
|
||||
{
|
||||
int32_t const* ptr_ = nullptr;
|
||||
uint32_t size_ = 0;
|
||||
std::vector<int32_t> buf_;
|
||||
|
||||
public:
|
||||
FieldLocator(std::vector<int32_t>&& buf)
|
||||
: ptr_(&buf[0]), size_(buf.size()), buf_(std::move(buf))
|
||||
{
|
||||
}
|
||||
|
||||
FieldLocator(int32_t const* ptr, uint32_t const size) : ptr_(ptr), size_(size)
|
||||
{
|
||||
}
|
||||
|
||||
FieldLocator(FieldLocator const&) = delete;
|
||||
FieldLocator&
|
||||
operator=(FieldLocator const&) = delete;
|
||||
FieldLocator(FieldLocator&&) = default;
|
||||
FieldLocator&
|
||||
operator=(FieldLocator&&) = default;
|
||||
|
||||
int32_t
|
||||
operator[](unsigned i) const
|
||||
{
|
||||
if (i >= size_)
|
||||
Throw<std::runtime_error>("index out of bounds");
|
||||
return ptr_[i];
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32_t
|
||||
size() const
|
||||
{
|
||||
return size_;
|
||||
}
|
||||
|
||||
[[nodiscard]] int32_t const*
|
||||
data() const
|
||||
{
|
||||
return ptr_;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
empty() const
|
||||
{
|
||||
return size_ == 0;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T, size_t Size = sizeof(T)>
|
||||
constexpr T
|
||||
adjustWasmEndianessHlp(T x)
|
||||
{
|
||||
static_assert(std::is_integral_v<T>, "Only integral types");
|
||||
if constexpr (Size > 1)
|
||||
{
|
||||
using U = std::make_unsigned_t<T>;
|
||||
U u = static_cast<U>(x);
|
||||
U const low = (u & 0xFF) << ((Size - 1) << 3);
|
||||
u = adjustWasmEndianessHlp<U, Size - 1>(u >> 8);
|
||||
return static_cast<T>(low | u);
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
template <typename T, size_t Size = sizeof(T)>
|
||||
constexpr T
|
||||
adjustWasmEndianess(T x)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
static_assert(std::is_integral_v<T>, "Only integral types");
|
||||
if constexpr (std::endian::native == std::endian::big)
|
||||
{
|
||||
return adjustWasmEndianessHlp(x);
|
||||
}
|
||||
return x;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
constexpr int32_t
|
||||
hfErrorToInt(HostFunctionError e)
|
||||
{
|
||||
return static_cast<int32_t>(e);
|
||||
}
|
||||
|
||||
template <class Body>
|
||||
std::invoke_result_t<Body>
|
||||
guarded(
|
||||
beast::Journal journal,
|
||||
std::invoke_result_t<Body> onThrow,
|
||||
Body&& body,
|
||||
std::source_location const location = std::source_location::current()) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
return body();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(journal.error()) << "wasm: " << location.function_name() << " threw: " << e.what();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
JLOG(journal.error()) << "wasm: " << location.function_name() << " threw";
|
||||
}
|
||||
|
||||
return onThrow;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
50
include/xrpl/tx/wasm/WasmVM.h
Normal file
50
include/xrpl/tx/wasm/WasmVM.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/tx/wasm/HostFunc.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// The export a programmable escrow's contract is run through.
|
||||
std::string_view inline constexpr escrowFunctionName = "escrow_finish";
|
||||
|
||||
// Run `wasmCode`'s `funcName` export with `gasLimit` gas, servicing its host calls
|
||||
// through `hfs`.
|
||||
//
|
||||
// On success the result is what the contract returned - positive means the escrow may
|
||||
// finish - together with the gas it consumed. On failure it is the TER to apply and,
|
||||
// when the number means anything, the gas to write to transaction metadata: a contract
|
||||
// that traps or exhausts its budget is charged for what it burned, while a `tecINTERNAL`
|
||||
// reports no cost because the fault is the node's rather than the transaction's.
|
||||
std::expected<EscrowResult, WasmTER>
|
||||
runEscrowWasm(
|
||||
Bytes const& wasmCode,
|
||||
HostFunctions& hfs,
|
||||
std::int64_t gasLimit,
|
||||
std::string_view funcName = escrowFunctionName) noexcept;
|
||||
|
||||
// Screen `wasmCode`: whether `runEscrowWasm` would refuse it before the contract's
|
||||
// first instruction. Compiles the module and reads its imports and exports; runs
|
||||
// nothing.
|
||||
//
|
||||
// Takes no `HostFunctions`, because the verdict comes from the compiled module alone.
|
||||
// That is what makes this callable from a transactor's `preflight`, which has no view
|
||||
// to build a host over.
|
||||
//
|
||||
// `temBAD_WASM` for every fault in the module - the transaction carries something this
|
||||
// engine cannot run, so it is refused before it can reach the ledger.
|
||||
// `telFAILED_PROCESSING` if the engine itself failed: nothing was learned about the
|
||||
// module, and a defect here is not evidence that the transaction is malformed.
|
||||
NotTEC
|
||||
preflightEscrowWasm(
|
||||
Bytes const& wasmCode,
|
||||
beast::Journal j,
|
||||
std::string_view funcName = escrowFunctionName) noexcept;
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,6 +1,8 @@
|
||||
# Linux Packaging
|
||||
|
||||
This directory contains all files needed to build RPM and Debian packages for `xrpld`.
|
||||
This directory contains all files needed to build RPM and Debian packages for
|
||||
`xrpld`. The packages also ship the `validator-keys` tool, so packaging requires
|
||||
a build configured with `-Dvalidator_keys=ON`.
|
||||
|
||||
## Directory layout
|
||||
|
||||
@@ -46,17 +48,28 @@ To print the full packaging matrix (artifact names and images) for the current
|
||||
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
|
||||
`reusable-package.yml`. That workflow generates its own packaging matrix from
|
||||
`package_configs` in `linux.json` (via `generate.py --packaging`) and fans out
|
||||
one job per distro. Each job downloads the pre-built `xrpld` binary artifact and
|
||||
runs in that distro's container, so the package format follows from the
|
||||
container's package manager. The packaging script derives the package version
|
||||
from the downloaded binary's `xrpld --version` output; no CMake configure or
|
||||
build step is needed inside the packaging job.
|
||||
one job per distro. Each job downloads the pre-built `xrpld` and `validator-keys`
|
||||
binary artifacts and runs in that distro's container, so the package format
|
||||
follows from the container's package manager. The packaging script derives the
|
||||
package version from the downloaded binary's `xrpld --version` output; no CMake
|
||||
configure or build step is needed inside the packaging job.
|
||||
|
||||
The binaries come from the `debian` and `rhel` build configurations in
|
||||
`linux.json`'s `configs` section, which pass `-Dvalidator_keys=ON` so that the
|
||||
build job produces `validator-keys` next to `xrpld` and uploads it as the
|
||||
`validator-keys-<config name>` artifact. The packaging entry for a distro names
|
||||
both artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`), so a
|
||||
packaged configuration must keep `-Dvalidator_keys=ON`.
|
||||
|
||||
`validator-keys` is fetched from an exact commit pinned in
|
||||
[`cmake/XrplValidatorKeys.cmake`](../cmake/XrplValidatorKeys.cmake), so a given
|
||||
`xrpld` version always packages the same tool; bump that commit deliberately.
|
||||
|
||||
### Locally (mirrors CI)
|
||||
|
||||
With an `xrpld` binary already built at `build/xrpld`, run the packaging step
|
||||
inside the same container CI uses. The image tag is derived from `linux.json`
|
||||
so you don't need to hardcode a SHA.
|
||||
With `xrpld` and `validator-keys` binaries already built at `build/xrpld` and
|
||||
`build/validator-keys`, run the packaging step inside the same container CI uses.
|
||||
The image tag is derived from `linux.json` so you don't need to hardcode a SHA.
|
||||
|
||||
```bash
|
||||
# From the repo root. Each distro's container image is the `image` field of its
|
||||
@@ -87,6 +100,7 @@ needed, but the host toolchain replaces the pinned CI image:
|
||||
```bash
|
||||
cmake \
|
||||
-Dxrpld=ON \
|
||||
-Dvalidator_keys=ON \
|
||||
-Dpkg_release=1 \
|
||||
-Dtests=OFF \
|
||||
..
|
||||
@@ -95,9 +109,11 @@ cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL
|
||||
```
|
||||
|
||||
The `cmake/XrplPackaging.cmake` module defines the `package` target only if at
|
||||
least one of `rpmbuild` / `dpkg-buildpackage` is present; `build_pkg.sh` then
|
||||
infers the package format from the host's package manager. The packaging script
|
||||
installs to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of
|
||||
least one of `rpmbuild` / `dpkg-buildpackage` is present and both the `xrpld` and
|
||||
`validator-keys` targets exist (`-Dxrpld=ON -Dvalidator_keys=ON`); the target
|
||||
builds both binaries before packaging. `build_pkg.sh` then infers the package
|
||||
format from the host's package manager. The packaging script installs to
|
||||
FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of
|
||||
`CMAKE_INSTALL_PREFIX`.
|
||||
|
||||
The package version is not a CMake input on this path: `build_pkg.sh` derives it
|
||||
@@ -156,13 +172,17 @@ CMake/CI integration. The CI workflow and the CMake `package` target both invoke
|
||||
and lets the script use defaults for the rest.
|
||||
|
||||
It resolves `SRC_DIR` and `BUILD_DIR` to absolute paths, then calls
|
||||
`stage_common()` to copy the binary, config files, and shared support files
|
||||
into the staging area, and invokes the platform build tool.
|
||||
`stage_common()` to copy the `xrpld` and `validator-keys` binaries, config files,
|
||||
and shared support files into the staging area, and invokes the platform build
|
||||
tool. Both binaries must be present in `BUILD_DIR` and must run in the packaging
|
||||
environment; a missing or non-runnable one fails early. That runtime check is
|
||||
what catches a binary still linked against the Nix store's ELF loader (see
|
||||
`patch_nix_binary` in `cmake/PatchNixBinary.cmake`).
|
||||
|
||||
### RPM
|
||||
|
||||
1. Creates the standard `rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}` tree inside the build directory.
|
||||
2. Copies `xrpld.spec` and all shared source files (binary, configs, service files) into `SOURCES/`.
|
||||
2. Copies `xrpld.spec` and all shared source files (binaries, configs, service files) into `SOURCES/`.
|
||||
3. Runs `rpmbuild -bb`, passing the normalized package metadata version as the
|
||||
`pkg_version` RPM macro and `PKG_RELEASE` as the `pkg_release` RPM macro.
|
||||
The spec uses manual `install` commands to place files, disables `dwz`, and
|
||||
@@ -182,7 +202,8 @@ service restart.
|
||||
### DEB
|
||||
|
||||
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
|
||||
2. Stages the binary, configs, `README.md`, and `LICENSE.md`.
|
||||
2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and
|
||||
`validator-keys-LICENSE`.
|
||||
3. Copies `package/debian/` control files into `debbuild/source/debian/`.
|
||||
4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically.
|
||||
5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user