mirror of
https://github.com/Xahau/xahaud.git
synced 2026-08-26 01:30:52 +00:00
Compare commits
688 Commits
jshookz
...
feature-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eed61594c3 | ||
|
|
ee058c5d63 | ||
|
|
2fe5ca261e | ||
|
|
2badcb15da | ||
|
|
d5a3b466d4 | ||
|
|
f8c3d5bc0d | ||
|
|
7dad88b44d | ||
|
|
1641485e8d | ||
|
|
fb918c3a7d | ||
|
|
dcad746261 | ||
|
|
ae8a413acd | ||
|
|
d125ac958a | ||
|
|
4e313098a6 | ||
|
|
9dcaebd8e5 | ||
|
|
1670645482 | ||
|
|
fd144943b2 | ||
|
|
7241f6195d | ||
|
|
b88d769535 | ||
|
|
a03aa1145d | ||
|
|
0a6f9e7983 | ||
|
|
e70dc740b7 | ||
|
|
263fbe222a | ||
|
|
286c836872 | ||
|
|
2c93c96245 | ||
|
|
f69520a426 | ||
|
|
c94d3f3473 | ||
|
|
fa763268e2 | ||
|
|
8ef38062af | ||
|
|
516fda4536 | ||
|
|
8f74614857 | ||
|
|
a6bcde3044 | ||
|
|
1be657098f | ||
|
|
fd6c6e87da | ||
|
|
a0875f518b | ||
|
|
3a4f86c5e8 | ||
|
|
0560c88e1a | ||
|
|
9f1614a234 | ||
|
|
5e27d411d1 | ||
|
|
1ff3ad46c3 | ||
|
|
8180336145 | ||
|
|
d52c6bac6f | ||
|
|
53500b1c8e | ||
|
|
b0e7affa73 | ||
|
|
f40041d018 | ||
|
|
4eee7c46cb | ||
|
|
e5d62430fb | ||
|
|
7917d437a3 | ||
|
|
2577c1a017 | ||
|
|
99a27974b7 | ||
|
|
f0e36c0ba4 | ||
|
|
4b229bda82 | ||
|
|
3e9da7623c | ||
|
|
4acc0dedd8 | ||
|
|
2a671fc3a7 | ||
|
|
6706d642f8 | ||
|
|
28cea3f21b | ||
|
|
59697f5ae7 | ||
|
|
e7330acbbe | ||
|
|
6b97686dde | ||
|
|
f5d9824e4a | ||
|
|
fdbba707e9 | ||
|
|
a31140a4b6 | ||
|
|
508a95f85e | ||
|
|
e6b8a55810 | ||
|
|
b0da027f7a | ||
|
|
94d8ba48af | ||
|
|
e18acab7af | ||
|
|
1ea862a6f3 | ||
|
|
25a60b33cd | ||
|
|
4f9d58eb46 | ||
|
|
c021365516 | ||
|
|
8806ee94d0 | ||
|
|
ec5d22f557 | ||
|
|
bf6cf9c84a | ||
|
|
142fde5acd | ||
|
|
66214c8ab3 | ||
|
|
2fdce2a7c4 | ||
|
|
80ca259233 | ||
|
|
b20d135388 | ||
|
|
9d44f6fb0c | ||
|
|
0eda858e09 | ||
|
|
1c2b726769 | ||
|
|
efb4e2e3e4 | ||
|
|
d9cafebff3 | ||
|
|
ffd50fc180 | ||
|
|
81b46570b8 | ||
|
|
fe039ef8fa | ||
|
|
389732038d | ||
|
|
7971c76943 | ||
|
|
45a7b17ef0 | ||
|
|
f02eaeabe0 | ||
|
|
c9d9b1ea39 | ||
|
|
eca5e299b2 | ||
|
|
dd07422823 | ||
|
|
111f73dae6 | ||
|
|
5cbedf6a70 | ||
|
|
a5bab40247 | ||
|
|
9f55a61a48 | ||
|
|
da9e6b3cc7 | ||
|
|
9286edc57f | ||
|
|
18569dd931 | ||
|
|
e7fcd7729a | ||
|
|
dc4d230eaa | ||
|
|
32bf843c2b | ||
|
|
1a8680efe4 | ||
|
|
0fd2496e8c | ||
|
|
82214b3117 | ||
|
|
f25fd80d72 | ||
|
|
6d6f36ab7a | ||
|
|
0c79ac2631 | ||
|
|
1c5949075d | ||
|
|
4bffbc0f78 | ||
|
|
572365baf5 | ||
|
|
018719fdaa | ||
|
|
b6b77e2eb3 | ||
|
|
9febb4957e | ||
|
|
691cec51bf | ||
|
|
130d4bc9fc | ||
|
|
f3c3fdbe8c | ||
|
|
210b77dc4a | ||
|
|
69b9866f4b | ||
|
|
dadf6c99b4 | ||
|
|
43a8be7429 | ||
|
|
4c2860a9e1 | ||
|
|
e6facf0d7c | ||
|
|
10bbfc790b | ||
|
|
79a07106f8 | ||
|
|
b827a4752f | ||
|
|
4c3e313e3a | ||
|
|
47b65260cb | ||
|
|
060c015cc5 | ||
|
|
eed809fcc0 | ||
|
|
8bd0d42bf5 | ||
|
|
5c01906a4c | ||
|
|
2da84ef6f7 | ||
|
|
0df7ba6daf | ||
|
|
aa819730e0 | ||
|
|
632a7fb520 | ||
|
|
6cfac96cc9 | ||
|
|
5a8e5d99ac | ||
|
|
37d9eaaa2b | ||
|
|
5657f75d3c | ||
|
|
97385c0f83 | ||
|
|
c2c1795dbd | ||
|
|
f8a8d8bfc0 | ||
|
|
62050233a5 | ||
|
|
0536e2e9eb | ||
|
|
f3897836f3 | ||
|
|
cdbac5e7f8 | ||
|
|
5f8145eadc | ||
|
|
f6e03d4529 | ||
|
|
95c86b29fe | ||
|
|
e6237b34ab | ||
|
|
4a2ca38b7f | ||
|
|
c5edcdbbcd | ||
|
|
2e9a56bf27 | ||
|
|
2a3ceea6b0 | ||
|
|
9d44673673 | ||
|
|
fa71bb62c2 | ||
|
|
a3084b2843 | ||
|
|
22d89fb897 | ||
|
|
e1926ba777 | ||
|
|
b8fad6acbc | ||
|
|
ae43022a5d | ||
|
|
de86c4342b | ||
|
|
78fe429f41 | ||
|
|
203eed213a | ||
|
|
9be37877b7 | ||
|
|
ee430c9438 | ||
|
|
f13868a56f | ||
|
|
c4841195e2 | ||
|
|
23d5a15111 | ||
|
|
d874a3725c | ||
|
|
f0dba68abe | ||
|
|
96c3727986 | ||
|
|
53b29c34f8 | ||
|
|
558016442d | ||
|
|
946f521586 | ||
|
|
2e35da2c1f | ||
|
|
86501e2916 | ||
|
|
4f3c0affb4 | ||
|
|
6e9a62f6f6 | ||
|
|
387a56819d | ||
|
|
726dde697e | ||
|
|
1287cce0f7 | ||
|
|
25073d6cb1 | ||
|
|
92c7839822 | ||
|
|
ab30826c5c | ||
|
|
dae0d64b3e | ||
|
|
0cdd7164f4 | ||
|
|
84d738ce8a | ||
|
|
5e2cbf62fe | ||
|
|
75d08560d4 | ||
|
|
b942a29fd8 | ||
|
|
3cb0583d0c | ||
|
|
56bf9f2eed | ||
|
|
3a865b5c0a | ||
|
|
4e7996a86a | ||
|
|
c558478337 | ||
|
|
7f6a29770c | ||
|
|
7161937720 | ||
|
|
f6e37bcc53 | ||
|
|
4dd3882b30 | ||
|
|
c7e5f54761 | ||
|
|
990e15a468 | ||
|
|
c03a92c5d2 | ||
|
|
9bd964b7ff | ||
|
|
96f84a1c15 | ||
|
|
effead7b0d | ||
|
|
dd678ee6ce | ||
|
|
3eaf7b9e3d | ||
|
|
02313caa00 | ||
|
|
7c5ddb06d8 | ||
|
|
e8d40472e9 | ||
|
|
ac659faec7 | ||
|
|
986646ec90 | ||
|
|
e3e1d847a4 | ||
|
|
fa876927a9 | ||
|
|
9a99c2d400 | ||
|
|
e001aca8ae | ||
|
|
777743792e | ||
|
|
3af3b33c07 | ||
|
|
030206d632 | ||
|
|
9324bd59e5 | ||
|
|
3c01901119 | ||
|
|
e8fa4e1154 | ||
|
|
d4650441fb | ||
|
|
67c2c44263 | ||
|
|
ee6c4ff31f | ||
|
|
63f99ad9d8 | ||
|
|
be649d5112 | ||
|
|
8c48e500e7 | ||
|
|
5195fc64b3 | ||
|
|
f849b70a96 | ||
|
|
8ad82f328d | ||
|
|
618314afb7 | ||
|
|
0668caacf2 | ||
|
|
583c4e2461 | ||
|
|
2fe984297a | ||
|
|
d6de1d90b5 | ||
|
|
3eda8d664e | ||
|
|
a0866d1559 | ||
|
|
4a12f4c231 | ||
|
|
a93b8cfdc4 | ||
|
|
c2bc4fabe8 | ||
|
|
94ffc86bd9 | ||
|
|
1d158a910b | ||
|
|
62dc85df86 | ||
|
|
d7a8467705 | ||
|
|
8cc4574e0f | ||
|
|
cf3a9ef96b | ||
|
|
806aa56aec | ||
|
|
cb1f932236 | ||
|
|
e01c0cba70 | ||
|
|
5e27c2fea9 | ||
|
|
19ec9ece35 | ||
|
|
a714eaa621 | ||
|
|
f0f998d897 | ||
|
|
1dbda8f6b3 | ||
|
|
5149f5ae9b | ||
|
|
46632be87b | ||
|
|
8bd79c4759 | ||
|
|
6661e7a40a | ||
|
|
46207a3fca | ||
|
|
a16e8886c2 | ||
|
|
268db0b6d2 | ||
|
|
73f3b7a167 | ||
|
|
1b4535cee5 | ||
|
|
bb6242213b | ||
|
|
0aa8e3ca67 | ||
|
|
516edb8147 | ||
|
|
1186a3a7c4 | ||
|
|
9d9329ece4 | ||
|
|
465131bb3e | ||
|
|
1b0c633eda | ||
|
|
ceabb893bf | ||
|
|
d5ca581cea | ||
|
|
8a81588b71 | ||
|
|
0a9023c334 | ||
|
|
c38567792e | ||
|
|
3cfc79ea59 | ||
|
|
eb3a3b888f | ||
|
|
c56cb34d6d | ||
|
|
306566ccb6 | ||
|
|
8ddbe0891a | ||
|
|
deb1b2af0b | ||
|
|
4dcb3f3557 | ||
|
|
efcd2a0ab7 | ||
|
|
8ddb0a1134 | ||
|
|
891077daa7 | ||
|
|
f0224cac1e | ||
|
|
845798b072 | ||
|
|
9f04dcf582 | ||
|
|
8a8a0b434d | ||
|
|
2fbf2e11bc | ||
|
|
bac359fd96 | ||
|
|
fcedd01761 | ||
|
|
9f24cd0b75 | ||
|
|
d6e7cf428b | ||
|
|
72f73ccbad | ||
|
|
a78f09a0fe | ||
|
|
f3526952b6 | ||
|
|
da31668a2a | ||
|
|
5dc214ab48 | ||
|
|
4e217fcc8f | ||
|
|
d5608c3110 | ||
|
|
7dddc8256a | ||
|
|
45a69538cb | ||
|
|
e8cbf0f968 | ||
|
|
1cdf850e4a | ||
|
|
cddcb83047 | ||
|
|
ed3c0f07a7 | ||
|
|
c74295e51d | ||
|
|
370e02ee1f | ||
|
|
38672fba9f | ||
|
|
932ccbe0e5 | ||
|
|
927ec66a9e | ||
|
|
3c108d3a87 | ||
|
|
07030e2b87 | ||
|
|
8373621445 | ||
|
|
b4208459cf | ||
|
|
d091ebfb7e | ||
|
|
229eb5b573 | ||
|
|
938d3055ce | ||
|
|
aee3a638ee | ||
|
|
4fb91ea9f5 | ||
|
|
acb492a2c9 | ||
|
|
92fe444323 | ||
|
|
9549901014 | ||
|
|
9f6e7dd315 | ||
|
|
f6d986bdbc | ||
|
|
e55bf43986 | ||
|
|
b4beb92c34 | ||
|
|
40edbfc7f2 | ||
|
|
ff205b1b81 | ||
|
|
ddfb1dbeb6 | ||
|
|
8bf1ece0a0 | ||
|
|
639e153f3b | ||
|
|
8e542c32e0 | ||
|
|
38f4d53ebf | ||
|
|
fe66a11c69 | ||
|
|
77e48d553c | ||
|
|
439031dc92 | ||
|
|
1a5b934881 | ||
|
|
7f0d2959e8 | ||
|
|
1440d1495f | ||
|
|
57f5a9d6cc | ||
|
|
cacd1f71fe | ||
|
|
93a6f0fbec | ||
|
|
c5b1cb222d | ||
|
|
e447f9f021 | ||
|
|
0757094ed2 | ||
|
|
4b8107d57c | ||
|
|
d4b9e2f22c | ||
|
|
3fc199017e | ||
|
|
4d55443976 | ||
|
|
0d1d649867 | ||
|
|
60469dbd86 | ||
|
|
3970912735 | ||
|
|
456f4144ba | ||
|
|
570cad4c44 | ||
|
|
b13868b71e | ||
|
|
288b9e6d25 | ||
|
|
e55c2c6dc8 | ||
|
|
35e981e509 | ||
|
|
b9a733c831 | ||
|
|
301e546aa9 | ||
|
|
e2da1db6d2 | ||
|
|
16cd02156e | ||
|
|
fb9e2710cc | ||
|
|
c8fad50d66 | ||
|
|
d9b5fc26fc | ||
|
|
72d03620f9 | ||
|
|
8368e12ab3 | ||
|
|
23f745bd01 | ||
|
|
27ae39b91a | ||
|
|
20d52d8b66 | ||
|
|
b070785dee | ||
|
|
def617e3f9 | ||
|
|
c322b59961 | ||
|
|
c3cc3513c9 | ||
|
|
a0be935227 | ||
|
|
6b1b18dd38 | ||
|
|
946f25249b | ||
|
|
a95306142e | ||
|
|
8d43154061 | ||
|
|
156a8cbb85 | ||
|
|
14ebe74a56 | ||
|
|
580f07ce35 | ||
|
|
d4c0ba3769 | ||
|
|
378d6b78c8 | ||
|
|
4b219cdef8 | ||
|
|
9d4b97c824 | ||
|
|
d6481a3869 | ||
|
|
0cf6f73441 | ||
|
|
08a6f3cd57 | ||
|
|
77d78236e8 | ||
|
|
c92c0656ec | ||
|
|
fbdec3be66 | ||
|
|
bb619cc100 | ||
|
|
10f22c84f2 | ||
|
|
dd21024c0e | ||
|
|
16a72172b4 | ||
|
|
265012e16a | ||
|
|
d072527bc5 | ||
|
|
5f5ce12fa6 | ||
|
|
0804a01b9b | ||
|
|
80cd1bed34 | ||
|
|
b4e98ac1d7 | ||
|
|
4f7e751fbd | ||
|
|
748fef6267 | ||
|
|
4d48c9f949 | ||
|
|
03c1216661 | ||
|
|
537474cb5f | ||
|
|
ec086d6765 | ||
|
|
7362c1dac1 | ||
|
|
f0550ca625 | ||
|
|
526b60bf3d | ||
|
|
9347b47639 | ||
|
|
9988568a08 | ||
|
|
13260b9ef7 | ||
|
|
65dab780de | ||
|
|
ee70e4cdbb | ||
|
|
7fb1509673 | ||
|
|
64620e2825 | ||
|
|
a87a7896ca | ||
|
|
443aca8611 | ||
|
|
804b76b4ab | ||
|
|
ab6571a20f | ||
|
|
331e1606a3 | ||
|
|
24d6dea1a2 | ||
|
|
03e0bb5fc3 | ||
|
|
0a77dbf68e | ||
|
|
60a9a2c9fb | ||
|
|
445d0070d8 | ||
|
|
61a8d8bba7 | ||
|
|
fbedb8a73a | ||
|
|
8ae541fcc1 | ||
|
|
c8f3f6f05f | ||
|
|
b12cee5d47 | ||
|
|
a3b1e45f4d | ||
|
|
3938ba7af4 | ||
|
|
96b1104646 | ||
|
|
92bdd2ed9f | ||
|
|
d87cfdc604 | ||
|
|
a956abb2d1 | ||
|
|
aa36a80ab7 | ||
|
|
e729aa11eb | ||
|
|
c58da3da58 | ||
|
|
0c2c59d258 | ||
|
|
15662eb1b1 | ||
|
|
492fe90643 | ||
|
|
ea413873b2 | ||
|
|
625419eab7 | ||
|
|
2218bdd7f3 | ||
|
|
f13233b00a | ||
|
|
a61f334ca2 | ||
|
|
53a119ce30 | ||
|
|
63d1197345 | ||
|
|
aafd5b940b | ||
|
|
efc497cf23 | ||
|
|
f4e78c9a24 | ||
|
|
7b5865c69c | ||
|
|
9f1ad521e1 | ||
|
|
26bbef8efd | ||
|
|
6e71f84867 | ||
|
|
ab9b48f67a | ||
|
|
cd00ed72d8 | ||
|
|
05a3e04f2d | ||
|
|
66f7294120 | ||
|
|
7f6ac75617 | ||
|
|
4150f0383c | ||
|
|
25123b370a | ||
|
|
f90ed41802 | ||
|
|
8c4c158d3a | ||
|
|
2d2951875d | ||
|
|
9bfca63574 | ||
|
|
1ba444ae7f | ||
|
|
f96d9b6e51 | ||
|
|
04077c1a55 | ||
|
|
d94079d762 | ||
|
|
92ec07a1be | ||
|
|
664db62588 | ||
|
|
03a436d918 | ||
|
|
7474048295 | ||
|
|
1ee660529e | ||
|
|
311dfa1c23 | ||
|
|
f27cd2c567 | ||
|
|
f34fdc297c | ||
|
|
65fa63883d | ||
|
|
d8c683fb4c | ||
|
|
fd53af304b | ||
|
|
2a3f0ec923 | ||
|
|
00f1f7ba30 | ||
|
|
49f05e4e47 | ||
|
|
1f51b9c594 | ||
|
|
88a548a8ef | ||
|
|
db302a0f78 | ||
|
|
383d9ec2e7 | ||
|
|
52671bfc99 | ||
|
|
8307fca3b9 | ||
|
|
6526621c16 | ||
|
|
2a9b1c9c22 | ||
|
|
54ca21b604 | ||
|
|
462db6004c | ||
|
|
cfca708aae | ||
|
|
5f70e5259c | ||
|
|
8697c5d821 | ||
|
|
9436e5868e | ||
|
|
c6fa973cf6 | ||
|
|
939e03714c | ||
|
|
969f98f57e | ||
|
|
435deb0e78 | ||
|
|
b80352e512 | ||
|
|
57c46c61fc | ||
|
|
37ff13df50 | ||
|
|
1b363b7eac | ||
|
|
9562b457cf | ||
|
|
724633ceb5 | ||
|
|
152d82e798 | ||
|
|
0bb31ce7ce | ||
|
|
4cb3de0497 | ||
|
|
c6b315412d | ||
|
|
72395bec75 | ||
|
|
8ed4d86f0f | ||
|
|
419fd16b9a | ||
|
|
a8097cd9a6 | ||
|
|
02a0552325 | ||
|
|
3698193b0a | ||
|
|
de43ca2385 | ||
|
|
8c747a1916 | ||
|
|
cea110f29a | ||
|
|
3ca056a94b | ||
|
|
705d8400db | ||
|
|
655b751698 | ||
|
|
f324081277 | ||
|
|
24a284180a | ||
|
|
6f003cc983 | ||
|
|
3a58020388 | ||
|
|
829441b52e | ||
|
|
3a055663cc | ||
|
|
985a194bdc | ||
|
|
869f366d8a | ||
|
|
03936aa928 | ||
|
|
6d180307ad | ||
|
|
f2ca499c97 | ||
|
|
bd68364f25 | ||
|
|
42a6407815 | ||
|
|
a387c853ab | ||
|
|
9311e567d3 | ||
|
|
c26582bdf9 | ||
|
|
417b999c7f | ||
|
|
0205be4500 | ||
|
|
89274b5387 | ||
|
|
b65d9faf12 | ||
|
|
aa1a7e5320 | ||
|
|
6f0f17aad9 | ||
|
|
407bfa1467 | ||
|
|
f0dfcf6b81 | ||
|
|
503d2ebf98 | ||
|
|
e52bc51384 | ||
|
|
91860db578 | ||
|
|
0b317a8e7a | ||
|
|
dbd230b695 | ||
|
|
30cefcba85 | ||
|
|
94edb5759d | ||
|
|
ce57b6a3a0 | ||
|
|
fca5cad470 | ||
|
|
bb77c2090b | ||
|
|
90a94294e4 | ||
|
|
c2209b4472 | ||
|
|
8fcb2ed336 | ||
|
|
fd1567d1ba | ||
|
|
d32f34d3bf | ||
|
|
c491c5c82f | ||
|
|
74817765ae | ||
|
|
fc23fa8535 | ||
|
|
34c0f17b6b | ||
|
|
765ad6a278 | ||
|
|
f623ca89b9 | ||
|
|
e4865f09f9 | ||
|
|
4c182e4738 | ||
|
|
d0c869c8a6 | ||
|
|
cac5efcd3c | ||
|
|
514e60b71c | ||
|
|
2a34e32e05 | ||
|
|
b969024a25 | ||
|
|
f30b9a4c3a | ||
|
|
0e019fec4e | ||
|
|
7e0c72fd22 | ||
|
|
07d741cdd7 | ||
|
|
b99c38c09d | ||
|
|
64e50209ff | ||
|
|
b1ce2103ad | ||
|
|
50c4cf1df3 | ||
|
|
6fc14f398d | ||
|
|
592a8600c7 | ||
|
|
e71768700a | ||
|
|
e598e405bd | ||
|
|
8af3ce2f5b | ||
|
|
b67cb78b97 | ||
|
|
8cfee6c8a3 | ||
|
|
8673599d2b | ||
|
|
0b1b82282e | ||
|
|
d4c5a7e8ab | ||
|
|
82837864fa | ||
|
|
e1caee6459 | ||
|
|
3206b4a4e1 | ||
|
|
ec65e622aa | ||
|
|
65837f49e1 | ||
|
|
0c2e09050e | ||
|
|
83922d5c20 | ||
|
|
6bae42ff01 | ||
|
|
35e86d926e | ||
|
|
9c4ee9315d | ||
|
|
0f17cf02aa | ||
|
|
7753dc3cbe | ||
|
|
cc7f3c59ae | ||
|
|
e5b21f026e | ||
|
|
e8c1b25ab4 | ||
|
|
b9dd854595 | ||
|
|
3bead8dcb6 | ||
|
|
908a78a1d9 | ||
|
|
a9e3dc41d4 | ||
|
|
02990eb4ee | ||
|
|
ce76632322 | ||
|
|
9eac54d690 | ||
|
|
24e4ac16ad | ||
|
|
94ce15d233 | ||
|
|
8f331a538e | ||
|
|
7425ab0a39 | ||
|
|
c5292bfe0d | ||
|
|
79b2f9f410 | ||
|
|
e8358a82b1 | ||
|
|
d850e740e1 | ||
|
|
61a166bcb0 | ||
|
|
41a41ec625 | ||
|
|
bc98c589b7 | ||
|
|
4f009e4698 | ||
|
|
b6811a6f59 | ||
|
|
ae88fd3d24 | ||
|
|
db3ed0c2eb | ||
|
|
960808b172 | ||
|
|
a9dffd38ff | ||
|
|
382e6fa673 | ||
|
|
2905b0509c | ||
|
|
4911c1bf52 | ||
|
|
1744d21410 | ||
|
|
34ff53f65d | ||
|
|
893f8d5a10 | ||
|
|
3e5389d652 | ||
|
|
c44dea3acf | ||
|
|
a6dd54fa48 | ||
|
|
28bd0a22d3 | ||
|
|
960fffcf82 | ||
|
|
e7867c07a1 | ||
|
|
a828e8a44d | ||
|
|
bb33e7cf64 | ||
|
|
7e8e0654cd | ||
|
|
38af0626e0 | ||
|
|
8500e86f57 | ||
|
|
1fc4fd9bfd | ||
|
|
e4875e5398 | ||
|
|
5b1b142be0 | ||
|
|
5ba832204a | ||
|
|
1257b3a65c | ||
|
|
6013ed2cb6 | ||
|
|
034010716e | ||
|
|
b28793b0fa | ||
|
|
4bce392c31 | ||
|
|
244a28b981 | ||
|
|
f2838351c9 | ||
|
|
dae082d6a5 | ||
|
|
619a4a68f7 | ||
|
|
4a6db8bb05 | ||
|
|
c86479bc58 | ||
|
|
dc6a2dc6ff | ||
|
|
c01b9a657b | ||
|
|
652b181b5d | ||
|
|
8329d78f32 | ||
|
|
bf4579c1d1 | ||
|
|
73e099eb23 | ||
|
|
2e311b4259 | ||
|
|
7c8e940091 | ||
|
|
9b90c50789 | ||
|
|
a18e2cb2c6 | ||
|
|
be5f425122 | ||
|
|
fc6f4762da |
@@ -0,0 +1,40 @@
|
||||
# Export stream terminal follow-up tests
|
||||
|
||||
## Scope
|
||||
|
||||
Added regression coverage in `src/test/consensus/ConsensusExtensions_test.cpp`
|
||||
for the LOW test-gap finding in `synthesis.md`:
|
||||
|
||||
- A tracked pending-latch origin that is absent from the next validated ledger
|
||||
emits one empty `terminal: true` replacement snapshot. The test verifies the
|
||||
retained owner/origin identity, validated-ledger cursor, empty share set,
|
||||
internal retirement, and suppression of a duplicate terminal snapshot.
|
||||
- A validated-ledger callback queued behind `exportStreamMutex_` is fenced by
|
||||
`stopExportShareService()`. The test verifies that stop cannot return while
|
||||
the stream lock is held and that the queued callback emits no snapshot after
|
||||
the service flag is cleared.
|
||||
|
||||
No production files were changed. Deterministic accepted-edge/terminal lock
|
||||
winner coverage was not added because there is no test seam between collector
|
||||
admission and stream-lock acquisition; adding one would require production
|
||||
instrumentation, while timing-only assertions would be flaky.
|
||||
|
||||
## Verification
|
||||
|
||||
- `x-format-changed`: passed.
|
||||
- `x-quick-check`: passed for `ConsensusExtensions_test.cpp`.
|
||||
- Narrow object build: passed for
|
||||
`CMakeFiles/rippled.dir/src/test/consensus/ConsensusExtensions_test.cpp.o`.
|
||||
- `/tmp/rippled-export-stream-tests --unittest=ripple.consensus.ConsensusExtensions`:
|
||||
passed, 1 suite, 66 cases, 9,628 assertions, 0 failures.
|
||||
|
||||
The normal incremental `rippled` target was stopped after Ninja scheduled 321
|
||||
steps from a stale build tree. A temporary binary was linked from existing
|
||||
objects plus the newly compiled test object to execute the narrow suite without
|
||||
a broad rebuild.
|
||||
|
||||
## Repository state
|
||||
|
||||
Work began at requested HEAD `018719fda`. A concurrent commit advanced the
|
||||
shared branch to `572365baf` (`fix(export): enforce global live latch cap`)
|
||||
before this follow-up was committed; that unrelated change was preserved.
|
||||
@@ -1,8 +1,5 @@
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
# This integration branch uploads the normal suite and JSHookz E2E coverage.
|
||||
notify:
|
||||
after_n_builds: 2
|
||||
|
||||
comment:
|
||||
behavior: default
|
||||
|
||||
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -11,9 +11,3 @@ LICENSE binary
|
||||
*.vcxproj text eol=crlf
|
||||
*.props text eol=crlf
|
||||
*.filters text eol=crlf
|
||||
|
||||
# JSHookz projections checked for freshness by the integration workflow.
|
||||
external/quickjs-provider/jshookz_provider.manifest.cmake linguist-generated=true
|
||||
external/quickjs-provider/jshookz_provider.manifest.json linguist-generated=true
|
||||
external/quickjs-provider/jshookz_provider.native-abi.json linguist-generated=true
|
||||
src/test/app/JSHooks_test_hooks.h linguist-generated=true
|
||||
|
||||
@@ -145,9 +145,6 @@ runs:
|
||||
# Export wasmedge if not already exported
|
||||
conan list wasmedge/0.11.2@xahaud/stable 2>/dev/null | (grep -q "not found" && exit 1 || exit 0) || \
|
||||
conan export external/wasmedge --version 0.11.2 --user xahaud --channel stable
|
||||
|
||||
# Keep the cached recipe in sync with this checkout; package binaries remain cached.
|
||||
conan export external/wasmtime --version 47.0.3 --user xahaud --channel stable
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
env:
|
||||
@@ -162,6 +159,4 @@ runs:
|
||||
--output-folder . \
|
||||
--build missing \
|
||||
--settings build_type=${{ inputs.configuration }} \
|
||||
--options '&:xrpld=True' \
|
||||
--options '&:tests=True' \
|
||||
..
|
||||
|
||||
1
.github/workflows/build-in-docker.yml
vendored
1
.github/workflows/build-in-docker.yml
vendored
@@ -33,7 +33,6 @@ jobs:
|
||||
fetch-depth: 2 # Only get the last 2 commits, to avoid fetching all history
|
||||
|
||||
build:
|
||||
if: github.head_ref != 'jshookz'
|
||||
runs-on: [self-hosted, xahaud-build]
|
||||
needs: [checkout]
|
||||
defaults:
|
||||
|
||||
5
.github/workflows/check-genesis-hooks.yml
vendored
5
.github/workflows/check-genesis-hooks.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: |
|
||||
# Download install.sh
|
||||
curl -o /tmp/wasienv-install.sh https://raw.githubusercontent.com/wasienv/wasienv/master/install.sh
|
||||
|
||||
|
||||
# Replace /bin to /local/bin
|
||||
sed -i 's|/bin|/local/bin|g' /tmp/wasienv-install.sh
|
||||
|
||||
@@ -82,9 +82,6 @@ jobs:
|
||||
command -v clang-format || (echo "Error: clang-format not found" && exit 1)
|
||||
echo "All tools verified successfully"
|
||||
|
||||
- name: Verify independent Wasmtime host catalogue fixture
|
||||
run: src/test/app/jshooks/raw-catalogue/generate.py --check
|
||||
|
||||
# Execute build script to regenerate xahau.h
|
||||
- name: Run build_xahau_h.sh
|
||||
run: |
|
||||
|
||||
236
.github/workflows/jshookz-e2e.yml
vendored
236
.github/workflows/jshookz-e2e.yml
vendored
@@ -1,236 +0,0 @@
|
||||
# End-to-end proof for the external jshookz toolchain and real Xahau Env.
|
||||
name: JSHookz end to end
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [jshookz]
|
||||
# PRs targeting jshookz/dev currently duplicate the branch-push e2e
|
||||
# (different github.ref, so concurrency does not cancel). Re-enable later.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: jshookz-e2e-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
JSHOOKZ_REF: xahaud-pinned
|
||||
HOOKZ_REF: 846f5f0f80861985dceb41ad6f02ff914f8a0c3d
|
||||
XAHAUD_SCRIPTS_REF: e41a712c68a613d95a8911cbf89711db2d95aa3b
|
||||
WASI_SDK_VERSION: "32"
|
||||
WASI_SDK_SHA256: 55fc523ebfbc98f69d1034fcfcb83d1ff5610cd9ab7eceef6cd097a30ba4ef93
|
||||
BINARYEN_VERSION: "128"
|
||||
BINARYEN_SHA256: 4ce79586d1c4762502eebe9a1db071fa5e446ef8897f2f766eb1cce5ec6dee9e
|
||||
WIZER_VERSION: "10.0.0"
|
||||
WIZER_SHA256: 1e9dfaa2f6c436ca7acfdbb82c166f5b8a0062068f12916a554e82c97ca8d371
|
||||
WASI_SDK_PATH: ${{ github.workspace }}/.tools/wasi-sdk
|
||||
HOOKZ_WASI_SDK: ${{ github.workspace }}/.tools/wasi-sdk
|
||||
BINARYEN_HOME: ${{ github.workspace }}/.tools/binaryen
|
||||
JSHOOKZ_PROVIDER_BUNDLE: ${{ github.workspace }}/.deps/jshookz/build/xahau-provider
|
||||
XAHAU_QJS_PROVIDER_WASM: ${{ github.workspace }}/.deps/jshookz/build/xahau-provider/jshookz_provider.wasm
|
||||
XAHAU_REQUIRE_QJS_PROVIDER_TESTS: "1"
|
||||
CC: gcc-13
|
||||
CXX: g++-13
|
||||
CCACHE_DIR: /github/home/.ccache-cache
|
||||
CMAKE_BUILD_PARALLEL_LEVEL: "2"
|
||||
|
||||
jobs:
|
||||
env-proof:
|
||||
runs-on: [self-hosted, generic, 20.04]
|
||||
timeout-minutes: 120
|
||||
container:
|
||||
image: ubuntu:24.04
|
||||
volumes:
|
||||
- /home/runner/.conan-cache:/.conan-cache
|
||||
- /home/runner/.ccache-cache:/github/home/.ccache-cache
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Install checkout prerequisite
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y git
|
||||
|
||||
- name: Check out Xahau integration
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out public jshookz
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sublimator/jshookz
|
||||
ref: ${{ env.JSHOOKZ_REF }}
|
||||
path: .deps/jshookz
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out public hookz
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sublimator/hookz
|
||||
ref: ${{ env.HOOKZ_REF }}
|
||||
path: .deps/hookz
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out public xahaud-scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sublimator/xahaud-scripts
|
||||
ref: ${{ env.XAHAUD_SCRIPTS_REF }}
|
||||
path: .deps/xahaud-scripts
|
||||
persist-credentials: false
|
||||
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install native and TypeScript tools
|
||||
run: |
|
||||
apt-get install -y \
|
||||
build-essential ccache clang-format-18 cmake curl g++-13 gcc-13 \
|
||||
libsqlite3-dev ninja-build pkg-config xz-utils
|
||||
ln -sf /usr/bin/clang-format-18 /usr/local/bin/clang-format
|
||||
ln -sf /usr/bin/gcov-13 /usr/local/bin/gcov
|
||||
npm install --global typescript@6.0.3
|
||||
uv python install 3.13
|
||||
uv tool install --python 3.13 conan==2.21.0
|
||||
uv tool install --python 3.13 'gcovr>=7,<9'
|
||||
|
||||
- name: Install wasi-sdk and Binaryen
|
||||
run: |
|
||||
mkdir -p .tools
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/wasi-sdk.tar.gz \
|
||||
"https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz"
|
||||
echo "$WASI_SDK_SHA256 /tmp/wasi-sdk.tar.gz" | sha256sum --check
|
||||
tar -xzf /tmp/wasi-sdk.tar.gz -C .tools
|
||||
mv ".tools/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux" "$WASI_SDK_PATH"
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/binaryen.tar.gz \
|
||||
"https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz"
|
||||
echo "$BINARYEN_SHA256 /tmp/binaryen.tar.gz" | sha256sum --check
|
||||
tar -xzf /tmp/binaryen.tar.gz -C .tools
|
||||
mv ".tools/binaryen-version_${BINARYEN_VERSION}" "$BINARYEN_HOME"
|
||||
echo "$BINARYEN_HOME/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Wizer
|
||||
run: |
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/wizer.tar.xz \
|
||||
"https://github.com/bytecodealliance/wizer/releases/download/v${WIZER_VERSION}/wizer-v${WIZER_VERSION}-x86_64-linux.tar.xz"
|
||||
echo "$WIZER_SHA256 /tmp/wizer.tar.xz" | sha256sum --check
|
||||
tar -xJf /tmp/wizer.tar.xz -C .tools
|
||||
echo "$GITHUB_WORKSPACE/.tools/wizer-v${WIZER_VERSION}-x86_64-linux" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install the Hook toolchain
|
||||
run: |
|
||||
if [ -f .deps/jshookz/python/jshookz/pyproject.toml ]; then
|
||||
jshookz_project=.deps/jshookz/python/jshookz
|
||||
else
|
||||
jshookz_project=.deps/jshookz/packages/jshookz
|
||||
fi
|
||||
uv sync --project "$jshookz_project" \
|
||||
--locked --group dev --python 3.13
|
||||
uv tool install --python 3.13 --force --from .deps/hookz hookz
|
||||
uv tool install --python 3.13 --force \
|
||||
--from .deps/xahaud-scripts xahaud-scripts
|
||||
jshookz_cmd="$jshookz_project/.venv/bin/jshookz"
|
||||
echo "JSHOOKZ_CMD=$jshookz_cmd" >> "$GITHUB_ENV"
|
||||
echo "JSHOOKZ_HOOK_COMPILER=$jshookz_cmd compile-hook" >> "$GITHUB_ENV"
|
||||
"$jshookz_cmd" info
|
||||
hookz --version
|
||||
x-run-tests --help >/dev/null
|
||||
|
||||
- name: Build and verify the sealed provider
|
||||
run: |
|
||||
"$JSHOOKZ_CMD" build provider
|
||||
test -s "$XAHAU_QJS_PROVIDER_WASM"
|
||||
cmp external/quickjs-provider/jshookz_provider.manifest.json \
|
||||
"$JSHOOKZ_PROVIDER_BUNDLE/jshookz_provider.manifest.json"
|
||||
cmp external/quickjs-provider/jshookz_provider.manifest.cmake \
|
||||
"$JSHOOKZ_PROVIDER_BUNDLE/jshookz_provider.manifest.cmake"
|
||||
cmp external/quickjs-provider/jshookz_provider.native-abi.json \
|
||||
"$JSHOOKZ_PROVIDER_BUNDLE/jshookz_provider.native-abi.json"
|
||||
cp "$XAHAU_QJS_PROVIDER_WASM" \
|
||||
external/quickjs-provider/jshookz_provider.wasm
|
||||
|
||||
- name: Prove generator-lock mutation reds
|
||||
run: python3 cmake/test_generate_quickjs_provider_bundle.py
|
||||
|
||||
- name: Configure Conan for Xahau
|
||||
run: |
|
||||
mkdir -p \
|
||||
/.conan-cache/conan2 \
|
||||
/.conan-cache/conan2_download \
|
||||
/.conan-cache/conan2_sources
|
||||
mkdir -p "$HOME/.conan2"
|
||||
printf '%s\n' \
|
||||
'core.cache:storage_path=/.conan-cache/conan2' \
|
||||
'core.download:download_cache=/.conan-cache/conan2_download' \
|
||||
'core.sources:download_cache=/.conan-cache/conan2_sources' \
|
||||
> "$HOME/.conan2/global.conf"
|
||||
mkdir -p "$HOME/.conan2/profiles"
|
||||
cat > "$HOME/.conan2/profiles/default" <<'EOF'
|
||||
[settings]
|
||||
arch=x86_64
|
||||
build_type=Debug
|
||||
compiler=gcc
|
||||
compiler.cppstd=20
|
||||
compiler.libcxx=libstdc++11
|
||||
compiler.version=13
|
||||
os=Linux
|
||||
|
||||
[buildenv]
|
||||
CC=/usr/bin/gcc-13
|
||||
CXX=/usr/bin/g++-13
|
||||
|
||||
[conf]
|
||||
tools.build:compiler_executables={"c": "/usr/bin/gcc-13", "cpp": "/usr/bin/g++-13"}
|
||||
|
||||
[options]
|
||||
&:xrpld=True
|
||||
&:tests=True
|
||||
date/*:use_system_tz_db=True
|
||||
EOF
|
||||
conan remote add --force xrplf https://conan.ripplex.io
|
||||
conan profile show
|
||||
conan export external/snappy --version 1.1.10 \
|
||||
--user xahaud --channel stable
|
||||
conan export external/soci --version 4.0.3 \
|
||||
--user xahaud --channel stable
|
||||
conan export external/wasmedge --version 0.11.2 \
|
||||
--user xahaud --channel stable
|
||||
conan export external/wasmtime --version 47.0.3 \
|
||||
--user xahaud --channel stable
|
||||
|
||||
- name: Build Xahau and run the authoritative Env suite
|
||||
run: |
|
||||
x-run-tests \
|
||||
--compile-hooks src/test/app/JSHooks_test.cpp \
|
||||
--build-type Coverage \
|
||||
--build-dir build-jshookz-e2e \
|
||||
--reconfigure-build \
|
||||
--ccache \
|
||||
-j 2 \
|
||||
-- \
|
||||
ripple.app.JSHooks,ripple.app.QuickJSHostPolicy,ripple.app.HookGuestMemory,ripple.app.HookAPI,ripple.app.HookWasmtimeHostCatalogue,ripple.app.HookArtifact
|
||||
|
||||
- name: Upload JSHookz coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: build-jshookz-e2e/coverage/coverage.json
|
||||
fail_ci_if_error: true
|
||||
disable_search: true
|
||||
verbose: true
|
||||
plugins: noop
|
||||
use_oidc: true
|
||||
|
||||
- name: Verify the checked-in fixture header is current
|
||||
run: git diff --exit-code -- src/test/app/JSHooks_test_hooks.h
|
||||
@@ -16,6 +16,8 @@ jobs:
|
||||
generator: ./hook/generate_extern.sh
|
||||
- target: hook/sfcodes.h
|
||||
generator: bash ./hook/generate_sfcodes.sh
|
||||
- target: hook/genesis/headers/sfcodes.h
|
||||
generator: bash ./hook/generate_sfcodes.sh
|
||||
- target: hook/tts.h
|
||||
generator: ./hook/generate_tts.sh
|
||||
- target: hook/ls_flags.h
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -127,5 +127,12 @@ bld.rippled/
|
||||
generated
|
||||
.vscode
|
||||
|
||||
# AI docs (local working documents)
|
||||
.ai-docs/
|
||||
|
||||
# Local formal-methods workspace; kept as a separate repository and optionally
|
||||
# symlinked here for navigation.
|
||||
formal/lean/xahau_consensus
|
||||
|
||||
# Suggested in-tree build directory
|
||||
/.build/
|
||||
|
||||
4
.testnet/.gitignore
vendored
Normal file
4
.testnet/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
output/
|
||||
__pycache__/
|
||||
scenarios/odd-cases/
|
||||
scenarios/suite-experiments.yml
|
||||
29
.testnet/scenarios/entropy/consensus_entropy_crash.py
Normal file
29
.testnet/scenarios/entropy/consensus_entropy_crash.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Scenario: ConsensusEntropy amendment crashes non-supporting node.
|
||||
|
||||
Votes ConsensusEntropy accept on all nodes except n4, then waits for n4
|
||||
to crash as the amendment activates without its support.
|
||||
|
||||
x-testnet run --scenario-script consensus_entropy_crash.py
|
||||
"""
|
||||
|
||||
from helpers import CONSENSUS_ENTROPY_FEATURE
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await ctx.wait_for_ledger_close()
|
||||
ctx.feature(CONSENSUS_ENTROPY_FEATURE, vetoed=False, exclude_nodes=[4])
|
||||
|
||||
log("Waiting for ConsensusEntropy to be voted for...")
|
||||
await ctx.wait_for_feature(
|
||||
CONSENSUS_ENTROPY_FEATURE,
|
||||
check=lambda s: not s.get("vetoed"),
|
||||
exclude_nodes=[4],
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
log("Waiting for n4 to crash...")
|
||||
op = await ctx.wait_for_nodes_down(nodes=[4], timeout=600)
|
||||
|
||||
ctx.assert_log("unsupported amendments activated", since=op.started, nodes=[4])
|
||||
ctx.assert_exit_status(0, nodes=[4])
|
||||
log("PASS: n4 shut down due to unsupported amendment")
|
||||
52
.testnet/scenarios/entropy/entropy_with_transactions.py
Normal file
52
.testnet/scenarios/entropy/entropy_with_transactions.py
Normal file
@@ -0,0 +1,52 @@
|
||||
""":descr: entropy stays valid under transaction load"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from helpers import require_entropy, get_entropy_tx, assert_valid_entropy
|
||||
|
||||
variants = [
|
||||
{"label": "light", "min_txns": 5, "max_txns": 10},
|
||||
{"label": "heavy", "min_txns": 50, "max_txns": 60},
|
||||
{"label": "super_heavy", "min_txns": 90, "max_txns": 120},
|
||||
]
|
||||
|
||||
|
||||
async def scenario(ctx, log, *, min_txns=5, max_txns=10, **_):
|
||||
await require_entropy(ctx, log)
|
||||
|
||||
gen = ctx.txn_generator(min_txns=min_txns, max_txns=max_txns)
|
||||
await gen.start()
|
||||
await gen.wait_until_ready()
|
||||
log(f"Transaction generator ready ({min_txns}-{max_txns} txns/ledger)")
|
||||
|
||||
# Wait for pipeline warmup + a few txn-bearing ledgers.
|
||||
await ctx.wait_for_ledgers(3, node_id=0, timeout=60)
|
||||
|
||||
start_seq = ctx.validated_ledger_index(0)
|
||||
await ctx.wait_for_ledgers(10, node_id=0, timeout=120)
|
||||
end_seq = ctx.validated_ledger_index(0)
|
||||
log(f"Inspecting ledgers {start_seq + 1} → {end_seq}")
|
||||
|
||||
digests = set()
|
||||
total_user_txns = 0
|
||||
|
||||
for seq in range(start_seq + 1, end_seq + 1):
|
||||
ce, user_txns = get_entropy_tx(ctx, seq)
|
||||
digest, count = assert_valid_entropy(ce, seq, seen_digests=digests)
|
||||
total_user_txns += len(user_txns)
|
||||
log(
|
||||
f" Ledger {seq}: EntropyCount={count} "
|
||||
f"user_txns={len(user_txns)} Digest={digest[:16]}..."
|
||||
)
|
||||
|
||||
await gen.stop()
|
||||
|
||||
log(
|
||||
f"Verified {end_seq - start_seq} ledgers: {total_user_txns} user txns, "
|
||||
f"all entropy valid and unique"
|
||||
)
|
||||
|
||||
if total_user_txns == 0:
|
||||
raise AssertionError("No user transactions were included in any ledger")
|
||||
|
||||
log("PASS")
|
||||
28
.testnet/scenarios/entropy/fallback_without_unl_report.py
Normal file
28
.testnet/scenarios/entropy/fallback_without_unl_report.py
Normal file
@@ -0,0 +1,28 @@
|
||||
""":descr: healthy non-standalone testnet without UNLReport mints Tier 1 fallback"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from helpers import require_entropy, get_entropy_tx, assert_consensus_fallback
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_entropy(ctx, log)
|
||||
|
||||
# Non-standalone nodes require a ledger-anchored UNLReport before assigning
|
||||
# validator_quorum / participant_aligned labels. Without it, the RNG pipeline
|
||||
# may still collect commits/reveals, but injection must remain Tier 1.
|
||||
await ctx.wait_for_ledgers(3, node_id=0, timeout=60)
|
||||
log("Pipeline warmed up without UNLReport")
|
||||
|
||||
start_seq = ctx.validated_ledger_index(0)
|
||||
await ctx.wait_for_ledgers(5, node_id=0, timeout=90)
|
||||
end_seq = ctx.validated_ledger_index(0)
|
||||
log(f"Inspecting ledgers {start_seq + 1} -> {end_seq}")
|
||||
|
||||
for seq in range(start_seq + 1, end_seq + 1):
|
||||
ce, _ = get_entropy_tx(ctx, seq)
|
||||
digest, count = assert_consensus_fallback(ce, seq)
|
||||
log(f" Ledger {seq}: EntropyCount={count} Digest={digest[:16]}...")
|
||||
|
||||
log(f"Verified {end_seq - start_seq} ledgers: all consensus_fallback")
|
||||
log("PASS")
|
||||
123
.testnet/scenarios/entropy/hook_entropy_api.py
Normal file
123
.testnet/scenarios/entropy/hook_entropy_api.py
Normal file
@@ -0,0 +1,123 @@
|
||||
""":descr: compile, install, and invoke a Hook using the entropy_cr_* API"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from helpers import require_entropy
|
||||
|
||||
|
||||
ENTROPY_HOOK_C = r"""
|
||||
#include <stdint.h>
|
||||
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t entropy_cr_dice(uint32_t sides, uint32_t min_tier);
|
||||
extern int64_t entropy_cr_random(
|
||||
uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
|
||||
extern int64_t entropy_cr_status(void);
|
||||
|
||||
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter) + 1)
|
||||
#define ENTROPY_TIER(x) (((uint64_t)(x) >> 32U) & 0xFFU)
|
||||
#define ENTROPY_COUNT(x) (((uint64_t)(x) >> 16U) & 0xFFFFU)
|
||||
#define ENTROPY_DENOMINATOR(x) ((uint64_t)(x) & 0xFFFFU)
|
||||
|
||||
int64_t
|
||||
hook(uint32_t reserved)
|
||||
{
|
||||
_g(1, 1);
|
||||
|
||||
int64_t status = entropy_cr_status();
|
||||
if (status < 0)
|
||||
return accept(0, 0, 10);
|
||||
|
||||
uint32_t tier = ENTROPY_TIER(status);
|
||||
uint32_t count = ENTROPY_COUNT(status);
|
||||
uint32_t denominator = ENTROPY_DENOMINATOR(status);
|
||||
if (tier < 3 || count < 4 || denominator < count)
|
||||
return accept(0, 0, 11);
|
||||
|
||||
int64_t die = entropy_cr_dice(6, 3);
|
||||
if (die < 0 || die >= 6)
|
||||
return accept(0, 0, 12);
|
||||
|
||||
uint8_t random_bytes[32];
|
||||
for (int i = 0; GUARD(32), i < 32; ++i)
|
||||
random_bytes[i] = 0;
|
||||
|
||||
if (entropy_cr_random((uint32_t)random_bytes, 32, 3) != 32)
|
||||
return accept(0, 0, 13);
|
||||
|
||||
int nonzero = 0;
|
||||
for (int i = 0; GUARD(32), i < 32; ++i)
|
||||
if (random_bytes[i] != 0)
|
||||
nonzero = 1;
|
||||
if (!nonzero)
|
||||
return accept(0, 0, 14);
|
||||
|
||||
return accept(0, 0, 0);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def assert_success(result, operation):
|
||||
meta = result.get("meta", result.get("metaData", {}))
|
||||
tx_result = meta.get("TransactionResult", result.get("engine_result", ""))
|
||||
if tx_result != "tesSUCCESS":
|
||||
raise AssertionError(f"{operation} failed: {result}")
|
||||
return meta
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_entropy(ctx, log)
|
||||
await ctx.wait_for_ledgers(3, node_id=0, timeout=60)
|
||||
|
||||
await ctx.fund_accounts({"entropy_api": 1000})
|
||||
account = ctx.account("entropy_api")
|
||||
|
||||
wasm = ctx.compile_hook(ENTROPY_HOOK_C, label="entropy-cr-api")
|
||||
install = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "SetHook",
|
||||
"Hooks": [
|
||||
{
|
||||
"Hook": {
|
||||
"CreateCode": wasm.hex().upper(),
|
||||
"HookOn": "0" * 64,
|
||||
"HookNamespace": "0" * 64,
|
||||
"HookApiVersion": 0,
|
||||
"Flags": 1,
|
||||
}
|
||||
}
|
||||
],
|
||||
"Fee": "100000000",
|
||||
},
|
||||
account.wallet,
|
||||
)
|
||||
assert_success(install, "SetHook")
|
||||
log("entropy_cr_* Hook installed")
|
||||
|
||||
invoke = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "Invoke",
|
||||
"Fee": "1000000",
|
||||
},
|
||||
account.wallet,
|
||||
)
|
||||
meta = assert_success(invoke, "Invoke")
|
||||
|
||||
executions = meta.get("HookExecutions", [])
|
||||
if len(executions) != 1:
|
||||
raise AssertionError(f"Expected one HookExecution, got: {executions}")
|
||||
|
||||
execution = executions[0].get("HookExecution", {})
|
||||
if execution.get("HookResult") != 3:
|
||||
raise AssertionError(f"Hook did not ACCEPT: {execution}")
|
||||
|
||||
if "HookReturnCode" not in execution:
|
||||
raise AssertionError(f"HookReturnCode missing from execution: {execution}")
|
||||
|
||||
return_code = execution["HookReturnCode"]
|
||||
if str(return_code) != "0":
|
||||
raise AssertionError(f"entropy_cr_* Hook check failed: {execution}")
|
||||
|
||||
log("entropy_cr_status, entropy_cr_dice, and entropy_cr_random passed")
|
||||
log("PASS")
|
||||
162
.testnet/scenarios/entropy/participant_aligned_smoke.py
Normal file
162
.testnet/scenarios/entropy/participant_aligned_smoke.py
Normal file
@@ -0,0 +1,162 @@
|
||||
""":descr: 5/6 validator_quorum, 4/6 participant_aligned (tier 2), recovery
|
||||
|
||||
Requires node_count: 6 (see suite.yml) — the smallest NON-degenerate Tier 2
|
||||
size. At n=6: tier2 floor = 4, validator quorum = 5, validation quorum = 5. So
|
||||
6/6, 5/6 present -> validator_quorum (EntropyTier=3)
|
||||
4/6 present -> participant_aligned (EntropyTier=2, count 4) <-- the band
|
||||
3/6 present -> consensus_fallback (EntropyTier=1)
|
||||
n=5 has NO tier-2 band (tier2 == quorum == 4), which is why the existing
|
||||
degradation smoke at 5 nodes only ever sees tier 3 / fallback.
|
||||
|
||||
KEY: the 4/6 window is BELOW the 80% validation quorum (5). The 4 survivors
|
||||
keep CLOSING ledgers that carry tier-2 entropy, but those ledgers do NOT
|
||||
validate until the network recovers — exactly the transition window Tier 2
|
||||
serves. So validated_ledger_index() stalls; we instead inspect a surviving
|
||||
node's CLOSED ledger (its LCL) directly, and cross-check the injection from the
|
||||
cohort's logs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from helpers import (
|
||||
require_entropy,
|
||||
get_entropy_tx,
|
||||
assert_participant_aligned,
|
||||
assert_validator_quorum,
|
||||
)
|
||||
|
||||
|
||||
def _closed_entropy(result):
|
||||
"""(seq, ConsensusEntropy tx) from a ctx.ledger('closed', transactions=True)
|
||||
result, or (None, None) if the fetch returned no usable ledger.
|
||||
|
||||
Enforces the per-ledger invariant that an entropy-enabled closed ledger
|
||||
carries EXACTLY ONE ConsensusEntropy pseudo-tx (mirroring get_entropy_tx):
|
||||
a duplicate or missing injection raises here with a clear error instead of
|
||||
being silently skipped and resurfacing later as a generic 'no tier-2 ledger'.
|
||||
"""
|
||||
if not result or not isinstance(result.get("ledger"), dict):
|
||||
return None, None
|
||||
led = result["ledger"]
|
||||
try:
|
||||
seq = int(led.get("ledger_index"))
|
||||
except (TypeError, ValueError):
|
||||
return None, None
|
||||
ce = [
|
||||
t
|
||||
for t in led.get("transactions", [])
|
||||
if isinstance(t, dict) and t.get("TransactionType") == "ConsensusEntropy"
|
||||
]
|
||||
if len(ce) != 1:
|
||||
raise AssertionError(
|
||||
f"Closed ledger {seq}: expected 1 ConsensusEntropy txn, got {len(ce)}"
|
||||
)
|
||||
return seq, ce[0]
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_entropy(ctx, log)
|
||||
|
||||
# Baseline: healthy 6/6 produces validator_quorum entropy.
|
||||
await ctx.wait_for_ledgers(1, node_id=0, timeout=30)
|
||||
|
||||
# --- 5/6: settles back to validator_quorum (5 present >= quorum 5) ---
|
||||
val_before_drop = ctx.validated_ledger_index(0)
|
||||
ctx.stop_node(5)
|
||||
await ctx.wait_for_nodes_down(nodes=[5], timeout=30)
|
||||
# Settle a few ledgers past the membership change. The ledger right at a
|
||||
# validator drop can carry a transient consensus_fallback (tier 1, count 0,
|
||||
# deterministic and by design) before the commit/reveal pipeline re-primes,
|
||||
# so we do NOT assume any single post-drop ledger is already tier 3.
|
||||
await ctx.wait_for_ledgers(4, node_id=0, timeout=90)
|
||||
|
||||
# 5/6 is at/above the 80% quorum (5), so steady state is validator_quorum.
|
||||
# Scan the post-drop validated ledgers (all carry the 5-node cohort, so a
|
||||
# tier-3 here has count == 5) and require at least one clean validator_quorum
|
||||
# — EntropyTier=3, count >= quorum, non-zero digest — tolerating the
|
||||
# transition fallback instead of depending on where the tip happened to land.
|
||||
val_5of6 = ctx.validated_ledger_index(0)
|
||||
t3_seq = None
|
||||
for seq in range(val_5of6, val_before_drop, -1):
|
||||
ce, _ = get_entropy_tx(ctx, seq)
|
||||
tier = ce.get("EntropyTier")
|
||||
log(f" 5/6 ledger {seq}: tier={tier} count={ce.get('EntropyCount')}")
|
||||
if tier == 3:
|
||||
assert_validator_quorum(ce, seq, min_count=5)
|
||||
t3_seq = seq
|
||||
break
|
||||
if t3_seq is None:
|
||||
raise AssertionError(
|
||||
f"5/6: no validator_quorum (tier 3) entropy in post-drop validated "
|
||||
f"ledgers {val_before_drop + 1}..{val_5of6}"
|
||||
)
|
||||
log(f"5/6: validator_quorum at validated seq {t3_seq}")
|
||||
|
||||
#@@start test-participant-aligned-window
|
||||
# --- 4/6: participant_aligned (Tier 2) degraded window ---
|
||||
ctx.stop_node(4)
|
||||
await ctx.wait_for_nodes_down(nodes=[4], timeout=30)
|
||||
|
||||
# ~12s window: confirm tier-2 INJECTION from the cohort's logs, and that the
|
||||
# round is NOT the below-quorum fallback path (which is what distinguishes
|
||||
# the tier-2 band from the tier-1 fallback regime).
|
||||
op = await ctx.sleep(12, name="tier2_window")
|
||||
selected_t2 = ctx.search_logs(
|
||||
r"RNG: entropy selected seq=\d+ tier=2 count=4",
|
||||
within=op.window,
|
||||
nodes=[0, 1, 2, 3],
|
||||
)
|
||||
log(f"4/6: 'entropy selected tier=2 count=4' logs: {selected_t2.count}")
|
||||
if selected_t2.count == 0:
|
||||
raise AssertionError(
|
||||
"4/6 window injected no participant_aligned (tier 2) entropy: no "
|
||||
"'RNG: entropy selected ... tier=2 count=4' on the surviving cohort"
|
||||
)
|
||||
ctx.assert_not_log(
|
||||
r"STALLDIAG: rng-commit-timeout-below-quorum",
|
||||
within=op.window,
|
||||
nodes=[0, 1, 2, 3],
|
||||
)
|
||||
|
||||
# Verify the on-ledger EntropyTier=2 DIRECTLY: validation is stalled (4 < 5),
|
||||
# so sample the surviving cohort's CLOSED ledger (its LCL — built but not yet
|
||||
# validated). At least one must be participant_aligned with EntropyCount=4.
|
||||
tier2_on_ledger = 0
|
||||
last_seq = None
|
||||
for _ in range(5):
|
||||
seq, ce = _closed_entropy(
|
||||
ctx.ledger("closed", transactions=True, node_id=0)
|
||||
)
|
||||
if ce is not None and seq is not None and seq != last_seq:
|
||||
last_seq = seq
|
||||
tier = ce.get("EntropyTier")
|
||||
count = ce.get("EntropyCount", -1)
|
||||
log(f" closed ledger {seq}: tier={tier} count={count}")
|
||||
if tier == 2:
|
||||
assert_participant_aligned(ce, seq, expected_count=4)
|
||||
tier2_on_ledger += 1
|
||||
await ctx.sleep(3)
|
||||
|
||||
if tier2_on_ledger == 0:
|
||||
raise AssertionError(
|
||||
"no closed participant_aligned (tier 2) ledger observed during the "
|
||||
"4/6 window (tier 2 was injected per logs, but not seen on a closed "
|
||||
"ledger)"
|
||||
)
|
||||
log(f"4/6: {tier2_on_ledger} participant_aligned closed ledger(s) verified")
|
||||
#@@end test-participant-aligned-window
|
||||
|
||||
# --- Recovery: liveness — validation resumes once quorum is restored ---
|
||||
ctx.start_node(4)
|
||||
ctx.start_node(5)
|
||||
await ctx.wait_for_ledgers(1, node_id=0, timeout=120)
|
||||
|
||||
val_recovered = ctx.validated_ledger_index(0)
|
||||
if not val_recovered or val_recovered <= val_5of6:
|
||||
raise AssertionError(
|
||||
f"Validated ledger did not advance after recovery "
|
||||
f"({val_5of6} -> {val_recovered})"
|
||||
)
|
||||
log(f"Recovered: validated seq {val_5of6} -> {val_recovered}")
|
||||
|
||||
log("PASS")
|
||||
164
.testnet/scenarios/entropy/quorum_degradation_smoke.py
Normal file
164
.testnet/scenarios/entropy/quorum_degradation_smoke.py
Normal file
@@ -0,0 +1,164 @@
|
||||
""":descr: 4/5 liveness, 3/5 fail-closed sub-quorum window, recovery"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from helpers import (
|
||||
require_entropy,
|
||||
get_entropy_tx,
|
||||
entropy_fields,
|
||||
assert_consensus_fallback,
|
||||
)
|
||||
|
||||
|
||||
def _closed_entropy(result):
|
||||
"""Return (seq, ConsensusEntropy tx) from a closed-ledger RPC result.
|
||||
|
||||
The 3/5 window is below validation quorum, so validated-ledger history is
|
||||
expected to stall. Sampling a surviving node's closed ledger catches any
|
||||
local LCL that advanced despite the sub-quorum condition.
|
||||
"""
|
||||
if not result or not isinstance(result.get("ledger"), dict):
|
||||
return None, None
|
||||
ledger = result["ledger"]
|
||||
try:
|
||||
seq = int(ledger.get("ledger_index"))
|
||||
except (TypeError, ValueError):
|
||||
return None, None
|
||||
ce = [
|
||||
tx
|
||||
for tx in ledger.get("transactions", [])
|
||||
if isinstance(tx, dict) and tx.get("TransactionType") == "ConsensusEntropy"
|
||||
]
|
||||
if len(ce) != 1:
|
||||
raise AssertionError(
|
||||
f"Closed ledger {seq}: expected 1 ConsensusEntropy txn, got {len(ce)}"
|
||||
)
|
||||
return seq, ce[0]
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_entropy(ctx, log)
|
||||
|
||||
# Baseline: wait 1 ledger to confirm network is healthy.
|
||||
await ctx.wait_for_ledgers(1, node_id=0, timeout=30)
|
||||
|
||||
# --- 4/5 liveness ---
|
||||
ctx.stop_node(4)
|
||||
await ctx.wait_for_nodes_down(nodes=[4], timeout=30)
|
||||
await ctx.wait_for_ledgers(1, node_id=0, timeout=30)
|
||||
log("4/5: liveness OK")
|
||||
|
||||
# Snapshot validated seq before dropping to 3/5.
|
||||
val_before = ctx.validated_ledger_index(0)
|
||||
|
||||
# --- 3/5 degraded window ---
|
||||
ctx.stop_node(3)
|
||||
await ctx.wait_for_nodes_down(nodes=[3], timeout=30)
|
||||
|
||||
# 10s ≈ 3 rounds at 3s cadence.
|
||||
await ctx.sleep(10)
|
||||
|
||||
val_after = ctx.validated_ledger_index(0)
|
||||
log(f"3/5: validated ledger {val_before} → {val_after}")
|
||||
|
||||
if val_after and val_before and val_after > val_before:
|
||||
raise AssertionError(
|
||||
f"3/5 sub-quorum window unexpectedly validated ledgers "
|
||||
f"({val_before} -> {val_after})"
|
||||
)
|
||||
|
||||
# If the surviving cohort exposes an advanced closed ledger despite being
|
||||
# below validation quorum, it must fail closed to consensus_fallback. This
|
||||
# keeps the entropy assertion live without pretending validated history
|
||||
# should advance at 3/5.
|
||||
degraded_fallback = 0
|
||||
last_closed = None
|
||||
for _ in range(5):
|
||||
seq, ce = _closed_entropy(ctx.ledger("closed", transactions=True, node_id=0))
|
||||
if seq and val_before and seq > val_before and seq != last_closed:
|
||||
last_closed = seq
|
||||
digest, count = assert_consensus_fallback(ce, seq)
|
||||
degraded_fallback += 1
|
||||
log(
|
||||
f" 3/5 closed ledger {seq}: EntropyCount={count} "
|
||||
f"Digest={digest[:16]}... FALLBACK"
|
||||
)
|
||||
await ctx.sleep(2)
|
||||
|
||||
log(f"3/5 closed-ledger fallback samples: {degraded_fallback}")
|
||||
|
||||
# Log checks tied to current transition mechanics:
|
||||
# - commit-set SHAMap publication is the observable output of entering the
|
||||
# commit sidecar phase
|
||||
# - ConvergingCommit transition is the gateway out of seq=0-only behavior
|
||||
# - rng-commit-timeout-below-quorum is the degraded-window fallback path
|
||||
ctx.log_level("LedgerConsensus", "trace")
|
||||
ctx.log_level("ConsensusExtensions", "trace")
|
||||
op = await ctx.sleep(6, name="stall_window")
|
||||
|
||||
ctx.assert_not_log(
|
||||
r"RNG: transitioned to ConvergingCommit", within=op.window, nodes=[0, 1, 2]
|
||||
)
|
||||
ctx.assert_not_log(
|
||||
r"RNG: built commitSet SHAMap", within=op.window, nodes=[0, 1, 2]
|
||||
)
|
||||
|
||||
gate_blocked = ctx.search_logs(
|
||||
r"STALLDIAG: establish gate blocked reason=(pause|no-tx-consensus)",
|
||||
within=op.window,
|
||||
nodes=[0, 1, 2],
|
||||
)
|
||||
log(f"3/5: establish gate-blocked logs in 6s: {gate_blocked.count}")
|
||||
|
||||
below_quorum = ctx.search_logs(
|
||||
r"STALLDIAG: rng-commit-timeout-below-quorum",
|
||||
within=op.window,
|
||||
nodes=[0, 1, 2],
|
||||
)
|
||||
log(f"3/5: RNG commit timeout below quorum logs in 6s: {below_quorum.count}")
|
||||
|
||||
# --- Recovery: restart nodes, verify ledger advancement ---
|
||||
ctx.start_node(3)
|
||||
ctx.start_node(4)
|
||||
await ctx.wait_for_ledgers(1, node_id=0, timeout=120)
|
||||
|
||||
val_recovered = ctx.validated_ledger_index(0)
|
||||
pre_recovery = max(v for v in [val_before, val_after] if v is not None)
|
||||
log(f"Recovered: validated seq {pre_recovery} → {val_recovered}")
|
||||
|
||||
if not val_recovered or val_recovered <= pre_recovery:
|
||||
raise AssertionError(
|
||||
f"Validated ledger did not advance after recovery "
|
||||
f"({pre_recovery} → {val_recovered})"
|
||||
)
|
||||
|
||||
# Inspect post-recovery ledgers separately from the degraded window above.
|
||||
# Once the network is back at quorum, validator-tier entropy is expected
|
||||
# again (transitional fallback ledgers are fine) and must be quorum-met.
|
||||
fallback_count = 0
|
||||
validator_count = 0
|
||||
for seq in range(pre_recovery + 1, val_recovered + 1):
|
||||
ce, _ = get_entropy_tx(ctx, seq)
|
||||
digest, entropy_count, is_fallback = entropy_fields(ce)
|
||||
|
||||
if is_fallback:
|
||||
fallback_count += 1
|
||||
else:
|
||||
validator_count += 1
|
||||
if entropy_count < 4:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: validator entropy with sub-quorum "
|
||||
f"EntropyCount={entropy_count} (need >= 4)"
|
||||
)
|
||||
|
||||
log(
|
||||
f" Ledger {seq}: EntropyCount={entropy_count} "
|
||||
f"{'FALLBACK' if is_fallback else 'VALIDATOR'}"
|
||||
)
|
||||
|
||||
log(
|
||||
f"Entropy summary: {fallback_count} fallback, "
|
||||
f"{validator_count} validator"
|
||||
)
|
||||
|
||||
log("PASS")
|
||||
44
.testnet/scenarios/entropy/quorum_recovery_smoke.py
Normal file
44
.testnet/scenarios/entropy/quorum_recovery_smoke.py
Normal file
@@ -0,0 +1,44 @@
|
||||
""":descr: drop 2 nodes (3/5 stall), restart both, verify recovery"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from helpers import require_entropy
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_entropy(ctx, log)
|
||||
|
||||
await ctx.wait_for_ledgers(1, node_id=0, timeout=60)
|
||||
log("Baseline OK")
|
||||
|
||||
# Drop 2 nodes → validation stall.
|
||||
ctx.stop_node(3)
|
||||
ctx.stop_node(4)
|
||||
await ctx.wait_for_nodes_down(nodes=[3, 4], timeout=30)
|
||||
|
||||
info = ctx.rpc.server_info(node_id=0)
|
||||
val_before = info.get("info", {}).get("validated_ledger", {}).get("seq", 0)
|
||||
log(f"Stalled at validated seq {val_before}")
|
||||
|
||||
# Let it sit for a few rounds in degraded state.
|
||||
await ctx.sleep(6)
|
||||
|
||||
# Bring both nodes back.
|
||||
ctx.start_node(3)
|
||||
ctx.start_node(4)
|
||||
log("Restarted n3 and n4, waiting for recovery...")
|
||||
|
||||
# Recovery: wait for ANY validated ledger advance on n0.
|
||||
await ctx.wait_for_ledger_close(node_id=0, timeout=60)
|
||||
|
||||
info = ctx.rpc.server_info(node_id=0)
|
||||
val_after = info.get("info", {}).get("validated_ledger", {}).get("seq", 0)
|
||||
log(f"Recovered: validated seq {val_before} → {val_after}")
|
||||
|
||||
if val_after <= val_before:
|
||||
raise AssertionError(
|
||||
f"Validated ledger did not advance after recovery "
|
||||
f"({val_before} → {val_after})"
|
||||
)
|
||||
|
||||
log("PASS")
|
||||
27
.testnet/scenarios/entropy/steady_state_entropy.py
Normal file
27
.testnet/scenarios/entropy/steady_state_entropy.py
Normal file
@@ -0,0 +1,27 @@
|
||||
""":descr: all 5 nodes healthy, every ledger has valid unique quorum-met entropy"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from helpers import require_entropy, get_entropy_tx, assert_valid_entropy
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_entropy(ctx, log)
|
||||
|
||||
# Wait for the RNG pipeline to warm up past initial proposal/sidecar gossip.
|
||||
await ctx.wait_for_ledgers(3, node_id=0, timeout=60)
|
||||
log("Pipeline warmed up")
|
||||
|
||||
start_seq = ctx.validated_ledger_index(0)
|
||||
await ctx.wait_for_ledgers(10, node_id=0, timeout=120)
|
||||
end_seq = ctx.validated_ledger_index(0)
|
||||
log(f"Inspecting ledgers {start_seq + 1} → {end_seq}")
|
||||
|
||||
digests = set()
|
||||
for seq in range(start_seq + 1, end_seq + 1):
|
||||
ce, _ = get_entropy_tx(ctx, seq)
|
||||
digest, count = assert_valid_entropy(ce, seq, seen_digests=digests)
|
||||
log(f" Ledger {seq}: EntropyCount={count} Digest={digest[:16]}...")
|
||||
|
||||
log(f"Verified {end_seq - start_seq} ledgers: all quorum entropy, all unique")
|
||||
log("PASS")
|
||||
108
.testnet/scenarios/export-suite.yml
Normal file
108
.testnet/scenarios/export-suite.yml
Normal file
@@ -0,0 +1,108 @@
|
||||
defaults:
|
||||
network:
|
||||
node_count: 5
|
||||
launcher: tmux
|
||||
find_ports: true
|
||||
slave_delay: 0.2
|
||||
features:
|
||||
- ConsensusEntropy
|
||||
- Export
|
||||
track_features:
|
||||
- ConsensusEntropy
|
||||
- Export
|
||||
unl_report: true
|
||||
log_levels:
|
||||
TxQ: info
|
||||
Protocol: debug
|
||||
Peer: debug
|
||||
LedgerConsensus: debug
|
||||
ConsensusExtensions: debug
|
||||
NetworkOPs: info
|
||||
rc:
|
||||
- rng_poll_ms=333
|
||||
|
||||
tests:
|
||||
# --- CE + Export (post-validation shares, sidecar root convergence) ---
|
||||
- name: steady_state_export_ce
|
||||
script: .testnet/scenarios/export/steady_state_export.py
|
||||
|
||||
- name: retriable_export_ce
|
||||
script: .testnet/scenarios/export/retriable_export.py
|
||||
|
||||
- name: export_degradation_ce
|
||||
script: .testnet/scenarios/export/export_degradation.py
|
||||
network:
|
||||
rc:
|
||||
- rng_poll_ms=333
|
||||
- n3:no_export_sig=true
|
||||
- n4:no_export_sig=true
|
||||
|
||||
- name: export_without_unl_report
|
||||
script: .testnet/scenarios/export/export_without_unl_report.py
|
||||
network:
|
||||
features:
|
||||
- Export
|
||||
track_features:
|
||||
- Export
|
||||
unl_report: false
|
||||
|
||||
- name: export_no_veto_missing_observation
|
||||
script: .testnet/scenarios/export/export_no_veto_missing_observation.py
|
||||
network:
|
||||
rc:
|
||||
- rng_poll_ms=333
|
||||
- n4:no_export_sig_hash=true
|
||||
|
||||
- name: export_share_subscription
|
||||
script: .testnet/scenarios/export/export_share_subscription.py
|
||||
|
||||
- name: export_two_member_committee_recovery
|
||||
script: .testnet/scenarios/export/export_committee_recovery.py
|
||||
|
||||
# CE + Export: 1 node suppressed, 4/5 = 80% quorum, should succeed
|
||||
- name: export_ce_one_node_down
|
||||
script: .testnet/scenarios/export/export_quorum.py
|
||||
params:
|
||||
expect_success: true
|
||||
network:
|
||||
rc:
|
||||
- rng_poll_ms=333
|
||||
- n4:no_export_sig=true
|
||||
|
||||
# --- Export only, no CE (80% active-view quorum) ---
|
||||
- name: export_only_all_up
|
||||
script: .testnet/scenarios/export/export_quorum.py
|
||||
params:
|
||||
expect_success: true
|
||||
network:
|
||||
features:
|
||||
- Export
|
||||
track_features:
|
||||
- Export
|
||||
|
||||
- name: export_only_one_node_down
|
||||
script: .testnet/scenarios/export/export_quorum.py
|
||||
params:
|
||||
expect_success: true
|
||||
network:
|
||||
features:
|
||||
- Export
|
||||
track_features:
|
||||
- Export
|
||||
rc:
|
||||
- rng_poll_ms=333
|
||||
- n4:no_export_sig=true
|
||||
|
||||
- name: export_only_two_nodes_down
|
||||
script: .testnet/scenarios/export/export_quorum.py
|
||||
params:
|
||||
expect_success: false
|
||||
network:
|
||||
features:
|
||||
- Export
|
||||
track_features:
|
||||
- Export
|
||||
rc:
|
||||
- rng_poll_ms=333
|
||||
- n3:no_export_sig=true
|
||||
- n4:no_export_sig=true
|
||||
126
.testnet/scenarios/export/export_committee_recovery.py
Normal file
126
.testnet/scenarios/export/export_committee_recovery.py
Normal file
@@ -0,0 +1,126 @@
|
||||
""":descr: a 2-of-5 Export committee recovers without a new intent
|
||||
|
||||
The intent selects only validators n0 and n4, so qC is 2. Validator n4 is
|
||||
stopped before admission: the network still validates the intent with 4/5, but
|
||||
one selected share cannot form a witness. Restarting n4 must republish its share
|
||||
for the same live latch and complete the witness without resubmitting Export.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from export_helpers import (
|
||||
EXPORT_RETRY_LEDGER_WINDOW,
|
||||
assert_export_latch,
|
||||
bitmap_positions,
|
||||
find_export_signature_witness,
|
||||
find_export_txns,
|
||||
require_export,
|
||||
submit_direct_export,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_export(ctx, log)
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
|
||||
alice = ctx.account("alice")
|
||||
bob = ctx.account("bob")
|
||||
|
||||
if not ctx.stop_node(4):
|
||||
raise AssertionError("Failed to stop selected validator n4")
|
||||
await ctx.wait_for_nodes_down(nodes=[4], timeout=30)
|
||||
log("Stopped selected validator n4; selected committee is n0+n4")
|
||||
|
||||
current = ctx.validated_ledger_index(0)
|
||||
result = await submit_direct_export(
|
||||
ctx,
|
||||
log,
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"Fee": "1000000",
|
||||
"ExportedTxn": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": alice.address,
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "10",
|
||||
"Sequence": 0,
|
||||
"TicketSequence": 1,
|
||||
"FirstLedgerSequence": current + 1,
|
||||
"LastLedgerSequence": current + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Flags": 2147483648,
|
||||
"SigningPubKey": "",
|
||||
},
|
||||
},
|
||||
alice.wallet,
|
||||
committee_node_ids=[0, 4],
|
||||
)
|
||||
if result.get("engine_result") != "tesSUCCESS":
|
||||
raise AssertionError(f"Export intent failed: {result}")
|
||||
|
||||
origin = result.get("hash")
|
||||
origin_seq = int(result.get("ledger_index"))
|
||||
if not origin:
|
||||
raise AssertionError(f"Validated Export missing hash: {result}")
|
||||
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
origin_hash=origin,
|
||||
expect_witness=False,
|
||||
)
|
||||
selected = {0, 1}
|
||||
|
||||
await ctx.wait_for_ledger(origin_seq + 1, node_id=0, timeout=30)
|
||||
if find_export_signature_witness(ctx, origin_seq + 1, origin):
|
||||
raise AssertionError(
|
||||
"Witness formed while one of two selected signers was down"
|
||||
)
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
origin_hash=origin,
|
||||
expect_witness=False,
|
||||
)
|
||||
log("No witness with only one selected signer; original latch remains pending")
|
||||
|
||||
if not ctx.start_node(4):
|
||||
raise AssertionError("Failed to restart selected validator n4")
|
||||
await ctx.wait_for_nodes(
|
||||
lambda node_id: ctx.rpc.server_info(node_id) is not None,
|
||||
nodes=[4],
|
||||
timeout=30,
|
||||
poll_interval=0.5,
|
||||
name="selected-export-validator-up",
|
||||
)
|
||||
log("Restarted selected validator n4")
|
||||
|
||||
witness = await wait_for_export_signature_witness(
|
||||
ctx, log, origin, after_ledger=origin_seq
|
||||
)
|
||||
contributors = bitmap_positions(witness["EntropyContributors"])
|
||||
if contributors != selected:
|
||||
raise AssertionError(
|
||||
f"Recovered witness contributors {contributors} != selected {selected}"
|
||||
)
|
||||
if len(witness["_WitnessSigners"]) != 2:
|
||||
raise AssertionError(
|
||||
f"Recovered witness has {len(witness['_WitnessSigners'])} signers, need 2"
|
||||
)
|
||||
witness_seq = int(witness["LedgerSequence"])
|
||||
for seq in range(origin_seq + 1, witness_seq + 1):
|
||||
if any(tx.get("Account") == alice.address for tx in find_export_txns(ctx, seq)):
|
||||
raise AssertionError(
|
||||
f"A second Export was submitted before recovery in ledger {seq}"
|
||||
)
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
origin_hash=origin,
|
||||
expect_witness=True,
|
||||
)
|
||||
log(f"Same Export {origin} completed after selected validator recovery")
|
||||
log("PASS")
|
||||
118
.testnet/scenarios/export/export_degradation.py
Normal file
118
.testnet/scenarios/export/export_degradation.py
Normal file
@@ -0,0 +1,118 @@
|
||||
""":descr: Submit ttEXPORT with 2 nodes suppressing export signatures and
|
||||
verify the admitted intent remains unwitnessed through its publication window.
|
||||
|
||||
Nodes 3 and 4 have runtime_config no_export_sig=true, so only 3/5 nodes
|
||||
provide export signatures. With 80% quorum = ceil(5*0.8) = 4 required,
|
||||
the export cannot reach quorum and no ExportSignatures witness may be recorded.
|
||||
|
||||
Flow:
|
||||
1. Fund alice and bob
|
||||
2. alice submits ttEXPORT with an explicit authority declaration
|
||||
3. Only 3/5 post-validation shares become available (need 4)
|
||||
4. Verify the publication window closes without a witness
|
||||
5. Verify subsequent payment still works (sequence not permanently blocked)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from export_helpers import (
|
||||
EXPORT_RETRY_LEDGER_WINDOW,
|
||||
assert_export_latch,
|
||||
require_export,
|
||||
submit_direct_export,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_export(ctx, log)
|
||||
|
||||
# --- Setup ---
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
|
||||
log("Accounts funded")
|
||||
|
||||
alice = ctx.account("alice")
|
||||
bob = ctx.account("bob")
|
||||
current_seq = ctx.validated_ledger_index(0)
|
||||
|
||||
log(f"Current ledger: {current_seq}")
|
||||
log("Nodes 3,4 have runtime_config no_export_sig=true (3/5 sigs, need 4)")
|
||||
|
||||
#@@start test-export-below-quorum-expiry
|
||||
# --- Submit intent; only 3/5 validators release shares. ---
|
||||
result = await submit_direct_export(
|
||||
ctx,
|
||||
log,
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Fee": "1000000",
|
||||
"ExportedTxn": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": alice.address,
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "10",
|
||||
"Sequence": 0,
|
||||
"TicketSequence": 1,
|
||||
"FirstLedgerSequence": current_seq + 1,
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Flags": 2147483648,
|
||||
"SigningPubKey": "",
|
||||
},
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
final_seq = result.get("ledger_index", ctx.validated_ledger_index(0))
|
||||
origin_hash = result.get("hash")
|
||||
engine_result = result.get("engine_result", "")
|
||||
log(f"Export intent admitted at ledger {final_seq}, result: {engine_result}")
|
||||
|
||||
if engine_result != "tesSUCCESS":
|
||||
raise AssertionError(f"Expected admitted intent, got {engine_result}")
|
||||
if not origin_hash:
|
||||
raise AssertionError(f"Validated Export missing hash: {result}")
|
||||
|
||||
await wait_for_export_signature_witness(
|
||||
ctx,
|
||||
log,
|
||||
origin_hash,
|
||||
after_ledger=final_seq,
|
||||
expect_witness=False,
|
||||
)
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
expect_exists=True,
|
||||
origin_hash=origin_hash,
|
||||
expect_witness=False,
|
||||
)
|
||||
#@@end test-export-below-quorum-expiry
|
||||
|
||||
# --- Verify subsequent payment works regardless ---
|
||||
log("Submitting payment from alice to bob...")
|
||||
pay_result = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "Payment",
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "12",
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
pay_engine = pay_result.get("engine_result", "")
|
||||
log(f"Payment result: {pay_engine}")
|
||||
|
||||
if pay_engine != "tesSUCCESS":
|
||||
raise AssertionError(
|
||||
f"Payment failed after unwitnessed export: {pay_engine} "
|
||||
f"-- sequence may be blocked"
|
||||
)
|
||||
|
||||
log("Payment succeeded -- account not permanently blocked")
|
||||
log("PASS")
|
||||
458
.testnet/scenarios/export/export_helpers.py
Normal file
458
.testnet/scenarios/export/export_helpers.py
Normal file
@@ -0,0 +1,458 @@
|
||||
"""Shared helpers for Export scenario tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from xahaud_scripts.testnet.config import (
|
||||
_decode_node_public_key,
|
||||
_unl_report_index,
|
||||
feature_name_to_hash,
|
||||
)
|
||||
|
||||
EXPORT_RETRY_LEDGER_WINDOW = 5
|
||||
EXPORT_PUBLICATION_LEDGER_WINDOW = 5
|
||||
|
||||
|
||||
async def require_export(
|
||||
ctx, log, *, require_unl_report=True, require_runtime_config=True
|
||||
):
|
||||
"""Wait for first ledger and assert Export is enabled.
|
||||
|
||||
Network-mode Export success requires a parent-ledger UNLReport-backed
|
||||
active validator view. Most export scenarios seed that report in genesis;
|
||||
assert it here so a success-path test cannot accidentally pass setup
|
||||
without the condition Export::doApply requires. The no-UNLReport retry
|
||||
scenario opts out deliberately.
|
||||
|
||||
The tracked export suite also uses XAHAUD_RUNTIME_TEST_CONFIG for polling
|
||||
and fault-injection knobs. Default binaries reject the runtime_config RPC,
|
||||
so check it up front rather than silently running without those knobs.
|
||||
"""
|
||||
await ctx.wait_for_ledger_close(timeout=120)
|
||||
|
||||
if require_runtime_config:
|
||||
result = ctx.rpc.runtime_config(0)
|
||||
if not result or result.get("error"):
|
||||
raise AssertionError(
|
||||
"Export suite requires a binary built with "
|
||||
"xahaud_runtime_test_config=ON; runtime_config RPC returned "
|
||||
f"{result}"
|
||||
)
|
||||
log("RuntimeConfig RPC active")
|
||||
|
||||
feature = ctx.feature_check(feature_name_to_hash("Export"), node_id=0)
|
||||
if not feature or not feature.get("enabled", False):
|
||||
raise AssertionError(f"Export not enabled: {feature}")
|
||||
log("Export enabled")
|
||||
|
||||
if require_unl_report:
|
||||
result = ctx.rpc.ledger_entry(0, _unl_report_index())
|
||||
node = (result or {}).get("node", {})
|
||||
active = node.get("ActiveValidators", [])
|
||||
if node.get("LedgerEntryType") != "UNLReport" or not active:
|
||||
raise AssertionError(
|
||||
"Export success scenario requires a ledger UNLReport with "
|
||||
f"ActiveValidators, got: {result}"
|
||||
)
|
||||
log(f"UNLReport active validators: {len(active)}")
|
||||
|
||||
|
||||
def find_export_txns(ctx, seq):
|
||||
"""Find Export transactions in a ledger.
|
||||
|
||||
Returns list of Export transaction dicts.
|
||||
"""
|
||||
result = ctx.ledger(seq, transactions=True)
|
||||
if not result:
|
||||
return []
|
||||
|
||||
txns = result.get("ledger", {}).get("transactions", [])
|
||||
return [tx for tx in txns if tx.get("TransactionType") == "Export"]
|
||||
|
||||
|
||||
def _validator_master_keys_by_node(ctx):
|
||||
"""Return generated validator master keys keyed by testnet node id."""
|
||||
network = json.loads((ctx.base_dir / "network.json").read_text())
|
||||
return {
|
||||
int(node["id"]): _decode_node_public_key(node["public_key"])
|
||||
for node in network["nodes"]
|
||||
}
|
||||
|
||||
|
||||
def _export_committee_fields(master_keys):
|
||||
"""Return the canonical roster and its protocol content digest."""
|
||||
encoded = [bytes.fromhex(key) for key in master_keys]
|
||||
if not encoded or len(encoded) > 32 or len(set(encoded)) != len(encoded):
|
||||
raise AssertionError("Export committee requires 1..32 unique masters")
|
||||
encoded.sort()
|
||||
roster = b"".join(encoded)
|
||||
preimage = b"ECM\0" + len(encoded).to_bytes(4, "big") + roster
|
||||
digest = hashlib.sha512(preimage).digest()[:32]
|
||||
return {
|
||||
"ExportCommitteeHash": digest.hex().upper(),
|
||||
"ExportCommittee": roster.hex().upper(),
|
||||
}
|
||||
|
||||
|
||||
def bitmap_positions(bitmap):
|
||||
"""Return set positions from a witness contributor bitmap."""
|
||||
raw = bytes.fromhex(bitmap) if isinstance(bitmap, str) else bytes(bitmap)
|
||||
return {
|
||||
byte_index * 8 + bit_index
|
||||
for byte_index, byte in enumerate(raw)
|
||||
for bit_index in range(8)
|
||||
if byte & (1 << bit_index)
|
||||
}
|
||||
|
||||
|
||||
def export_authority(ctx, *, require_unl_report=True, committee_node_ids=None):
|
||||
"""Build an account-owned committee declaration for a direct Export."""
|
||||
ledger_result = ctx.ledger("validated") or {}
|
||||
ledger = ledger_result.get("ledger", {})
|
||||
ledger_hash = ledger_result.get("ledger_hash") or ledger.get("hash")
|
||||
if not ledger_hash:
|
||||
raise AssertionError(f"Validated ledger hash unavailable: {ledger_result}")
|
||||
|
||||
report = (
|
||||
ctx.rpc.request(
|
||||
0,
|
||||
"ledger_entry",
|
||||
{"index": _unl_report_index(), "ledger_hash": ledger_hash},
|
||||
)
|
||||
or {}
|
||||
)
|
||||
active = report.get("node", {}).get("ActiveValidators", [])
|
||||
if not active:
|
||||
if require_unl_report:
|
||||
raise AssertionError(f"UNLReport active validators unavailable: {report}")
|
||||
# The negative scenario still submits a structurally valid roster so
|
||||
# source eligibility, rather than client construction, rejects it.
|
||||
masters = _validator_master_keys_by_node(ctx)
|
||||
selected_ids = (
|
||||
sorted(masters) if committee_node_ids is None else committee_node_ids
|
||||
)
|
||||
return _export_committee_fields([masters[node_id] for node_id in selected_ids])
|
||||
|
||||
active_keys = set()
|
||||
for entry in active:
|
||||
validator = entry.get("ActiveValidator", entry)
|
||||
key = validator.get("PublicKey")
|
||||
if not key:
|
||||
raise AssertionError(f"Malformed UNLReport validator entry: {entry}")
|
||||
active_keys.add(key.upper())
|
||||
active_keys = sorted(active_keys, key=bytes.fromhex)
|
||||
|
||||
if committee_node_ids is None:
|
||||
selected_keys = active_keys
|
||||
else:
|
||||
masters = _validator_master_keys_by_node(ctx)
|
||||
selected_keys = []
|
||||
for node_id in committee_node_ids:
|
||||
if node_id not in masters:
|
||||
raise AssertionError(f"Unknown testnet validator node n{node_id}")
|
||||
if masters[node_id] not in active_keys:
|
||||
raise AssertionError(
|
||||
f"Validator n{node_id} is absent from the active UNLReport"
|
||||
)
|
||||
selected_keys.append(masters[node_id])
|
||||
if not selected_keys:
|
||||
raise AssertionError("Export committee must select at least one validator")
|
||||
|
||||
return _export_committee_fields(selected_keys)
|
||||
|
||||
|
||||
async def create_export_committee(
|
||||
ctx, log, wallet, *, committee_node_ids=None
|
||||
):
|
||||
"""Create one immutable account-owned committee and return its fields."""
|
||||
authority = export_authority(ctx, committee_node_ids=committee_node_ids)
|
||||
result = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"ExportCommittee": authority["ExportCommittee"],
|
||||
"Fee": "1000000",
|
||||
},
|
||||
wallet,
|
||||
)
|
||||
meta = result.get("meta", result.get("metaData", {}))
|
||||
if meta.get("TransactionResult") != "tesSUCCESS":
|
||||
raise AssertionError(f"Export committee setup failed: {result}")
|
||||
log(f"Export committee created: {authority['ExportCommitteeHash']}")
|
||||
return authority
|
||||
|
||||
|
||||
def find_export_signature_witness(ctx, seq, origin_hash):
|
||||
"""Find a later-ledger ExportSignatures witness for an Export origin."""
|
||||
result = ctx.ledger(seq, transactions=True)
|
||||
txns = (result or {}).get("ledger", {}).get("transactions", [])
|
||||
for tx in txns:
|
||||
if not isinstance(tx, dict):
|
||||
continue
|
||||
if tx.get("TransactionType") != "ExportSignatures":
|
||||
continue
|
||||
if tx.get("TransactionHash") == origin_hash:
|
||||
return tx
|
||||
return None
|
||||
|
||||
|
||||
async def wait_for_export_signature_witness(
|
||||
ctx,
|
||||
log,
|
||||
origin_hash,
|
||||
*,
|
||||
after_ledger,
|
||||
max_ledgers=EXPORT_PUBLICATION_LEDGER_WINDOW,
|
||||
expect_witness=True,
|
||||
):
|
||||
"""Wait through the publication window for an origin-keyed witness."""
|
||||
scanned = after_ledger
|
||||
target = after_ledger + max_ledgers
|
||||
while scanned < target:
|
||||
current = ctx.validated_ledger_index(0)
|
||||
if current is None or current <= scanned:
|
||||
await ctx.wait_for_ledger(scanned + 1, node_id=0, timeout=30)
|
||||
current = ctx.validated_ledger_index(0)
|
||||
if current is None or current <= scanned:
|
||||
continue
|
||||
|
||||
scan_through = min(current, target)
|
||||
for seq in range(scanned + 1, scan_through + 1):
|
||||
witness = find_export_signature_witness(ctx, seq, origin_hash)
|
||||
if witness:
|
||||
if not expect_witness:
|
||||
raise AssertionError(
|
||||
f"Unexpected ExportSignatures witness in ledger {seq}"
|
||||
)
|
||||
log(f" ExportSignatures witness found in ledger {seq}")
|
||||
return assert_export_witness(witness, origin_hash, seq, log)
|
||||
scanned = scan_through
|
||||
|
||||
if expect_witness:
|
||||
raise AssertionError(
|
||||
f"No ExportSignatures witness for {origin_hash} within "
|
||||
f"{max_ledgers} validated ledgers"
|
||||
)
|
||||
log(f" No witness observed through ledger {scanned}")
|
||||
return None
|
||||
|
||||
|
||||
async def wait_for_validated_transaction(
|
||||
ctx, tx_hash, *, after_ledger, max_ledgers=EXPORT_RETRY_LEDGER_WINDOW
|
||||
):
|
||||
"""Resolve a raw non-tes submit result to validated transaction evidence."""
|
||||
checked = after_ledger
|
||||
target = after_ledger + max_ledgers
|
||||
while True:
|
||||
result = ctx.rpc.request(0, "tx", {"transaction": tx_hash}) or {}
|
||||
if result.get("validated"):
|
||||
return result
|
||||
if checked >= target:
|
||||
break
|
||||
await ctx.wait_for_ledger(checked + 1, node_id=0, timeout=30)
|
||||
current = ctx.validated_ledger_index(0)
|
||||
checked = min(target, max(checked + 1, current or checked + 1))
|
||||
raise AssertionError(f"Transaction {tx_hash} did not validate by ledger {target}")
|
||||
|
||||
|
||||
async def submit_direct_export(
|
||||
ctx,
|
||||
log,
|
||||
tx,
|
||||
wallet,
|
||||
*,
|
||||
timeout=60,
|
||||
max_rebases=2,
|
||||
committee_node_ids=None,
|
||||
):
|
||||
"""Submit a direct Export, rebasing after a validated parent mismatch."""
|
||||
for attempt in range(max_rebases + 1):
|
||||
current = ctx.validated_ledger_index(0)
|
||||
if current is None:
|
||||
raise AssertionError("Validated ledger unavailable before Export")
|
||||
|
||||
candidate = dict(tx)
|
||||
candidate.update(export_authority(ctx, committee_node_ids=committee_node_ids))
|
||||
candidate["LastLedgerSequence"] = current + EXPORT_RETRY_LEDGER_WINDOW
|
||||
result = await ctx.submit_and_wait(candidate, wallet, timeout=timeout)
|
||||
if result.get("engine_result") != "tecEXPORT_COMMITTEE_UNAVAILABLE":
|
||||
return result
|
||||
|
||||
tx_hash = result.get("hash") or result.get("tx_json", {}).get("hash")
|
||||
if not tx_hash:
|
||||
raise AssertionError(f"Committee eligibility failure missing tx hash: {result}")
|
||||
validated = result
|
||||
if not result.get("validated"):
|
||||
validated = await wait_for_validated_transaction(
|
||||
ctx, tx_hash, after_ledger=current
|
||||
)
|
||||
meta = validated.get("meta", {})
|
||||
if meta.get("TransactionResult") != "tecEXPORT_COMMITTEE_UNAVAILABLE":
|
||||
raise AssertionError(
|
||||
f"Unexpected validated rebase result for {tx_hash}: {validated}"
|
||||
)
|
||||
log(f" Direct Export parent changed; rebasing attempt {attempt + 1}")
|
||||
|
||||
raise AssertionError(f"Direct Export parent changed more than {max_rebases} times")
|
||||
|
||||
|
||||
def dst_param(address):
|
||||
"""Encode an address as a HookParameter entry for the DST param."""
|
||||
from xrpl.core.addresscodec import decode_classic_address
|
||||
|
||||
dst_hex = decode_classic_address(address).hex().upper()
|
||||
return {
|
||||
"HookParameter": {
|
||||
"HookParameterName": "445354", # "DST"
|
||||
"HookParameterValue": dst_hex,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def assert_hook_accepted(meta, log, *, expected_emits=1, expected_exports=None):
|
||||
"""Assert hook executed with ACCEPT and expected emission counts.
|
||||
|
||||
Checks sfHookExecutions in transaction metadata.
|
||||
Returns the hook execution entry for further inspection.
|
||||
"""
|
||||
hook_execs = meta.get("HookExecutions", [])
|
||||
if not hook_execs:
|
||||
raise AssertionError("No HookExecutions in metadata")
|
||||
|
||||
exec_entry = hook_execs[0].get("HookExecution", {})
|
||||
hook_result = exec_entry.get("HookResult", -1)
|
||||
emit_count = exec_entry.get("HookEmitCount", -1)
|
||||
export_count = exec_entry.get("HookExportCount")
|
||||
return_code = exec_entry.get("HookReturnCode", "")
|
||||
|
||||
log(
|
||||
f" HookResult={hook_result} EmitCount={emit_count} "
|
||||
f"ExportCount={export_count} ReturnCode={return_code}"
|
||||
)
|
||||
|
||||
# HookResult 3 = ExitType::ACCEPT
|
||||
if hook_result != 3:
|
||||
raise AssertionError(
|
||||
f"Hook did not ACCEPT: HookResult={hook_result} ReturnCode={return_code}"
|
||||
)
|
||||
|
||||
if emit_count != expected_emits:
|
||||
raise AssertionError(f"Expected {expected_emits} emits, got {emit_count}")
|
||||
|
||||
if expected_exports is not None and export_count != expected_exports:
|
||||
raise AssertionError(f"Expected {expected_exports} exports, got {export_count}")
|
||||
|
||||
# ReturnCode 0 = success; non-zero = ASSERT line number in hook
|
||||
if return_code and str(return_code) != "0":
|
||||
raise AssertionError(
|
||||
f"Hook returned error code {return_code} "
|
||||
f"(likely ASSERT failure at that line)"
|
||||
)
|
||||
|
||||
return exec_entry
|
||||
|
||||
|
||||
def _signer_entries(witness):
|
||||
entries = []
|
||||
for entry in witness.get("ExportSigners", []):
|
||||
signer = entry.get("ExportSigner", entry)
|
||||
entries.append(signer)
|
||||
return entries
|
||||
|
||||
|
||||
def assert_export_witness(witness, origin_hash, ledger_seq, log):
|
||||
"""Assert a later-ledger witness contains one ordered signature record."""
|
||||
if witness.get("TransactionType") != "ExportSignatures":
|
||||
raise AssertionError("Expected ExportSignatures witness")
|
||||
if witness.get("TransactionHash") != origin_hash:
|
||||
raise AssertionError("ExportSignatures origin binding mismatch")
|
||||
if witness.get("LedgerSequence") != ledger_seq:
|
||||
raise AssertionError("ExportSignatures ledger binding mismatch")
|
||||
if witness.get("Signers"):
|
||||
raise AssertionError("Witness must not contain ordinary Signers")
|
||||
if witness.get("ExportedTxn", {}).get("Signers"):
|
||||
raise AssertionError("Witness ExportedTxn must be unsigned")
|
||||
contributors = witness.get("EntropyContributors")
|
||||
if not contributors:
|
||||
raise AssertionError("ExportSignatures missing contributor bitmap")
|
||||
|
||||
signers = _signer_entries(witness)
|
||||
if not signers:
|
||||
raise AssertionError("ExportSignatures has no ExportSigners")
|
||||
if any(
|
||||
not signer.get("SigningPubKey") or not signer.get("TxnSignature")
|
||||
for signer in signers
|
||||
):
|
||||
raise AssertionError("ExportSignatures has a malformed ExportSigner")
|
||||
contributor_count = sum(byte.bit_count() for byte in bytes.fromhex(contributors))
|
||||
if contributor_count != len(signers):
|
||||
raise AssertionError(
|
||||
"ExportSignatures contributor bitmap and ordered signer count differ"
|
||||
)
|
||||
log(f" Witness signers: {len(signers)} validator(s)")
|
||||
witness["_WitnessSigners"] = signers
|
||||
return witness
|
||||
|
||||
|
||||
def assert_export_latch(
|
||||
ctx,
|
||||
account_address,
|
||||
log,
|
||||
*,
|
||||
expect_exists=True,
|
||||
origin_hash=None,
|
||||
expect_witness=None,
|
||||
ledger_hash=None,
|
||||
):
|
||||
"""Assert Export latch exists (or doesn't) for the account."""
|
||||
params = {"account": account_address, "ledger_index": "validated"}
|
||||
if ledger_hash is not None:
|
||||
del params["ledger_index"]
|
||||
params["ledger_hash"] = ledger_hash
|
||||
obj_result = ctx.rpc.request(0, "account_objects", params)
|
||||
if not obj_result or obj_result.get("error"):
|
||||
raise AssertionError(f"account_objects RPC failed: {obj_result}")
|
||||
if obj_result.get("validated") is not True:
|
||||
raise AssertionError(f"account_objects result is not validated: {obj_result}")
|
||||
if ledger_hash is not None and obj_result.get("ledger_hash") != ledger_hash:
|
||||
raise AssertionError(
|
||||
"account_objects returned wrong ledger: "
|
||||
f"expected {ledger_hash}, got {obj_result.get('ledger_hash')}"
|
||||
)
|
||||
|
||||
all_objects = obj_result.get("account_objects", [])
|
||||
export_latches = [
|
||||
obj for obj in all_objects if obj.get("LedgerEntryType") == "ExportLatch"
|
||||
]
|
||||
log(f" Export latches: {len(export_latches)}")
|
||||
|
||||
if origin_hash is not None:
|
||||
export_latches = [
|
||||
latch
|
||||
for latch in export_latches
|
||||
if latch.get("TransactionHash") == origin_hash
|
||||
]
|
||||
|
||||
if expect_exists and not export_latches:
|
||||
raise AssertionError("Expected Export latch but none found")
|
||||
if not expect_exists and export_latches:
|
||||
raise AssertionError(
|
||||
f"Expected no Export latches but found {len(export_latches)}"
|
||||
)
|
||||
|
||||
for latch in export_latches:
|
||||
if "Digest" not in latch:
|
||||
raise AssertionError(
|
||||
"ExportLatch missing signature-independent intent Digest"
|
||||
)
|
||||
if "TransactionHash" not in latch:
|
||||
raise AssertionError("ExportLatch missing Export origin TransactionHash")
|
||||
if "ExportCommitteeHash" not in latch:
|
||||
raise AssertionError("ExportLatch missing ExportCommitteeHash")
|
||||
if expect_witness is True and "ExportSignatureHash" not in latch:
|
||||
raise AssertionError("ExportLatch missing ExportSignatureHash")
|
||||
if expect_witness is False and "ExportSignatureHash" in latch:
|
||||
raise AssertionError("Pending ExportLatch unexpectedly witnessed")
|
||||
|
||||
return export_latches
|
||||
@@ -0,0 +1,96 @@
|
||||
""":descr: Export succeeds when quorum sidecar material exists but one active
|
||||
validator withholds exportSigSetHash observation.
|
||||
|
||||
Node 4 has runtime_config no_export_sig_hash=true. It still attaches export
|
||||
signatures, but it does not publish its exportSigSetHash in proposals. The
|
||||
remaining 4/5 active validators can still align on the same export sidecar
|
||||
hash, so the round must not retry/expire just because fullObservation is false.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from export_helpers import (
|
||||
EXPORT_RETRY_LEDGER_WINDOW,
|
||||
assert_export_latch,
|
||||
require_export,
|
||||
submit_direct_export,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_export(ctx, log)
|
||||
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
|
||||
log("Accounts funded")
|
||||
|
||||
alice = ctx.account("alice")
|
||||
bob = ctx.account("bob")
|
||||
current_seq = ctx.validated_ledger_index(0)
|
||||
|
||||
log(f"Current ledger: {current_seq}")
|
||||
log("Node 4 withholds exportSigSetHash but still attaches export signatures")
|
||||
|
||||
export_start = ctx.mark("export-no-veto-submit-start")
|
||||
result = await submit_direct_export(
|
||||
ctx,
|
||||
log,
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Fee": "1000000",
|
||||
"ExportedTxn": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": alice.address,
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "10",
|
||||
"Sequence": 0,
|
||||
"TicketSequence": 1,
|
||||
"FirstLedgerSequence": current_seq + 1,
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Flags": 2147483648,
|
||||
"SigningPubKey": "",
|
||||
},
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
final_seq = result.get("ledger_index", ctx.validated_ledger_index(0))
|
||||
origin_hash = result.get("hash")
|
||||
engine_result = result.get("engine_result", "")
|
||||
|
||||
log(f"Export completed at ledger {final_seq}, result: {engine_result}")
|
||||
if engine_result != "tesSUCCESS":
|
||||
raise AssertionError(f"Expected tesSUCCESS, got {engine_result}")
|
||||
if not origin_hash:
|
||||
raise AssertionError(f"Validated Export missing hash: {result}")
|
||||
|
||||
witness = await wait_for_export_signature_witness(
|
||||
ctx, log, origin_hash, after_ledger=final_seq
|
||||
)
|
||||
signers = witness.get("_WitnessSigners", [])
|
||||
if len(signers) < 4:
|
||||
raise AssertionError(f"Expected at least 4 signers, got {len(signers)}")
|
||||
log(f"Export signer count: {len(signers)}")
|
||||
|
||||
# The validated witness proves the missing observation did not veto the
|
||||
# round. Pin the injected fault separately; the internal no-veto diagnostic
|
||||
# may be flushed after witness observation and is not part of the contract.
|
||||
withhold_logs = ctx.assert_log(
|
||||
r"Export: withholding exportSigSetHash",
|
||||
since=export_start,
|
||||
)
|
||||
log(f"Export sidecar hash withholding logs: {withhold_logs.count}")
|
||||
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
expect_exists=True,
|
||||
origin_hash=origin_hash,
|
||||
expect_witness=True,
|
||||
)
|
||||
|
||||
log("PASS")
|
||||
129
.testnet/scenarios/export/export_quorum.py
Normal file
129
.testnet/scenarios/export/export_quorum.py
Normal file
@@ -0,0 +1,129 @@
|
||||
""":descr: Test Export witness quorum behavior. Every valid intent is admitted;
|
||||
enough selected validators produce a later witness, while a below-quorum intent
|
||||
remains unwitnessed through its bounded publication window.
|
||||
|
||||
Parameterized via `expect_success` kwarg from suite.yml.
|
||||
|
||||
Flow:
|
||||
1. Fund alice and bob
|
||||
2. alice submits ttEXPORT
|
||||
3. Verify the intent validates with tesSUCCESS
|
||||
4. Verify a later witness exists only when the committee reaches quorum
|
||||
5. Verify subsequent payment works regardless
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from export_helpers import (
|
||||
EXPORT_RETRY_LEDGER_WINDOW,
|
||||
assert_export_latch,
|
||||
require_export,
|
||||
submit_direct_export,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
|
||||
|
||||
async def scenario(ctx, log, expect_success=True):
|
||||
await require_export(ctx, log)
|
||||
|
||||
# --- Setup ---
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
|
||||
log("Accounts funded")
|
||||
|
||||
alice = ctx.account("alice")
|
||||
bob = ctx.account("bob")
|
||||
current_seq = ctx.validated_ledger_index(0)
|
||||
|
||||
log(f"Current ledger: {current_seq}")
|
||||
outcome = "success" if expect_success else "failure (below quorum)"
|
||||
log(f"Expecting export {outcome}")
|
||||
|
||||
# --- Submit ttEXPORT ---
|
||||
result = await submit_direct_export(
|
||||
ctx,
|
||||
log,
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Fee": "1000000",
|
||||
"ExportedTxn": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": alice.address,
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "10",
|
||||
"Sequence": 0,
|
||||
"TicketSequence": 1,
|
||||
"FirstLedgerSequence": current_seq + 1,
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Flags": 2147483648,
|
||||
"SigningPubKey": "",
|
||||
},
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
final_seq = result.get("ledger_index", ctx.validated_ledger_index(0))
|
||||
origin_hash = result.get("hash")
|
||||
engine_result = result.get("engine_result", "")
|
||||
|
||||
log(f"Export at ledger {final_seq}, result: {engine_result}")
|
||||
if engine_result != "tesSUCCESS":
|
||||
raise AssertionError(f"Expected intent tesSUCCESS, got {engine_result}")
|
||||
if not origin_hash:
|
||||
raise AssertionError(f"Validated Export missing hash: {result}")
|
||||
|
||||
if expect_success:
|
||||
await wait_for_export_signature_witness(
|
||||
ctx, log, origin_hash, after_ledger=final_seq
|
||||
)
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
expect_exists=True,
|
||||
origin_hash=origin_hash,
|
||||
expect_witness=True,
|
||||
)
|
||||
|
||||
log("Export succeeded as expected (active-view quorum reached)")
|
||||
else:
|
||||
await wait_for_export_signature_witness(
|
||||
ctx,
|
||||
log,
|
||||
origin_hash,
|
||||
after_ledger=final_seq,
|
||||
expect_witness=False,
|
||||
)
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
expect_exists=True,
|
||||
origin_hash=origin_hash,
|
||||
expect_witness=False,
|
||||
)
|
||||
log("Intent remained unwitnessed as expected (below committee quorum)")
|
||||
|
||||
# --- Verify subsequent payment works ---
|
||||
log("Submitting payment from alice to bob...")
|
||||
pay_result = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "Payment",
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "12",
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
pay_engine = pay_result.get("engine_result", "")
|
||||
log(f"Payment result: {pay_engine}")
|
||||
|
||||
if pay_engine != "tesSUCCESS":
|
||||
raise AssertionError(f"Payment failed: {pay_engine}")
|
||||
|
||||
log("Payment succeeded -- account not blocked")
|
||||
log("PASS")
|
||||
226
.testnet/scenarios/export/export_share_subscription.py
Normal file
226
.testnet/scenarios/export/export_share_subscription.py
Normal file
@@ -0,0 +1,226 @@
|
||||
""":descr: subscribe over a real WebSocket to post-validation Export shares
|
||||
|
||||
The subscriber opens before the Export is submitted. It proves the stream
|
||||
publishes a quorum of independently attributable shares for the exact validated
|
||||
origin and that those same signature records form the later ledger witness.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
|
||||
import websockets
|
||||
from xahaud_scripts.testnet.config import _decode_node_public_key
|
||||
|
||||
from export_helpers import (
|
||||
EXPORT_RETRY_LEDGER_WINDOW,
|
||||
assert_export_latch,
|
||||
bitmap_positions,
|
||||
export_authority,
|
||||
require_export,
|
||||
submit_direct_export,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
|
||||
|
||||
def _witness_records(witness):
|
||||
positions = sorted(bitmap_positions(witness["EntropyContributors"]))
|
||||
signers = witness["_WitnessSigners"]
|
||||
if len(positions) != len(signers):
|
||||
raise AssertionError("Witness bitmap and signer count differ")
|
||||
return {
|
||||
(
|
||||
position,
|
||||
signer["SigningPubKey"].upper(),
|
||||
signer["TxnSignature"].upper(),
|
||||
)
|
||||
for position, signer in zip(positions, signers, strict=True)
|
||||
}
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_export(ctx, log)
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
|
||||
alice = ctx.account("alice")
|
||||
bob = ctx.account("bob")
|
||||
|
||||
network = json.loads((ctx.base_dir / "network.json").read_text())
|
||||
node0 = next(node for node in network["nodes"] if int(node["id"]) == 0)
|
||||
ws_url = f"ws://127.0.0.1:{node0['port_ws']}"
|
||||
events = []
|
||||
|
||||
async with websockets.connect(ws_url, open_timeout=10) as websocket:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"id": 1,
|
||||
"command": "subscribe",
|
||||
"streams": ["export_signatures"],
|
||||
}
|
||||
)
|
||||
)
|
||||
ack = json.loads(await asyncio.wait_for(websocket.recv(), timeout=10))
|
||||
if ack.get("status") != "success":
|
||||
raise AssertionError(f"export_signatures subscription failed: {ack}")
|
||||
log("Subscribed to export_signatures over WebSocket")
|
||||
|
||||
async def receive_events():
|
||||
while True:
|
||||
event = json.loads(await websocket.recv())
|
||||
if event.get("stream") == "export_signatures":
|
||||
events.append(event)
|
||||
|
||||
reader = asyncio.create_task(receive_events())
|
||||
try:
|
||||
current = ctx.validated_ledger_index(0)
|
||||
committee_size = (
|
||||
len(bytes.fromhex(export_authority(ctx)["ExportCommittee"])) // 33
|
||||
)
|
||||
result = await submit_direct_export(
|
||||
ctx,
|
||||
log,
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"Fee": "1000000",
|
||||
"ExportedTxn": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": alice.address,
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "10",
|
||||
"Sequence": 0,
|
||||
"TicketSequence": 1,
|
||||
"FirstLedgerSequence": current + 1,
|
||||
"LastLedgerSequence": current + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Flags": 2147483648,
|
||||
"SigningPubKey": "",
|
||||
},
|
||||
},
|
||||
alice.wallet,
|
||||
)
|
||||
if result.get("engine_result") != "tesSUCCESS":
|
||||
raise AssertionError(f"Export failed: {result}")
|
||||
|
||||
origin = result.get("hash")
|
||||
origin_seq = int(result.get("ledger_index"))
|
||||
if not origin:
|
||||
raise AssertionError(f"Validated Export missing hash: {result}")
|
||||
origin_ledger = ctx.ledger(origin_seq) or {}
|
||||
origin_hash = origin_ledger.get("ledger_hash") or origin_ledger.get(
|
||||
"ledger", {}
|
||||
).get("hash")
|
||||
if not origin_hash:
|
||||
raise AssertionError(
|
||||
f"Validated origin ledger {origin_seq} missing hash"
|
||||
)
|
||||
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
origin_hash=origin,
|
||||
expect_witness=False,
|
||||
)
|
||||
selected = set(range(committee_size))
|
||||
quorum = (4 * committee_size + 4) // 5
|
||||
|
||||
witness = await wait_for_export_signature_witness(
|
||||
ctx, log, origin, after_ledger=origin_seq
|
||||
)
|
||||
expected_records = _witness_records(witness)
|
||||
if len(expected_records) < quorum:
|
||||
raise AssertionError(
|
||||
f"Witness contains only {len(expected_records)} distinct records; "
|
||||
f"need quorum {quorum}"
|
||||
)
|
||||
|
||||
deadline = asyncio.get_running_loop().time() + 10
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
matching = [
|
||||
event for event in events if event.get("origin_txid") == origin
|
||||
]
|
||||
events_by_record = {}
|
||||
for event in matching:
|
||||
record = (
|
||||
int(event["committee_position"]),
|
||||
_decode_node_public_key(event["signing_key"]),
|
||||
event["signature"].upper(),
|
||||
)
|
||||
events_by_record.setdefault(record, []).append(event)
|
||||
if expected_records <= events_by_record.keys():
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
raise AssertionError(
|
||||
"WebSocket stream did not publish every signature used by "
|
||||
f"the witness: expected={expected_records}, "
|
||||
f"observed={set(events_by_record)}"
|
||||
)
|
||||
|
||||
unique_positions = set()
|
||||
validated_hashes = {}
|
||||
for event in matching:
|
||||
if event.get("type") != "exportSignatureReceived":
|
||||
raise AssertionError(f"Unexpected Export stream event: {event}")
|
||||
if event.get("version") != 1:
|
||||
raise AssertionError(f"Unexpected Export share version: {event}")
|
||||
if event.get("owner") != alice.address:
|
||||
raise AssertionError(f"Export stream owner mismatch: {event}")
|
||||
if int(event.get("origin_ledger_seq", 0)) != origin_seq:
|
||||
raise AssertionError(
|
||||
f"Export stream origin sequence mismatch: {event}"
|
||||
)
|
||||
if event.get("origin_ledger_hash") != origin_hash:
|
||||
raise AssertionError(f"Export stream origin hash mismatch: {event}")
|
||||
position = int(event.get("committee_position", -1))
|
||||
if position not in selected:
|
||||
raise AssertionError(
|
||||
f"Unselected validator streamed a share: {event}"
|
||||
)
|
||||
unique_positions.add(position)
|
||||
|
||||
witness_seq = int(witness["LedgerSequence"])
|
||||
for event in matching:
|
||||
ledger_index = event.get("ledger_index")
|
||||
ledger_hash = event.get("ledger_hash")
|
||||
if isinstance(ledger_index, bool) or not isinstance(ledger_index, int):
|
||||
raise AssertionError(
|
||||
f"Export stream event missing numeric ledger_index: {event}"
|
||||
)
|
||||
if not isinstance(ledger_hash, str) or not ledger_hash:
|
||||
raise AssertionError(
|
||||
f"Export stream event missing ledger_hash: {event}"
|
||||
)
|
||||
if ledger_index not in validated_hashes:
|
||||
observed_ledger = ctx.ledger(ledger_index) or {}
|
||||
validated_hashes[ledger_index] = observed_ledger.get(
|
||||
"ledger_hash"
|
||||
) or observed_ledger.get("ledger", {}).get("hash")
|
||||
if ledger_hash != validated_hashes[ledger_index]:
|
||||
raise AssertionError(
|
||||
"Export stream cursor does not name the validated ledger: "
|
||||
f"event={event}, expected_hash={validated_hashes[ledger_index]}"
|
||||
)
|
||||
|
||||
for record in expected_records:
|
||||
if not any(
|
||||
origin_seq <= event["ledger_index"] < witness_seq
|
||||
for event in events_by_record[record]
|
||||
):
|
||||
raise AssertionError(
|
||||
"Witness signature lacked a pre-witness stream event with a "
|
||||
f"validated cursor: record={record}, "
|
||||
f"events={events_by_record[record]}"
|
||||
)
|
||||
log(
|
||||
f"WebSocket exposed {len(unique_positions)} selected shares; "
|
||||
f"witness used {len(expected_records)}"
|
||||
)
|
||||
finally:
|
||||
reader.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await reader
|
||||
|
||||
log("PASS")
|
||||
97
.testnet/scenarios/export/export_without_unl_report.py
Normal file
97
.testnet/scenarios/export/export_without_unl_report.py
Normal file
@@ -0,0 +1,97 @@
|
||||
""":descr: Export fails closed without a ledger-anchored UNLReport view.
|
||||
|
||||
Network-mode Export must not derive authority from a node-local trusted-config
|
||||
view. An explicit parent binding still fails if that parent has no UNLReport,
|
||||
and no Export latch is created.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from export_helpers import (
|
||||
EXPORT_RETRY_LEDGER_WINDOW,
|
||||
assert_export_latch,
|
||||
export_authority,
|
||||
require_export,
|
||||
wait_for_validated_transaction,
|
||||
)
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_export(ctx, log, require_unl_report=False)
|
||||
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
|
||||
log("Accounts funded")
|
||||
|
||||
alice = ctx.account("alice")
|
||||
bob = ctx.account("bob")
|
||||
current_seq = ctx.validated_ledger_index(0)
|
||||
|
||||
log(f"Current ledger: {current_seq}")
|
||||
log("UNLReport intentionally absent; export must not use local config view")
|
||||
|
||||
result = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Fee": "1000000",
|
||||
**export_authority(ctx, require_unl_report=False),
|
||||
"ExportedTxn": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": alice.address,
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "10",
|
||||
"Sequence": 0,
|
||||
"TicketSequence": 1,
|
||||
"FirstLedgerSequence": current_seq + 1,
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Flags": 2147483648,
|
||||
"SigningPubKey": "",
|
||||
},
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
engine_result = result.get("engine_result", "")
|
||||
log(f"Export submit result: {engine_result}")
|
||||
|
||||
if engine_result == "tesSUCCESS":
|
||||
raise AssertionError(
|
||||
"Export should not succeed without a ledger-anchored UNLReport view"
|
||||
)
|
||||
|
||||
if engine_result != "tecEXPORT_COMMITTEE_UNAVAILABLE":
|
||||
raise AssertionError(
|
||||
"Expected tecEXPORT_COMMITTEE_UNAVAILABLE without UNLReport view, "
|
||||
f"got {engine_result}"
|
||||
)
|
||||
|
||||
tx_hash = result.get("tx_json", {}).get("hash")
|
||||
if not tx_hash:
|
||||
raise AssertionError(f"Rejected Export missing tx hash: {result}")
|
||||
validated = await wait_for_validated_transaction(
|
||||
ctx, tx_hash, after_ledger=current_seq
|
||||
)
|
||||
meta = validated.get("meta", validated.get("metaData", {}))
|
||||
if meta.get("TransactionResult") != "tecEXPORT_COMMITTEE_UNAVAILABLE":
|
||||
raise AssertionError(f"Unexpected validated result: {validated}")
|
||||
|
||||
final_seq = validated.get("ledger_index")
|
||||
final_ledger = ctx.ledger(final_seq) or {}
|
||||
ledger_hash = final_ledger.get("ledger_hash") or final_ledger.get("ledger", {}).get(
|
||||
"hash"
|
||||
)
|
||||
if not ledger_hash:
|
||||
raise AssertionError(f"Validated failure ledger unavailable: {final_ledger}")
|
||||
log(f"Export failure validated in ledger {final_seq}")
|
||||
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
expect_exists=False,
|
||||
ledger_hash=ledger_hash,
|
||||
)
|
||||
|
||||
log("PASS")
|
||||
110
.testnet/scenarios/export/retriable_export.py
Normal file
110
.testnet/scenarios/export/retriable_export.py
Normal file
@@ -0,0 +1,110 @@
|
||||
""":descr: Submit ttEXPORT directly (no hook), verify the intent is admitted
|
||||
and a later validated ledger records its signature witness. Then submit a
|
||||
payment from the same account to verify sequence handling remains independent.
|
||||
|
||||
Flow:
|
||||
1. Fund alice and bob
|
||||
2. alice submits ttEXPORT with an explicit parent-universe declaration
|
||||
3. Validation releases shares; a later ledger records ExportSignatures
|
||||
4. alice submits a Payment to bob -> should succeed (sequence not blocked)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from export_helpers import (
|
||||
EXPORT_RETRY_LEDGER_WINDOW,
|
||||
assert_export_latch,
|
||||
require_export,
|
||||
submit_direct_export,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await require_export(ctx, log)
|
||||
|
||||
# --- Setup ---
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
|
||||
log("Accounts funded")
|
||||
|
||||
alice = ctx.account("alice")
|
||||
bob = ctx.account("bob")
|
||||
current_seq = ctx.validated_ledger_index(0)
|
||||
|
||||
log(f"Current ledger: {current_seq}")
|
||||
|
||||
# --- 1. Submit ttEXPORT ---
|
||||
result = await submit_direct_export(
|
||||
ctx,
|
||||
log,
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Fee": "1000000",
|
||||
"ExportedTxn": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": alice.address,
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "10",
|
||||
"Sequence": 0,
|
||||
"TicketSequence": 1,
|
||||
"FirstLedgerSequence": current_seq + 1,
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Flags": 2147483648,
|
||||
"SigningPubKey": "",
|
||||
},
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
export_seq = result.get("ledger_index", ctx.validated_ledger_index(0))
|
||||
origin_hash = result.get("hash")
|
||||
engine_result = result.get("engine_result", "")
|
||||
log(f"Export completed at ledger {export_seq}, result: {engine_result}")
|
||||
|
||||
if engine_result != "tesSUCCESS":
|
||||
raise AssertionError(f"Expected tesSUCCESS for export, got {engine_result}")
|
||||
|
||||
if not origin_hash:
|
||||
raise AssertionError(f"Validated Export missing hash: {result}")
|
||||
|
||||
await wait_for_export_signature_witness(
|
||||
ctx, log, origin_hash, after_ledger=export_seq
|
||||
)
|
||||
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
expect_exists=True,
|
||||
origin_hash=origin_hash,
|
||||
expect_witness=True,
|
||||
)
|
||||
|
||||
# --- 2. Submit Payment from same account ---
|
||||
log("Submitting payment from alice to bob...")
|
||||
pay_result = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "Payment",
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "12",
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
pay_engine = pay_result.get("engine_result", "")
|
||||
log(f"Payment result: {pay_engine}")
|
||||
|
||||
if pay_engine != "tesSUCCESS":
|
||||
raise AssertionError(f"Payment failed: {pay_engine}")
|
||||
|
||||
log(
|
||||
f"Both transactions succeeded: "
|
||||
f"Export at ledger {export_seq}, Payment at ledger {ctx.validated_ledger_index(0)}"
|
||||
)
|
||||
log("Sequence handling OK - export didn't block subsequent txns")
|
||||
log("PASS")
|
||||
230
.testnet/scenarios/export/steady_state_export.py
Normal file
230
.testnet/scenarios/export/steady_state_export.py
Normal file
@@ -0,0 +1,230 @@
|
||||
""":descr: install xport hook, trigger export, verify emitted ttEXPORT lifecycle
|
||||
|
||||
1. Fund alice (hook holder), bob (trigger), carol (export destination)
|
||||
2. Install xport hook on alice
|
||||
3. bob pays alice with DST=carol → hook calls xport() → emits ttEXPORT
|
||||
4. Emitted ttEXPORT enters a validated ledger and releases signatures
|
||||
5. Verify a later ledger records its ExportSignatures witness
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from export_helpers import (
|
||||
require_export,
|
||||
find_export_txns,
|
||||
dst_param,
|
||||
assert_hook_accepted,
|
||||
assert_export_latch,
|
||||
create_export_committee,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
|
||||
# C source for the xport hook — verbatim from src/test/app/Export_test_hooks.h
|
||||
# On Payment to the hook account, exports a 1 XAH payment to the DST param.
|
||||
XPORT_HOOK_C = r"""
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t committee_hash_ptr, uint32_t committee_hash_len);
|
||||
extern int64_t xport_reserve(uint32_t count);
|
||||
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
|
||||
extern int64_t otxn_type(void);
|
||||
extern int64_t ledger_seq(void);
|
||||
|
||||
#define SBUF(x) (uint32_t)(x), sizeof(x)
|
||||
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
|
||||
|
||||
#define ttPAYMENT 0
|
||||
#define tfCANONICAL 0x80000000UL
|
||||
#define amAMOUNT 1
|
||||
#define amFEE 8
|
||||
#define atACCOUNT 1
|
||||
#define atDESTINATION 3
|
||||
|
||||
#define ENCODE_TT(buf_out, tt) \
|
||||
buf_out[0] = 0x12U; buf_out[1] = (tt >> 8) & 0xFFU; buf_out[2] = tt & 0xFFU; buf_out += 3;
|
||||
|
||||
#define ENCODE_FLAGS(buf_out, flags) \
|
||||
buf_out[0] = 0x22U; buf_out[1] = (flags >> 24) & 0xFFU; buf_out[2] = (flags >> 16) & 0xFFU; \
|
||||
buf_out[3] = (flags >> 8) & 0xFFU; buf_out[4] = flags & 0xFFU; buf_out += 5;
|
||||
|
||||
#define ENCODE_SEQUENCE(buf_out, seq) \
|
||||
buf_out[0] = 0x24U; buf_out[1] = (seq >> 24) & 0xFFU; buf_out[2] = (seq >> 16) & 0xFFU; \
|
||||
buf_out[3] = (seq >> 8) & 0xFFU; buf_out[4] = seq & 0xFFU; buf_out += 5;
|
||||
|
||||
#define ENCODE_FLS(buf_out, fls) \
|
||||
buf_out[0] = 0x20U; buf_out[1] = 0x1AU; buf_out[2] = (fls >> 24) & 0xFFU; \
|
||||
buf_out[3] = (fls >> 16) & 0xFFU; buf_out[4] = (fls >> 8) & 0xFFU; \
|
||||
buf_out[5] = fls & 0xFFU; buf_out += 6;
|
||||
|
||||
#define ENCODE_LLS(buf_out, lls) \
|
||||
buf_out[0] = 0x20U; buf_out[1] = 0x1BU; buf_out[2] = (lls >> 24) & 0xFFU; \
|
||||
buf_out[3] = (lls >> 16) & 0xFFU; buf_out[4] = (lls >> 8) & 0xFFU; \
|
||||
buf_out[5] = lls & 0xFFU; buf_out += 6;
|
||||
|
||||
#define ENCODE_DROPS(buf_out, drops, amt_type) \
|
||||
buf_out[0] = 0x60U + amt_type; buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
|
||||
buf_out[2] = (drops >> 48) & 0xFFU; buf_out[3] = (drops >> 40) & 0xFFU; \
|
||||
buf_out[4] = (drops >> 32) & 0xFFU; buf_out[5] = (drops >> 24) & 0xFFU; \
|
||||
buf_out[6] = (drops >> 16) & 0xFFU; buf_out[7] = (drops >> 8) & 0xFFU; \
|
||||
buf_out[8] = drops & 0xFFU; buf_out += 9;
|
||||
|
||||
#define ENCODE_SIGNING_PUBKEY_EMPTY(buf_out) \
|
||||
buf_out[0] = 0x73U; buf_out[1] = 0x00U; buf_out += 2;
|
||||
|
||||
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
|
||||
buf_out[0] = 0x80U + acc_type; buf_out[1] = 0x14U; \
|
||||
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; buf_out += 22;
|
||||
|
||||
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
|
||||
if (otxn_type() != ttPAYMENT)
|
||||
return accept(0, 0, 0);
|
||||
|
||||
ASSERT(xport_reserve(1) == 1);
|
||||
|
||||
uint8_t dst[20];
|
||||
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
|
||||
ASSERT(dst_len == 20);
|
||||
|
||||
uint8_t acc[20];
|
||||
ASSERT(hook_account(SBUF(acc)) == 20);
|
||||
|
||||
uint32_t cls = (uint32_t)ledger_seq();
|
||||
|
||||
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
|
||||
uint8_t* buf = tx;
|
||||
|
||||
ENCODE_TT(buf, ttPAYMENT);
|
||||
ENCODE_FLAGS(buf, tfCANONICAL);
|
||||
ENCODE_SEQUENCE(buf, 0);
|
||||
ENCODE_FLS(buf, cls + 1);
|
||||
ENCODE_LLS(buf, cls + 5);
|
||||
// sfTicketSequence = UINT32 field 41 = 0x20 0x29
|
||||
buf[0] = 0x20U; buf[1] = 0x29U;
|
||||
buf[2] = 0; buf[3] = 0; buf[4] = 0; buf[5] = 1;
|
||||
buf += 6;
|
||||
|
||||
uint64_t drops = 1000000;
|
||||
ENCODE_DROPS(buf, drops, amAMOUNT);
|
||||
ENCODE_DROPS(buf, 10, amFEE);
|
||||
|
||||
ENCODE_SIGNING_PUBKEY_EMPTY(buf);
|
||||
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
|
||||
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
|
||||
|
||||
uint8_t hash[32];
|
||||
static const uint8_t committee_hash[32] = { COMMITTEE_HASH_BYTES };
|
||||
int64_t xport_result = xport(
|
||||
SBUF(hash), (uint32_t)tx, buf - tx, SBUF(committee_hash));
|
||||
ASSERT(xport_result == 32);
|
||||
|
||||
return accept(0, 0, 0);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
# Wait for network to start and amendments to activate
|
||||
await require_export(ctx, log)
|
||||
|
||||
# --- Setup ---
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 10000, "carol": 1000})
|
||||
log("Accounts funded")
|
||||
|
||||
alice = ctx.account("alice")
|
||||
carol = ctx.account("carol")
|
||||
|
||||
authority = await create_export_committee(ctx, log, alice.wallet)
|
||||
committee_bytes = bytes.fromhex(authority["ExportCommitteeHash"])
|
||||
committee_initializer = ", ".join(f"0x{byte:02X}" for byte in committee_bytes)
|
||||
hook_source = XPORT_HOOK_C.replace("COMMITTEE_HASH_BYTES", committee_initializer)
|
||||
|
||||
# Compile and install xport hook on alice
|
||||
wasm = ctx.compile_hook(hook_source, label="xport")
|
||||
await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "SetHook",
|
||||
"Hooks": [
|
||||
{
|
||||
"Hook": {
|
||||
"CreateCode": wasm.hex().upper(),
|
||||
"HookOn": "0" * 64,
|
||||
"HookNamespace": "0" * 64,
|
||||
"HookApiVersion": 0,
|
||||
"Flags": 1, # hsfOVERRIDE
|
||||
}
|
||||
}
|
||||
],
|
||||
"Fee": "100000000",
|
||||
},
|
||||
alice.wallet,
|
||||
)
|
||||
log(
|
||||
f"Hook installed on alice ({alice.address[:12]}...) "
|
||||
f"ledger {ctx.validated_ledger_index(0)}"
|
||||
)
|
||||
|
||||
# --- Trigger ---
|
||||
# bob pays alice → hook calls xport() → emits ttEXPORT
|
||||
trigger_result = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "Payment",
|
||||
"Destination": alice.address,
|
||||
"Amount": "100000000",
|
||||
"Fee": "1000000",
|
||||
"HookParameters": [dst_param(carol.address)],
|
||||
},
|
||||
ctx.account("bob").wallet,
|
||||
)
|
||||
trigger_seq = ctx.validated_ledger_index(0)
|
||||
log(f"Export triggered at ledger {trigger_seq}")
|
||||
|
||||
# xport() schedules a ttEXPORT through the emitted directory, but hook
|
||||
# metadata reports it separately from ordinary HookEmissions.
|
||||
trigger_meta = trigger_result.get("meta", {})
|
||||
assert_hook_accepted(trigger_meta, log, expected_emits=0, expected_exports=1)
|
||||
|
||||
# --- Verify: check each ledger close for the Export transaction ---
|
||||
max_ledgers = 10
|
||||
for i in range(max_ledgers):
|
||||
await ctx.wait_for_ledgers(1, node_id=0, timeout=30)
|
||||
seq = ctx.validated_ledger_index(0)
|
||||
exports = find_export_txns(ctx, seq)
|
||||
if exports:
|
||||
export_tx = exports[0]
|
||||
meta = export_tx.get("meta", export_tx.get("metaData", {}))
|
||||
result = meta.get("TransactionResult", "")
|
||||
log(f"Ledger {seq}: Export txn found, result={result}")
|
||||
|
||||
if result != "tesSUCCESS":
|
||||
raise AssertionError(f"Export did not succeed: {result}")
|
||||
|
||||
origin_hash = export_tx.get("hash")
|
||||
if not origin_hash:
|
||||
raise AssertionError(f"Export missing transaction hash: {export_tx}")
|
||||
await wait_for_export_signature_witness(
|
||||
ctx, log, origin_hash, after_ledger=seq
|
||||
)
|
||||
|
||||
assert_export_latch(
|
||||
ctx,
|
||||
alice.address,
|
||||
log,
|
||||
expect_exists=True,
|
||||
origin_hash=origin_hash,
|
||||
expect_witness=True,
|
||||
)
|
||||
|
||||
log("PASS")
|
||||
return
|
||||
log(f"Ledger {seq}: no Export txn yet")
|
||||
|
||||
raise AssertionError(
|
||||
f"No Export transaction found after {max_ledgers} ledger closes"
|
||||
)
|
||||
180
.testnet/scenarios/helpers.py
Normal file
180
.testnet/scenarios/helpers.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Shared helpers for ConsensusEntropy scenario tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from xahaud_scripts.testnet.config import feature_name_to_hash
|
||||
|
||||
ZERO_DIGEST = "0" * 64
|
||||
CONSENSUS_ENTROPY_FEATURE = feature_name_to_hash("ConsensusEntropy")
|
||||
|
||||
|
||||
def feature_hash(name: str) -> str:
|
||||
"""Return the amendment hash accepted by feature RPC."""
|
||||
return feature_name_to_hash(name)
|
||||
|
||||
|
||||
def feature_status(ctx, name: str, node_id=0):
|
||||
"""Query a feature by amendment hash; feature RPC names are ambiguous."""
|
||||
return ctx.feature_check(feature_hash(name), node_id=node_id)
|
||||
|
||||
|
||||
def consensus_entropy_feature(ctx, node_id=0):
|
||||
"""Query ConsensusEntropy by amendment hash."""
|
||||
return feature_status(ctx, "ConsensusEntropy", node_id=node_id)
|
||||
|
||||
|
||||
async def require_entropy(ctx, log):
|
||||
"""Wait for first ledger and assert ConsensusEntropy is enabled."""
|
||||
await ctx.wait_for_ledger_close(timeout=120)
|
||||
feature = consensus_entropy_feature(ctx, node_id=0)
|
||||
if not feature or not feature.get("enabled", False):
|
||||
raise AssertionError(f"ConsensusEntropy not enabled: {feature}")
|
||||
log("ConsensusEntropy enabled")
|
||||
|
||||
|
||||
def get_entropy_tx(ctx, seq):
|
||||
"""Fetch ledger and return (ce_tx, user_txns) or raise."""
|
||||
result = ctx.ledger(seq, transactions=True)
|
||||
if not result:
|
||||
raise AssertionError(f"Ledger {seq}: fetch failed")
|
||||
|
||||
ledger = result.get("ledger")
|
||||
if not isinstance(ledger, dict):
|
||||
raise AssertionError(f"Ledger {seq}: fetch returned no ledger: {result}")
|
||||
|
||||
txns = ledger.get("transactions", [])
|
||||
ce = [tx for tx in txns if tx.get("TransactionType") == "ConsensusEntropy"]
|
||||
user = [tx for tx in txns if tx.get("TransactionType") != "ConsensusEntropy"]
|
||||
|
||||
if len(ce) != 1:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: expected 1 ConsensusEntropy txn, got {len(ce)}"
|
||||
)
|
||||
|
||||
return ce[0], user
|
||||
|
||||
|
||||
def entropy_fields(ce_tx):
|
||||
"""Return (digest, entropy_count, is_fallback) from a ConsensusEntropy tx.
|
||||
|
||||
consensus_fallback rounds carry a deterministic non-zero consensus-bound
|
||||
digest with EntropyCount=0 and EntropyTier=1 (consensus_fallback).
|
||||
Validator entropy has EntropyTier=3 (validator_quorum).
|
||||
|
||||
WARNING: is_fallback is ``tier != 3``, so it lumps participant_aligned
|
||||
(Tier 2) in with fallback. It is only safe where no Tier 2 band exists
|
||||
(e.g. 5-node networks, where tier2 == quorum). For band-aware scenarios use
|
||||
the explicit assert_consensus_fallback / assert_participant_aligned /
|
||||
assert_validator_quorum helpers, which check EntropyTier directly.
|
||||
"""
|
||||
digest = ce_tx.get("Digest", "")
|
||||
entropy_count = ce_tx.get("EntropyCount", -1)
|
||||
tier = ce_tx.get("EntropyTier", None)
|
||||
if tier is not None:
|
||||
is_fallback = tier != 3
|
||||
else:
|
||||
is_fallback = entropy_count == 0
|
||||
return digest, entropy_count, is_fallback
|
||||
|
||||
|
||||
def assert_participant_aligned(ce_tx, seq, expected_count=None):
|
||||
"""Assert participant_aligned (Tier 2) entropy on a ConsensusEntropy tx.
|
||||
|
||||
Tier 2 is the sub-quorum band: the agreed reveal cohort is >= the
|
||||
participant floor but < the 80% validator quorum, so it carries
|
||||
EntropyTier=2 with a deterministic non-zero digest. NOTE entropy_fields()'s
|
||||
is_fallback lumps tier 2 in with fallback (is_fallback = tier != 3), so the
|
||||
tier must be checked EXPLICITLY here.
|
||||
"""
|
||||
digest = ce_tx.get("Digest", "")
|
||||
count = ce_tx.get("EntropyCount", -1)
|
||||
tier = ce_tx.get("EntropyTier", None)
|
||||
if tier != 2:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: expected EntropyTier==2 (participant_aligned), "
|
||||
f"got {tier} (EntropyCount={count})"
|
||||
)
|
||||
if not digest or digest == ZERO_DIGEST:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: participant_aligned digest must be non-zero, got "
|
||||
f"{digest[:16]}..."
|
||||
)
|
||||
if expected_count is not None and count != expected_count:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: participant_aligned EntropyCount must be "
|
||||
f"{expected_count} (the surviving cohort), got {count}"
|
||||
)
|
||||
return digest, count
|
||||
|
||||
|
||||
def assert_validator_quorum(ce_tx, seq, min_count=None):
|
||||
"""Assert validator_quorum (Tier 3) entropy on a ConsensusEntropy tx:
|
||||
EntropyTier=3, a deterministic non-zero digest, and (optionally)
|
||||
EntropyCount >= min_count (the active quorum). The count can EXCEED the
|
||||
quorum (e.g. a still-full 6/6 ledger caught at a 6->5 transition), so check
|
||||
>=, not ==.
|
||||
"""
|
||||
digest = ce_tx.get("Digest", "")
|
||||
count = ce_tx.get("EntropyCount", -1)
|
||||
tier = ce_tx.get("EntropyTier", None)
|
||||
if tier != 3:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: expected EntropyTier==3 (validator_quorum), got "
|
||||
f"{tier} (EntropyCount={count})"
|
||||
)
|
||||
if not digest or digest == ZERO_DIGEST:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: validator_quorum digest must be non-zero, got "
|
||||
f"{digest[:16]}..."
|
||||
)
|
||||
if min_count is not None and count < min_count:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: validator_quorum EntropyCount={count} < quorum "
|
||||
f"{min_count}"
|
||||
)
|
||||
return digest, count
|
||||
|
||||
|
||||
def assert_consensus_fallback(ce_tx, seq):
|
||||
"""Assert consensus_fallback (Tier 1) entropy on a ConsensusEntropy tx:
|
||||
EntropyTier=1, EntropyCount=0, and a deterministic NON-zero digest.
|
||||
"""
|
||||
digest = ce_tx.get("Digest", "")
|
||||
count = ce_tx.get("EntropyCount", -1)
|
||||
tier = ce_tx.get("EntropyTier", None)
|
||||
if tier != 1:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: expected EntropyTier==1 (consensus_fallback), got "
|
||||
f"{tier} (EntropyCount={count})"
|
||||
)
|
||||
if count != 0:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: consensus_fallback EntropyCount must be 0, got "
|
||||
f"{count}"
|
||||
)
|
||||
if not digest or digest == ZERO_DIGEST:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: consensus_fallback digest must be non-zero, got "
|
||||
f"{digest[:16]}..."
|
||||
)
|
||||
return digest, count
|
||||
|
||||
|
||||
def assert_valid_entropy(ce_tx, seq, seen_digests=None):
|
||||
"""Assert quorum-met validator entropy. Optionally check uniqueness."""
|
||||
digest, entropy_count, is_fallback = entropy_fields(ce_tx)
|
||||
|
||||
if is_fallback or not digest or digest == ZERO_DIGEST:
|
||||
raise AssertionError(f"Ledger {seq}: fallback/empty Digest")
|
||||
|
||||
if entropy_count < 4:
|
||||
raise AssertionError(
|
||||
f"Ledger {seq}: EntropyCount={entropy_count} < 4 (sub-quorum)"
|
||||
)
|
||||
|
||||
if seen_digests is not None:
|
||||
if digest in seen_digests:
|
||||
raise AssertionError(f"Ledger {seq}: duplicate Digest {digest[:16]}...")
|
||||
seen_digests.add(digest)
|
||||
|
||||
return digest, entropy_count
|
||||
90
.testnet/scenarios/latency-suite.yml
Normal file
90
.testnet/scenarios/latency-suite.yml
Normal file
@@ -0,0 +1,90 @@
|
||||
defaults:
|
||||
network:
|
||||
node_count: 5
|
||||
launcher: tmux
|
||||
find_ports: true
|
||||
slave_delay: 0.2
|
||||
features:
|
||||
- ConsensusEntropy
|
||||
- Export
|
||||
track_features:
|
||||
- ConsensusEntropy
|
||||
- Export
|
||||
unl_report: true
|
||||
log_levels:
|
||||
TxQ: info
|
||||
Protocol: debug
|
||||
Peer: debug
|
||||
LedgerConsensus: debug
|
||||
ConsensusExtensions: debug
|
||||
NetworkOPs: info
|
||||
rc:
|
||||
- rng_poll_ms=250
|
||||
tests:
|
||||
- name: latency_baseline_ce
|
||||
script: .testnet/scenarios/perf/ce_export_latency_probe.py
|
||||
params:
|
||||
warmup_ledgers: 3
|
||||
ledgers: 8
|
||||
submit_export: false
|
||||
|
||||
- name: latency_baseline_export
|
||||
script: .testnet/scenarios/perf/ce_export_latency_probe.py
|
||||
params:
|
||||
warmup_ledgers: 3
|
||||
ledgers: 8
|
||||
submit_export: true
|
||||
|
||||
- name: latency_proposal_delay_export
|
||||
script: .testnet/scenarios/perf/ce_export_latency_probe.py
|
||||
params:
|
||||
warmup_ledgers: 3
|
||||
ledgers: 8
|
||||
submit_export: true
|
||||
network:
|
||||
rc:
|
||||
- rng_poll_ms=250
|
||||
- delay=100,jitter=25,msg=proposal
|
||||
|
||||
- name: latency_directed_pair_delay_export
|
||||
script: .testnet/scenarios/perf/ce_export_latency_probe.py
|
||||
params:
|
||||
warmup_ledgers: 3
|
||||
ledgers: 8
|
||||
submit_export: true
|
||||
network:
|
||||
rc:
|
||||
- rng_poll_ms=250
|
||||
- n0->n2:delay=750,jitter=100,msg=proposal
|
||||
- n2->n0:delay=750,jitter=100,msg=proposal
|
||||
|
||||
- name: latency_slow_minority_export
|
||||
script: .testnet/scenarios/perf/ce_export_latency_probe.py
|
||||
params:
|
||||
warmup_ledgers: 3
|
||||
ledgers: 8
|
||||
submit_export: true
|
||||
export_timeout: 120
|
||||
network:
|
||||
rc:
|
||||
- rng_poll_ms=250
|
||||
- n3->n0:delay=500,jitter=100,msg=proposal
|
||||
- n3->n1:delay=500,jitter=100,msg=proposal
|
||||
- n3->n2:delay=500,jitter=100,msg=proposal
|
||||
- n4->n0:delay=500,jitter=100,msg=proposal
|
||||
- n4->n1:delay=500,jitter=100,msg=proposal
|
||||
- n4->n2:delay=500,jitter=100,msg=proposal
|
||||
- n0->n3:delay=500,jitter=100,msg=proposal
|
||||
- n1->n3:delay=500,jitter=100,msg=proposal
|
||||
- n2->n3:delay=500,jitter=100,msg=proposal
|
||||
- n0->n4:delay=500,jitter=100,msg=proposal
|
||||
- n1->n4:delay=500,jitter=100,msg=proposal
|
||||
- n2->n4:delay=500,jitter=100,msg=proposal
|
||||
|
||||
- name: latency_export_no_veto_with_delay
|
||||
script: .testnet/scenarios/export/export_no_veto_missing_observation.py
|
||||
network:
|
||||
rc:
|
||||
- rng_poll_ms=250
|
||||
- delay=300,jitter=100,msg=proposal
|
||||
- n4:no_export_sig_hash=true
|
||||
205
.testnet/scenarios/perf/ce_export_latency_probe.py
Normal file
205
.testnet/scenarios/perf/ce_export_latency_probe.py
Normal file
@@ -0,0 +1,205 @@
|
||||
""":descr: measure CE/export behavior while RuntimeConfig injects latency/drop.
|
||||
|
||||
The suite supplies runtime fault injection through network.rc. This scenario
|
||||
does not mutate RuntimeConfig itself; it observes what the launched network does
|
||||
under that condition and logs enough counters to compare variants.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import json
|
||||
|
||||
from export.export_helpers import (
|
||||
EXPORT_RETRY_LEDGER_WINDOW,
|
||||
require_export,
|
||||
submit_direct_export,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
from helpers import consensus_entropy_feature, get_entropy_tx
|
||||
|
||||
|
||||
async def _require_runtime_config(ctx, log):
|
||||
result = ctx.rpc.runtime_config(0)
|
||||
if not result or result.get("error"):
|
||||
raise AssertionError(
|
||||
"Latency probe requires a binary built with "
|
||||
"xahaud_runtime_test_config=ON; runtime_config RPC returned "
|
||||
f"{result}"
|
||||
)
|
||||
log("RuntimeConfig RPC active")
|
||||
|
||||
|
||||
async def _require_consensus_entropy(ctx, log):
|
||||
feature = consensus_entropy_feature(ctx, node_id=0)
|
||||
if not feature or not feature.get("enabled", False):
|
||||
raise AssertionError(f"ConsensusEntropy not enabled: {feature}")
|
||||
log("ConsensusEntropy enabled")
|
||||
|
||||
|
||||
def _log_runtime_config(ctx, log):
|
||||
for node_id in range(ctx.node_count):
|
||||
cfg = ctx.rpc.runtime_config(node_id)
|
||||
if cfg is None:
|
||||
raise AssertionError(f"runtime_config RPC failed on node {node_id}")
|
||||
log(
|
||||
f"runtime_config n{node_id}: "
|
||||
f"{json.dumps(cfg, sort_keys=True, separators=(',', ':'))}"
|
||||
)
|
||||
|
||||
|
||||
async def _submit_direct_export(ctx, log, *, timeout):
|
||||
await ctx.fund_accounts({"alice": 10000, "bob": 1000})
|
||||
alice = ctx.account("alice")
|
||||
bob = ctx.account("bob")
|
||||
current_seq = ctx.validated_ledger_index(0)
|
||||
|
||||
if current_seq is None:
|
||||
raise AssertionError("validated ledger is not available before Export")
|
||||
|
||||
log(f"Submitting direct Export at validated ledger {current_seq}")
|
||||
started = ctx.mark("latency-export-submit-start")
|
||||
result = await submit_direct_export(
|
||||
ctx,
|
||||
log,
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW,
|
||||
"Fee": "1000000",
|
||||
"ExportedTxn": {
|
||||
"TransactionType": "Payment",
|
||||
"Account": alice.address,
|
||||
"Destination": bob.address,
|
||||
"Amount": "1000000",
|
||||
"Fee": "10",
|
||||
"Sequence": 0,
|
||||
"TicketSequence": 1,
|
||||
"FirstLedgerSequence": current_seq + 1,
|
||||
"LastLedgerSequence": current_seq + 10,
|
||||
"Flags": 2147483648,
|
||||
"SigningPubKey": "",
|
||||
},
|
||||
},
|
||||
alice.wallet,
|
||||
timeout=timeout,
|
||||
)
|
||||
engine_result = result.get("engine_result", "")
|
||||
if engine_result != "tesSUCCESS":
|
||||
raise AssertionError(f"Expected Export tesSUCCESS, got {engine_result}")
|
||||
|
||||
origin_hash = result.get("hash")
|
||||
origin_seq = result.get("ledger_index", ctx.validated_ledger_index(0))
|
||||
if not origin_hash:
|
||||
raise AssertionError(f"Validated Export missing hash: {result}")
|
||||
witness = await wait_for_export_signature_witness(
|
||||
ctx, log, origin_hash, after_ledger=origin_seq
|
||||
)
|
||||
ended = ctx.mark("latency-export-submit-end")
|
||||
elapsed = (ended.monotonic_ns - started.monotonic_ns) / 1_000_000_000
|
||||
log(f"Export intent+witness result={engine_result} elapsed={elapsed:.3f}s")
|
||||
|
||||
signers = witness.get("_WitnessSigners", [])
|
||||
log(f"Export signer count={len(signers)}")
|
||||
return started, ended
|
||||
|
||||
|
||||
def _summarize_logs(ctx, log, *, label, started, ended):
|
||||
patterns = {
|
||||
"rng_selected": r"RNG: entropy selected",
|
||||
"rng_fallback": r"tier=1",
|
||||
"rng_participant_aligned": r"tier=2",
|
||||
"rng_validator_quorum": r"tier=3",
|
||||
"export_quorum_timeout": r"Export: exportSigSet quorum alignment timeout",
|
||||
"export_missing_observation_ignored": (
|
||||
r"Export: missing exportSigSetHash observation ignored"
|
||||
),
|
||||
}
|
||||
for name, pattern in patterns.items():
|
||||
result = ctx.search_logs(pattern, since=started, until=ended, limit=500)
|
||||
log(f"log_count {label}.{name}={result.count}")
|
||||
|
||||
|
||||
async def scenario(
|
||||
ctx,
|
||||
log,
|
||||
*,
|
||||
warmup_ledgers=3,
|
||||
ledgers=8,
|
||||
submit_export=False,
|
||||
export_timeout=90,
|
||||
):
|
||||
await ctx.wait_for_ledger_close(timeout=120)
|
||||
await _require_runtime_config(ctx, log)
|
||||
_log_runtime_config(ctx, log)
|
||||
await _require_consensus_entropy(ctx, log)
|
||||
|
||||
if submit_export:
|
||||
# require_export also asserts the UNLReport precondition for successful
|
||||
# network-mode Export. Keep that explicit in perf runs so a missing
|
||||
# report does not masquerade as a latency failure.
|
||||
await require_export(ctx, log, require_runtime_config=False)
|
||||
|
||||
await ctx.wait_for_ledgers(warmup_ledgers, node_id=0, timeout=120)
|
||||
warm_seq = ctx.validated_ledger_index(0)
|
||||
log(f"Warmup complete at validated ledger {warm_seq}")
|
||||
|
||||
export_window = None
|
||||
if submit_export:
|
||||
export_window = await _submit_direct_export(ctx, log, timeout=export_timeout)
|
||||
|
||||
started = ctx.mark("latency-probe-start")
|
||||
start_seq = ctx.validated_ledger_index(0)
|
||||
await ctx.wait_for_ledgers(ledgers, node_id=0, timeout=max(120, ledgers * 30))
|
||||
ended = ctx.mark("latency-probe-end")
|
||||
end_seq = ctx.validated_ledger_index(0)
|
||||
|
||||
if start_seq is None or end_seq is None:
|
||||
raise AssertionError("validated ledger index unavailable during probe")
|
||||
|
||||
elapsed = (ended.monotonic_ns - started.monotonic_ns) / 1_000_000_000
|
||||
closed = max(0, end_seq - start_seq)
|
||||
cadence = elapsed / closed if closed else 0.0
|
||||
log(
|
||||
f"Observed validated ledgers {start_seq + 1}..{end_seq} "
|
||||
f"closed={closed} elapsed={elapsed:.3f}s cadence={cadence:.3f}s/ledger"
|
||||
)
|
||||
|
||||
tiers: Counter[int] = Counter()
|
||||
counts: Counter[int] = Counter()
|
||||
missing_entropy = 0
|
||||
for seq in range(start_seq + 1, end_seq + 1):
|
||||
try:
|
||||
ce, user_txns = get_entropy_tx(ctx, seq)
|
||||
except AssertionError as exc:
|
||||
missing_entropy += 1
|
||||
log(f" Ledger {seq}: no ConsensusEntropy tx ({exc})")
|
||||
continue
|
||||
|
||||
tier = ce.get("EntropyTier", -1)
|
||||
count = ce.get("EntropyCount", -1)
|
||||
tiers[tier] += 1
|
||||
counts[count] += 1
|
||||
log(
|
||||
f" Ledger {seq}: tier={tier} count={count} "
|
||||
f"user_txns={len(user_txns)} digest={ce.get('Digest', '')[:16]}..."
|
||||
)
|
||||
|
||||
log(
|
||||
"SUMMARY "
|
||||
f"closed={closed} elapsed_s={elapsed:.3f} cadence_s={cadence:.3f} "
|
||||
f"tiers={dict(sorted(tiers.items()))} "
|
||||
f"counts={dict(sorted(counts.items()))} "
|
||||
f"missing_entropy={missing_entropy}"
|
||||
)
|
||||
|
||||
_summarize_logs(ctx, log, label="probe", started=started, ended=ended)
|
||||
if export_window is not None:
|
||||
_summarize_logs(
|
||||
ctx,
|
||||
log,
|
||||
label="export",
|
||||
started=export_window[0],
|
||||
ended=export_window[1],
|
||||
)
|
||||
|
||||
log("PASS")
|
||||
82
.testnet/scenarios/rollout/mixed_binary_boundary.py
Normal file
82
.testnet/scenarios/rollout/mixed_binary_boundary.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Mixed-binary rollout boundary for ConsensusEntropy.
|
||||
|
||||
Topology (launch with per-node binaries):
|
||||
n0-n2 @export-rng feature-export-rng build, supports Export + ConsensusEntropy, UNL validators
|
||||
n3-n5 @release 2026.6.21 mainnet release build, NO CE support, non-UNL trackers
|
||||
|
||||
What this proves:
|
||||
Phase 1 CE inactive -> heterogeneous net is healthy. New validators emit
|
||||
legacy 32-byte proposal positions, so old nodes parse them and track
|
||||
validated ledgers.
|
||||
Phase 2 CE activates -> the old @release nodes hit the upgrade boundary. Per
|
||||
this branch's setAmendmentBlocked() (Change.cpp / LedgerMaster.cpp ->
|
||||
NetworkOPs) they become amendment-blocked and DROP to CONNECTED, but
|
||||
KEEP RUNNING (RPC stays up) -- they do NOT crash. This is the
|
||||
"upgrade validators before activating" rollout invariant, observed.
|
||||
|
||||
Launch (fast dev check, CE enabled at genesis):
|
||||
x-testnet run -n 6 \
|
||||
--node-binary n0:@export-rng --node-binary n1:@export-rng --node-binary n2:@export-rng \
|
||||
--node-binary n3:@release --node-binary n4:@release --node-binary n5:@release \
|
||||
--feature @ConsensusEntropy \
|
||||
--scenario-script .testnet/scenarios/rollout/mixed_binary_boundary.py --teardown
|
||||
|
||||
Launch (real transition, activates at the next flag ledger ~256; seed pre-satisfies the 1-min hold):
|
||||
x-testnet run -n 6 <same --node-binary set> \
|
||||
--seed-majority @ConsensusEntropy \
|
||||
--scenario-script .testnet/scenarios/rollout/mixed_binary_boundary.py --teardown
|
||||
"""
|
||||
|
||||
from helpers import CONSENSUS_ENTROPY_FEATURE
|
||||
|
||||
VALIDATORS = [0, 1, 2]
|
||||
OLD_NODES = [3, 4, 5]
|
||||
|
||||
|
||||
def _ce_enabled(ctx, node_id=0):
|
||||
s = ctx.feature_check(CONSENSUS_ENTROPY_FEATURE, node_id=node_id)
|
||||
return bool(s and s.get("enabled"))
|
||||
|
||||
|
||||
def _blocked(ctx, nid):
|
||||
info = ctx.rpc.server_info(nid) or {}
|
||||
return bool(info.get("info", {}).get("amendment_blocked"))
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
await ctx.wait_for_ledger_close(timeout=90)
|
||||
|
||||
if not _ce_enabled(ctx):
|
||||
# Phase 1: with CE inactive the old @release trackers must stay in sync.
|
||||
target = (await ctx.wait_for_ledgers(3, timeout=180)).result
|
||||
for nid in OLD_NODES:
|
||||
await ctx.wait_for_ledger(target, node_id=nid, timeout=120)
|
||||
log(f"phase1 OK: old @release nodes tracked to ledger {target} (CE inactive)")
|
||||
|
||||
# Trigger phase 2: vote CE up on the new validators only.
|
||||
ctx.feature(CONSENSUS_ENTROPY_FEATURE, vetoed=False, nodes=VALIDATORS)
|
||||
log("voted ConsensusEntropy accept on n0-n2; awaiting activation...")
|
||||
else:
|
||||
log("CE enabled at genesis; skipping phase 1, checking the boundary directly")
|
||||
|
||||
await ctx.wait_for_feature(
|
||||
CONSENSUS_ENTROPY_FEATURE,
|
||||
check=lambda s: s.get("enabled"),
|
||||
nodes=VALIDATORS,
|
||||
timeout=1200,
|
||||
)
|
||||
log("ConsensusEntropy ENABLED on validators n0-n2")
|
||||
|
||||
# The upgrade boundary: old non-UNL nodes must become amendment-blocked...
|
||||
await ctx.wait_for_nodes(
|
||||
lambda nid: _blocked(ctx, nid), nodes=OLD_NODES, timeout=180
|
||||
)
|
||||
# ...report it in the log...
|
||||
ctx.assert_log("server blocked", nodes=OLD_NODES)
|
||||
# ...and still be alive (RPC responsive) -> blocked, not crashed.
|
||||
for nid in OLD_NODES:
|
||||
assert ctx.rpc.server_info(nid), f"n{nid} RPC unreachable (crashed?)"
|
||||
|
||||
log(
|
||||
"PASS: n3-n5 (@release) amendment-blocked and STILL RUNNING -- upgrade boundary confirmed"
|
||||
)
|
||||
118
.testnet/scenarios/rollout/rolling_upgrade.py
Normal file
118
.testnet/scenarios/rollout/rolling_upgrade.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Realistic rolling binary upgrade, then ConsensusEntropy activation.
|
||||
|
||||
Topology (ALL start on @release, a build with NO CE support):
|
||||
n0-n4 5 UNL validators (quorum 4)
|
||||
n5 tracker (non-UNL) -> WILL be upgraded; keeps working after activation
|
||||
n6 tracker (non-UNL) -> NOT upgraded (the straggler); becomes
|
||||
amendment-blocked after activation but does NOT crash
|
||||
|
||||
TOPOLOGY-AWARE RESTARTS: the fixed-peer mesh takes time to form, and a node whose
|
||||
peers haven't fully connected acts as a junction -- restarting it partitions the
|
||||
rest (observed: restarting n0 dropped n1 to 0 peers -> isolated -> stalled). So we
|
||||
require a well-connected mesh (>= MESH_MIN peers on every node) BEFORE rolling and
|
||||
after EACH restart, before touching the next node. If the mesh never forms this
|
||||
fails loud (a peering problem), rather than silently stalling consensus.
|
||||
|
||||
Why --seed-majority is used (NOT a magic vote): the validators still VOTE the
|
||||
amendment up (ctx.feature below). Seeding only pre-writes the sfMajorities record
|
||||
with CloseTime=0 so the hold is already satisfied, collapsing activation to ONE
|
||||
flag ledger. If the validators don't vote yes it is cleared (tfLostMajority). See
|
||||
prepare_genesis_file() in testnet/config.py.
|
||||
|
||||
Arc: all-old healthy + mesh formed -> rolling-upgrade 5 validators + 1 tracker
|
||||
(mesh re-forms between each) -> vote CE -> activation at the flag ledger ->
|
||||
n5 (upgraded) keeps tracking, n6 (straggler) amendment-blocked but not crashed.
|
||||
|
||||
Run: x-testnet --rippled-path @release suite \\
|
||||
.testnet/scenarios/rollout/rollout-suite.yml --stop-on-fail
|
||||
"""
|
||||
|
||||
from helpers import CONSENSUS_ENTROPY_FEATURE
|
||||
|
||||
VALIDATORS = [0, 1, 2, 3, 4]
|
||||
UPGRADED_TRACKER = 5
|
||||
STRAGGLER_TRACKER = 6
|
||||
ALL_NODES = VALIDATORS + [UPGRADED_TRACKER, STRAGGLER_TRACKER]
|
||||
# Full mesh is 6 peers each (7 nodes). Require >= 4 so no node is a junction:
|
||||
# removing any one still leaves the rest connected.
|
||||
MESH_MIN = 4
|
||||
|
||||
|
||||
def _info(ctx, nid):
|
||||
return (ctx.rpc.server_info(nid) or {}).get("info", {})
|
||||
|
||||
|
||||
def _blocked(ctx, nid):
|
||||
return bool(_info(ctx, nid).get("amendment_blocked"))
|
||||
|
||||
|
||||
def _peers(ctx, nid):
|
||||
return int(_info(ctx, nid).get("peers") or 0)
|
||||
|
||||
|
||||
async def _wait_mesh(ctx, log, *, timeout=180):
|
||||
"""Wait until every node is well-connected (>= MESH_MIN peers), so a restart
|
||||
can't partition the network. Fails loud (TimeoutError) if the mesh never
|
||||
forms -- that means a peering problem, not a transient."""
|
||||
await ctx.wait_for_nodes(
|
||||
lambda x: _peers(ctx, x) >= MESH_MIN, nodes=ALL_NODES, timeout=timeout
|
||||
)
|
||||
peers = {nid: _peers(ctx, nid) for nid in ALL_NODES}
|
||||
log(f"peer mesh healthy (>= {MESH_MIN} peers each): {peers}")
|
||||
|
||||
|
||||
async def scenario(ctx, log):
|
||||
# 1. all-old net healthy AND the full peer mesh has formed before we touch it
|
||||
await ctx.wait_for_ledger_close(timeout=90)
|
||||
base = (await ctx.wait_for_ledgers(2, timeout=120)).result
|
||||
for nid in ALL_NODES:
|
||||
await ctx.wait_for_ledger(base, node_id=nid, timeout=120)
|
||||
await _wait_mesh(ctx, log, timeout=180)
|
||||
log(f"all-old net healthy at ledger {base} (CE inactive)")
|
||||
|
||||
# 2. rolling-upgrade validators (then one tracker), one at a time. After each
|
||||
# restart, give the mesh time to re-form (the restarted node re-dials its
|
||||
# fixed peers) BEFORE rolling the next, so we never take out a junction.
|
||||
for nid in [*VALIDATORS, UPGRADED_TRACKER]:
|
||||
ref = next(v for v in VALIDATORS if v != nid)
|
||||
role = "validator" if nid in VALIDATORS else "tracker"
|
||||
log(f"rolling upgrade: n{nid} ({role}) -> @export-rng")
|
||||
await ctx.restart_node_with_binary(nid, "@export-rng", delay=3)
|
||||
await _wait_mesh(ctx, log, timeout=180) # mesh re-formed before next roll
|
||||
target = (await ctx.wait_for_ledgers(2, node_id=ref, timeout=180)).result
|
||||
await ctx.wait_for_ledger(target, node_id=nid, timeout=180)
|
||||
log(f"n{nid} rejoined; mesh re-formed; quorum advanced to {target}")
|
||||
log(
|
||||
f"validators + n{UPGRADED_TRACKER} on @export-rng; "
|
||||
f"n{STRAGGLER_TRACKER} left on @release; CE still inactive"
|
||||
)
|
||||
|
||||
# 3. vote CE up on the validators (real vote; the seed only pre-satisfied the hold)
|
||||
ctx.feature(CONSENSUS_ENTROPY_FEATURE, vetoed=False, nodes=VALIDATORS)
|
||||
log("voted ConsensusEntropy accept on n0-n4; crossing the flag ledger...")
|
||||
|
||||
# 4. activation at the flag ledger
|
||||
await ctx.wait_for_feature(
|
||||
CONSENSUS_ENTROPY_FEATURE,
|
||||
check=lambda s: s.get("enabled"),
|
||||
nodes=VALIDATORS,
|
||||
timeout=900,
|
||||
)
|
||||
log("ConsensusEntropy ENABLED on the upgraded quorum")
|
||||
|
||||
# 5. upgraded tracker keeps working; straggler is amendment-blocked (not crashed)
|
||||
await ctx.wait_for_nodes(
|
||||
lambda x: _blocked(ctx, x), nodes=[STRAGGLER_TRACKER], timeout=180
|
||||
)
|
||||
ctx.assert_log("server blocked", nodes=[STRAGGLER_TRACKER])
|
||||
assert not _blocked(ctx, UPGRADED_TRACKER), (
|
||||
f"n{UPGRADED_TRACKER} (upgraded tracker) should NOT be amendment-blocked"
|
||||
)
|
||||
await ctx.wait_for_ledgers(2, node_id=UPGRADED_TRACKER, timeout=120)
|
||||
for nid in (UPGRADED_TRACKER, STRAGGLER_TRACKER):
|
||||
assert ctx.rpc.server_info(nid), f"n{nid} RPC down (crashed?)"
|
||||
log(
|
||||
f"PASS: rolling upgrade preserved quorum 4 (mesh-gated); CE activated; "
|
||||
f"n{UPGRADED_TRACKER} (upgraded tracker) still tracking; "
|
||||
f"n{STRAGGLER_TRACKER} (@release straggler) amendment-blocked and still running"
|
||||
)
|
||||
42
.testnet/scenarios/rollout/rollout-suite.yml
Normal file
42
.testnet/scenarios/rollout/rollout-suite.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
# Rolling binary upgrade -> ConsensusEntropy activation.
|
||||
#
|
||||
# All nodes start on @release (set via `--rippled-path @release` on the CLI); the
|
||||
# scenario rolling-restarts them to @export-rng, then casts a REAL vote. The
|
||||
# --seed-majority (majority_features) only pre-satisfies the amendment hold time
|
||||
# so activation lands at ONE flag ledger instead of two -- it is NOT a vote, and
|
||||
# is cleared (tfLostMajority) if the validators don't vote yes before the flag
|
||||
# ledger. start_ledger gives runway for the 6 restarts + vote to finish before
|
||||
# the first flag ledger (256), so the seed survives to a real vote.
|
||||
#
|
||||
# Run:
|
||||
# x-testnet --rippled-path @release suite \
|
||||
# .testnet/scenarios/rollout/rollout-suite.yml --stop-on-fail
|
||||
#
|
||||
# Suite runs auto-snapshot to .testnet/output/runs/ on failure (survives
|
||||
# teardown), so `x-testnet logs-search --run latest/rolling_upgrade_ce ...` works.
|
||||
|
||||
defaults:
|
||||
network:
|
||||
node_count: 7 # n0-n4 validators, n5/n6 non-UNL trackers
|
||||
validators: 5
|
||||
quorum: 4 # 80%: a restart leaves exactly quorum on the peers up
|
||||
find_ports: true
|
||||
start_ledger: 210 # measured: the mesh-gated upgrade+vote is ~23 ledgers, so
|
||||
# the vote lands ~233 with a ~23-ledger margin before flag
|
||||
# ledger 256 (activation). Miss -> vote after 256 -> next
|
||||
# flag is 512 (~13 min) or tfLostMajority clears the seed.
|
||||
majority_features:
|
||||
- ConsensusEntropy # pre-satisfy the hold ONLY (still needs a real vote)
|
||||
track_features:
|
||||
- ConsensusEntropy
|
||||
log_levels:
|
||||
LedgerConsensus: debug
|
||||
NetworkOPs: info
|
||||
# Dense localhost mesh (needed for rolling restarts): the generator gives each
|
||||
# peer a distinct 127.0.0.<id+1> in [ips_fixed] (rippled dedups fixed peers by
|
||||
# IP, ignoring port). On macOS bring those aliases up first (see below); Linux
|
||||
# routes all of 127/8 already. Works with stock binaries -- no rebuild.
|
||||
|
||||
tests:
|
||||
- name: rolling_upgrade_ce
|
||||
script: .testnet/scenarios/rollout/rolling_upgrade.py
|
||||
63
.testnet/scenarios/suite.yml
Normal file
63
.testnet/scenarios/suite.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
defaults:
|
||||
network:
|
||||
node_count: 5
|
||||
launcher: tmux
|
||||
find_ports: true
|
||||
slave_delay: 0.2
|
||||
features:
|
||||
- ConsensusEntropy
|
||||
track_features:
|
||||
- ConsensusEntropy
|
||||
unl_report: true
|
||||
log_levels:
|
||||
TxQ: info
|
||||
Protocol: debug
|
||||
Peer: debug
|
||||
LedgerConsensus: debug
|
||||
ConsensusExtensions: debug
|
||||
NetworkOPs: info
|
||||
rc:
|
||||
- rng_poll_ms=333
|
||||
|
||||
tests:
|
||||
- name: steady_state_entropy
|
||||
script: .testnet/scenarios/entropy/steady_state_entropy.py
|
||||
|
||||
- name: fallback_without_unl_report
|
||||
script: .testnet/scenarios/entropy/fallback_without_unl_report.py
|
||||
network:
|
||||
unl_report: false
|
||||
|
||||
- name: steady_state_entropy_fast_start
|
||||
script: .testnet/scenarios/entropy/steady_state_entropy.py
|
||||
network:
|
||||
env:
|
||||
XAHAUD_RUNTIME_TEST_CONFIG: '{"set":{"global":{"rng_poll_ms":333,"bootstrap_fast_start":true}}}'
|
||||
|
||||
- name: entropy_with_transactions
|
||||
script: .testnet/scenarios/entropy/entropy_with_transactions.py
|
||||
|
||||
- name: hook_entropy_api
|
||||
script: .testnet/scenarios/entropy/hook_entropy_api.py
|
||||
|
||||
- name: quorum_recovery_smoke
|
||||
script: .testnet/scenarios/entropy/quorum_recovery_smoke.py
|
||||
|
||||
- name: quorum_degradation_smoke
|
||||
script: .testnet/scenarios/entropy/quorum_degradation_smoke.py
|
||||
network:
|
||||
log_levels:
|
||||
LedgerConsensus: trace
|
||||
ConsensusExtensions: trace
|
||||
|
||||
# Tier 2 (participant_aligned) needs 6 nodes: n=5 has no band (tier2 ==
|
||||
# quorum). At 6, the 4/6 window is the participant_aligned band.
|
||||
- name: participant_aligned_smoke
|
||||
script: .testnet/scenarios/entropy/participant_aligned_smoke.py
|
||||
network:
|
||||
node_count: 6
|
||||
log_levels:
|
||||
LedgerConsensus: trace
|
||||
ConsensusExtensions: trace
|
||||
|
||||
# Export scenarios: see export-suite.yml
|
||||
2
BUILD.md
2
BUILD.md
@@ -35,7 +35,7 @@ See [System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
Building rippled generally requires git, Python, Conan, CMake, and a C++ compiler. Some guidance on setting up such a [C++ development environment can be found here](./docs/build/environment.md).
|
||||
|
||||
- [Python 3.7](https://www.python.org/downloads/) (configure also uses it to project the QuickJS provider)
|
||||
- [Python 3.7](https://www.python.org/downloads/)
|
||||
- [Conan 2.x](https://conan.io/downloads)
|
||||
- [CMake 3.16](https://cmake.org/download/)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ libxrpl.server > xrpl.basics
|
||||
libxrpl.server > xrpl.json
|
||||
libxrpl.server > xrpl.protocol
|
||||
libxrpl.server > xrpl.server
|
||||
test.app > test.shamap
|
||||
test.app > test.toplevel
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
@@ -21,6 +22,7 @@ test.app > xrpld.ledger
|
||||
test.app > xrpld.nodestore
|
||||
test.app > xrpld.overlay
|
||||
test.app > xrpld.rpc
|
||||
test.app > xrpld.shamap
|
||||
test.app > xrpl.hook
|
||||
test.app > xrpl.json
|
||||
test.app > xrpl.protocol
|
||||
@@ -36,6 +38,7 @@ test.beast > xrpl.basics
|
||||
test.conditions > xrpl.basics
|
||||
test.conditions > xrpld.conditions
|
||||
test.consensus > test.csf
|
||||
test.consensus > test.jtx
|
||||
test.consensus > test.toplevel
|
||||
test.consensus > test.unit_test
|
||||
test.consensus > xrpl.basics
|
||||
@@ -43,6 +46,9 @@ test.consensus > xrpld.app
|
||||
test.consensus > xrpld.consensus
|
||||
test.consensus > xrpld.core
|
||||
test.consensus > xrpld.ledger
|
||||
test.consensus > xrpld.overlay
|
||||
test.consensus > xrpld.shamap
|
||||
test.consensus > xrpl.json
|
||||
test.consensus > xrpl.protocol
|
||||
test.core > test.jtx
|
||||
test.core > test.toplevel
|
||||
|
||||
145
CMakeLists.txt
145
CMakeLists.txt
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
cmake_minimum_required (VERSION 3.16)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -18,52 +18,47 @@ if(POLICY CMP0144)
|
||||
cmake_policy(SET CMP0144 NEW)
|
||||
endif()
|
||||
|
||||
project(xrpl)
|
||||
project (xrpl)
|
||||
set(Boost_NO_BOOST_CMAKE ON)
|
||||
|
||||
# make GIT_COMMIT_HASH define available to all sources
|
||||
# Only sources that expose Git metadata should rebuild when HEAD changes.
|
||||
set(git_info_sources
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/xrpld/app/main/Main.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/xrpld/app/misc/NetworkOPs.cpp")
|
||||
|
||||
find_package(Git)
|
||||
if(Git_FOUND)
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} --git-dir=${CMAKE_CURRENT_SOURCE_DIR}/.git
|
||||
rev-parse HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
OUTPUT_VARIABLE gch)
|
||||
if(gch)
|
||||
set(GIT_COMMIT_HASH "${gch}")
|
||||
message(STATUS gch: ${GIT_COMMIT_HASH})
|
||||
add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
||||
endif()
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} --git-dir=${CMAKE_CURRENT_SOURCE_DIR}/.git rev-parse HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE gch)
|
||||
if(gch)
|
||||
set(GIT_COMMIT_HASH "${gch}")
|
||||
message(STATUS gch: ${GIT_COMMIT_HASH})
|
||||
set_property(SOURCE ${git_info_sources} APPEND PROPERTY
|
||||
COMPILE_DEFINITIONS "GIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} --git-dir=${CMAKE_CURRENT_SOURCE_DIR}/.git
|
||||
rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
OUTPUT_VARIABLE gb)
|
||||
if(gb)
|
||||
set(GIT_BRANCH "${gb}")
|
||||
message(STATUS gb: ${GIT_BRANCH})
|
||||
add_definitions(-DGIT_BRANCH="${GIT_BRANCH}")
|
||||
endif()
|
||||
endif() # git
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} --git-dir=${CMAKE_CURRENT_SOURCE_DIR}/.git rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE gb)
|
||||
if(gb)
|
||||
set(GIT_BRANCH "${gb}")
|
||||
message(STATUS gb: ${GIT_BRANCH})
|
||||
set_property(SOURCE ${git_info_sources} APPEND PROPERTY
|
||||
COMPILE_DEFINITIONS "GIT_BRANCH=\"${GIT_BRANCH}\"")
|
||||
endif()
|
||||
endif() #git
|
||||
|
||||
# make SOURCE_ROOT_PATH define available for logging
|
||||
set(SOURCE_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/")
|
||||
add_definitions(-DSOURCE_ROOT_PATH="${SOURCE_ROOT_PATH}")
|
||||
|
||||
# BEAST_ENHANCED_LOGGING - adds file:line numbers and formatting to logs
|
||||
# Automatically enabled for Debug builds via generator expression Can be
|
||||
# explicitly controlled with -DBEAST_ENHANCED_LOGGING=ON/OFF
|
||||
option(
|
||||
BEAST_ENHANCED_LOGGING
|
||||
"Include file and line numbers in log messages (auto: Debug=ON, Release=OFF)"
|
||||
OFF)
|
||||
# Automatically enabled for Debug builds via generator expression
|
||||
# Can be explicitly controlled with -DBEAST_ENHANCED_LOGGING=ON/OFF
|
||||
option(BEAST_ENHANCED_LOGGING "Include file and line numbers in log messages (auto: Debug=ON, Release=OFF)" OFF)
|
||||
message(STATUS "BEAST_ENHANCED_LOGGING option: ${BEAST_ENHANCED_LOGGING}")
|
||||
|
||||
if(thread_safety_analysis)
|
||||
add_compile_options(
|
||||
-Wthread-safety -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS
|
||||
-DRIPPLE_ENABLE_THREAD_SAFETY_ANNOTATIONS)
|
||||
add_compile_options(-Wthread-safety -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS -DRIPPLE_ENABLE_THREAD_SAFETY_ANNOTATIONS)
|
||||
add_compile_options("-stdlib=libc++")
|
||||
add_link_options("-stdlib=libc++")
|
||||
endif()
|
||||
@@ -73,48 +68,40 @@ option(USE_CONAN "Use Conan package manager for dependencies" OFF)
|
||||
if(CMAKE_TOOLCHAIN_FILE)
|
||||
# Check if the toolchain file path contains "conan_toolchain"
|
||||
if(CMAKE_TOOLCHAIN_FILE MATCHES "conan_toolchain")
|
||||
set(USE_CONAN
|
||||
ON
|
||||
CACHE BOOL "Using Conan detected from toolchain file" FORCE)
|
||||
set(USE_CONAN ON CACHE BOOL "Using Conan detected from toolchain file" FORCE)
|
||||
message(STATUS "Conan toolchain detected: ${CMAKE_TOOLCHAIN_FILE}")
|
||||
message(STATUS "Building with Conan dependencies")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(CheckCXXCompilerFlag)
|
||||
include(FetchContent)
|
||||
include(ExternalProject)
|
||||
include(CMakeFuncs) # must come *after* ExternalProject b/c it overrides one
|
||||
# function in EP
|
||||
if(target)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"The target option has been removed - use native cmake options to control build"
|
||||
)
|
||||
endif()
|
||||
include (CheckCXXCompilerFlag)
|
||||
include (FetchContent)
|
||||
include (ExternalProject)
|
||||
include (CMakeFuncs) # must come *after* ExternalProject b/c it overrides one function in EP
|
||||
if (target)
|
||||
message (FATAL_ERROR "The target option has been removed - use native cmake options to control build")
|
||||
endif ()
|
||||
|
||||
include(RippledSanity)
|
||||
include(RippledVersion)
|
||||
include(RippledSettings)
|
||||
|
||||
# this check has to remain in the top-level cmake because of the early return
|
||||
# statement
|
||||
if(packages_only)
|
||||
if(NOT TARGET rpm)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"packages_only requested, but targets were not created - is docker installed?"
|
||||
)
|
||||
# this check has to remain in the top-level cmake
|
||||
# because of the early return statement
|
||||
if (packages_only)
|
||||
if (NOT TARGET rpm)
|
||||
message (FATAL_ERROR "packages_only requested, but targets were not created - is docker installed?")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
return ()
|
||||
endif ()
|
||||
include(RippledCompiler)
|
||||
include(RippledInterface)
|
||||
|
||||
include(deps/Boost)
|
||||
find_package(OpenSSL 1.1.1 REQUIRED)
|
||||
set_target_properties(OpenSSL::SSL PROPERTIES INTERFACE_COMPILE_DEFINITIONS
|
||||
OPENSSL_NO_SSL2)
|
||||
set_target_properties(OpenSSL::SSL PROPERTIES
|
||||
INTERFACE_COMPILE_DEFINITIONS OPENSSL_NO_SSL2
|
||||
)
|
||||
set(SECP256K1_INSTALL TRUE)
|
||||
add_subdirectory(external/secp256k1)
|
||||
add_library(secp256k1::secp256k1 ALIAS secp256k1)
|
||||
@@ -122,9 +109,9 @@ add_subdirectory(external/ed25519-donna)
|
||||
add_subdirectory(external/antithesis-sdk)
|
||||
find_package(gRPC REQUIRED)
|
||||
find_package(lz4 REQUIRED)
|
||||
# Target names with :: are not allowed in a generator expression. We need to
|
||||
# pull the include directories and imported location properties from separate
|
||||
# targets.
|
||||
# Target names with :: are not allowed in a generator expression.
|
||||
# We need to pull the include directories and imported location properties
|
||||
# from separate targets.
|
||||
find_package(LibArchive REQUIRED)
|
||||
find_package(SOCI REQUIRED)
|
||||
find_package(SQLite3 REQUIRED)
|
||||
@@ -132,9 +119,9 @@ find_package(SQLite3 REQUIRED)
|
||||
option(rocksdb "Enable RocksDB" ON)
|
||||
if(rocksdb)
|
||||
find_package(RocksDB REQUIRED)
|
||||
set_target_properties(
|
||||
RocksDB::rocksdb PROPERTIES INTERFACE_COMPILE_DEFINITIONS
|
||||
RIPPLE_ROCKSDB_AVAILABLE=1)
|
||||
set_target_properties(RocksDB::rocksdb PROPERTIES
|
||||
INTERFACE_COMPILE_DEFINITIONS RIPPLE_ROCKSDB_AVAILABLE=1
|
||||
)
|
||||
target_link_libraries(ripple_libs INTERFACE RocksDB::rocksdb)
|
||||
endif()
|
||||
|
||||
@@ -143,12 +130,8 @@ find_package(date REQUIRED)
|
||||
find_package(xxHash REQUIRED)
|
||||
find_package(magic_enum REQUIRED)
|
||||
|
||||
include(deps/WasmEdge)
|
||||
if(xrpld)
|
||||
include(deps/Wasmtime)
|
||||
include(QuickJSProvider)
|
||||
endif()
|
||||
if(TARGET nudb::core)
|
||||
include(deps/WasmEdge)
|
||||
if(TARGET nudb::core)
|
||||
set(nudb nudb::core)
|
||||
elseif(TARGET NuDB::nudb)
|
||||
set(nudb NuDB::nudb)
|
||||
@@ -157,16 +140,16 @@ else()
|
||||
endif()
|
||||
target_link_libraries(ripple_libs INTERFACE ${nudb})
|
||||
|
||||
target_link_libraries(
|
||||
ripple_libs
|
||||
INTERFACE ed25519::ed25519
|
||||
LibArchive::LibArchive
|
||||
lz4::lz4
|
||||
OpenSSL::Crypto
|
||||
OpenSSL::SSL
|
||||
secp256k1::secp256k1
|
||||
soci::soci
|
||||
SQLite::SQLite3)
|
||||
target_link_libraries(ripple_libs INTERFACE
|
||||
ed25519::ed25519
|
||||
LibArchive::LibArchive
|
||||
lz4::lz4
|
||||
OpenSSL::Crypto
|
||||
OpenSSL::SSL
|
||||
secp256k1::secp256k1
|
||||
soci::soci
|
||||
SQLite::SQLite3
|
||||
)
|
||||
|
||||
if(coverage)
|
||||
include(RippledCov)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Note:** Throughout this README, references to "we" or "our" pertain to the community and contributors involved in the Xahau network. It does not imply a legal entity or a specific collection of individuals.
|
||||
|
||||
[Xahau](https://xahau.network/) is a decentralized cryptographic ledger that builds upon the robust foundation of the XRP Ledger. It inherits the XRP Ledger's Byzantine Fault Tolerant consensus algorithm and enhances it with additional features and functionalities. Developers and users familiar with the XRP Ledger will find that most documentation and tutorials available on [xrpl.org](https://xrpl.org) are relevant and applicable to Xahau, including those related to running validators and managing validator keys. For Xahau specific documentation you can visit our [documentation](https://xahau.network/)
|
||||
[Xahau](https://xahau.network/) is a decentralized cryptographic ledger that builds upon the robust foundation of the XRP Ledger. It inherits the XRP Ledger's Byzantine Fault Tolerant consensus algorithm under the normal XRPL assumptions about configured validator-list overlap, timing, and fault bounds, and enhances it with additional features and functionalities. Developers and users familiar with the XRP Ledger will find that most documentation and tutorials available on [xrpl.org](https://xrpl.org) are relevant and applicable to Xahau, including those related to running validators and managing validator keys. For Xahau specific documentation you can visit our [documentation](https://xahau.network/)
|
||||
|
||||
## XAH
|
||||
XAH is the public, counterparty-free asset native to Xahau and functions primarily as network gas. Transactions submitted to the Xahau network must supply an appropriate amount of XAH, to be burnt by the network as a fee, in order to be successfully included in a validated ledger. In addition, XAH also acts as a bridge currency within the Xahau DEX. XAH is traded on the open-market and is available for anyone to access. Xahau was created in 2023 with a supply of 600 million units of XAH.
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and import one committed jshookz provider snapshot into Xahau."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SNAPSHOT_FILES = (
|
||||
"jshookz_provider.manifest.cmake",
|
||||
"jshookz_provider.manifest.json",
|
||||
"jshookz_provider.native-abi.json",
|
||||
"jshookz_provider.wasm",
|
||||
)
|
||||
API_SNAPSHOT_FILES = {
|
||||
"python/jshookz/src/jshookz/types/api-artifacts.json": "api-artifacts.json",
|
||||
"python/jshookz/src/jshookz/types/hooks-api.d.ts": "hooks-api.d.ts",
|
||||
"python/jshookz/src/jshookz/types/xahau-quickjs-v1.d.ts":
|
||||
"xahau-quickjs-v1.d.ts",
|
||||
"python/jshookz/src/jshookz/types/xahau-quickjs-v1.surface.json":
|
||||
"xahau-quickjs-v1.surface.json",
|
||||
}
|
||||
|
||||
|
||||
def run(*args: str, cwd: Path, capture: bool = False) -> str:
|
||||
completed = subprocess.run(
|
||||
args,
|
||||
cwd=cwd,
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE if capture else None,
|
||||
)
|
||||
return completed.stdout.strip() if capture else ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Build a committed jshookz checkout and copy its sealed "
|
||||
"provider lock into Xahau. CI tracks jshookz main; the lock "
|
||||
"is what must match."
|
||||
)
|
||||
)
|
||||
parser.add_argument("jshookz_checkout", type=Path)
|
||||
parser.add_argument(
|
||||
"--no-build",
|
||||
action="store_true",
|
||||
help="import an already-built build/xahau-provider bundle",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
xahau_root = Path(__file__).resolve().parents[1]
|
||||
jshookz_root = args.jshookz_checkout.expanduser().resolve()
|
||||
cli = next(
|
||||
(
|
||||
candidate
|
||||
for candidate in (
|
||||
jshookz_root / "python/jshookz/.venv/bin/jshookz",
|
||||
jshookz_root / "packages/jshookz/.venv/bin/jshookz",
|
||||
)
|
||||
if candidate.is_file()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if cli is None:
|
||||
parser.error(
|
||||
"missing jshookz CLI under python/jshookz/.venv or "
|
||||
"packages/jshookz/.venv"
|
||||
)
|
||||
bundle = jshookz_root / "build/xahau-provider"
|
||||
destination = xahau_root / "external/quickjs-provider"
|
||||
|
||||
dirty = run(
|
||||
"git",
|
||||
"status",
|
||||
"--porcelain",
|
||||
"--untracked-files=no",
|
||||
cwd=jshookz_root,
|
||||
capture=True,
|
||||
)
|
||||
if dirty:
|
||||
parser.error(
|
||||
"jshookz checkout has uncommitted tracked changes; commit them "
|
||||
"before creating a pinned snapshot"
|
||||
)
|
||||
|
||||
commit = run("git", "rev-parse", "HEAD", cwd=jshookz_root, capture=True)
|
||||
if not args.no_build:
|
||||
run(str(cli), "build", "provider", cwd=jshookz_root)
|
||||
|
||||
missing = [name for name in SNAPSHOT_FILES if not (bundle / name).is_file()]
|
||||
missing.extend(
|
||||
source for source in API_SNAPSHOT_FILES if not (jshookz_root / source).is_file()
|
||||
)
|
||||
if missing:
|
||||
parser.error(f"incomplete provider bundle: {', '.join(missing)}")
|
||||
|
||||
for name in SNAPSHOT_FILES:
|
||||
shutil.copy2(bundle / name, destination / name)
|
||||
for source, name in API_SNAPSHOT_FILES.items():
|
||||
shutil.copy2(jshookz_root / source, destination / name)
|
||||
|
||||
manifest = json.loads((bundle / "jshookz_provider.manifest.json").read_text())
|
||||
wasi_sdk = os.environ.get("HOOKZ_WASI_SDK")
|
||||
if not wasi_sdk:
|
||||
try:
|
||||
wasi_sdk = str(
|
||||
Path(
|
||||
run(
|
||||
"mise",
|
||||
"where",
|
||||
"wasi-sdk",
|
||||
cwd=jshookz_root,
|
||||
capture=True,
|
||||
)
|
||||
)
|
||||
/ "wasi-sdk"
|
||||
)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
wasi_sdk = "/path/to/wasi-sdk"
|
||||
|
||||
print(f"jshookz commit: {commit}")
|
||||
print(f"provider sha256: {manifest['provider']['sha256']}")
|
||||
print(f"bytecode ABI: {manifest['bytecode_abi_id']}")
|
||||
print(f"runtime profile: {manifest['runtime_profile_id']}")
|
||||
print("snapshot imported; regenerate fixtures and run the focused suite with:")
|
||||
print(
|
||||
" env "
|
||||
f"JSHOOKZ_HOOK_COMPILER={shlex.quote(f'{cli} compile-hook')} "
|
||||
f"HOOKZ_WASI_SDK={shlex.quote(wasi_sdk)} "
|
||||
f"XAHAU_QJS_PROVIDER_WASM={shlex.quote(str(bundle / 'jshookz_provider.wasm'))} "
|
||||
"XAHAU_REQUIRE_QJS_PROVIDER_TESTS=1 "
|
||||
"x-run-tests --compile-hooks src/test/app/JSHooks_test.cpp "
|
||||
"--build-dir build --ccache -- "
|
||||
"ripple.app.JSHooks,ripple.app.QuickJSHostPolicy,"
|
||||
"ripple.app.HookGuestMemory,ripple.app.HookAPI"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,928 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Project the sealed QuickJS bundle into one C++ translation unit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
CHUNK = 64
|
||||
CMAKE_SET = re.compile(r'^set\(XAHAU_QUICKJS_([A-Z0-9_]+) "([^"]*)"\)\s*$')
|
||||
WASM_VALTYPE = {"i32", "i64"}
|
||||
WASM_BYTE = {"i32": "0x7f", "i64": "0x7e"}
|
||||
WASM_STACK_BYTES = 131072
|
||||
WASM_PAGE_BYTES = 65536
|
||||
PROVIDER_MEMORY_MINIMUM_PAGES = 7
|
||||
PROVIDER_MEMORY_MAXIMUM_PAGES = 512
|
||||
PROVIDER_MEMORY_MAX_BYTES = PROVIDER_MEMORY_MAXIMUM_PAGES * WASM_PAGE_BYTES
|
||||
SEALED_MANIFEST_SHA256 = (
|
||||
"1673b27d21063957cece9095cceb7e2dbfa53b6f49a5b7f824a486d7d90b2391"
|
||||
)
|
||||
SEALED_PROVIDER_SHA256 = (
|
||||
"f60ab333acb547e50d7d087960839bd11833cb133d496af98e0b72a1bb3b84df"
|
||||
)
|
||||
SEALED_PROVIDER_SIZE = 1116638
|
||||
SEALED_NATIVE_ABI_SHA256 = (
|
||||
"328ec938dcad875f3bdf25b8d779dd77f3571589818ca9271cb98c64c7018f99"
|
||||
)
|
||||
SEALED_BYTECODE_ABI_ID = (
|
||||
"75ea54f357d397c4b33899e495bb385dad975a43b1c4a7a2cead30474327d33e"
|
||||
)
|
||||
SEALED_RUNTIME_PROFILE_ID = (
|
||||
"29acf12e170436e5ccadfbc118d02a60909f86a88262106652b2f71994b0ea25"
|
||||
)
|
||||
SEALED_WASMTIME_VERSION = "47.0.3"
|
||||
SEALED_HOOK_API_VERSION = 1
|
||||
SEALED_HOST_ADAPTER_POLICY = "xahau-raw-hook-host-v1"
|
||||
SEALED_BROAD_DECLARATION_SHA256 = (
|
||||
"649079b831bc68eff0f440644649a505addafb08a3f5174e4031c8de6aedffa0"
|
||||
)
|
||||
SEALED_EXACT_V1_DECLARATION_SHA256 = (
|
||||
"20881232f0602e35de0fcbb8b5a3c1511606392749a0f79daebb386204f4f4da"
|
||||
)
|
||||
SEALED_SURFACE_SHA256 = (
|
||||
"96a9218bf38abbf1bc9f2839408700d03572e0b065d625263f70477bc32597ac"
|
||||
)
|
||||
SEALED_API_ARTIFACT_MANIFEST_SHA256 = (
|
||||
"95babf622bf5edff76fdcbd9746bad8f1f8b275a3d4bacdc320ff4deae543eba"
|
||||
)
|
||||
API_ARTIFACT_SCHEMA = "jshookz.api-artifacts.v1"
|
||||
SEALED_API_ARTIFACTS = {
|
||||
"python/jshookz/src/jshookz/types/hooks-api.d.ts": (
|
||||
"broad_declaration",
|
||||
"hooks-api.d.ts",
|
||||
SEALED_BROAD_DECLARATION_SHA256,
|
||||
),
|
||||
"python/jshookz/src/jshookz/types/xahau-quickjs-v1.d.ts": (
|
||||
"exact_v1_declaration",
|
||||
"xahau-quickjs-v1.d.ts",
|
||||
SEALED_EXACT_V1_DECLARATION_SHA256,
|
||||
),
|
||||
"python/jshookz/src/jshookz/types/xahau-quickjs-v1.surface.json": (
|
||||
"selected_surface",
|
||||
"xahau-quickjs-v1.surface.json",
|
||||
SEALED_SURFACE_SHA256,
|
||||
),
|
||||
}
|
||||
SEALED_LIMITS = {
|
||||
"host_work_base_per_call": 1,
|
||||
"host_work_budget": 1000000,
|
||||
"host_work_meter": "base-plus-addressed-byte-v1",
|
||||
"host_work_per_addressed_byte": 1,
|
||||
"quickjs_heap_bytes": 16777216,
|
||||
"quickjs_stack_bytes": 65536,
|
||||
"serialized_object_max_bytes": 1048576,
|
||||
"serialized_object_max_fields": 32768,
|
||||
"serialized_object_max_scopes": 32769,
|
||||
"serialized_object_max_depth": 10,
|
||||
"wasmtime_fuel_per_initialization": 5000000,
|
||||
"wasmtime_fuel_per_invocation": 50000000,
|
||||
}
|
||||
SEALED_HOST_WORK_ADDRESSED_LENGTH_INDICES = {
|
||||
"accept": [1],
|
||||
"emit": [1, 3],
|
||||
"hook_account": [1],
|
||||
"ledger_last_hash": [1],
|
||||
"prepare": [1, 3],
|
||||
"rollback": [1],
|
||||
"state": [1, 3],
|
||||
"state_set": [1, 3],
|
||||
"trace": [1, 3],
|
||||
}
|
||||
SEALED_ENGINE_CONFIGURATION = {
|
||||
"consume_fuel": True,
|
||||
"cranelift_nan_canonicalization": True,
|
||||
"wasi": False,
|
||||
"wasm_memory64": False,
|
||||
"wasm_multi_memory": False,
|
||||
"wasm_relaxed_simd": False,
|
||||
"wasm_tail_call": False,
|
||||
"wasm_threads": False,
|
||||
}
|
||||
IMPORT_KEYS = frozenset({"module", "name", "params", "results"})
|
||||
FUNCTION_EXPORT_KEYS = frozenset({"kind", "name", "params", "results"})
|
||||
MEMORY_EXPORT_KEYS = frozenset(
|
||||
{"kind", "name", "minimum_pages", "maximum_pages", "memory64", "shared"}
|
||||
)
|
||||
SURFACE_KEYS = frozenset(
|
||||
{"declaration", "declaration_sha256", "manifest", "schema", "sha256"}
|
||||
)
|
||||
NESTED_SURFACE_KEYS = frozenset({"declaration", "manifest", "schema"})
|
||||
|
||||
|
||||
def _import_row(name: str, params: list[str], results: list[str]) -> dict[str, Any]:
|
||||
return {"module": "env", "name": name, "params": params, "results": results}
|
||||
|
||||
|
||||
def _function_export(name: str, params: list[str], results: list[str]) -> dict[str, Any]:
|
||||
return {"kind": "function", "name": name, "params": params, "results": results}
|
||||
|
||||
|
||||
SEALED_IMPORTS = [
|
||||
_import_row("accept", ["i32", "i32", "i64"], ["i64"]),
|
||||
_import_row("emit", ["i32", "i32", "i32", "i32"], ["i64"]),
|
||||
_import_row("etxn_reserve", ["i32"], ["i64"]),
|
||||
_import_row("hook_account", ["i32", "i32"], ["i64"]),
|
||||
_import_row("ledger_last_hash", ["i32", "i32"], ["i64"]),
|
||||
_import_row("ledger_last_time", [], ["i64"]),
|
||||
_import_row("ledger_seq", [], ["i64"]),
|
||||
_import_row("otxn_type", [], ["i64"]),
|
||||
_import_row("prepare", ["i32", "i32", "i32", "i32"], ["i64"]),
|
||||
_import_row("rollback", ["i32", "i32", "i64"], ["i64"]),
|
||||
_import_row("state", ["i32", "i32", "i32", "i32"], ["i64"]),
|
||||
_import_row("state_set", ["i32", "i32", "i32", "i32"], ["i64"]),
|
||||
_import_row("trace", ["i32", "i32", "i32", "i32", "i32"], ["i64"]),
|
||||
]
|
||||
SEALED_EXPORTS = [
|
||||
_function_export("_initialize", [], []),
|
||||
_function_export("free", ["i32"], []),
|
||||
_function_export("malloc", ["i32"], ["i32"]),
|
||||
{
|
||||
"kind": "memory",
|
||||
"maximum_pages": 512,
|
||||
"memory64": False,
|
||||
"minimum_pages": 7,
|
||||
"name": "memory",
|
||||
"shared": False,
|
||||
},
|
||||
_function_export("qjs_cbak", ["i32", "i32", "i32"], ["i32"]),
|
||||
_function_export("qjs_compile", ["i32", "i32"], ["i32"]),
|
||||
_function_export("qjs_compile_module", ["i32", "i32"], ["i32"]),
|
||||
_function_export("qjs_destroy", [], []),
|
||||
_function_export("qjs_enable_coverage", ["i32"], []),
|
||||
_function_export("qjs_eval", ["i32", "i32"], ["i32"]),
|
||||
_function_export("qjs_eval_bytecode", ["i32", "i32"], ["i32"]),
|
||||
_function_export("qjs_eval_module", ["i32", "i32"], ["i32"]),
|
||||
_function_export("qjs_get_bytecode_len", [], ["i32"]),
|
||||
_function_export("qjs_get_bytecode_ptr", [], ["i32"]),
|
||||
_function_export("qjs_get_result_len", [], ["i32"]),
|
||||
_function_export("qjs_get_result_ptr", [], ["i32"]),
|
||||
_function_export("qjs_hook", ["i32", "i32", "i32"], ["i32"]),
|
||||
_function_export("qjs_init", [], []),
|
||||
_function_export("qjs_set_max_stack_size", ["i32"], []),
|
||||
_function_export("qjs_set_memory_limit", ["i32"], []),
|
||||
_function_export("qjs_set_seed", ["i32"], []),
|
||||
_function_export("qjs_validate_hook_module", ["i32", "i32"], ["i32"]),
|
||||
]
|
||||
|
||||
|
||||
class LockError(ValueError):
|
||||
"""A sealed QuickJS lock identity failed a fail-closed check."""
|
||||
|
||||
|
||||
def quote(value: str) -> str:
|
||||
if not isinstance(value, str) or any(c in value for c in "\n\r"):
|
||||
raise LockError(f"invalid QuickJS policy string: {value!r}")
|
||||
return json.dumps(value)
|
||||
|
||||
|
||||
def parse_cmake(path: Path) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
for line in path.read_text().splitlines():
|
||||
match = CMAKE_SET.match(line)
|
||||
if match:
|
||||
values[match.group(1)] = match.group(2)
|
||||
return values
|
||||
|
||||
|
||||
def hex_bytes(value: str) -> str:
|
||||
if len(value) != 64 or any(c not in "0123456789abcdefABCDEF" for c in value):
|
||||
raise LockError(f"QuickJS identity must be 32 hexadecimal bytes: {value!r}")
|
||||
return ", ".join(f"0x{value[i:i + 2]}" for i in range(0, 64, 2))
|
||||
|
||||
|
||||
def require(cmake: dict[str, str], key: str) -> str:
|
||||
if key not in cmake:
|
||||
raise LockError(f"QuickJS cmake projection missing {key}")
|
||||
return cmake[key]
|
||||
|
||||
|
||||
def require_int(cmake: dict[str, str], key: str) -> int:
|
||||
return int(require(cmake, key))
|
||||
|
||||
|
||||
def require_hex(value: object, label: str) -> str:
|
||||
text = str(value).lower()
|
||||
if len(text) != 64 or any(c not in "0123456789abcdef" for c in text):
|
||||
raise LockError(f"{label} is not a SHA-256 hex digest: {value!r}")
|
||||
return text
|
||||
|
||||
|
||||
def require_sealed(actual: object, expected: object, label: str) -> None:
|
||||
if actual != expected:
|
||||
raise LockError(
|
||||
f"{label} disagrees with the sealed F0 table: "
|
||||
f"{actual!r} != {expected!r}"
|
||||
)
|
||||
|
||||
|
||||
def validate_api_artifacts(bundle: Path) -> dict[str, str]:
|
||||
manifest_path = bundle / "api-artifacts.json"
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
if not isinstance(manifest, dict) or set(manifest) != {"artifacts", "schema"}:
|
||||
raise LockError("API artifact manifest has extra or missing keys")
|
||||
if manifest.get("schema") != API_ARTIFACT_SCHEMA:
|
||||
raise LockError("API artifact manifest schema disagrees with the sealed F0 table")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, dict) or set(artifacts) != set(SEALED_API_ARTIFACTS):
|
||||
raise LockError("API artifact manifest file set disagrees with the sealed F0 table")
|
||||
|
||||
identities: dict[str, str] = {}
|
||||
for source_path, artifact in SEALED_API_ARTIFACTS.items():
|
||||
identity_name, local_name, sealed_digest = artifact
|
||||
actual_digest = hashlib.sha256((bundle / local_name).read_bytes()).hexdigest()
|
||||
declared_digest = require_hex(
|
||||
artifacts.get(source_path), f"API artifact {source_path} SHA-256"
|
||||
)
|
||||
if actual_digest != declared_digest:
|
||||
raise LockError(
|
||||
f"API artifact {source_path} disagrees with its manifest digest"
|
||||
)
|
||||
require_sealed(actual_digest, sealed_digest, f"API artifact {source_path}")
|
||||
identities[identity_name] = actual_digest
|
||||
|
||||
actual_manifest = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
|
||||
require_sealed(
|
||||
actual_manifest,
|
||||
SEALED_API_ARTIFACT_MANIFEST_SHA256,
|
||||
"API artifact manifest SHA-256",
|
||||
)
|
||||
identities["api_artifact_manifest"] = actual_manifest
|
||||
return identities
|
||||
|
||||
|
||||
def cpp_bool(value: object) -> str:
|
||||
if value is True:
|
||||
return "true"
|
||||
if value is False:
|
||||
return "false"
|
||||
raise LockError(f"expected JSON boolean, got {value!r}")
|
||||
|
||||
|
||||
def join_types(values: object, label: str) -> str:
|
||||
if not isinstance(values, list) or not all(isinstance(item, str) for item in values):
|
||||
raise LockError(f"{label} is not a list of wasm value types: {values!r}")
|
||||
for item in values:
|
||||
if item not in WASM_VALTYPE:
|
||||
raise LockError(f"{label} has unsupported wasm value type {item!r}")
|
||||
return ",".join(values)
|
||||
|
||||
|
||||
def wasm_signature(params: object, results: object, label: str) -> list[str]:
|
||||
join_types(params, f"{label} params")
|
||||
join_types(results, f"{label} results")
|
||||
assert isinstance(params, list) and isinstance(results, list)
|
||||
return [WASM_BYTE[item] for item in results] + [WASM_BYTE[item] for item in params]
|
||||
|
||||
|
||||
def embed_body(data: bytes | None) -> str:
|
||||
if data is None:
|
||||
return (
|
||||
"std::span<std::uint8_t const>\n"
|
||||
"embeddedQuickJSProvider()\n"
|
||||
"{\n"
|
||||
" return {};\n"
|
||||
"}\n"
|
||||
)
|
||||
lines = []
|
||||
for offset in range(0, len(data), CHUNK):
|
||||
chunk = data[offset : offset + CHUNK]
|
||||
lines.append(' "' + "".join(f"\\x{b:02x}" for b in chunk) + '"')
|
||||
return (
|
||||
"namespace {\n"
|
||||
"\n"
|
||||
"constexpr char const sealedProvider[] =\n"
|
||||
+ "\n".join(lines)
|
||||
+ ";\n"
|
||||
"\n"
|
||||
"static_assert(\n"
|
||||
f" sizeof(sealedProvider) - 1 == {len(data)},\n"
|
||||
' "embedded provider size disagrees with its manifest");\n'
|
||||
"\n"
|
||||
"} // namespace\n"
|
||||
"\n"
|
||||
"std::span<std::uint8_t const>\n"
|
||||
"embeddedQuickJSProvider()\n"
|
||||
"{\n"
|
||||
" return {\n"
|
||||
" reinterpret_cast<std::uint8_t const*>(sealedProvider),\n"
|
||||
" sizeof(sealedProvider) - 1};\n"
|
||||
"}\n"
|
||||
)
|
||||
|
||||
|
||||
def format_export_row(item: dict[str, Any]) -> str:
|
||||
kind = item["kind"]
|
||||
name = item["name"]
|
||||
if kind == "memory":
|
||||
fields = (
|
||||
quote(kind),
|
||||
quote(name),
|
||||
quote(""),
|
||||
quote(""),
|
||||
f"{int(item['minimum_pages'])}U",
|
||||
f"{int(item['maximum_pages'])}U",
|
||||
cpp_bool(item["memory64"]),
|
||||
cpp_bool(item["shared"]),
|
||||
)
|
||||
elif kind == "function":
|
||||
fields = (
|
||||
quote(kind),
|
||||
quote(name),
|
||||
quote(join_types(item["params"], f"export {name} params")),
|
||||
quote(join_types(item["results"], f"export {name} results")),
|
||||
"0U",
|
||||
"0U",
|
||||
"false",
|
||||
"false",
|
||||
)
|
||||
else:
|
||||
raise LockError(f"unsupported provider export kind: {kind!r}")
|
||||
return "{" + ", ".join(fields) + "}"
|
||||
|
||||
|
||||
def require_typed_exports(exports: object, label: str) -> list[dict[str, Any]]:
|
||||
if not isinstance(exports, list) or not all(isinstance(item, dict) for item in exports):
|
||||
raise LockError(f"{label} must be a list of typed export rows")
|
||||
names: list[str] = []
|
||||
memory_rows: list[dict[str, Any]] = []
|
||||
for item in exports:
|
||||
kind = item.get("kind")
|
||||
name = item.get("name")
|
||||
if not isinstance(kind, str) or not isinstance(name, str) or not name:
|
||||
raise LockError(f"{label} row is missing kind/name: {item!r}")
|
||||
if kind == "function":
|
||||
if set(item) != FUNCTION_EXPORT_KEYS:
|
||||
raise LockError(f"{label} function {name} has extra or missing keys")
|
||||
join_types(item.get("params"), f"{label} {name} params")
|
||||
join_types(item.get("results"), f"{label} {name} results")
|
||||
elif kind == "memory":
|
||||
if set(item) != MEMORY_EXPORT_KEYS:
|
||||
raise LockError(f"{label} memory row has extra or missing keys")
|
||||
for key in ("minimum_pages", "maximum_pages"):
|
||||
if not isinstance(item.get(key), int):
|
||||
raise LockError(f"{label} memory row missing integer {key}")
|
||||
if item.get("memory64") is not False or item.get("shared") is not False:
|
||||
raise LockError(
|
||||
f"{label} memory row is not memory64=false / shared=false"
|
||||
)
|
||||
memory_rows.append(item)
|
||||
else:
|
||||
raise LockError(f"{label} has unsupported export kind {kind!r}")
|
||||
names.append(name)
|
||||
if len(names) != len(set(names)):
|
||||
raise LockError(f"{label} contains duplicate export names")
|
||||
if names != sorted(names):
|
||||
raise LockError(f"{label} must be normalized by export name")
|
||||
if len(memory_rows) != 1 or memory_rows[0].get("name") != "memory":
|
||||
raise LockError(f"{label} must export exactly one memory named memory")
|
||||
return exports
|
||||
|
||||
|
||||
def require_typed_imports(imports: object, label: str) -> list[dict[str, Any]]:
|
||||
if not isinstance(imports, list) or not all(isinstance(item, dict) for item in imports):
|
||||
raise LockError(f"{label} must be a list of typed import rows")
|
||||
names: list[str] = []
|
||||
for item in imports:
|
||||
module = item.get("module")
|
||||
name = item.get("name")
|
||||
if not isinstance(module, str) or not isinstance(name, str) or not name:
|
||||
raise LockError(f"{label} row is missing module/name: {item!r}")
|
||||
if set(item) != IMPORT_KEYS:
|
||||
raise LockError(f"{label} {name} has extra or missing keys")
|
||||
wasm_signature(item.get("params"), item.get("results"), f"{label} {name}")
|
||||
names.append(name)
|
||||
if len(names) != len(set(names)):
|
||||
raise LockError(f"{label} contains duplicate import names")
|
||||
return imports
|
||||
|
||||
|
||||
def require_build(build: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(build, dict):
|
||||
raise LockError(f"{label} is missing build metadata")
|
||||
stack = build.get("wasm_stack_bytes")
|
||||
memory = build.get("wasm_memory_max_bytes")
|
||||
if stack != WASM_STACK_BYTES:
|
||||
raise LockError(
|
||||
f"{label} wasm_stack_bytes is {stack!r}, expected {WASM_STACK_BYTES}"
|
||||
)
|
||||
if memory != PROVIDER_MEMORY_MAX_BYTES:
|
||||
raise LockError(
|
||||
f"{label} wasm_memory_max_bytes is {memory!r}, "
|
||||
f"expected {PROVIDER_MEMORY_MAX_BYTES}"
|
||||
)
|
||||
return build
|
||||
|
||||
|
||||
def cross_compare_int(cmake: dict[str, str], key: str, actual: object, label: str) -> int:
|
||||
expected = require_int(cmake, key)
|
||||
if actual != expected:
|
||||
raise LockError(
|
||||
f"{label} disagrees between JSON ({actual!r}) and CMake {key} ({expected!r})"
|
||||
)
|
||||
return expected
|
||||
|
||||
|
||||
def cross_compare_str(cmake: dict[str, str], key: str, actual: object, label: str) -> str:
|
||||
expected = require(cmake, key)
|
||||
if str(actual) != expected:
|
||||
raise LockError(
|
||||
f"{label} disagrees between JSON ({actual!r}) and CMake {key} ({expected!r})"
|
||||
)
|
||||
return expected
|
||||
|
||||
|
||||
def validate_lock(
|
||||
profile: dict[str, Any],
|
||||
native: dict[str, Any],
|
||||
cmake: dict[str, str],
|
||||
wasmtime_version: str,
|
||||
api_identities: dict[str, str],
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any], str, str, int]:
|
||||
schema = require(cmake, "MANIFEST_SCHEMA")
|
||||
if schema != "xahau.quickjs.runtime-profile-lock.v1" or profile.get("schema") != schema:
|
||||
raise LockError(f"unsupported QuickJS provider manifest schema: {schema}")
|
||||
|
||||
expected_sha = require_hex(require(cmake, "PROVIDER_SHA256"), "CMake provider SHA-256")
|
||||
json_sha = require_hex(profile.get("provider", {}).get("sha256"), "JSON provider SHA-256")
|
||||
if json_sha != expected_sha:
|
||||
raise LockError("provider sha256 disagrees between JSON lock and cmake")
|
||||
require_sealed(expected_sha, SEALED_PROVIDER_SHA256, "provider SHA-256")
|
||||
expected_size = require_int(cmake, "PROVIDER_SIZE")
|
||||
json_size = profile.get("provider", {}).get("size")
|
||||
if json_size != expected_size:
|
||||
raise LockError("provider size disagrees between JSON lock and cmake")
|
||||
require_sealed(expected_size, SEALED_PROVIDER_SIZE, "provider size")
|
||||
|
||||
bytecode = require_hex(require(cmake, "BYTECODE_ABI_ID"), "CMake bytecode ABI")
|
||||
if require_hex(profile.get("bytecode_abi_id"), "JSON bytecode ABI") != bytecode:
|
||||
raise LockError("bytecode ABI disagrees between JSON lock and cmake")
|
||||
require_sealed(bytecode, SEALED_BYTECODE_ABI_ID, "bytecode ABI")
|
||||
runtime_profile = require_hex(
|
||||
require(cmake, "RUNTIME_PROFILE_ID"), "CMake runtime-profile ID"
|
||||
)
|
||||
if require_hex(profile.get("runtime_profile_id"), "JSON runtime-profile ID") != runtime_profile:
|
||||
raise LockError("runtime-profile ID disagrees between JSON lock and cmake")
|
||||
require_sealed(
|
||||
runtime_profile, SEALED_RUNTIME_PROFILE_ID, "runtime-profile ID"
|
||||
)
|
||||
|
||||
pinned_wasmtime = require(cmake, "WASMTIME_VERSION")
|
||||
engine = profile.get("source", {}).get("engine", {})
|
||||
engine_version = engine.get("version")
|
||||
if wasmtime_version != pinned_wasmtime or engine_version != pinned_wasmtime:
|
||||
raise LockError(
|
||||
f"QuickJS provider requires Wasmtime {pinned_wasmtime}, "
|
||||
f"but CMake resolved {wasmtime_version} and JSON has {engine_version!r}"
|
||||
)
|
||||
require_sealed(pinned_wasmtime, SEALED_WASMTIME_VERSION, "Wasmtime version")
|
||||
require_sealed(
|
||||
engine.get("configuration"),
|
||||
SEALED_ENGINE_CONFIGURATION,
|
||||
"Wasmtime engine configuration",
|
||||
)
|
||||
|
||||
limits = profile.get("source", {}).get("limits")
|
||||
if not isinstance(limits, dict):
|
||||
raise LockError("JSON runtime-profile source is missing limits")
|
||||
cross_compare_int(
|
||||
cmake, "SERIALIZED_OBJECT_MAX_BYTES", limits.get("serialized_object_max_bytes"),
|
||||
"serialized_object_max_bytes",
|
||||
)
|
||||
cross_compare_int(
|
||||
cmake, "SERIALIZED_OBJECT_MAX_FIELDS", limits.get("serialized_object_max_fields"),
|
||||
"serialized_object_max_fields",
|
||||
)
|
||||
cross_compare_int(
|
||||
cmake, "SERIALIZED_OBJECT_MAX_SCOPES", limits.get("serialized_object_max_scopes"),
|
||||
"serialized_object_max_scopes",
|
||||
)
|
||||
cross_compare_int(
|
||||
cmake, "SERIALIZED_OBJECT_MAX_DEPTH", limits.get("serialized_object_max_depth"),
|
||||
"serialized_object_max_depth",
|
||||
)
|
||||
for key, expected in SEALED_LIMITS.items():
|
||||
require_sealed(limits.get(key), expected, key)
|
||||
require_sealed(
|
||||
limits.get("host_work_addressed_length_indices"),
|
||||
SEALED_HOST_WORK_ADDRESSED_LENGTH_INDICES,
|
||||
"host_work_addressed_length_indices",
|
||||
)
|
||||
cross_compare_int(cmake, "HEAP_BYTES", limits.get("quickjs_heap_bytes"), "quickjs_heap_bytes")
|
||||
cross_compare_int(cmake, "STACK_BYTES", limits.get("quickjs_stack_bytes"), "quickjs_stack_bytes")
|
||||
cross_compare_int(
|
||||
cmake, "INITIALIZATION_FUEL", limits.get("wasmtime_fuel_per_initialization"),
|
||||
"initialization fuel",
|
||||
)
|
||||
cross_compare_int(
|
||||
cmake, "INVOCATION_FUEL", limits.get("wasmtime_fuel_per_invocation"),
|
||||
"invocation fuel",
|
||||
)
|
||||
cross_compare_int(cmake, "HOST_WORK_BUDGET", limits.get("host_work_budget"), "host_work_budget")
|
||||
cross_compare_int(
|
||||
cmake, "HOST_WORK_BASE_PER_CALL", limits.get("host_work_base_per_call"),
|
||||
"host_work_base_per_call",
|
||||
)
|
||||
cross_compare_int(
|
||||
cmake, "HOST_WORK_PER_ADDRESSED_BYTE", limits.get("host_work_per_addressed_byte"),
|
||||
"host_work_per_addressed_byte",
|
||||
)
|
||||
cross_compare_str(cmake, "HOST_WORK_METER", limits.get("host_work_meter"), "host_work_meter")
|
||||
host_adapter_policy = cross_compare_str(
|
||||
cmake, "HOST_ADAPTER_POLICY",
|
||||
profile.get("source", {}).get("execution", {}).get("host_adapter_policy"),
|
||||
"host_adapter_policy",
|
||||
)
|
||||
require_sealed(
|
||||
host_adapter_policy, SEALED_HOST_ADAPTER_POLICY, "host_adapter_policy"
|
||||
)
|
||||
hook_api_version = cross_compare_int(
|
||||
cmake, "HOOK_API_VERSION",
|
||||
profile.get("source", {}).get("artifact", {}).get("hook_api_version"),
|
||||
"hook_api_version",
|
||||
)
|
||||
require_sealed(hook_api_version, SEALED_HOOK_API_VERSION, "hook_api_version")
|
||||
|
||||
provider = profile.get("provider")
|
||||
source_provider = profile.get("source", {}).get("provider")
|
||||
if not isinstance(provider, dict) or not isinstance(source_provider, dict):
|
||||
raise LockError("JSON lock is missing provider or source.provider")
|
||||
require_build(provider.get("build"), "provider.build")
|
||||
require_build(source_provider.get("build"), "source.provider.build")
|
||||
require_sealed(
|
||||
source_provider.get("forbidden_import_modules"),
|
||||
["wasi_snapshot_preview1"],
|
||||
"forbidden provider import modules",
|
||||
)
|
||||
|
||||
expected_import_count = require_int(cmake, "PROVIDER_IMPORT_COUNT")
|
||||
provider_imports = require_typed_imports(provider.get("imports"), "provider.imports")
|
||||
source_imports = require_typed_imports(
|
||||
source_provider.get("imports"), "source.provider.imports"
|
||||
)
|
||||
native_imports = native.get("selected")
|
||||
if not isinstance(native_imports, list):
|
||||
raise LockError("native ABI snapshot is missing selected imports")
|
||||
if provider_imports != source_imports:
|
||||
raise LockError("provider import signatures disagree between JSON copies")
|
||||
if provider_imports != SEALED_IMPORTS:
|
||||
raise LockError(
|
||||
"provider import signatures disagree with the sealed F0 table"
|
||||
)
|
||||
if (
|
||||
len(provider_imports) != expected_import_count
|
||||
or len(native_imports) != expected_import_count
|
||||
):
|
||||
raise LockError("QuickJS provider/native ABI import counts differ")
|
||||
provider_names = [item["name"] for item in provider_imports]
|
||||
native_names = [item["name"] for item in native_imports]
|
||||
if len(set(provider_names)) != expected_import_count or set(provider_names) != set(
|
||||
native_names
|
||||
):
|
||||
raise LockError("QuickJS provider/native ABI import sets differ")
|
||||
native_by_name = {item["name"]: item for item in native_imports}
|
||||
for item in provider_imports:
|
||||
native_item = native_by_name[item["name"]]
|
||||
expected_sig = wasm_signature(
|
||||
item["params"], item["results"], f"import {item['name']}"
|
||||
)
|
||||
if native_item.get("wasm_signature") != expected_sig:
|
||||
raise LockError(
|
||||
f"import {item['name']} wasm signature disagrees between JSON and native ABI"
|
||||
)
|
||||
|
||||
expected_export_count = require_int(cmake, "PROVIDER_EXPORT_COUNT")
|
||||
provider_exports = require_typed_exports(provider.get("exports"), "provider.exports")
|
||||
allowed_exports = require_typed_exports(
|
||||
source_provider.get("allowed_exports"), "source.provider.allowed_exports"
|
||||
)
|
||||
if len(provider_exports) != expected_export_count:
|
||||
raise LockError("QuickJS provider export count disagrees with its CMake lock")
|
||||
if provider_exports != allowed_exports:
|
||||
raise LockError("provider export signatures disagree between JSON copies")
|
||||
memory = next(item for item in provider_exports if item["kind"] == "memory")
|
||||
if (
|
||||
int(memory["minimum_pages"]) != PROVIDER_MEMORY_MINIMUM_PAGES
|
||||
or int(memory["maximum_pages"]) != PROVIDER_MEMORY_MAXIMUM_PAGES
|
||||
or memory["memory64"] is not False
|
||||
or memory["shared"] is not False
|
||||
):
|
||||
raise LockError(
|
||||
"QuickJS provider memory shape is not min 7 / max 512 / "
|
||||
"memory64=false / shared=false"
|
||||
)
|
||||
if provider_exports != SEALED_EXPORTS:
|
||||
raise LockError(
|
||||
"provider export signatures disagree with the sealed F0 table"
|
||||
)
|
||||
|
||||
surface = profile.get("javascript_surface")
|
||||
nested_surface = profile.get("source", {}).get("javascript_surface")
|
||||
if not isinstance(surface, dict) or not isinstance(nested_surface, dict):
|
||||
raise LockError("JSON lock is missing javascript_surface")
|
||||
if set(surface) != SURFACE_KEYS or set(nested_surface) != NESTED_SURFACE_KEYS:
|
||||
raise LockError("javascript_surface has extra or missing keys")
|
||||
declaration_sha = require_hex(
|
||||
surface.get("declaration_sha256"), "javascript surface declaration SHA-256"
|
||||
)
|
||||
surface_sha = require_hex(surface.get("sha256"), "javascript surface SHA-256")
|
||||
require_sealed(
|
||||
declaration_sha,
|
||||
api_identities["exact_v1_declaration"],
|
||||
"exact-v1 declaration SHA-256",
|
||||
)
|
||||
require_sealed(
|
||||
surface_sha, api_identities["selected_surface"], "selected surface SHA-256"
|
||||
)
|
||||
for key in ("declaration", "manifest", "schema"):
|
||||
if surface.get(key) != nested_surface.get(key):
|
||||
raise LockError(f"javascript_surface.{key} disagrees between JSON copies")
|
||||
|
||||
return (
|
||||
provider_imports,
|
||||
provider_exports,
|
||||
native_imports,
|
||||
declaration_sha,
|
||||
surface_sha,
|
||||
expected_size,
|
||||
)
|
||||
|
||||
|
||||
def render_source(
|
||||
*,
|
||||
provenance: str,
|
||||
cmake: dict[str, str],
|
||||
native: dict[str, Any],
|
||||
provider_imports: list[dict[str, Any]],
|
||||
provider_exports: list[dict[str, Any]],
|
||||
native_imports: list[dict[str, Any]],
|
||||
declaration_sha: str,
|
||||
surface_sha: str,
|
||||
broad_declaration_sha: str,
|
||||
api_artifact_manifest_sha: str,
|
||||
manifest_sha: str,
|
||||
expected_sha: str,
|
||||
expected_size: int,
|
||||
wasm_bytes: bytes | None,
|
||||
actual_native: str,
|
||||
) -> str:
|
||||
provider_names = [item["name"] for item in provider_imports]
|
||||
provider_rows = ",\n ".join(
|
||||
"{"
|
||||
+ ", ".join(
|
||||
(
|
||||
quote(item["module"]),
|
||||
quote(item["name"]),
|
||||
quote(join_types(item["params"], f"import {item['name']} params")),
|
||||
quote(join_types(item["results"], f"import {item['name']} results")),
|
||||
)
|
||||
)
|
||||
+ "}"
|
||||
for item in provider_imports
|
||||
)
|
||||
native_rows = ",\n ".join(
|
||||
"{"
|
||||
+ ", ".join(
|
||||
(
|
||||
quote(item["name"]),
|
||||
quote(item["return_type"]),
|
||||
quote(",".join(item["param_types"])),
|
||||
quote(item["amendment"] or ""),
|
||||
)
|
||||
)
|
||||
+ "}"
|
||||
for item in native_imports
|
||||
)
|
||||
export_rows = ",\n ".join(format_export_row(item) for item in provider_exports)
|
||||
source = native["source"]
|
||||
memory = next(item for item in provider_exports if item["kind"] == "memory")
|
||||
return f"""// Generated by cmake/GenerateQuickJSProviderBundle.py; do not edit.
|
||||
// Source: {provenance}
|
||||
#include <xrpld/app/hook/QuickJSHookRuntime.h>
|
||||
#include <xrpld/app/hook/detail/QuickJSProviderProfile.h>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
namespace hook::artifact::generated {{
|
||||
|
||||
std::array<std::uint8_t, 32> const providerSHA256 = {{
|
||||
{hex_bytes(expected_sha)}}};
|
||||
std::array<std::uint8_t, 32> const bytecodeABI = {{
|
||||
{hex_bytes(require(cmake, "BYTECODE_ABI_ID"))}}};
|
||||
std::array<std::uint8_t, 32> const runtimeProfile = {{
|
||||
{hex_bytes(require(cmake, "RUNTIME_PROFILE_ID"))}}};
|
||||
std::string_view const providerManifestSHA256 = {quote(manifest_sha)};
|
||||
std::size_t const providerSize = {expected_size};
|
||||
std::uint16_t const hookApiVersion = {require(cmake, "HOOK_API_VERSION")};
|
||||
std::uint64_t const initializationFuel = {require(cmake, "INITIALIZATION_FUEL")}ULL;
|
||||
std::uint64_t const invocationFuel = {require(cmake, "INVOCATION_FUEL")}ULL;
|
||||
std::string_view const hostWorkMeter = {quote(require(cmake, "HOST_WORK_METER"))};
|
||||
std::uint64_t const hostWorkBudget = {require(cmake, "HOST_WORK_BUDGET")}ULL;
|
||||
std::uint64_t const hostWorkBasePerCall = {require(cmake, "HOST_WORK_BASE_PER_CALL")}ULL;
|
||||
std::uint64_t const hostWorkPerAddressedByte =
|
||||
{require(cmake, "HOST_WORK_PER_ADDRESSED_BYTE")}ULL;
|
||||
std::string_view const hostAdapterPolicy = {quote(require(cmake, "HOST_ADAPTER_POLICY"))};
|
||||
std::uint32_t const heapBytes = {require(cmake, "HEAP_BYTES")}U;
|
||||
std::uint32_t const stackBytes = {require(cmake, "STACK_BYTES")}U;
|
||||
std::uint32_t const wasmStackBytes = {WASM_STACK_BYTES}U;
|
||||
std::uint32_t const serializedObjectMaxBytes =
|
||||
{require(cmake, "SERIALIZED_OBJECT_MAX_BYTES")}U;
|
||||
std::uint32_t const serializedObjectMaxFields =
|
||||
{require(cmake, "SERIALIZED_OBJECT_MAX_FIELDS")}U;
|
||||
std::uint32_t const serializedObjectMaxScopes =
|
||||
{require(cmake, "SERIALIZED_OBJECT_MAX_SCOPES")}U;
|
||||
std::uint32_t const serializedObjectMaxDepth =
|
||||
{require(cmake, "SERIALIZED_OBJECT_MAX_DEPTH")}U;
|
||||
std::uint32_t const providerMemoryMinimumPages = {int(memory["minimum_pages"])}U;
|
||||
std::uint32_t const providerMemoryMaximumPages = {int(memory["maximum_pages"])}U;
|
||||
bool const providerMemory64 = false;
|
||||
bool const providerMemoryShared = false;
|
||||
std::string_view const javascriptBroadDeclarationSHA256 =
|
||||
{quote(broad_declaration_sha)};
|
||||
std::string_view const javascriptExactV1DeclarationSHA256 =
|
||||
{quote(declaration_sha)};
|
||||
std::string_view const javascriptSurfaceSHA256 = {quote(surface_sha)};
|
||||
std::string_view const javascriptAPIArtifactManifestSHA256 =
|
||||
{quote(api_artifact_manifest_sha)};
|
||||
|
||||
namespace {{
|
||||
|
||||
std::string_view const providerImportNames[] = {{
|
||||
{", ".join(quote(name) for name in provider_names)}}};
|
||||
std::string_view const providerExportNames[] = {{
|
||||
{", ".join(quote(item["name"]) for item in provider_exports)}}};
|
||||
ProviderImportSignature const providerImportSignatureData[] = {{
|
||||
{provider_rows}}};
|
||||
ProviderExportSignature const providerExportSignatureData[] = {{
|
||||
{export_rows}}};
|
||||
NativeImportSignature const nativeImportSignatureData[] = {{
|
||||
{native_rows}}};
|
||||
|
||||
}} // namespace
|
||||
|
||||
std::span<std::string_view const> const providerImports{{providerImportNames}};
|
||||
std::span<std::string_view const> const providerExports{{providerExportNames}};
|
||||
std::span<ProviderImportSignature const> const providerImportSignatures{{
|
||||
providerImportSignatureData}};
|
||||
std::span<ProviderExportSignature const> const providerExportSignatures{{
|
||||
providerExportSignatureData}};
|
||||
std::span<NativeImportSignature const> const nativeImportSignatures{{
|
||||
nativeImportSignatureData}};
|
||||
std::string_view const nativeABISourceRepository = {quote(source["repository"])};
|
||||
std::string_view const nativeABISourceCommit = {quote(source["commit"])};
|
||||
std::string_view const nativeABISourcePath = {quote(source["path"])};
|
||||
std::string_view const nativeABISHA256 = {quote(actual_native)};
|
||||
std::size_t const nativeABICatalogueCount = {source["macro_function_count"]};
|
||||
|
||||
}} // namespace hook::artifact::generated
|
||||
|
||||
namespace hook {{
|
||||
|
||||
{embed_body(wasm_bytes)}
|
||||
}} // namespace hook
|
||||
"""
|
||||
|
||||
|
||||
def project_bundle(bundle: Path, wasmtime_version: str, output: Path) -> int:
|
||||
profile_path = bundle / "jshookz_provider.manifest.json"
|
||||
cmake_path = bundle / "jshookz_provider.manifest.cmake"
|
||||
native_path = bundle / "jshookz_provider.native-abi.json"
|
||||
api_manifest_path = bundle / "api-artifacts.json"
|
||||
api_paths = tuple(
|
||||
bundle / local_name
|
||||
for _, local_name, _ in SEALED_API_ARTIFACTS.values()
|
||||
)
|
||||
for path in (profile_path, cmake_path, native_path, api_manifest_path, *api_paths):
|
||||
if not path.is_file():
|
||||
print(f"missing sealed QuickJS bundle file: {path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
cmake = parse_cmake(cmake_path)
|
||||
actual_manifest = hashlib.sha256(profile_path.read_bytes()).hexdigest()
|
||||
if actual_manifest != require(cmake, "MANIFEST_SHA256"):
|
||||
print(
|
||||
"QuickJS provider JSON manifest does not match its CMake projection",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
actual_native = hashlib.sha256(native_path.read_bytes()).hexdigest()
|
||||
if actual_native != require(cmake, "NATIVE_ABI_SHA256"):
|
||||
print(
|
||||
"QuickJS native ABI snapshot does not match its sealed digest",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
try:
|
||||
profile = json.loads(profile_path.read_text())
|
||||
native = json.loads(native_path.read_text())
|
||||
api_identities = validate_api_artifacts(bundle)
|
||||
(
|
||||
provider_imports,
|
||||
provider_exports,
|
||||
native_imports,
|
||||
declaration_sha,
|
||||
surface_sha,
|
||||
expected_size,
|
||||
) = validate_lock(
|
||||
profile, native, cmake, wasmtime_version, api_identities
|
||||
)
|
||||
require_sealed(
|
||||
actual_manifest, SEALED_MANIFEST_SHA256, "provider manifest SHA-256"
|
||||
)
|
||||
require_sealed(
|
||||
actual_native, SEALED_NATIVE_ABI_SHA256, "native ABI SHA-256"
|
||||
)
|
||||
except LockError as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
wasm_name = require(cmake, "PROVIDER_FILE")
|
||||
wasm_path = bundle / wasm_name
|
||||
expected_sha = require(cmake, "PROVIDER_SHA256").lower()
|
||||
fingerprint = hashlib.sha256()
|
||||
for path in (
|
||||
profile_path,
|
||||
cmake_path,
|
||||
native_path,
|
||||
api_manifest_path,
|
||||
*api_paths,
|
||||
Path(__file__),
|
||||
):
|
||||
fingerprint.update(path.read_bytes())
|
||||
fingerprint.update(wasmtime_version.encode())
|
||||
wasm_bytes: bytes | None = None
|
||||
if wasm_path.is_file():
|
||||
wasm_bytes = wasm_path.read_bytes()
|
||||
digest = hashlib.sha256(wasm_bytes).hexdigest()
|
||||
if digest != expected_sha or len(wasm_bytes) != expected_size:
|
||||
print(
|
||||
"QuickJS provider WASM does not match its generated manifest",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
fingerprint.update(b"wasm")
|
||||
fingerprint.update(bytes.fromhex(digest))
|
||||
provenance = f"{wasm_path} sha256={digest} size={len(wasm_bytes)}"
|
||||
else:
|
||||
fingerprint.update(b"absent")
|
||||
provenance = "no provider binary was present in the bundle directory"
|
||||
|
||||
stamp_path = output.with_name(output.name + ".stamp")
|
||||
stamp = fingerprint.hexdigest()
|
||||
if output.is_file() and stamp_path.is_file() and stamp_path.read_text() == stamp:
|
||||
return 0
|
||||
|
||||
if wasm_bytes is None:
|
||||
print(
|
||||
f"No QuickJS provider WASM at {wasm_path}; nothing is embedded "
|
||||
"and the daemon will refuse to start",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
text = render_source(
|
||||
provenance=provenance,
|
||||
cmake=cmake,
|
||||
native=native,
|
||||
provider_imports=provider_imports,
|
||||
provider_exports=provider_exports,
|
||||
native_imports=native_imports,
|
||||
declaration_sha=declaration_sha,
|
||||
surface_sha=surface_sha,
|
||||
broad_declaration_sha=api_identities["broad_declaration"],
|
||||
api_artifact_manifest_sha=api_identities["api_artifact_manifest"],
|
||||
manifest_sha=actual_manifest,
|
||||
expected_sha=expected_sha,
|
||||
expected_size=expected_size,
|
||||
wasm_bytes=wasm_bytes,
|
||||
actual_native=actual_native,
|
||||
)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(text)
|
||||
stamp_path.write_text(stamp)
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--bundle-dir", type=Path, required=True)
|
||||
parser.add_argument("--wasmtime-version", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return project_bundle(args.bundle_dir, args.wasmtime_version, args.output)
|
||||
except LockError as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,90 +0,0 @@
|
||||
set(XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/external/quickjs-provider"
|
||||
CACHE PATH
|
||||
"Directory containing the sealed QuickJS provider WASM and generated manifests")
|
||||
get_filename_component(XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR
|
||||
"${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}"
|
||||
ABSOLUTE
|
||||
BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
set(XAHAU_QUICKJS_PROVIDER_MANIFEST
|
||||
"${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}/jshookz_provider.manifest.cmake")
|
||||
set(XAHAU_QUICKJS_PROVIDER_PROFILE_LOCK
|
||||
"${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}/jshookz_provider.manifest.json")
|
||||
set(XAHAU_QUICKJS_NATIVE_ABI
|
||||
"${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}/jshookz_provider.native-abi.json")
|
||||
set(XAHAU_QUICKJS_API_ARTIFACT_MANIFEST
|
||||
"${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}/api-artifacts.json")
|
||||
set(XAHAU_QUICKJS_BROAD_DECLARATION
|
||||
"${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}/hooks-api.d.ts")
|
||||
set(XAHAU_QUICKJS_EXACT_V1_DECLARATION
|
||||
"${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}/xahau-quickjs-v1.d.ts")
|
||||
set(XAHAU_QUICKJS_SELECTED_SURFACE
|
||||
"${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}/xahau-quickjs-v1.surface.json")
|
||||
|
||||
foreach(_bundle_file
|
||||
"${XAHAU_QUICKJS_PROVIDER_MANIFEST}"
|
||||
"${XAHAU_QUICKJS_PROVIDER_PROFILE_LOCK}"
|
||||
"${XAHAU_QUICKJS_NATIVE_ABI}"
|
||||
"${XAHAU_QUICKJS_API_ARTIFACT_MANIFEST}"
|
||||
"${XAHAU_QUICKJS_BROAD_DECLARATION}"
|
||||
"${XAHAU_QUICKJS_EXACT_V1_DECLARATION}"
|
||||
"${XAHAU_QUICKJS_SELECTED_SURFACE}")
|
||||
if(NOT EXISTS "${_bundle_file}")
|
||||
message(FATAL_ERROR
|
||||
"Missing sealed QuickJS provider bundle file: ${_bundle_file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(NOT DEFINED wasmtime_VERSION_STRING OR wasmtime_VERSION_STRING STREQUAL "")
|
||||
message(FATAL_ERROR
|
||||
"QuickJS provider requires an exact Wasmtime version, but the resolved "
|
||||
"package did not publish one")
|
||||
endif()
|
||||
|
||||
find_program(XAHAU_QUICKJS_PYTHON NAMES python3 python)
|
||||
if(NOT XAHAU_QUICKJS_PYTHON)
|
||||
message(FATAL_ERROR
|
||||
"Python is required to project the sealed QuickJS provider bundle")
|
||||
endif()
|
||||
|
||||
set(XAHAU_QUICKJS_GENERATED_INCLUDE_DIR
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
set(XAHAU_QUICKJS_PROVIDER_VALUES_SOURCE
|
||||
"${XAHAU_QUICKJS_GENERATED_INCLUDE_DIR}/xrpld/app/hook/detail/QuickJSProviderValues.cpp")
|
||||
set(XAHAU_QUICKJS_BUNDLE_SCRIPT
|
||||
"${CMAKE_CURRENT_LIST_DIR}/GenerateQuickJSProviderBundle.py")
|
||||
|
||||
# The gitignored wasm is not a CMake depend (ninja cannot name a missing
|
||||
# file). Refresh on every rippled build; the script no-ops when inputs
|
||||
# including wasm existence/digest are unchanged.
|
||||
add_custom_target(xahau_quickjs_refresh_provider
|
||||
COMMAND
|
||||
"${XAHAU_QUICKJS_PYTHON}"
|
||||
"${XAHAU_QUICKJS_BUNDLE_SCRIPT}"
|
||||
--bundle-dir "${XAHAU_QUICKJS_PROVIDER_BUNDLE_DIR}"
|
||||
--wasmtime-version "${wasmtime_VERSION_STRING}"
|
||||
--output "${XAHAU_QUICKJS_PROVIDER_VALUES_SOURCE}"
|
||||
BYPRODUCTS "${XAHAU_QUICKJS_PROVIDER_VALUES_SOURCE}"
|
||||
DEPENDS
|
||||
"${XAHAU_QUICKJS_PROVIDER_PROFILE_LOCK}"
|
||||
"${XAHAU_QUICKJS_PROVIDER_MANIFEST}"
|
||||
"${XAHAU_QUICKJS_NATIVE_ABI}"
|
||||
"${XAHAU_QUICKJS_API_ARTIFACT_MANIFEST}"
|
||||
"${XAHAU_QUICKJS_BROAD_DECLARATION}"
|
||||
"${XAHAU_QUICKJS_EXACT_V1_DECLARATION}"
|
||||
"${XAHAU_QUICKJS_SELECTED_SURFACE}"
|
||||
"${XAHAU_QUICKJS_BUNDLE_SCRIPT}"
|
||||
COMMENT "Projecting sealed QuickJS provider values"
|
||||
VERBATIM)
|
||||
|
||||
message(STATUS
|
||||
"QuickJS provider values: ${XAHAU_QUICKJS_PROVIDER_VALUES_SOURCE}")
|
||||
|
||||
add_custom_target(xahau_quickjs_lock_mutations
|
||||
COMMAND
|
||||
"${XAHAU_QUICKJS_PYTHON}"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/test_generate_quickjs_provider_bundle.py"
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Proving sealed QuickJS lock mutation reds"
|
||||
VERBATIM)
|
||||
@@ -165,6 +165,9 @@ if(xrpld)
|
||||
if(tests)
|
||||
target_compile_definitions(rippled PUBLIC ENABLE_TESTS)
|
||||
endif()
|
||||
if(xahaud_runtime_test_config)
|
||||
target_compile_definitions(rippled PUBLIC XAHAUD_ENABLE_RUNTIME_TEST_CONFIG=1)
|
||||
endif()
|
||||
target_include_directories(rippled
|
||||
PRIVATE
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
|
||||
@@ -174,97 +177,25 @@ if(xrpld)
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/xrpld/*.cpp"
|
||||
)
|
||||
target_sources(rippled PRIVATE ${sources})
|
||||
add_dependencies(rippled xahau_quickjs_refresh_provider)
|
||||
target_sources(rippled PRIVATE "${XAHAU_QUICKJS_PROVIDER_VALUES_SOURCE}")
|
||||
|
||||
if(tests)
|
||||
file(GLOB_RECURSE sources CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test/*.cpp"
|
||||
)
|
||||
if(HOOKS_TEST_ONLY OR DEFINED ENV{HOOKS_TEST_ONLY})
|
||||
list(FILTER sources EXCLUDE REGEX "_test\\.cpp$")
|
||||
message(STATUS "HOOKS_TEST_ONLY: excluded *_test.cpp from src/test/")
|
||||
endif()
|
||||
target_sources(rippled PRIVATE ${sources})
|
||||
|
||||
# External Env tests live next to the producer. Point at a directory of
|
||||
# *_test.cpp files (typically jshookz/env-tests). hookz build-test-hooks
|
||||
# must be on PATH. Coverage is not wired here.
|
||||
set(HOOKS_TEST_DIR "" CACHE PATH "External hook Env-test directory")
|
||||
if(NOT HOOKS_TEST_DIR AND DEFINED ENV{HOOKS_TEST_DIR})
|
||||
set(HOOKS_TEST_DIR $ENV{HOOKS_TEST_DIR})
|
||||
set(HOOKS_TEST_DIR "$ENV{HOOKS_TEST_DIR}")
|
||||
endif()
|
||||
if(NOT HOOKS_SOURCE_DIR AND DEFINED ENV{HOOKS_SOURCE_DIR})
|
||||
set(HOOKS_SOURCE_DIR $ENV{HOOKS_SOURCE_DIR})
|
||||
endif()
|
||||
if(HOOKS_TEST_DIR AND EXISTS "${HOOKS_TEST_DIR}")
|
||||
file(GLOB EXTERNAL_HOOK_TESTS CONFIGURE_DEPENDS
|
||||
if(HOOKS_TEST_DIR)
|
||||
file(GLOB_RECURSE hook_test_sources CONFIGURE_DEPENDS
|
||||
"${HOOKS_TEST_DIR}/*_test.cpp"
|
||||
)
|
||||
if(EXTERNAL_HOOK_TESTS)
|
||||
set(_hooks_extra_args "")
|
||||
set(_hooks_source_deps "")
|
||||
if(HOOKS_SOURCE_DIR)
|
||||
foreach(_dir ${HOOKS_SOURCE_DIR})
|
||||
list(APPEND _hooks_extra_args "--hooks-c-dir" "${_dir}")
|
||||
string(REGEX REPLACE "^[^=]+=" "" _hook_dir "${_dir}")
|
||||
if(EXISTS "${_hook_dir}")
|
||||
file(GLOB_RECURSE _hook_dir_deps CONFIGURE_DEPENDS
|
||||
"${_hook_dir}/*.c"
|
||||
"${_hook_dir}/*.h"
|
||||
"${_hook_dir}/*.ts"
|
||||
"${_hook_dir}/*.js"
|
||||
)
|
||||
if(HOOKS_TEST_DIR)
|
||||
list(FILTER _hook_dir_deps EXCLUDE REGEX "^${HOOKS_TEST_DIR}/")
|
||||
endif()
|
||||
list(APPEND _hooks_source_deps ${_hook_dir_deps})
|
||||
endif()
|
||||
endforeach()
|
||||
list(REMOVE_DUPLICATES _hooks_source_deps)
|
||||
endif()
|
||||
set(_hooks_always_run OFF)
|
||||
if(HOOKS_FORCE_RECOMPILE OR DEFINED ENV{HOOKS_FORCE_RECOMPILE})
|
||||
list(APPEND _hooks_extra_args "--force-write" "--no-cache")
|
||||
set(_hooks_always_run ON)
|
||||
message(STATUS "Hook force recompile enabled (cache bypassed)")
|
||||
endif()
|
||||
foreach(_test_file ${EXTERNAL_HOOK_TESTS})
|
||||
get_filename_component(_stem ${_test_file} NAME_WE)
|
||||
set(_hooks_header "${HOOKS_TEST_DIR}/${_stem}_hooks.h")
|
||||
if(_hooks_always_run)
|
||||
add_custom_target(compile_hooks_${_stem} ALL
|
||||
COMMAND hookz build-test-hooks "${_test_file}" ${_hooks_extra_args}
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Compiling hooks for ${_stem} (forced)"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND EXTERNAL_HOOK_TARGETS compile_hooks_${_stem})
|
||||
else()
|
||||
add_custom_command(
|
||||
OUTPUT "${_hooks_header}"
|
||||
COMMAND hookz build-test-hooks "${_test_file}" ${_hooks_extra_args}
|
||||
DEPENDS "${_test_file}" ${_hooks_source_deps}
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Compiling hooks for ${_stem}"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND EXTERNAL_HOOK_HEADERS "${_hooks_header}")
|
||||
endif()
|
||||
endforeach()
|
||||
if(_hooks_always_run)
|
||||
foreach(_tgt ${EXTERNAL_HOOK_TARGETS})
|
||||
add_dependencies(rippled ${_tgt})
|
||||
endforeach()
|
||||
else()
|
||||
add_custom_target(compile_external_hooks DEPENDS ${EXTERNAL_HOOK_HEADERS})
|
||||
add_dependencies(rippled compile_external_hooks)
|
||||
endif()
|
||||
target_sources(rippled PRIVATE ${EXTERNAL_HOOK_TESTS})
|
||||
set_property(
|
||||
SOURCE ${EXTERNAL_HOOK_TESTS}
|
||||
APPEND PROPERTY INCLUDE_DIRECTORIES "${HOOKS_TEST_DIR}"
|
||||
)
|
||||
message(STATUS "Including external hook tests from: ${HOOKS_TEST_DIR}")
|
||||
if(hook_test_sources)
|
||||
message(STATUS "Including external hook Env tests from ${HOOKS_TEST_DIR}")
|
||||
target_sources(rippled PRIVATE ${hook_test_sources})
|
||||
target_include_directories(rippled PRIVATE "${HOOKS_TEST_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -273,7 +204,6 @@ if(xrpld)
|
||||
Ripple::boost
|
||||
Ripple::opts
|
||||
Ripple::libs
|
||||
wasmtime::wasmtime
|
||||
xrpl.libxrpl
|
||||
# Workaround for a Conan 1.x bug that prevents static linking of libstdc++
|
||||
# when a dependency (snappy) modifies system_libs. See the comment in
|
||||
|
||||
@@ -22,6 +22,9 @@ target_compile_definitions (opts
|
||||
$<$<BOOL:${beast_no_unit_test_inline}>:BEAST_NO_UNIT_TEST_INLINE=1>
|
||||
$<$<BOOL:${beast_disable_autolink}>:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1>
|
||||
$<$<BOOL:${single_io_service_thread}>:RIPPLE_SINGLE_IO_SERVICE_THREAD=1>
|
||||
# Enhanced logging is enabled for Debug builds, or explicitly via
|
||||
# -DBEAST_ENHANCED_LOGGING=ON for other build types.
|
||||
$<$<OR:$<CONFIG:Debug>,$<BOOL:${BEAST_ENHANCED_LOGGING}>>:BEAST_ENHANCED_LOGGING=1>
|
||||
$<$<BOOL:${voidstar}>:ENABLE_VOIDSTAR>)
|
||||
target_compile_options (opts
|
||||
INTERFACE
|
||||
|
||||
@@ -12,6 +12,13 @@ option(xrpld "Build xrpld" ON)
|
||||
|
||||
option(tests "Build tests" ON)
|
||||
|
||||
option(xahaud_runtime_test_config
|
||||
"Enable XAHAUD_RUNTIME_TEST_CONFIG env and runtime_config RPC fault-injection controls"
|
||||
OFF)
|
||||
# Conan 2 local opt-in:
|
||||
# [conf]
|
||||
# tools.cmake.cmaketoolchain:extra_variables={"xahaud_runtime_test_config":"ON"}
|
||||
|
||||
option(unity "Creates a build using UNITY support in cmake. This is the default" ON)
|
||||
if(unity)
|
||||
if(NOT is_ci)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
find_package(wasmtime REQUIRED)
|
||||
@@ -1,756 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail-closed generator/checker mutation reds for the sealed QuickJS lock."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = Path(__file__).with_name("GenerateQuickJSProviderBundle.py")
|
||||
BUNDLE = ROOT / "external" / "quickjs-provider"
|
||||
WASM_VERSION = "47.0.3"
|
||||
CMAKE_SET = re.compile(r'^set\(XAHAU_QUICKJS_([A-Z0-9_]+) "([^"]*)"\)\s*$')
|
||||
|
||||
PIN = {
|
||||
"provider_sha256": "f60ab333acb547e50d7d087960839bd11833cb133d496af98e0b72a1bb3b84df",
|
||||
"provider_size": "1116638",
|
||||
"manifest_sha256": "1673b27d21063957cece9095cceb7e2dbfa53b6f49a5b7f824a486d7d90b2391",
|
||||
"runtime_profile_id": "29acf12e170436e5ccadfbc118d02a60909f86a88262106652b2f71994b0ea25",
|
||||
"broad_declaration_sha256": "649079b831bc68eff0f440644649a505addafb08a3f5174e4031c8de6aedffa0",
|
||||
"exact_v1_declaration_sha256": "20881232f0602e35de0fcbb8b5a3c1511606392749a0f79daebb386204f4f4da",
|
||||
"surface_sha256": "96a9218bf38abbf1bc9f2839408700d03572e0b065d625263f70477bc32597ac",
|
||||
"api_artifact_manifest_sha256": "95babf622bf5edff76fdcbd9746bad8f1f8b275a3d4bacdc320ff4deae543eba",
|
||||
"bytecode_abi": "75ea54f357d397c4b33899e495bb385dad975a43b1c4a7a2cead30474327d33e",
|
||||
"native_abi": "328ec938dcad875f3bdf25b8d779dd77f3571589818ca9271cb98c64c7018f99",
|
||||
"wasm_stack_bytes": "131072",
|
||||
}
|
||||
|
||||
RECEIPT_B = {
|
||||
"provider_sha256": "40f9ac0203afa9296196c627bc94e669ad789322ddc1e4ab58ba0cf6d32a5e21",
|
||||
"provider_size": "1101461",
|
||||
"runtime_profile_id": "60950ca8b6fe4dd2a35559367051998bc04f75e8aa41934dd4caad0de5a1c3ba",
|
||||
"declaration_sha256": "56b4b2974b8a63a550721abd60350e392990762e76666f050b1a7c810e584957",
|
||||
"surface_sha256": "b112346b95da74d04930ba86bd597e56a428e41e11581804ccc49907ae206eb0",
|
||||
}
|
||||
|
||||
TYPED_IMPORT_ROWS = (
|
||||
'{"env", "accept", "i32,i32,i64", "i64"}',
|
||||
'{"env", "emit", "i32,i32,i32,i32", "i64"}',
|
||||
'{"env", "etxn_reserve", "i32", "i64"}',
|
||||
'{"env", "hook_account", "i32,i32", "i64"}',
|
||||
'{"env", "ledger_last_hash", "i32,i32", "i64"}',
|
||||
'{"env", "ledger_last_time", "", "i64"}',
|
||||
'{"env", "ledger_seq", "", "i64"}',
|
||||
'{"env", "otxn_type", "", "i64"}',
|
||||
'{"env", "prepare", "i32,i32,i32,i32", "i64"}',
|
||||
'{"env", "rollback", "i32,i32,i64", "i64"}',
|
||||
'{"env", "state", "i32,i32,i32,i32", "i64"}',
|
||||
'{"env", "state_set", "i32,i32,i32,i32", "i64"}',
|
||||
'{"env", "trace", "i32,i32,i32,i32,i32", "i64"}',
|
||||
)
|
||||
|
||||
TYPED_EXPORT_ROWS = (
|
||||
'{"function", "_initialize", "", "", 0U, 0U, false, false}',
|
||||
'{"function", "free", "i32", "", 0U, 0U, false, false}',
|
||||
'{"function", "malloc", "i32", "i32", 0U, 0U, false, false}',
|
||||
'{"memory", "memory", "", "", 7U, 512U, false, false}',
|
||||
'{"function", "qjs_cbak", "i32,i32,i32", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_compile", "i32,i32", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_compile_module", "i32,i32", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_destroy", "", "", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_enable_coverage", "i32", "", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_eval", "i32,i32", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_eval_bytecode", "i32,i32", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_eval_module", "i32,i32", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_get_bytecode_len", "", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_get_bytecode_ptr", "", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_get_result_len", "", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_get_result_ptr", "", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_hook", "i32,i32,i32", "i32", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_init", "", "", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_set_max_stack_size", "i32", "", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_set_memory_limit", "i32", "", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_set_seed", "i32", "", 0U, 0U, false, false}',
|
||||
'{"function", "qjs_validate_hook_module", "i32,i32", "i32", 0U, 0U, false, false}',
|
||||
)
|
||||
|
||||
|
||||
def json_path(bundle: Path) -> Path:
|
||||
return bundle / "jshookz_provider.manifest.json"
|
||||
|
||||
|
||||
def cmake_path(bundle: Path) -> Path:
|
||||
return bundle / "jshookz_provider.manifest.cmake"
|
||||
|
||||
|
||||
def native_path(bundle: Path) -> Path:
|
||||
return bundle / "jshookz_provider.native-abi.json"
|
||||
|
||||
|
||||
def wasm_path(bundle: Path) -> Path:
|
||||
return bundle / "jshookz_provider.wasm"
|
||||
|
||||
|
||||
def api_manifest_path(bundle: Path) -> Path:
|
||||
return bundle / "api-artifacts.json"
|
||||
|
||||
|
||||
def broad_declaration_path(bundle: Path) -> Path:
|
||||
return bundle / "hooks-api.d.ts"
|
||||
|
||||
|
||||
def exact_v1_declaration_path(bundle: Path) -> Path:
|
||||
return bundle / "xahau-quickjs-v1.d.ts"
|
||||
|
||||
|
||||
def surface_path(bundle: Path) -> Path:
|
||||
return bundle / "xahau-quickjs-v1.surface.json"
|
||||
|
||||
|
||||
def load_json(bundle: Path) -> dict:
|
||||
return json.loads(json_path(bundle).read_text())
|
||||
|
||||
|
||||
def write_json(bundle: Path, data: dict) -> None:
|
||||
json_path(bundle).write_text(json.dumps(data, indent=2) + "\n")
|
||||
|
||||
|
||||
def set_cmake(bundle: Path, key: str, value: str) -> None:
|
||||
path = cmake_path(bundle)
|
||||
lines = []
|
||||
replaced = False
|
||||
for line in path.read_text().splitlines():
|
||||
match = CMAKE_SET.match(line)
|
||||
if match and match.group(1) == key:
|
||||
lines.append(f'set(XAHAU_QUICKJS_{key} "{value}")')
|
||||
replaced = True
|
||||
else:
|
||||
lines.append(line)
|
||||
if not replaced:
|
||||
raise AssertionError(f"CMake lock is missing {key}")
|
||||
path.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def rehash_manifest(bundle: Path) -> None:
|
||||
digest = hashlib.sha256(json_path(bundle).read_bytes()).hexdigest()
|
||||
set_cmake(bundle, "MANIFEST_SHA256", digest)
|
||||
|
||||
|
||||
def rehash_native(bundle: Path) -> None:
|
||||
digest = hashlib.sha256(native_path(bundle).read_bytes()).hexdigest()
|
||||
set_cmake(bundle, "NATIVE_ABI_SHA256", digest)
|
||||
|
||||
|
||||
def load_api_manifest(bundle: Path) -> dict:
|
||||
return json.loads(api_manifest_path(bundle).read_text())
|
||||
|
||||
|
||||
def write_api_manifest(bundle: Path, data: dict) -> None:
|
||||
api_manifest_path(bundle).write_text(json.dumps(data, indent=2) + "\n")
|
||||
|
||||
|
||||
def clone_bundle(root: Path) -> Path:
|
||||
dest = root / "bundle"
|
||||
shutil.copytree(BUNDLE, dest, ignore=shutil.ignore_patterns("README.md"))
|
||||
return dest
|
||||
|
||||
|
||||
def generate(
|
||||
bundle: Path, work: Path, wasmtime_version: str = WASM_VERSION
|
||||
) -> tuple[int, str, str]:
|
||||
output = work / "QuickJSProviderValues.cpp"
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--bundle-dir",
|
||||
str(bundle),
|
||||
"--wasmtime-version",
|
||||
wasmtime_version,
|
||||
"--output",
|
||||
str(output),
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
cpp = output.read_text() if output.is_file() else ""
|
||||
return completed.returncode, completed.stderr, cpp
|
||||
|
||||
|
||||
def hex_bytes(value: str) -> str:
|
||||
return ", ".join(f"0x{value[i:i + 2]}" for i in range(0, 64, 2))
|
||||
|
||||
|
||||
def pin_holds(returncode: int, cpp: str) -> bool:
|
||||
if returncode != 0 or not cpp:
|
||||
return False
|
||||
required = [
|
||||
hex_bytes(PIN["provider_sha256"]),
|
||||
f"std::size_t const providerSize = {PIN['provider_size']};",
|
||||
f'"{PIN["manifest_sha256"]}"',
|
||||
hex_bytes(PIN["runtime_profile_id"]),
|
||||
f'"{PIN["broad_declaration_sha256"]}"',
|
||||
f'"{PIN["exact_v1_declaration_sha256"]}"',
|
||||
f'"{PIN["surface_sha256"]}"',
|
||||
f'"{PIN["api_artifact_manifest_sha256"]}"',
|
||||
hex_bytes(PIN["bytecode_abi"]),
|
||||
f'"{PIN["native_abi"]}"',
|
||||
f"std::uint32_t const wasmStackBytes = {PIN['wasm_stack_bytes']}U;",
|
||||
"std::uint32_t const serializedObjectMaxBytes =\n 1048576U;",
|
||||
"std::uint32_t const serializedObjectMaxFields =\n 32768U;",
|
||||
"std::uint32_t const serializedObjectMaxScopes =\n 32769U;",
|
||||
"std::uint32_t const serializedObjectMaxDepth =\n 10U;",
|
||||
"std::uint32_t const providerMemoryMinimumPages = 7U;",
|
||||
"std::uint32_t const providerMemoryMaximumPages = 512U;",
|
||||
"constexpr char const sealedProvider[] =",
|
||||
f"sizeof(sealedProvider) - 1 == {PIN['provider_size']}",
|
||||
]
|
||||
if any(item not in cpp for item in required):
|
||||
return False
|
||||
return all(row in cpp for row in (*TYPED_IMPORT_ROWS, *TYPED_EXPORT_ROWS))
|
||||
|
||||
|
||||
class GenerateQuickJSProviderBundleTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
missing = [
|
||||
path.name
|
||||
for path in (
|
||||
json_path(BUNDLE),
|
||||
cmake_path(BUNDLE),
|
||||
native_path(BUNDLE),
|
||||
wasm_path(BUNDLE),
|
||||
api_manifest_path(BUNDLE),
|
||||
broad_declaration_path(BUNDLE),
|
||||
exact_v1_declaration_path(BUNDLE),
|
||||
surface_path(BUNDLE),
|
||||
SCRIPT,
|
||||
)
|
||||
if not path.is_file()
|
||||
]
|
||||
if missing:
|
||||
raise AssertionError(
|
||||
"sealed QuickJS bundle is incomplete: " + ", ".join(missing)
|
||||
)
|
||||
|
||||
def mutate(
|
||||
self, mutator, wasmtime_version: str = WASM_VERSION
|
||||
) -> tuple[int, str, str]:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
bundle = clone_bundle(work)
|
||||
mutator(bundle)
|
||||
return generate(bundle, work / "out", wasmtime_version)
|
||||
|
||||
def assert_generator_red(
|
||||
self, mutator, fragment: str, wasmtime_version: str = WASM_VERSION
|
||||
) -> None:
|
||||
returncode, stderr, cpp = self.mutate(mutator, wasmtime_version)
|
||||
self.assertNotEqual(returncode, 0, stderr)
|
||||
self.assertIn(fragment, stderr)
|
||||
self.assertFalse(pin_holds(returncode, cpp), stderr)
|
||||
|
||||
def test_unmutated_lock_projects_pin_and_typed_exports(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
returncode, stderr, cpp = generate(clone_bundle(work), work / "out")
|
||||
self.assertEqual(returncode, 0, stderr)
|
||||
self.assertTrue(pin_holds(returncode, cpp), stderr)
|
||||
self.assertEqual(cpp.count("ProviderExportSignature const"), 2)
|
||||
|
||||
def test_provider_bytes_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
path = wasm_path(bundle)
|
||||
data = bytearray(path.read_bytes())
|
||||
data[-1] ^= 0xFF
|
||||
path.write_bytes(data)
|
||||
|
||||
self.assert_generator_red(mutate, "WASM does not match")
|
||||
|
||||
def test_provider_size_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(bundle, "PROVIDER_SIZE", "1116637")
|
||||
|
||||
self.assert_generator_red(mutate, "provider size disagrees")
|
||||
|
||||
def test_coordinated_provider_size_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
value = 1116637
|
||||
data = load_json(bundle)
|
||||
data["provider"]["size"] = value
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "PROVIDER_SIZE", str(value))
|
||||
|
||||
self.assert_generator_red(mutate, "provider size disagrees with the sealed F0")
|
||||
|
||||
def test_provider_sha_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(
|
||||
bundle,
|
||||
"PROVIDER_SHA256",
|
||||
"00" + PIN["provider_sha256"][2:],
|
||||
)
|
||||
|
||||
self.assert_generator_red(mutate, "provider sha256 disagrees")
|
||||
|
||||
def test_coordinated_provider_sha_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
value = "00" + PIN["provider_sha256"][2:]
|
||||
data = load_json(bundle)
|
||||
data["provider"]["sha256"] = value
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "PROVIDER_SHA256", value)
|
||||
|
||||
self.assert_generator_red(mutate, "provider SHA-256 disagrees with the sealed F0")
|
||||
|
||||
def test_manifest_sha_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(
|
||||
bundle,
|
||||
"MANIFEST_SHA256",
|
||||
"00" + PIN["manifest_sha256"][2:],
|
||||
)
|
||||
|
||||
self.assert_generator_red(mutate, "JSON manifest does not match")
|
||||
|
||||
def test_coordinated_manifest_identity_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["diagnostic_note"] = "not F0"
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "provider manifest SHA-256")
|
||||
|
||||
def test_runtime_profile_id_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(
|
||||
bundle,
|
||||
"RUNTIME_PROFILE_ID",
|
||||
"00" + PIN["runtime_profile_id"][2:],
|
||||
)
|
||||
|
||||
self.assert_generator_red(mutate, "runtime-profile ID disagrees")
|
||||
|
||||
def test_coordinated_runtime_profile_id_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
value = "00" + PIN["runtime_profile_id"][2:]
|
||||
data = load_json(bundle)
|
||||
data["runtime_profile_id"] = value
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "RUNTIME_PROFILE_ID", value)
|
||||
|
||||
self.assert_generator_red(mutate, "runtime-profile ID disagrees with the sealed F0")
|
||||
|
||||
def _coordinated_api_artifact_mutation(
|
||||
self, source_path: str, local_path, fragment: str
|
||||
) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
path = local_path(bundle)
|
||||
path.write_bytes(path.read_bytes() + b"\n")
|
||||
manifest = load_api_manifest(bundle)
|
||||
manifest["artifacts"][source_path] = hashlib.sha256(
|
||||
path.read_bytes()
|
||||
).hexdigest()
|
||||
write_api_manifest(bundle, manifest)
|
||||
|
||||
self.assert_generator_red(mutate, fragment)
|
||||
|
||||
def test_broad_declaration_identity_mutation(self) -> None:
|
||||
self._coordinated_api_artifact_mutation(
|
||||
"python/jshookz/src/jshookz/types/hooks-api.d.ts",
|
||||
broad_declaration_path,
|
||||
"hooks-api.d.ts disagrees with the sealed F0 table",
|
||||
)
|
||||
|
||||
def test_exact_v1_declaration_identity_mutation(self) -> None:
|
||||
self._coordinated_api_artifact_mutation(
|
||||
"python/jshookz/src/jshookz/types/xahau-quickjs-v1.d.ts",
|
||||
exact_v1_declaration_path,
|
||||
"xahau-quickjs-v1.d.ts disagrees with the sealed F0 table",
|
||||
)
|
||||
|
||||
def test_selected_surface_identity_mutation(self) -> None:
|
||||
self._coordinated_api_artifact_mutation(
|
||||
"python/jshookz/src/jshookz/types/xahau-quickjs-v1.surface.json",
|
||||
surface_path,
|
||||
"xahau-quickjs-v1.surface.json disagrees with the sealed F0 table",
|
||||
)
|
||||
|
||||
def test_api_artifact_manifest_identity_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_api_manifest(bundle)
|
||||
api_manifest_path(bundle).write_text(json.dumps(data, indent=4) + "\n")
|
||||
|
||||
self.assert_generator_red(mutate, "API artifact manifest SHA-256")
|
||||
|
||||
def test_declaration_sha_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["javascript_surface"]["declaration_sha256"] = (
|
||||
"00" + PIN["exact_v1_declaration_sha256"][2:]
|
||||
)
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "declaration SHA-256 disagrees")
|
||||
|
||||
def test_surface_sha_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["javascript_surface"]["sha256"] = "00" + PIN["surface_sha256"][2:]
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "surface SHA-256 disagrees")
|
||||
|
||||
def test_bytecode_abi_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(bundle, "BYTECODE_ABI_ID", "00" + PIN["bytecode_abi"][2:])
|
||||
|
||||
self.assert_generator_red(mutate, "bytecode ABI disagrees")
|
||||
|
||||
def test_coordinated_bytecode_abi_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
value = "00" + PIN["bytecode_abi"][2:]
|
||||
data = load_json(bundle)
|
||||
data["bytecode_abi_id"] = value
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "BYTECODE_ABI_ID", value)
|
||||
|
||||
self.assert_generator_red(mutate, "bytecode ABI disagrees with the sealed F0")
|
||||
|
||||
def test_native_abi_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(bundle, "NATIVE_ABI_SHA256", "00" + PIN["native_abi"][2:])
|
||||
|
||||
self.assert_generator_red(mutate, "native ABI snapshot does not match")
|
||||
|
||||
def test_coordinated_native_abi_identity_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
path = native_path(bundle)
|
||||
path.write_bytes(path.read_bytes() + b"\n")
|
||||
rehash_native(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "native ABI SHA-256")
|
||||
|
||||
def test_import_signature_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["provider"]["imports"][0]["params"].append("i32")
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "import signatures disagree")
|
||||
|
||||
def test_export_signature_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
for item in data["provider"]["exports"]:
|
||||
if item["name"] == "qjs_hook":
|
||||
item["params"].append("i32")
|
||||
break
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "export signatures disagree")
|
||||
|
||||
def test_coordinated_export_signature_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
for collection in (
|
||||
data["provider"]["exports"],
|
||||
data["source"]["provider"]["allowed_exports"],
|
||||
):
|
||||
for item in collection:
|
||||
if item["name"] == "qjs_hook":
|
||||
item["params"].append("i32")
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "sealed F0 table")
|
||||
|
||||
def test_coordinated_wasi_import_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
for collection in (
|
||||
data["provider"]["imports"],
|
||||
data["source"]["provider"]["imports"],
|
||||
):
|
||||
for item in collection:
|
||||
if item["name"] == "accept":
|
||||
item["module"] = "wasi_snapshot_preview1"
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "sealed F0 table")
|
||||
|
||||
def test_coordinated_import_signature_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
for collection in (
|
||||
data["provider"]["imports"],
|
||||
data["source"]["provider"]["imports"],
|
||||
):
|
||||
for item in collection:
|
||||
if item["name"] == "accept":
|
||||
item["params"].append("i32")
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "sealed F0 table")
|
||||
|
||||
def test_export_extra_key_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
for collection in (
|
||||
data["provider"]["exports"],
|
||||
data["source"]["provider"]["allowed_exports"],
|
||||
):
|
||||
for item in collection:
|
||||
if item["name"] == "qjs_init":
|
||||
item["note"] = "extra"
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "extra or missing keys")
|
||||
|
||||
def _memory_shape_mutation(self, key: str, value) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
for collection in (
|
||||
data["provider"]["exports"],
|
||||
data["source"]["provider"]["allowed_exports"],
|
||||
):
|
||||
for item in collection:
|
||||
if item["kind"] == "memory":
|
||||
item[key] = value
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "memory")
|
||||
|
||||
def test_memory_minimum_shape_mutation(self) -> None:
|
||||
self._memory_shape_mutation("minimum_pages", 6)
|
||||
|
||||
def test_memory_maximum_shape_mutation(self) -> None:
|
||||
self._memory_shape_mutation("maximum_pages", 511)
|
||||
|
||||
def test_memory64_shape_mutation(self) -> None:
|
||||
self._memory_shape_mutation("memory64", True)
|
||||
|
||||
def test_shared_memory_shape_mutation(self) -> None:
|
||||
self._memory_shape_mutation("shared", True)
|
||||
|
||||
def test_serialized_object_max_bytes_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(bundle, "SERIALIZED_OBJECT_MAX_BYTES", "1048577")
|
||||
|
||||
self.assert_generator_red(mutate, "serialized_object_max_bytes")
|
||||
|
||||
def test_serialized_object_max_fields_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(bundle, "SERIALIZED_OBJECT_MAX_FIELDS", "32769")
|
||||
|
||||
self.assert_generator_red(mutate, "serialized_object_max_fields")
|
||||
|
||||
def test_serialized_object_max_scopes_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(bundle, "SERIALIZED_OBJECT_MAX_SCOPES", "32770")
|
||||
|
||||
self.assert_generator_red(mutate, "serialized_object_max_scopes")
|
||||
|
||||
def test_serialized_object_max_depth_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
set_cmake(bundle, "SERIALIZED_OBJECT_MAX_DEPTH", "11")
|
||||
|
||||
self.assert_generator_red(mutate, "serialized_object_max_depth")
|
||||
|
||||
def _coordinated_limit(self, json_key: str, cmake_key: str, value: int) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["source"]["limits"][json_key] = value
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, cmake_key, str(value))
|
||||
|
||||
self.assert_generator_red(mutate, "sealed F0 table")
|
||||
|
||||
def test_coordinated_serialized_object_max_bytes_mutation(self) -> None:
|
||||
self._coordinated_limit(
|
||||
"serialized_object_max_bytes", "SERIALIZED_OBJECT_MAX_BYTES", 1048578
|
||||
)
|
||||
|
||||
def test_coordinated_serialized_object_max_fields_mutation(self) -> None:
|
||||
self._coordinated_limit(
|
||||
"serialized_object_max_fields", "SERIALIZED_OBJECT_MAX_FIELDS", 1
|
||||
)
|
||||
|
||||
def test_coordinated_serialized_object_max_scopes_mutation(self) -> None:
|
||||
self._coordinated_limit(
|
||||
"serialized_object_max_scopes", "SERIALIZED_OBJECT_MAX_SCOPES", 1
|
||||
)
|
||||
|
||||
def test_coordinated_serialized_object_max_depth_mutation(self) -> None:
|
||||
self._coordinated_limit(
|
||||
"serialized_object_max_depth", "SERIALIZED_OBJECT_MAX_DEPTH", 1
|
||||
)
|
||||
|
||||
def test_coordinated_heap_limit_mutation(self) -> None:
|
||||
self._coordinated_limit("quickjs_heap_bytes", "HEAP_BYTES", 16777215)
|
||||
|
||||
def test_coordinated_stack_limit_mutation(self) -> None:
|
||||
self._coordinated_limit("quickjs_stack_bytes", "STACK_BYTES", 65535)
|
||||
|
||||
def test_coordinated_initialization_fuel_mutation(self) -> None:
|
||||
self._coordinated_limit(
|
||||
"wasmtime_fuel_per_initialization", "INITIALIZATION_FUEL", 4999999
|
||||
)
|
||||
|
||||
def test_coordinated_invocation_fuel_mutation(self) -> None:
|
||||
self._coordinated_limit(
|
||||
"wasmtime_fuel_per_invocation", "INVOCATION_FUEL", 49999999
|
||||
)
|
||||
|
||||
def test_coordinated_host_work_budget_mutation(self) -> None:
|
||||
self._coordinated_limit("host_work_budget", "HOST_WORK_BUDGET", 999999)
|
||||
|
||||
def test_coordinated_host_work_base_mutation(self) -> None:
|
||||
self._coordinated_limit(
|
||||
"host_work_base_per_call", "HOST_WORK_BASE_PER_CALL", 2
|
||||
)
|
||||
|
||||
def test_coordinated_host_work_per_byte_mutation(self) -> None:
|
||||
self._coordinated_limit(
|
||||
"host_work_per_addressed_byte", "HOST_WORK_PER_ADDRESSED_BYTE", 2
|
||||
)
|
||||
|
||||
def test_coordinated_host_work_meter_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["source"]["limits"]["host_work_meter"] = "other-meter"
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "HOST_WORK_METER", "other-meter")
|
||||
|
||||
self.assert_generator_red(mutate, "host_work_meter")
|
||||
|
||||
def test_host_work_address_mapping_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["source"]["limits"]["host_work_addressed_length_indices"][
|
||||
"accept"
|
||||
] = [0]
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "host_work_addressed_length_indices")
|
||||
|
||||
def test_coordinated_host_adapter_policy_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["source"]["execution"]["host_adapter_policy"] = "other-policy"
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "HOST_ADAPTER_POLICY", "other-policy")
|
||||
|
||||
self.assert_generator_red(mutate, "host_adapter_policy")
|
||||
|
||||
def test_coordinated_hook_api_version_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["source"]["artifact"]["hook_api_version"] = 2
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "HOOK_API_VERSION", "2")
|
||||
|
||||
self.assert_generator_red(mutate, "hook_api_version")
|
||||
|
||||
def test_wasmtime_engine_configuration_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["source"]["engine"]["configuration"]["wasi"] = True
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "Wasmtime engine configuration")
|
||||
|
||||
def test_coordinated_wasmtime_version_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["source"]["engine"]["version"] = "48.0.0"
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "WASMTIME_VERSION", "48.0.0")
|
||||
|
||||
self.assert_generator_red(
|
||||
mutate, "Wasmtime version disagrees with the sealed F0", "48.0.0"
|
||||
)
|
||||
|
||||
def test_provider_memory_max_bytes_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["provider"]["build"]["wasm_memory_max_bytes"] = 33554431
|
||||
data["source"]["provider"]["build"][
|
||||
"wasm_memory_max_bytes"
|
||||
] = 33554431
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "wasm_memory_max_bytes")
|
||||
|
||||
def test_wasm_stack_bytes_mutation(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["provider"]["build"]["wasm_stack_bytes"] = 65536
|
||||
data["source"]["provider"]["build"]["wasm_stack_bytes"] = 65536
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
|
||||
self.assert_generator_red(mutate, "wasm_stack_bytes")
|
||||
|
||||
def test_stale_receipt_b_lock_control(self) -> None:
|
||||
def mutate(bundle: Path) -> None:
|
||||
data = load_json(bundle)
|
||||
data["provider"]["sha256"] = RECEIPT_B["provider_sha256"]
|
||||
data["provider"]["size"] = int(RECEIPT_B["provider_size"])
|
||||
data["runtime_profile_id"] = RECEIPT_B["runtime_profile_id"]
|
||||
data["javascript_surface"]["declaration_sha256"] = RECEIPT_B[
|
||||
"declaration_sha256"
|
||||
]
|
||||
data["javascript_surface"]["sha256"] = RECEIPT_B["surface_sha256"]
|
||||
for collection in (
|
||||
data["provider"]["exports"],
|
||||
data["source"]["provider"]["allowed_exports"],
|
||||
):
|
||||
for item in collection:
|
||||
if item["kind"] == "memory":
|
||||
item["minimum_pages"] = 6
|
||||
write_json(bundle, data)
|
||||
rehash_manifest(bundle)
|
||||
set_cmake(bundle, "PROVIDER_SHA256", RECEIPT_B["provider_sha256"])
|
||||
set_cmake(bundle, "PROVIDER_SIZE", RECEIPT_B["provider_size"])
|
||||
set_cmake(
|
||||
bundle, "RUNTIME_PROFILE_ID", RECEIPT_B["runtime_profile_id"]
|
||||
)
|
||||
|
||||
self.assert_generator_red(
|
||||
mutate, "provider SHA-256 disagrees with the sealed F0"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
256
conanfile.py
256
conanfile.py
@@ -2,162 +2,158 @@ from conan import ConanFile
|
||||
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
|
||||
import re
|
||||
|
||||
|
||||
class Xrpl(ConanFile):
|
||||
name = "xrpl"
|
||||
name = 'xrpl'
|
||||
|
||||
license = "ISC"
|
||||
author = "John Freeman <jfreeman@ripple.com>"
|
||||
url = "https://github.com/xrplf/rippled"
|
||||
description = "The XRP Ledger"
|
||||
settings = "os", "compiler", "build_type", "arch"
|
||||
license = 'ISC'
|
||||
author = 'John Freeman <jfreeman@ripple.com>'
|
||||
url = 'https://github.com/xrplf/rippled'
|
||||
description = 'The XRP Ledger'
|
||||
settings = 'os', 'compiler', 'build_type', 'arch'
|
||||
options = {
|
||||
"assertions": [True, False],
|
||||
"coverage": [True, False],
|
||||
"fPIC": [True, False],
|
||||
"jemalloc": [True, False],
|
||||
"rocksdb": [True, False],
|
||||
"shared": [True, False],
|
||||
"static": [True, False],
|
||||
"tests": [True, False],
|
||||
"unity": [True, False],
|
||||
"xrpld": [True, False],
|
||||
"with_wasmedge": [True, False],
|
||||
"tool_requires_b2": [True, False],
|
||||
'assertions': [True, False],
|
||||
'coverage': [True, False],
|
||||
'fPIC': [True, False],
|
||||
'jemalloc': [True, False],
|
||||
'rocksdb': [True, False],
|
||||
'shared': [True, False],
|
||||
'static': [True, False],
|
||||
'tests': [True, False],
|
||||
'unity': [True, False],
|
||||
'xrpld': [True, False],
|
||||
'with_wasmedge': [True, False],
|
||||
'tool_requires_b2': [True, False],
|
||||
}
|
||||
|
||||
requires = [
|
||||
"date/3.0.3",
|
||||
"grpc/1.50.1",
|
||||
"libarchive/3.7.6",
|
||||
"magic_enum/0.9.5",
|
||||
"nudb/2.0.8",
|
||||
"openssl/3.6.0",
|
||||
"soci/4.0.3@xahaud/stable",
|
||||
"xxhash/0.8.2",
|
||||
"zlib/1.3.1",
|
||||
'date/3.0.3',
|
||||
'grpc/1.50.1',
|
||||
'libarchive/3.7.6',
|
||||
'magic_enum/0.9.5',
|
||||
'nudb/2.0.8',
|
||||
'openssl/3.6.0',
|
||||
'soci/4.0.3@xahaud/stable',
|
||||
'xxhash/0.8.2',
|
||||
'zlib/1.3.1',
|
||||
]
|
||||
|
||||
tool_requires = [
|
||||
"protobuf/3.21.12",
|
||||
'protobuf/3.21.12',
|
||||
]
|
||||
|
||||
default_options = {
|
||||
"assertions": False,
|
||||
"coverage": False,
|
||||
"fPIC": True,
|
||||
"jemalloc": False,
|
||||
"rocksdb": True,
|
||||
"shared": False,
|
||||
"static": True,
|
||||
"tests": False,
|
||||
"unity": False,
|
||||
"xrpld": False,
|
||||
"with_wasmedge": True,
|
||||
"tool_requires_b2": False,
|
||||
"date/*:header_only": False,
|
||||
"grpc/*:shared": False,
|
||||
"grpc/*:secure": True,
|
||||
"libarchive/*:shared": False,
|
||||
"libarchive/*:with_acl": False,
|
||||
"libarchive/*:with_bzip2": False,
|
||||
"libarchive/*:with_cng": False,
|
||||
"libarchive/*:with_expat": False,
|
||||
"libarchive/*:with_iconv": False,
|
||||
"libarchive/*:with_libxml2": False,
|
||||
"libarchive/*:with_lz4": True,
|
||||
"libarchive/*:with_lzma": False,
|
||||
"libarchive/*:with_lzo": False,
|
||||
"libarchive/*:with_nettle": False,
|
||||
"libarchive/*:with_openssl": False,
|
||||
"libarchive/*:with_pcreposix": False,
|
||||
"libarchive/*:with_xattr": False,
|
||||
"libarchive/*:with_zlib": False,
|
||||
"lz4/*:shared": False,
|
||||
"openssl/*:shared": False,
|
||||
"protobuf/*:shared": False,
|
||||
"protobuf/*:with_zlib": True,
|
||||
"rocksdb/*:enable_sse": False,
|
||||
"rocksdb/*:lite": False,
|
||||
"rocksdb/*:shared": False,
|
||||
"rocksdb/*:use_rtti": True,
|
||||
"rocksdb/*:with_jemalloc": False,
|
||||
"rocksdb/*:with_lz4": True,
|
||||
"rocksdb/*:with_snappy": True,
|
||||
"snappy/*:shared": False,
|
||||
"soci/*:shared": False,
|
||||
"soci/*:with_sqlite3": True,
|
||||
"soci/*:with_boost": True,
|
||||
"wasmtime/*:shared": False,
|
||||
"xxhash/*:shared": False,
|
||||
'assertions': False,
|
||||
'coverage': False,
|
||||
'fPIC': True,
|
||||
'jemalloc': False,
|
||||
'rocksdb': True,
|
||||
'shared': False,
|
||||
'static': True,
|
||||
'tests': False,
|
||||
'unity': False,
|
||||
'xrpld': False,
|
||||
'with_wasmedge': True,
|
||||
'tool_requires_b2': False,
|
||||
|
||||
'date/*:header_only': False,
|
||||
'grpc/*:shared': False,
|
||||
'grpc/*:secure': True,
|
||||
'libarchive/*:shared': False,
|
||||
'libarchive/*:with_acl': False,
|
||||
'libarchive/*:with_bzip2': False,
|
||||
'libarchive/*:with_cng': False,
|
||||
'libarchive/*:with_expat': False,
|
||||
'libarchive/*:with_iconv': False,
|
||||
'libarchive/*:with_libxml2': False,
|
||||
'libarchive/*:with_lz4': True,
|
||||
'libarchive/*:with_lzma': False,
|
||||
'libarchive/*:with_lzo': False,
|
||||
'libarchive/*:with_nettle': False,
|
||||
'libarchive/*:with_openssl': False,
|
||||
'libarchive/*:with_pcreposix': False,
|
||||
'libarchive/*:with_xattr': False,
|
||||
'libarchive/*:with_zlib': False,
|
||||
'lz4/*:shared': False,
|
||||
'openssl/*:shared': False,
|
||||
'protobuf/*:shared': False,
|
||||
'protobuf/*:with_zlib': True,
|
||||
'rocksdb/*:enable_sse': False,
|
||||
'rocksdb/*:lite': False,
|
||||
'rocksdb/*:shared': False,
|
||||
'rocksdb/*:use_rtti': True,
|
||||
'rocksdb/*:with_jemalloc': False,
|
||||
'rocksdb/*:with_lz4': True,
|
||||
'rocksdb/*:with_snappy': True,
|
||||
'snappy/*:shared': False,
|
||||
'soci/*:shared': False,
|
||||
'soci/*:with_sqlite3': True,
|
||||
'soci/*:with_boost': True,
|
||||
'xxhash/*:shared': False,
|
||||
}
|
||||
|
||||
def set_version(self):
|
||||
path = f"{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp"
|
||||
regex = r"versionString\s?=\s?\"(.*)\""
|
||||
with open(path, "r") as file:
|
||||
path = f'{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp'
|
||||
regex = r'versionString\s?=\s?\"(.*)\"'
|
||||
with open(path, 'r') as file:
|
||||
matches = (re.search(regex, line) for line in file)
|
||||
match = next(m for m in matches if m)
|
||||
self.version = match.group(1)
|
||||
|
||||
def build_requirements(self):
|
||||
# These provide build tools (protoc, grpc plugins) that run during build
|
||||
self.tool_requires("grpc/1.50.1")
|
||||
self.tool_requires('grpc/1.50.1')
|
||||
# Explicitly require b2 (e.g. for building from source for glibc compatibility)
|
||||
if self.options.tool_requires_b2:
|
||||
self.tool_requires("b2/5.3.2")
|
||||
self.tool_requires('b2/5.3.2')
|
||||
|
||||
def configure(self):
|
||||
if self.settings.compiler == "apple-clang":
|
||||
self.options["boost/*"].visibility = "global"
|
||||
if self.settings.compiler == 'apple-clang':
|
||||
self.options['boost/*'].visibility = 'global'
|
||||
|
||||
def requirements(self):
|
||||
# Force sqlite3 version to avoid conflicts with soci
|
||||
self.requires("sqlite3/3.47.0", override=True)
|
||||
self.requires('sqlite3/3.47.0', override=True)
|
||||
# Force our custom snappy build for all dependencies
|
||||
self.requires("snappy/1.1.10@xahaud/stable", override=True)
|
||||
self.requires('snappy/1.1.10@xahaud/stable', override=True)
|
||||
# Force boost version for all dependencies to avoid conflicts
|
||||
self.requires("boost/1.86.0", override=True)
|
||||
self.requires("lz4/1.10.0", force=True)
|
||||
self.requires('boost/1.86.0', override=True)
|
||||
self.requires('lz4/1.10.0', force=True)
|
||||
|
||||
if self.options.with_wasmedge:
|
||||
self.requires("wasmedge/0.11.2@xahaud/stable")
|
||||
if self.options.xrpld:
|
||||
self.requires("wasmtime/47.0.3@xahaud/stable")
|
||||
self.requires('wasmedge/0.11.2@xahaud/stable')
|
||||
if self.options.jemalloc:
|
||||
self.requires("jemalloc/5.3.0")
|
||||
self.requires('jemalloc/5.3.0')
|
||||
if self.options.rocksdb:
|
||||
self.requires("rocksdb/6.29.5")
|
||||
self.requires('rocksdb/6.29.5')
|
||||
|
||||
exports_sources = (
|
||||
"CMakeLists.txt",
|
||||
"bin/getRippledInfo",
|
||||
"cfg/*",
|
||||
"cmake/*",
|
||||
"external/*",
|
||||
"include/*",
|
||||
"src/*",
|
||||
'CMakeLists.txt',
|
||||
'bin/getRippledInfo',
|
||||
'cfg/*',
|
||||
'cmake/*',
|
||||
'external/*',
|
||||
'include/*',
|
||||
'src/*',
|
||||
)
|
||||
|
||||
def layout(self):
|
||||
cmake_layout(self)
|
||||
# Fix this setting to follow the default introduced in Conan 1.48
|
||||
# to align with our build instructions.
|
||||
self.folders.generators = "build/generators"
|
||||
|
||||
generators = "CMakeDeps"
|
||||
self.folders.generators = 'build/generators'
|
||||
|
||||
generators = 'CMakeDeps'
|
||||
def generate(self):
|
||||
tc = CMakeToolchain(self)
|
||||
tc.variables["tests"] = self.options.tests
|
||||
tc.variables["assert"] = self.options.assertions
|
||||
tc.variables["coverage"] = self.options.coverage
|
||||
tc.variables["jemalloc"] = self.options.jemalloc
|
||||
tc.variables["rocksdb"] = self.options.rocksdb
|
||||
tc.variables["BUILD_SHARED_LIBS"] = self.options.shared
|
||||
tc.variables["static"] = self.options.static
|
||||
tc.variables["unity"] = self.options.unity
|
||||
tc.variables["xrpld"] = self.options.xrpld
|
||||
tc.variables['tests'] = self.options.tests
|
||||
tc.variables['assert'] = self.options.assertions
|
||||
tc.variables['coverage'] = self.options.coverage
|
||||
tc.variables['jemalloc'] = self.options.jemalloc
|
||||
tc.variables['rocksdb'] = self.options.rocksdb
|
||||
tc.variables['BUILD_SHARED_LIBS'] = self.options.shared
|
||||
tc.variables['static'] = self.options.static
|
||||
tc.variables['unity'] = self.options.unity
|
||||
tc.variables['xrpld'] = self.options.xrpld
|
||||
tc.generate()
|
||||
|
||||
def build(self):
|
||||
@@ -172,29 +168,29 @@ class Xrpl(ConanFile):
|
||||
cmake.install()
|
||||
|
||||
def package_info(self):
|
||||
libxrpl = self.cpp_info.components["libxrpl"]
|
||||
libxrpl = self.cpp_info.components['libxrpl']
|
||||
libxrpl.libs = [
|
||||
"xrpl",
|
||||
"xrpl.libpb",
|
||||
"ed25519",
|
||||
"secp256k1",
|
||||
'xrpl',
|
||||
'xrpl.libpb',
|
||||
'ed25519',
|
||||
'secp256k1',
|
||||
]
|
||||
# TODO: Fix the protobufs to include each other relative to
|
||||
# `include/`, not `include/ripple/proto/`.
|
||||
libxrpl.includedirs = ["include", "include/ripple/proto"]
|
||||
libxrpl.includedirs = ['include', 'include/ripple/proto']
|
||||
libxrpl.requires = [
|
||||
"boost::boost",
|
||||
"date::date",
|
||||
"grpc::grpc++",
|
||||
"libarchive::libarchive",
|
||||
"lz4::lz4",
|
||||
"nudb::nudb",
|
||||
"openssl::crypto",
|
||||
"protobuf::libprotobuf",
|
||||
"soci::soci",
|
||||
"sqlite3::sqlite",
|
||||
"xxhash::xxhash",
|
||||
"zlib::zlib",
|
||||
'boost::boost',
|
||||
'date::date',
|
||||
'grpc::grpc++',
|
||||
'libarchive::libarchive',
|
||||
'lz4::lz4',
|
||||
'nudb::nudb',
|
||||
'openssl::crypto',
|
||||
'protobuf::libprotobuf',
|
||||
'soci::soci',
|
||||
'sqlite3::sqlite',
|
||||
'xxhash::xxhash',
|
||||
'zlib::zlib',
|
||||
]
|
||||
if self.options.rocksdb:
|
||||
libxrpl.requires.append("rocksdb::librocksdb")
|
||||
libxrpl.requires.append('rocksdb::librocksdb')
|
||||
|
||||
48
external/quickjs-provider/README.md
vendored
48
external/quickjs-provider/README.md
vendored
@@ -1,48 +0,0 @@
|
||||
# QuickJS provider manifest
|
||||
|
||||
These files are generated or sealed together by a jshookz source checkout:
|
||||
|
||||
```sh
|
||||
# Current layout (cpp/provider, python/jshookz). Older checkouts used
|
||||
# packages/jshookz; bin/update-jshookz-snapshot accepts either CLI path.
|
||||
uv sync --project python/jshookz --locked --group dev
|
||||
python/jshookz/.venv/bin/jshookz build provider
|
||||
```
|
||||
|
||||
Update this Xahau branch from a clean, committed jshookz checkout with one
|
||||
command:
|
||||
|
||||
```sh
|
||||
bin/update-jshookz-snapshot /path/to/jshookz
|
||||
```
|
||||
|
||||
That builds and imports the sealed lock files plus the broad/exact-v1
|
||||
declarations, selected-surface manifest, and API-artifact manifest. CI checks out jshookz
|
||||
`main` and fails if `build provider` does not reproduce that lock. The
|
||||
importer then prints a self-contained `x-run-tests` command that
|
||||
regenerates the embedded Hook fixtures and verifies the cut.
|
||||
|
||||
`jshookz_provider.manifest.json` is the verified runtime-profile lock.
|
||||
`jshookz_provider.manifest.cmake` is its minimal CMake projection. Xahau checks
|
||||
the JSON hash before generating the C++ profile constants, and the runtime
|
||||
checks any supplied provider WASM against the projected size and SHA-256.
|
||||
`api-artifacts.json` closes the three tracked API artifacts; Xahau verifies its
|
||||
own digest and every named artifact before projecting their identities. These
|
||||
files add no parallel CMake lock-value fields.
|
||||
|
||||
The provider binary is not vendored in this integration slice. Tests inject
|
||||
the exact built artifact. Configure needs python3 to project these lock files.
|
||||
A daemon whose embed is empty will not start.
|
||||
|
||||
For a local producer-to-consumer build, point Xahau straight at the emitted
|
||||
bundle instead of copying it:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build \
|
||||
-DXAHAU_QUICKJS_PROVIDER_BUNDLE_DIR=/path/to/jshookz/build/xahau-provider
|
||||
```
|
||||
|
||||
A build-time generator writes `QuickJSProviderValues.cpp` from those
|
||||
locks and, if present, the gitignored wasm. Configure only checks that
|
||||
the three lock files exist. Changing the bundle rebuilds that one
|
||||
translation unit; hash, Wasmtime, and optional-wasm checks run then.
|
||||
8
external/quickjs-provider/api-artifacts.json
vendored
8
external/quickjs-provider/api-artifacts.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"artifacts": {
|
||||
"python/jshookz/src/jshookz/types/hooks-api.d.ts": "649079b831bc68eff0f440644649a505addafb08a3f5174e4031c8de6aedffa0",
|
||||
"python/jshookz/src/jshookz/types/xahau-quickjs-v1.d.ts": "20881232f0602e35de0fcbb8b5a3c1511606392749a0f79daebb386204f4f4da",
|
||||
"python/jshookz/src/jshookz/types/xahau-quickjs-v1.surface.json": "96a9218bf38abbf1bc9f2839408700d03572e0b065d625263f70477bc32597ac"
|
||||
},
|
||||
"schema": "jshookz.api-artifacts.v1"
|
||||
}
|
||||
3209
external/quickjs-provider/hooks-api.d.ts
vendored
3209
external/quickjs-provider/hooks-api.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
# Generated by jshookz build provider; do not edit.
|
||||
set(XAHAU_QUICKJS_MANIFEST_SCHEMA "xahau.quickjs.runtime-profile-lock.v1")
|
||||
set(XAHAU_QUICKJS_MANIFEST_SHA256 "1673b27d21063957cece9095cceb7e2dbfa53b6f49a5b7f824a486d7d90b2391")
|
||||
set(XAHAU_QUICKJS_PROVIDER_FILE "jshookz_provider.wasm")
|
||||
set(XAHAU_QUICKJS_PROVIDER_SHA256 "f60ab333acb547e50d7d087960839bd11833cb133d496af98e0b72a1bb3b84df")
|
||||
set(XAHAU_QUICKJS_PROVIDER_SIZE "1116638")
|
||||
set(XAHAU_QUICKJS_PROVIDER_IMPORT_COUNT "13")
|
||||
set(XAHAU_QUICKJS_PROVIDER_EXPORT_COUNT "22")
|
||||
set(XAHAU_QUICKJS_NATIVE_ABI_FILE "jshookz_provider.native-abi.json")
|
||||
set(XAHAU_QUICKJS_NATIVE_ABI_SHA256 "328ec938dcad875f3bdf25b8d779dd77f3571589818ca9271cb98c64c7018f99")
|
||||
set(XAHAU_QUICKJS_BYTECODE_ABI_ID "75ea54f357d397c4b33899e495bb385dad975a43b1c4a7a2cead30474327d33e")
|
||||
set(XAHAU_QUICKJS_RUNTIME_PROFILE_ID "29acf12e170436e5ccadfbc118d02a60909f86a88262106652b2f71994b0ea25")
|
||||
set(XAHAU_QUICKJS_HOOK_API_VERSION "1")
|
||||
set(XAHAU_QUICKJS_WASMTIME_VERSION "47.0.3")
|
||||
set(XAHAU_QUICKJS_INITIALIZATION_FUEL "5000000")
|
||||
set(XAHAU_QUICKJS_INVOCATION_FUEL "50000000")
|
||||
set(XAHAU_QUICKJS_HOST_WORK_METER "base-plus-addressed-byte-v1")
|
||||
set(XAHAU_QUICKJS_HOST_WORK_BUDGET "1000000")
|
||||
set(XAHAU_QUICKJS_HOST_WORK_BASE_PER_CALL "1")
|
||||
set(XAHAU_QUICKJS_HOST_WORK_PER_ADDRESSED_BYTE "1")
|
||||
set(XAHAU_QUICKJS_HOST_ADAPTER_POLICY "xahau-raw-hook-host-v1")
|
||||
set(XAHAU_QUICKJS_HEAP_BYTES "16777216")
|
||||
set(XAHAU_QUICKJS_STACK_BYTES "65536")
|
||||
set(XAHAU_QUICKJS_SERIALIZED_OBJECT_MAX_BYTES "1048576")
|
||||
set(XAHAU_QUICKJS_SERIALIZED_OBJECT_MAX_FIELDS "32768")
|
||||
set(XAHAU_QUICKJS_SERIALIZED_OBJECT_MAX_SCOPES "32769")
|
||||
set(XAHAU_QUICKJS_SERIALIZED_OBJECT_MAX_DEPTH "10")
|
||||
@@ -1,854 +0,0 @@
|
||||
{
|
||||
"bytecode_abi_id": "75ea54f357d397c4b33899e495bb385dad975a43b1c4a7a2cead30474327d33e",
|
||||
"javascript_surface": {
|
||||
"declaration": "python/jshookz/src/jshookz/types/xahau-quickjs-v1.d.ts",
|
||||
"declaration_sha256": "20881232f0602e35de0fcbb8b5a3c1511606392749a0f79daebb386204f4f4da",
|
||||
"manifest": "python/jshookz/src/jshookz/types/xahau-quickjs-v1.surface.json",
|
||||
"schema": "jshookz.javascript-surface.v1",
|
||||
"sha256": "96a9218bf38abbf1bc9f2839408700d03572e0b065d625263f70477bc32597ac"
|
||||
},
|
||||
"provider": {
|
||||
"build": {
|
||||
"wasm_memory_max_bytes": 33554432,
|
||||
"wasm_stack_bytes": 131072
|
||||
},
|
||||
"exports": [
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "_initialize",
|
||||
"params": [],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "free",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "malloc",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "memory",
|
||||
"maximum_pages": 512,
|
||||
"memory64": false,
|
||||
"minimum_pages": 7,
|
||||
"name": "memory",
|
||||
"shared": false
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_cbak",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_compile",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_compile_module",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_destroy",
|
||||
"params": [],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_enable_coverage",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_eval",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_eval_bytecode",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_eval_module",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_get_bytecode_len",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_get_bytecode_ptr",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_get_result_len",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_get_result_ptr",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_hook",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_init",
|
||||
"params": [],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_set_max_stack_size",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_set_memory_limit",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_set_seed",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_validate_hook_module",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"imports": [
|
||||
{
|
||||
"module": "env",
|
||||
"name": "accept",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i64"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "emit",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "etxn_reserve",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "hook_account",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "ledger_last_hash",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "ledger_last_time",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "ledger_seq",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "otxn_type",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "prepare",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "rollback",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i64"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "state",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "state_set",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "trace",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sha256": "f60ab333acb547e50d7d087960839bd11833cb133d496af98e0b72a1bb3b84df",
|
||||
"size": 1116638
|
||||
},
|
||||
"runtime_profile_id": "29acf12e170436e5ccadfbc118d02a60909f86a88262106652b2f71994b0ea25",
|
||||
"schema": "xahau.quickjs.runtime-profile-lock.v1",
|
||||
"source": {
|
||||
"artifact": {
|
||||
"envelope_version": 1,
|
||||
"hook_api_version": 1,
|
||||
"kind": "quickjs-bytecode"
|
||||
},
|
||||
"bytecode_abi": {
|
||||
"entry_exports": {
|
||||
"callback": "optional-callable",
|
||||
"main": "required-callable"
|
||||
},
|
||||
"quickjs_bytecode_version": 5,
|
||||
"quickjs_version": "2025-09-13",
|
||||
"read_flags": [
|
||||
"JS_READ_OBJ_BYTECODE"
|
||||
],
|
||||
"root": "es-module",
|
||||
"write_flags": [
|
||||
"JS_WRITE_OBJ_BYTECODE"
|
||||
]
|
||||
},
|
||||
"engine": {
|
||||
"configuration": {
|
||||
"consume_fuel": true,
|
||||
"cranelift_nan_canonicalization": true,
|
||||
"wasi": false,
|
||||
"wasm_memory64": false,
|
||||
"wasm_multi_memory": false,
|
||||
"wasm_relaxed_simd": false,
|
||||
"wasm_tail_call": false,
|
||||
"wasm_threads": false
|
||||
},
|
||||
"implementation": "wasmtime-native-c-api",
|
||||
"module_lifecycle": "compile-once-fresh-store-and-instance-per-invocation",
|
||||
"version": "47.0.3"
|
||||
},
|
||||
"execution": {
|
||||
"host_adapter_policy": "xahau-raw-hook-host-v1",
|
||||
"host_work_exhaustion": "uncatchable-wasmtime-trap-to-wasm-error",
|
||||
"initialization": "finite-cold-qjs-init-budget-before-invocation-fuel-reset",
|
||||
"ordinary_host_status": "returned-as-i64-never-trapped",
|
||||
"pending_jobs_after_entry": "not-drained",
|
||||
"result_failure_policy": "rollback.onFail-preserves-coded-failure;rollback.requirePresent-requires-success-and-non-nullish-value;rollback.requireTruthy-requires-success-and-truthy-value;accept.unlessPresent-accepts-on-failure-or-nullish-value;accept.unlessTruthy-accepts-on-failure-or-falsy-value;guards-use-explicit-message-and-optional-code-default-zero",
|
||||
"sfHookInstructionCount": "initial-wasmtime-invocation-fuel-minus-remaining-fuel",
|
||||
"terminals": "record-hook-result-and-uncatchable-wasmtime-trap"
|
||||
},
|
||||
"javascript_surface": {
|
||||
"declaration": "python/jshookz/src/jshookz/types/xahau-quickjs-v1.d.ts",
|
||||
"manifest": "python/jshookz/src/jshookz/types/xahau-quickjs-v1.surface.json",
|
||||
"schema": "jshookz.javascript-surface.v1"
|
||||
},
|
||||
"limits": {
|
||||
"host_work_addressed_length_indices": {
|
||||
"accept": [
|
||||
1
|
||||
],
|
||||
"emit": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"hook_account": [
|
||||
1
|
||||
],
|
||||
"ledger_last_hash": [
|
||||
1
|
||||
],
|
||||
"prepare": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"rollback": [
|
||||
1
|
||||
],
|
||||
"state": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"state_set": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"trace": [
|
||||
1,
|
||||
3
|
||||
]
|
||||
},
|
||||
"host_work_base_per_call": 1,
|
||||
"host_work_budget": 1000000,
|
||||
"host_work_meter": "base-plus-addressed-byte-v1",
|
||||
"host_work_per_addressed_byte": 1,
|
||||
"quickjs_heap_bytes": 16777216,
|
||||
"quickjs_stack_bytes": 65536,
|
||||
"serialized_object_max_bytes": 1048576,
|
||||
"serialized_object_max_depth": 10,
|
||||
"serialized_object_max_fields": 32768,
|
||||
"serialized_object_max_scopes": 32769,
|
||||
"wasmtime_fuel_per_initialization": 5000000,
|
||||
"wasmtime_fuel_per_invocation": 50000000
|
||||
},
|
||||
"name": "xahau-quickjs-v1",
|
||||
"provider": {
|
||||
"allowed_exports": [
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "_initialize",
|
||||
"params": [],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "free",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "malloc",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "memory",
|
||||
"maximum_pages": 512,
|
||||
"memory64": false,
|
||||
"minimum_pages": 7,
|
||||
"name": "memory",
|
||||
"shared": false
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_cbak",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_compile",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_compile_module",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_destroy",
|
||||
"params": [],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_enable_coverage",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_eval",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_eval_bytecode",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_eval_module",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_get_bytecode_len",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_get_bytecode_ptr",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_get_result_len",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_get_result_ptr",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_hook",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_init",
|
||||
"params": [],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_set_max_stack_size",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_set_memory_limit",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_set_seed",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": []
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "qjs_validate_hook_module",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"c_flags": [
|
||||
"-fwrapv",
|
||||
"-funsigned-char",
|
||||
"-mllvm",
|
||||
"-wasm-enable-sjlj"
|
||||
],
|
||||
"cxx_flags": [
|
||||
"-fno-exceptions",
|
||||
"-fno-rtti",
|
||||
"-mllvm",
|
||||
"-wasm-enable-sjlj"
|
||||
],
|
||||
"optimization": "O2",
|
||||
"target": "wasm32-wasi",
|
||||
"wasi_sdk": "32",
|
||||
"wasm_memory_max_bytes": 33554432,
|
||||
"wasm_stack_bytes": 131072
|
||||
},
|
||||
"forbidden_import_modules": [
|
||||
"wasi_snapshot_preview1"
|
||||
],
|
||||
"imports": [
|
||||
{
|
||||
"module": "env",
|
||||
"name": "accept",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i64"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "emit",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "etxn_reserve",
|
||||
"params": [
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "hook_account",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "ledger_last_hash",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "ledger_last_time",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "ledger_seq",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "otxn_type",
|
||||
"params": [],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "prepare",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "rollback",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i64"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "state",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "state_set",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "env",
|
||||
"name": "trace",
|
||||
"params": [
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32",
|
||||
"i32"
|
||||
],
|
||||
"results": [
|
||||
"i64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"javascript": {
|
||||
"atomics": false,
|
||||
"date_now": "previous-ledger-close-ripple-epoch-to-unix-milliseconds",
|
||||
"dynamic_eval": "enabled-and-metered",
|
||||
"finalization_registry": false,
|
||||
"math_random": "absent-use-ledger-random-api",
|
||||
"number": "enabled",
|
||||
"promise": "required-for-module-evaluation-no-post-entry-job-drain",
|
||||
"shared_array_buffer": false,
|
||||
"timezone": "UTC",
|
||||
"weak_ref": false
|
||||
}
|
||||
},
|
||||
"schema": "xahau.quickjs.runtime-profile-source.v1"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"selected":[{"amendment":null,"name":"accept","param_types":["uint32_t","uint32_t","int64_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f","0x7e"]},{"amendment":null,"name":"rollback","param_types":["uint32_t","uint32_t","int64_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f","0x7e"]},{"amendment":null,"name":"ledger_seq","param_types":[],"return_type":"int64_t","wasm_signature":["0x7e"]},{"amendment":null,"name":"ledger_last_time","param_types":[],"return_type":"int64_t","wasm_signature":["0x7e"]},{"amendment":null,"name":"ledger_last_hash","param_types":["uint32_t","uint32_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f"]},{"amendment":null,"name":"otxn_type","param_types":[],"return_type":"int64_t","wasm_signature":["0x7e"]},{"amendment":null,"name":"hook_account","param_types":["uint32_t","uint32_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f"]},{"amendment":null,"name":"trace","param_types":["uint32_t","uint32_t","uint32_t","uint32_t","uint32_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f","0x7f","0x7f","0x7f"]},{"amendment":null,"name":"state","param_types":["uint32_t","uint32_t","uint32_t","uint32_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f","0x7f","0x7f"]},{"amendment":null,"name":"state_set","param_types":["uint32_t","uint32_t","uint32_t","uint32_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f","0x7f","0x7f"]},{"amendment":"featureHooksUpdate2","name":"prepare","param_types":["uint32_t","uint32_t","uint32_t","uint32_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f","0x7f","0x7f"]},{"amendment":null,"name":"etxn_reserve","param_types":["uint32_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f"]},{"amendment":null,"name":"emit","param_types":["uint32_t","uint32_t","uint32_t","uint32_t"],"return_type":"int64_t","wasm_signature":["0x7e","0x7f","0x7f","0x7f","0x7f"]}],"source":{"commit":"bb244ef7729503a0317bcff0f8fdaa93ca5cb7d2","macro_function_count":75,"path":"include/xrpl/hook/hook_api.macro","repository":"https://github.com/Xahau/xahaud"}}
|
||||
1760
external/quickjs-provider/xahau-quickjs-v1.d.ts
vendored
1760
external/quickjs-provider/xahau-quickjs-v1.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,581 +0,0 @@
|
||||
{
|
||||
"byte_policies": {
|
||||
"AccountID.from": {
|
||||
"0": "bytesLike"
|
||||
},
|
||||
"AccountID.fromHex": {
|
||||
"0": "hexString"
|
||||
},
|
||||
"Hash256.from": {
|
||||
"0": "bytesLike"
|
||||
},
|
||||
"Hash256.fromHex": {
|
||||
"0": "hexString"
|
||||
},
|
||||
"STBlob.equals": {
|
||||
"0": "bytesLikeOrSTBlob"
|
||||
},
|
||||
"STBlob.from": {
|
||||
"0": "bytesLike"
|
||||
},
|
||||
"STBlob.fromHex": {
|
||||
"0": "hexString"
|
||||
},
|
||||
"accept": {
|
||||
"0": "lifecycleMessage"
|
||||
},
|
||||
"emit.prepare": {
|
||||
"0": "bytesLikeOrSTBlob"
|
||||
},
|
||||
"emit.tx": {
|
||||
"0": "bytesLikeOrSTBlob"
|
||||
},
|
||||
"rollback": {
|
||||
"0": "lifecycleMessage"
|
||||
},
|
||||
"state.get": {
|
||||
"0": "stateKeyLike"
|
||||
},
|
||||
"state.set": {
|
||||
"0": "stateKeyLike",
|
||||
"1": "stateValueLike"
|
||||
}
|
||||
},
|
||||
"declaration": {
|
||||
"path": "python/jshookz/src/jshookz/types/xahau-quickjs-v1.d.ts",
|
||||
"sha256": "20881232f0602e35de0fcbb8b5a3c1511606392749a0f79daebb386204f4f4da"
|
||||
},
|
||||
"globals": {
|
||||
"AccountID": "object",
|
||||
"Amount": "object",
|
||||
"Currency": "object",
|
||||
"Field": "object",
|
||||
"Hash": "object",
|
||||
"Hash128": "object",
|
||||
"Hash160": "object",
|
||||
"Hash192": "object",
|
||||
"Hash256": "object",
|
||||
"IOUAmount": "object",
|
||||
"Issue": "object",
|
||||
"MPTAmount": "object",
|
||||
"NativeAmount": "object",
|
||||
"Path": "object",
|
||||
"PathHop": "object",
|
||||
"PathSet": "object",
|
||||
"Result": "object",
|
||||
"STArray": "object",
|
||||
"STBlob": "object",
|
||||
"STObject": "object",
|
||||
"SerializedField": "object",
|
||||
"UInt": "object",
|
||||
"UInt16": "object",
|
||||
"UInt32": "object",
|
||||
"UInt64": "object",
|
||||
"UInt8": "object",
|
||||
"Vector256": "object",
|
||||
"VoidResult": "object",
|
||||
"XChainBridge": "object",
|
||||
"XFLDecimal": "object",
|
||||
"accept": "function",
|
||||
"emit": "object",
|
||||
"hook": "object",
|
||||
"ledger": "object",
|
||||
"otxn": "object",
|
||||
"rollback": "function",
|
||||
"state": "object",
|
||||
"trace": "function",
|
||||
"util": "object"
|
||||
},
|
||||
"namespaces": {
|
||||
"accept": {
|
||||
"unlessPresent": "function",
|
||||
"unlessTruthy": "function",
|
||||
"when": "function"
|
||||
},
|
||||
"emit": {
|
||||
"prepare": "function",
|
||||
"reserve": "function",
|
||||
"tx": "function"
|
||||
},
|
||||
"hook": {
|
||||
"account": "function"
|
||||
},
|
||||
"ledger": {
|
||||
"lastHash": "value",
|
||||
"lastTime": "value",
|
||||
"sequence": "value"
|
||||
},
|
||||
"otxn": {
|
||||
"type": "function"
|
||||
},
|
||||
"rollback": {
|
||||
"onAllFail": "function",
|
||||
"onAnyFail": "function",
|
||||
"onFail": "function",
|
||||
"requirePresent": "function",
|
||||
"requireTruthy": "function",
|
||||
"when": "function"
|
||||
},
|
||||
"state": {
|
||||
"get": "function",
|
||||
"set": "function"
|
||||
},
|
||||
"util": {
|
||||
"decodeObject": "function",
|
||||
"safeDecodeObject": "function",
|
||||
"validateObject": "function"
|
||||
}
|
||||
},
|
||||
"prototypes": {
|
||||
"AccountID": {
|
||||
"members": {
|
||||
"equals": "function",
|
||||
"isZero": "function",
|
||||
"toBytes": "function",
|
||||
"toHex": "function"
|
||||
},
|
||||
"own": [
|
||||
"equals",
|
||||
"isZero",
|
||||
"toBytes",
|
||||
"toHex"
|
||||
],
|
||||
"probe": "AccountID.from(new Uint8Array(20))"
|
||||
},
|
||||
"Amount": {
|
||||
"members": {
|
||||
"asIOU": "function",
|
||||
"asMPT": "function",
|
||||
"asNative": "function",
|
||||
"byteLength": "value",
|
||||
"compare": "function",
|
||||
"currency": "value",
|
||||
"drops": "value",
|
||||
"equals": "function",
|
||||
"isIOU": "function",
|
||||
"isMPT": "function",
|
||||
"isNative": "function",
|
||||
"issue": "value",
|
||||
"issuer": "value",
|
||||
"kind": "value",
|
||||
"mptIssuanceId": "value",
|
||||
"toBytes": "function",
|
||||
"toString": "function",
|
||||
"toXFL": "function",
|
||||
"value": "value"
|
||||
},
|
||||
"own": [
|
||||
"asIOU",
|
||||
"asMPT",
|
||||
"asNative",
|
||||
"byteLength",
|
||||
"compare",
|
||||
"currency",
|
||||
"drops",
|
||||
"equals",
|
||||
"isIOU",
|
||||
"isMPT",
|
||||
"isNative",
|
||||
"issue",
|
||||
"issuer",
|
||||
"kind",
|
||||
"mptIssuanceId",
|
||||
"toBytes",
|
||||
"toString",
|
||||
"toXFL",
|
||||
"value"
|
||||
],
|
||||
"probe": "util.decodeObject(STBlob.fromHex('61D4838D7EA4C680000000000000000000000000005553440000000000B5F762798A53D543A014CAF8B297CFF8F2F937E8')).Amount"
|
||||
},
|
||||
"Currency": {
|
||||
"members": {
|
||||
"byteLength": "value",
|
||||
"equals": "function",
|
||||
"isNative": "value",
|
||||
"toBytes": "function",
|
||||
"toHex": "function",
|
||||
"toString": "function"
|
||||
},
|
||||
"own": [
|
||||
"byteLength",
|
||||
"equals",
|
||||
"isNative",
|
||||
"toBytes",
|
||||
"toHex",
|
||||
"toString"
|
||||
],
|
||||
"probe": "util.decodeObject(new Uint8Array()).withField('BaseAsset', new Uint8Array(20)).BaseAsset"
|
||||
},
|
||||
"Hash128": {
|
||||
"members": {
|
||||
"byteLength": "value",
|
||||
"compare": "function",
|
||||
"equals": "function",
|
||||
"isZero": "function",
|
||||
"toBytes": "function",
|
||||
"toHex": "function"
|
||||
},
|
||||
"own": [
|
||||
"byteLength",
|
||||
"compare",
|
||||
"equals",
|
||||
"isZero",
|
||||
"toBytes",
|
||||
"toHex"
|
||||
],
|
||||
"probe": "util.decodeObject(new Uint8Array()).withField('EmailHash', new Uint8Array(16)).EmailHash"
|
||||
},
|
||||
"Hash160": {
|
||||
"members": {
|
||||
"byteLength": "value",
|
||||
"compare": "function",
|
||||
"equals": "function",
|
||||
"isZero": "function",
|
||||
"toBytes": "function",
|
||||
"toHex": "function"
|
||||
},
|
||||
"own": [
|
||||
"byteLength",
|
||||
"compare",
|
||||
"equals",
|
||||
"isZero",
|
||||
"toBytes",
|
||||
"toHex"
|
||||
],
|
||||
"probe": "util.decodeObject(new Uint8Array()).withField('TakerPaysCurrency', new Uint8Array(20)).TakerPaysCurrency"
|
||||
},
|
||||
"Hash192": {
|
||||
"members": {
|
||||
"byteLength": "value",
|
||||
"compare": "function",
|
||||
"equals": "function",
|
||||
"isZero": "function",
|
||||
"toBytes": "function",
|
||||
"toHex": "function"
|
||||
},
|
||||
"own": [
|
||||
"byteLength",
|
||||
"compare",
|
||||
"equals",
|
||||
"isZero",
|
||||
"toBytes",
|
||||
"toHex"
|
||||
],
|
||||
"probe": "util.decodeObject(new Uint8Array()).withField('MPTokenIssuanceID', new Uint8Array(24)).MPTokenIssuanceID"
|
||||
},
|
||||
"Hash256": {
|
||||
"members": {
|
||||
"byteLength": "value",
|
||||
"compare": "function",
|
||||
"equals": "function",
|
||||
"isZero": "function",
|
||||
"toBytes": "function",
|
||||
"toHex": "function"
|
||||
},
|
||||
"own": [
|
||||
"byteLength",
|
||||
"compare",
|
||||
"equals",
|
||||
"isZero",
|
||||
"toBytes",
|
||||
"toHex"
|
||||
],
|
||||
"probe": "Hash256.from(new Uint8Array(32))"
|
||||
},
|
||||
"Issue": {
|
||||
"members": {
|
||||
"currency": "value",
|
||||
"equals": "function",
|
||||
"issuer": "value",
|
||||
"kind": "value",
|
||||
"mptIssuanceId": "value",
|
||||
"toBytes": "function"
|
||||
},
|
||||
"own": [
|
||||
"currency",
|
||||
"equals",
|
||||
"issuer",
|
||||
"kind",
|
||||
"mptIssuanceId",
|
||||
"toBytes"
|
||||
],
|
||||
"probe": "util.decodeObject(new Uint8Array()).withField('LockingChainIssue', new Uint8Array(20)).LockingChainIssue"
|
||||
},
|
||||
"Path": {
|
||||
"members": {
|
||||
"[Symbol.iterator]": "function",
|
||||
"at": "function",
|
||||
"length": "value"
|
||||
},
|
||||
"own": [
|
||||
"[Symbol.iterator]",
|
||||
"at",
|
||||
"length"
|
||||
],
|
||||
"probe": "util.decodeObject(STBlob.fromHex('011201B5F762798A53D543A014CAF8B297CFF8F2F937E800')).Paths.at(0)"
|
||||
},
|
||||
"PathHop": {
|
||||
"members": {
|
||||
"account": "value",
|
||||
"currency": "value",
|
||||
"issuer": "value"
|
||||
},
|
||||
"own": [
|
||||
"account",
|
||||
"currency",
|
||||
"issuer"
|
||||
],
|
||||
"probe": "util.decodeObject(STBlob.fromHex('011201B5F762798A53D543A014CAF8B297CFF8F2F937E800')).Paths.at(0).at(0)"
|
||||
},
|
||||
"PathSet": {
|
||||
"members": {
|
||||
"[Symbol.iterator]": "function",
|
||||
"at": "function",
|
||||
"length": "value",
|
||||
"toBytes": "function"
|
||||
},
|
||||
"own": [
|
||||
"[Symbol.iterator]",
|
||||
"at",
|
||||
"length",
|
||||
"toBytes"
|
||||
],
|
||||
"probe": "util.decodeObject(STBlob.fromHex('011201B5F762798A53D543A014CAF8B297CFF8F2F937E800')).Paths"
|
||||
},
|
||||
"Result": {
|
||||
"frozen": true,
|
||||
"members": {
|
||||
"okMapOr": "function",
|
||||
"okOr": "function",
|
||||
"okOrHandle": "function"
|
||||
},
|
||||
"own": [
|
||||
"okMapOr",
|
||||
"okOr",
|
||||
"okOrHandle"
|
||||
],
|
||||
"probe": "UInt64.from(0n)"
|
||||
},
|
||||
"STArray": {
|
||||
"exotic": {
|
||||
"array": false,
|
||||
"enumerable": [
|
||||
"0"
|
||||
],
|
||||
"extensible": false,
|
||||
"own": [
|
||||
"0",
|
||||
"length"
|
||||
]
|
||||
},
|
||||
"members": {
|
||||
"[Symbol.iterator]": "function",
|
||||
"at": "function",
|
||||
"length": "value",
|
||||
"toJSON": "function"
|
||||
},
|
||||
"own": [
|
||||
"[Symbol.iterator]",
|
||||
"at",
|
||||
"toJSON"
|
||||
],
|
||||
"probe": "util.decodeObject(new Uint8Array([0xF9, 0xEA, 0x22, 0, 0, 0, 1, 0xE1, 0xF1])).Memos"
|
||||
},
|
||||
"STBlob": {
|
||||
"members": {
|
||||
"byteAt": "function",
|
||||
"byteLength": "value",
|
||||
"equals": "function",
|
||||
"toBytes": "function",
|
||||
"toHex": "function"
|
||||
},
|
||||
"own": [
|
||||
"byteAt",
|
||||
"byteLength",
|
||||
"equals",
|
||||
"toBytes",
|
||||
"toHex"
|
||||
],
|
||||
"probe": "STBlob.from(new Uint8Array(0))"
|
||||
},
|
||||
"STObject": {
|
||||
"exotic": {
|
||||
"array": false,
|
||||
"enumerable": [
|
||||
"Flags"
|
||||
],
|
||||
"extensible": false,
|
||||
"own": [
|
||||
"Flags"
|
||||
]
|
||||
},
|
||||
"members": {
|
||||
"fieldBytes": "function",
|
||||
"get": "function",
|
||||
"has": "function",
|
||||
"toBytes": "function",
|
||||
"toJSON": "function",
|
||||
"withField": "function",
|
||||
"withoutField": "function"
|
||||
},
|
||||
"own": [
|
||||
"fieldBytes",
|
||||
"get",
|
||||
"has",
|
||||
"toBytes",
|
||||
"toJSON",
|
||||
"withField",
|
||||
"withoutField"
|
||||
],
|
||||
"probe": "util.decodeObject(new Uint8Array([0x22, 0, 0, 0, 9]))"
|
||||
},
|
||||
"UInt": {
|
||||
"members": {
|
||||
"add": "function",
|
||||
"bits": "value",
|
||||
"byteLength": "value",
|
||||
"compare": "function",
|
||||
"equals": "function",
|
||||
"isZero": "function",
|
||||
"saturatingAdd": "function",
|
||||
"saturatingSubtract": "function",
|
||||
"subtract": "function",
|
||||
"toBigInt": "function",
|
||||
"toNumber": "function",
|
||||
"toString": "function",
|
||||
"wrappingAdd": "function",
|
||||
"wrappingMultiply": "function",
|
||||
"wrappingSubtract": "function"
|
||||
},
|
||||
"own": [
|
||||
"add",
|
||||
"bits",
|
||||
"byteLength",
|
||||
"compare",
|
||||
"equals",
|
||||
"isZero",
|
||||
"saturatingAdd",
|
||||
"saturatingSubtract",
|
||||
"subtract",
|
||||
"toBigInt",
|
||||
"toNumber",
|
||||
"toString",
|
||||
"wrappingAdd",
|
||||
"wrappingMultiply",
|
||||
"wrappingSubtract"
|
||||
],
|
||||
"probe": "UInt8.zero"
|
||||
},
|
||||
"Vector256": {
|
||||
"exotic": {
|
||||
"array": false,
|
||||
"enumerable": [
|
||||
"0"
|
||||
],
|
||||
"extensible": false,
|
||||
"own": [
|
||||
"0",
|
||||
"length"
|
||||
]
|
||||
},
|
||||
"members": {
|
||||
"[Symbol.iterator]": "function",
|
||||
"at": "function",
|
||||
"length": "value",
|
||||
"toBytes": "function"
|
||||
},
|
||||
"own": [
|
||||
"[Symbol.iterator]",
|
||||
"at",
|
||||
"toBytes"
|
||||
],
|
||||
"probe": "util.decodeObject(STBlob.fromHex('011320000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F')).Indexes"
|
||||
},
|
||||
"VoidResult": {
|
||||
"frozen": true,
|
||||
"members": {
|
||||
"moot": "function",
|
||||
"okMapOr": "function",
|
||||
"okOr": "function",
|
||||
"okOrHandle": "function"
|
||||
},
|
||||
"own": [
|
||||
"moot"
|
||||
],
|
||||
"probe": "state.set('surface-probe', new Uint8Array(0))"
|
||||
},
|
||||
"XChainBridge": {
|
||||
"members": {
|
||||
"IssuingChainDoor": "value",
|
||||
"IssuingChainIssue": "value",
|
||||
"LockingChainDoor": "value",
|
||||
"LockingChainIssue": "value",
|
||||
"equals": "function",
|
||||
"toBytes": "function"
|
||||
},
|
||||
"own": [
|
||||
"IssuingChainDoor",
|
||||
"IssuingChainIssue",
|
||||
"LockingChainDoor",
|
||||
"LockingChainIssue",
|
||||
"equals",
|
||||
"toBytes"
|
||||
],
|
||||
"probe": "util.decodeObject(STBlob.fromHex('011914B5F762798A53D543A014CAF8B297CFF8F2F937E8000000000000000000000000000000000000000014B5F762798A53D543A014CAF8B297CFF8F2F937E80000000000000000000000000000000000000000')).XChainBridge"
|
||||
},
|
||||
"XFLDecimal": {
|
||||
"members": {
|
||||
"isNegative": "function",
|
||||
"isZero": "function"
|
||||
},
|
||||
"own": [
|
||||
"isNegative",
|
||||
"isZero"
|
||||
],
|
||||
"probe": "util.decodeObject(STBlob.fromHex('61D4838D7EA4C680000000000000000000000000005553440000000000B5F762798A53D543A014CAF8B297CFF8F2F937E8')).Amount.toXFL()"
|
||||
}
|
||||
},
|
||||
"schema": "jshookz.javascript-surface.v1",
|
||||
"statics": {
|
||||
"AccountID": {
|
||||
"from": "function",
|
||||
"fromHex": "function",
|
||||
"one": "value",
|
||||
"zero": "value"
|
||||
},
|
||||
"Hash256": {
|
||||
"from": "function",
|
||||
"fromHex": "function",
|
||||
"zero": "value"
|
||||
},
|
||||
"STBlob": {
|
||||
"from": "function",
|
||||
"fromHex": "function"
|
||||
},
|
||||
"UInt16": {
|
||||
"from": "function",
|
||||
"max": "value",
|
||||
"mulDiv": "function",
|
||||
"zero": "value"
|
||||
},
|
||||
"UInt32": {
|
||||
"from": "function",
|
||||
"max": "value",
|
||||
"mulDiv": "function",
|
||||
"zero": "value"
|
||||
},
|
||||
"UInt64": {
|
||||
"from": "function",
|
||||
"max": "value",
|
||||
"mulDiv": "function",
|
||||
"mulDivXfl": "function",
|
||||
"zero": "value"
|
||||
},
|
||||
"UInt8": {
|
||||
"from": "function",
|
||||
"max": "value",
|
||||
"mulDiv": "function",
|
||||
"zero": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
27
external/wasmtime/conandata.yml
vendored
27
external/wasmtime/conandata.yml
vendored
@@ -1,27 +0,0 @@
|
||||
sources:
|
||||
47.0.3:
|
||||
Linux:
|
||||
armv8:
|
||||
sha256: c1ef99bb4ef0151282f339e22a67f8229d9ab336f9a17387e65f140b79c45be1
|
||||
url: https://github.com/bytecodealliance/wasmtime/releases/download/v47.0.3/wasmtime-v47.0.3-aarch64-linux-c-api.tar.xz
|
||||
x86_64:
|
||||
sha256: aaa3621f2a3d8393696702897f8f78a1cc504437d500701496d560125aefd732
|
||||
url: https://github.com/bytecodealliance/wasmtime/releases/download/v47.0.3/wasmtime-v47.0.3-x86_64-linux-c-api.tar.xz
|
||||
Macos:
|
||||
armv8:
|
||||
sha256: 1854c8f03a764c89afe77fa88d9092ab89a368e527cd27a12959b1d91152324e
|
||||
url: https://github.com/bytecodealliance/wasmtime/releases/download/v47.0.3/wasmtime-v47.0.3-aarch64-macos-c-api.tar.xz
|
||||
x86_64:
|
||||
sha256: 627622087b77b92c163e826ec6ebf834a70d78735828043edf7ace263f8a9e62
|
||||
url: https://github.com/bytecodealliance/wasmtime/releases/download/v47.0.3/wasmtime-v47.0.3-x86_64-macos-c-api.tar.xz
|
||||
MinGW:
|
||||
x86_64:
|
||||
sha256: 355a0c7b49b92bb011e74a4047cc1186271aeb8c8cfcc4360248caaf3c4d5f38
|
||||
url: https://github.com/bytecodealliance/wasmtime/releases/download/v47.0.3/wasmtime-v47.0.3-x86_64-mingw-c-api.zip
|
||||
Windows:
|
||||
armv8:
|
||||
sha256: 57107b14b28ac2152e903501fe1722007b90fc11ccd63ad6d62ed574c5dd90db
|
||||
url: https://github.com/bytecodealliance/wasmtime/releases/download/v47.0.3/wasmtime-v47.0.3-aarch64-windows-c-api.zip
|
||||
x86_64:
|
||||
sha256: 952dfbc864295eccc89caa92bc303686017ca4ea205a1f21878809f374bb77c5
|
||||
url: https://github.com/bytecodealliance/wasmtime/releases/download/v47.0.3/wasmtime-v47.0.3-x86_64-windows-c-api.zip
|
||||
162
external/wasmtime/conanfile.py
vendored
162
external/wasmtime/conanfile.py
vendored
@@ -1,162 +0,0 @@
|
||||
import os
|
||||
|
||||
from conan import ConanFile
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.files import copy, get
|
||||
from conan.tools.microsoft import is_msvc
|
||||
|
||||
|
||||
required_conan_version = ">=2.0"
|
||||
|
||||
|
||||
class WasmtimeConan(ConanFile):
|
||||
name = "wasmtime"
|
||||
description = "Standalone WebAssembly runtime using Cranelift"
|
||||
license = "Apache-2.0 WITH LLVM-exception"
|
||||
url = "https://github.com/Xahau/xahaud"
|
||||
homepage = "https://github.com/bytecodealliance/wasmtime"
|
||||
topics = ("webassembly", "wasm")
|
||||
package_type = "library"
|
||||
settings = "os", "arch", "compiler", "build_type"
|
||||
options = {"shared": [True, False]}
|
||||
default_options = {"shared": False}
|
||||
no_copy_source = True
|
||||
|
||||
@property
|
||||
def _sources_os_key(self):
|
||||
if is_msvc(self):
|
||||
return "Windows"
|
||||
if self.settings.os == "Windows" and self.settings.compiler == "gcc":
|
||||
return "MinGW"
|
||||
return str(self.settings.os)
|
||||
|
||||
def configure(self):
|
||||
self.settings.rm_safe("compiler.libcxx")
|
||||
self.settings.rm_safe("compiler.cppstd")
|
||||
|
||||
def package_id(self):
|
||||
# Official release archives contain a prebuilt C ABI. Compiler patch
|
||||
# versions do not change the packaged bytes.
|
||||
if self.info.settings.compiler == "clang":
|
||||
self.info.settings.compiler = "gcc"
|
||||
del self.info.settings.compiler.version
|
||||
|
||||
def validate(self):
|
||||
try:
|
||||
self.conan_data["sources"][self.version][self._sources_os_key][
|
||||
str(self.settings.arch)
|
||||
]
|
||||
except KeyError as error:
|
||||
raise ConanInvalidConfiguration(
|
||||
"No official Wasmtime C-API archive for "
|
||||
f"{self.settings.os}/{self.settings.arch}"
|
||||
) from error
|
||||
|
||||
def build(self):
|
||||
get(
|
||||
self,
|
||||
**self.conan_data["sources"][self.version][self._sources_os_key][
|
||||
str(self.settings.arch)
|
||||
],
|
||||
destination=self.build_folder,
|
||||
strip_root=True,
|
||||
)
|
||||
|
||||
def package(self):
|
||||
copy(
|
||||
self,
|
||||
"*.h",
|
||||
dst=os.path.join(self.package_folder, "include"),
|
||||
src=os.path.join(self.build_folder, "include"),
|
||||
)
|
||||
copy(
|
||||
self,
|
||||
"*.hh",
|
||||
dst=os.path.join(self.package_folder, "include"),
|
||||
src=os.path.join(self.build_folder, "include"),
|
||||
)
|
||||
|
||||
source_lib = os.path.join(self.build_folder, "lib")
|
||||
package_lib = os.path.join(self.package_folder, "lib")
|
||||
package_bin = os.path.join(self.package_folder, "bin")
|
||||
if self.options.shared:
|
||||
copy(
|
||||
self,
|
||||
"wasmtime.dll.lib",
|
||||
dst=package_lib,
|
||||
src=source_lib,
|
||||
keep_path=False,
|
||||
)
|
||||
copy(
|
||||
self,
|
||||
"wasmtime.dll",
|
||||
dst=package_bin,
|
||||
src=source_lib,
|
||||
keep_path=False,
|
||||
)
|
||||
copy(
|
||||
self,
|
||||
"libwasmtime.dll.a",
|
||||
dst=package_lib,
|
||||
src=source_lib,
|
||||
keep_path=False,
|
||||
)
|
||||
copy(
|
||||
self,
|
||||
"libwasmtime.so*",
|
||||
dst=package_lib,
|
||||
src=source_lib,
|
||||
keep_path=False,
|
||||
)
|
||||
copy(
|
||||
self,
|
||||
"libwasmtime.dylib",
|
||||
dst=package_lib,
|
||||
src=source_lib,
|
||||
keep_path=False,
|
||||
)
|
||||
else:
|
||||
copy(
|
||||
self,
|
||||
"wasmtime.lib",
|
||||
dst=package_lib,
|
||||
src=source_lib,
|
||||
keep_path=False,
|
||||
)
|
||||
copy(
|
||||
self,
|
||||
"libwasmtime.a",
|
||||
dst=package_lib,
|
||||
src=source_lib,
|
||||
keep_path=False,
|
||||
)
|
||||
|
||||
copy(
|
||||
self,
|
||||
"LICENSE",
|
||||
dst=os.path.join(self.package_folder, "licenses"),
|
||||
src=self.build_folder,
|
||||
)
|
||||
|
||||
def package_info(self):
|
||||
if self.options.shared:
|
||||
self.cpp_info.libs = [
|
||||
"wasmtime.dll" if self.settings.os == "Windows" else "wasmtime"
|
||||
]
|
||||
else:
|
||||
if self.settings.os == "Windows":
|
||||
self.cpp_info.defines = ["WASM_API_EXTERN=", "WASI_API_EXTERN="]
|
||||
self.cpp_info.libs = ["wasmtime"]
|
||||
|
||||
if self.settings.os == "Windows":
|
||||
self.cpp_info.system_libs = [
|
||||
"ws2_32",
|
||||
"bcrypt",
|
||||
"advapi32",
|
||||
"userenv",
|
||||
"ntdll",
|
||||
"shell32",
|
||||
"ole32",
|
||||
]
|
||||
elif self.settings.os in ("Linux", "FreeBSD"):
|
||||
self.cpp_info.system_libs = ["pthread", "dl", "m", "rt"]
|
||||
@@ -47,5 +47,8 @@
|
||||
#define MEM_OVERLAP -43
|
||||
#define TOO_MANY_STATE_MODIFICATIONS -44
|
||||
#define TOO_MANY_NAMESPACES -45
|
||||
#define EXPORT_FAILURE -46
|
||||
#define TOO_MANY_EXPORTED_TXN -47
|
||||
#define TOO_LITTLE_ENTROPY -48
|
||||
#define HOOK_ERROR_CODES
|
||||
#endif //HOOK_ERROR_CODES
|
||||
|
||||
@@ -339,6 +339,50 @@ prepare(
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
|
||||
extern int64_t
|
||||
xport_reserve(uint32_t count);
|
||||
|
||||
extern int64_t
|
||||
xport(
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len,
|
||||
uint32_t committee_hash_ptr,
|
||||
uint32_t committee_hash_len);
|
||||
|
||||
extern int64_t
|
||||
xport_cancel(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
|
||||
|
||||
/*
|
||||
Consensus entropy APIs.
|
||||
|
||||
min_tier is a required fail-closed floor:
|
||||
1 = consensus_fallback, 2 = participant_aligned,
|
||||
3 = validator_quorum, 4 = validator_full.
|
||||
|
||||
entropy_cr_status returns a packed non-negative value:
|
||||
bits 32..39 tier, 16..31 count, 0..15 denominator.
|
||||
Check for a negative error before using the ENTROPY_* macros.
|
||||
|
||||
Classify tier before count/denominator arithmetic: fallback is tier 1
|
||||
with count=denominator=0. Common policies are denominator-count <= 1,
|
||||
5*count >= 4*denominator (use widened arithmetic), or count >= floor.
|
||||
|
||||
entropy_cr_dice/entropy_cr_random return TOO_LITTLE_ENTROPY if fresh
|
||||
visible entropy is below
|
||||
min_tier. Open-ledger and simulate execution are provisional previews;
|
||||
final ordered execution may see a different entropy object.
|
||||
*/
|
||||
extern int64_t
|
||||
entropy_cr_dice(uint32_t sides, uint32_t min_tier);
|
||||
|
||||
extern int64_t
|
||||
entropy_cr_random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
|
||||
|
||||
extern int64_t
|
||||
entropy_cr_status(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -41,6 +41,29 @@ APPLY_HOOK="$SCRIPT_DIR/../include/xrpl/hook/hook_api.macro"
|
||||
# Insert __attribute__((noduplicate)) before _g
|
||||
sub(/[[:space:]]+_g/, " __attribute__((noduplicate)) _g", line);
|
||||
}
|
||||
|
||||
if (line ~ /[[:space:]]+entropy_cr_dice[[:space:]]*\(/) {
|
||||
print "/*";
|
||||
print " Consensus entropy APIs.";
|
||||
print "";
|
||||
print " min_tier is a required fail-closed floor:";
|
||||
print " 1 = consensus_fallback, 2 = participant_aligned,";
|
||||
print " 3 = validator_quorum, 4 = validator_full.";
|
||||
print "";
|
||||
print " entropy_cr_status returns a packed non-negative value:";
|
||||
print " bits 32..39 tier, 16..31 count, 0..15 denominator.";
|
||||
print " Check for a negative error before using the ENTROPY_* macros.";
|
||||
print "";
|
||||
print " Classify tier before count/denominator arithmetic: fallback is tier 1";
|
||||
print " with count=denominator=0. Common policies are denominator-count <= 1,";
|
||||
print " 5*count >= 4*denominator (use widened arithmetic), or count >= floor.";
|
||||
print "";
|
||||
print " entropy_cr_dice/entropy_cr_random return TOO_LITTLE_ENTROPY if fresh";
|
||||
print " visible entropy is below";
|
||||
print " min_tier. Open-ledger and simulate execution are provisional previews;";
|
||||
print " final ordered execution may see a different entropy object.";
|
||||
print "*/";
|
||||
}
|
||||
|
||||
# printf("\n");
|
||||
|
||||
|
||||
@@ -348,5 +348,21 @@ util_verify(
|
||||
uint32_t kread_len);
|
||||
|
||||
extern int64_t xpop_slot(uint32_t, uint32_t);
|
||||
|
||||
extern int64_t
|
||||
xport_reserve(uint32_t count);
|
||||
|
||||
extern int64_t
|
||||
xport(
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len,
|
||||
uint32_t committee_hash_ptr,
|
||||
uint32_t committee_hash_len);
|
||||
|
||||
extern int64_t
|
||||
xport_cancel(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
|
||||
|
||||
#define HOOK_EXTERN
|
||||
#endif // HOOK_EXTERN
|
||||
#endif // HOOK_EXTERN
|
||||
|
||||
@@ -47,4 +47,4 @@
|
||||
#include "macro.h"
|
||||
#include "types.h"
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -607,31 +607,37 @@ int out_len = 0;\
|
||||
#define PREPARE_PAYMENT_SIMPLE_SIZE 248U
|
||||
#endif
|
||||
|
||||
#define PREPARE_PAYMENT_SIMPLE(buf_out_master, drops_amount_raw, to_address, dest_tag_raw, src_tag_raw)\
|
||||
{\
|
||||
uint8_t* buf_out = buf_out_master;\
|
||||
uint8_t acc[20];\
|
||||
uint64_t drops_amount = (drops_amount_raw);\
|
||||
uint32_t dest_tag = (dest_tag_raw);\
|
||||
uint32_t src_tag = (src_tag_raw);\
|
||||
uint32_t cls = (uint32_t)ledger_seq();\
|
||||
hook_account(SBUF(acc));\
|
||||
_01_02_ENCODE_TT (buf_out, ttPAYMENT ); /* uint16 | size 3 */ \
|
||||
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
|
||||
_02_03_ENCODE_TAG_SRC (buf_out, src_tag ); /* uint32 | size 5 */ \
|
||||
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
|
||||
_02_14_ENCODE_TAG_DST (buf_out, dest_tag ); /* uint32 | size 5 */ \
|
||||
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
|
||||
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
|
||||
_06_01_ENCODE_DROPS_AMOUNT (buf_out, drops_amount ); /* amount | size 9 */ \
|
||||
uint8_t* fee_ptr = buf_out;\
|
||||
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
|
||||
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
|
||||
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
|
||||
_08_03_ENCODE_ACCOUNT_DST (buf_out, to_address ); /* account | size 22 */ \
|
||||
int64_t edlen = etxn_details((uint32_t)buf_out, PREPARE_PAYMENT_SIMPLE_SIZE); /* emitdet | size 1?? */ \
|
||||
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_SIZE); \
|
||||
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
|
||||
#define PREPARE_PAYMENT_SIMPLE( \
|
||||
buf_out_master, drops_amount_raw, to_address, dest_tag_raw, src_tag_raw) \
|
||||
{ \
|
||||
uint8_t* buf_out = buf_out_master; \
|
||||
uint8_t acc[20]; \
|
||||
uint64_t drops_amount = (drops_amount_raw); \
|
||||
uint32_t dest_tag = (dest_tag_raw); \
|
||||
uint32_t src_tag = (src_tag_raw); \
|
||||
uint32_t cls = (uint32_t)ledger_seq(); \
|
||||
hook_account(SBUF(acc)); \
|
||||
_01_02_ENCODE_TT(buf_out, ttPAYMENT); /* uint16 | size 3 */ \
|
||||
_02_02_ENCODE_FLAGS(buf_out, tfCANONICAL); /* uint32 | size 5 */ \
|
||||
_02_03_ENCODE_TAG_SRC(buf_out, src_tag); /* uint32 | size 5 */ \
|
||||
_02_04_ENCODE_SEQUENCE(buf_out, 0); /* uint32 | size 5 */ \
|
||||
_02_14_ENCODE_TAG_DST(buf_out, dest_tag); /* uint32 | size 5 */ \
|
||||
_02_26_ENCODE_FLS(buf_out, cls + 1); /* uint32 | size 6 */ \
|
||||
_02_27_ENCODE_LLS(buf_out, cls + 5); /* uint32 | size 6 */ \
|
||||
_06_01_ENCODE_DROPS_AMOUNT( \
|
||||
buf_out, drops_amount); /* amount | size 9 */ \
|
||||
uint8_t* fee_ptr = buf_out; \
|
||||
_06_08_ENCODE_DROPS_FEE(buf_out, 0); /* amount | size 9 */ \
|
||||
_07_03_ENCODE_SIGNING_PUBKEY_NULL(buf_out); /* pk | size 35 */ \
|
||||
_08_01_ENCODE_ACCOUNT_SRC(buf_out, acc); /* account | size 22 */ \
|
||||
_08_03_ENCODE_ACCOUNT_DST( \
|
||||
buf_out, to_address); /* account | size 22 */ \
|
||||
int64_t edlen = etxn_details( \
|
||||
(uint32_t)buf_out, \
|
||||
PREPARE_PAYMENT_SIMPLE_SIZE); /* emitdet | size 1?? */ \
|
||||
int64_t fee = \
|
||||
etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_SIZE); \
|
||||
_06_08_ENCODE_DROPS_FEE(fee_ptr, fee); \
|
||||
}
|
||||
|
||||
#ifdef HAS_CALLBACK
|
||||
@@ -639,33 +645,35 @@ int out_len = 0;\
|
||||
#else
|
||||
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE 287
|
||||
#endif
|
||||
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE(buf_out_master, tlamt, to_address, dest_tag_raw, src_tag_raw)\
|
||||
{\
|
||||
uint8_t* buf_out = buf_out_master;\
|
||||
uint8_t acc[20];\
|
||||
uint32_t dest_tag = (dest_tag_raw);\
|
||||
uint32_t src_tag = (src_tag_raw);\
|
||||
uint32_t cls = (uint32_t)ledger_seq();\
|
||||
hook_account(SBUF(acc));\
|
||||
_01_02_ENCODE_TT (buf_out, ttPAYMENT ); /* uint16 | size 3 */ \
|
||||
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
|
||||
_02_03_ENCODE_TAG_SRC (buf_out, src_tag ); /* uint32 | size 5 */ \
|
||||
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
|
||||
_02_14_ENCODE_TAG_DST (buf_out, dest_tag ); /* uint32 | size 5 */ \
|
||||
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
|
||||
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
|
||||
_06_01_ENCODE_TL_AMOUNT (buf_out, tlamt ); /* amount | size 48 */ \
|
||||
uint8_t* fee_ptr = buf_out;\
|
||||
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
|
||||
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
|
||||
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
|
||||
_08_03_ENCODE_ACCOUNT_DST (buf_out, to_address ); /* account | size 22 */ \
|
||||
etxn_details((uint32_t)buf_out, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); /* emitdet | size 1?? */ \
|
||||
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); \
|
||||
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
|
||||
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE( \
|
||||
buf_out_master, tlamt, to_address, dest_tag_raw, src_tag_raw) \
|
||||
{ \
|
||||
uint8_t* buf_out = buf_out_master; \
|
||||
uint8_t acc[20]; \
|
||||
uint32_t dest_tag = (dest_tag_raw); \
|
||||
uint32_t src_tag = (src_tag_raw); \
|
||||
uint32_t cls = (uint32_t)ledger_seq(); \
|
||||
hook_account(SBUF(acc)); \
|
||||
_01_02_ENCODE_TT(buf_out, ttPAYMENT); /* uint16 | size 3 */ \
|
||||
_02_02_ENCODE_FLAGS(buf_out, tfCANONICAL); /* uint32 | size 5 */ \
|
||||
_02_03_ENCODE_TAG_SRC(buf_out, src_tag); /* uint32 | size 5 */ \
|
||||
_02_04_ENCODE_SEQUENCE(buf_out, 0); /* uint32 | size 5 */ \
|
||||
_02_14_ENCODE_TAG_DST(buf_out, dest_tag); /* uint32 | size 5 */ \
|
||||
_02_26_ENCODE_FLS(buf_out, cls + 1); /* uint32 | size 6 */ \
|
||||
_02_27_ENCODE_LLS(buf_out, cls + 5); /* uint32 | size 6 */ \
|
||||
_06_01_ENCODE_TL_AMOUNT(buf_out, tlamt); /* amount | size 48 */ \
|
||||
uint8_t* fee_ptr = buf_out; \
|
||||
_06_08_ENCODE_DROPS_FEE(buf_out, 0); /* amount | size 9 */ \
|
||||
_07_03_ENCODE_SIGNING_PUBKEY_NULL(buf_out); /* pk | size 35 */ \
|
||||
_08_01_ENCODE_ACCOUNT_SRC(buf_out, acc); /* account | size 22 */ \
|
||||
_08_03_ENCODE_ACCOUNT_DST( \
|
||||
buf_out, to_address); /* account | size 22 */ \
|
||||
etxn_details( \
|
||||
(uint32_t)buf_out, \
|
||||
PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); /* emitdet | size 1?? */ \
|
||||
int64_t fee = etxn_fee_base( \
|
||||
buf_out_master, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); \
|
||||
_06_08_ENCODE_DROPS_FEE(fee_ptr, fee); \
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -3,18 +3,31 @@
|
||||
#define sfCloseResolution ((16U << 16U) + 1U)
|
||||
#define sfMethod ((16U << 16U) + 2U)
|
||||
#define sfTransactionResult ((16U << 16U) + 3U)
|
||||
#define sfScale ((16U << 16U) + 4U)
|
||||
#define sfAssetScale ((16U << 16U) + 5U)
|
||||
#define sfTickSize ((16U << 16U) + 16U)
|
||||
#define sfUNLModifyDisabling ((16U << 16U) + 17U)
|
||||
#define sfHookResult ((16U << 16U) + 18U)
|
||||
#define sfWasLockingChainSend ((16U << 16U) + 19U)
|
||||
#define sfSidecarType ((16U << 16U) + 20U)
|
||||
#define sfEntropyTier ((16U << 16U) + 21U)
|
||||
#define sfLedgerEntryType ((1U << 16U) + 1U)
|
||||
#define sfTransactionType ((1U << 16U) + 2U)
|
||||
#define sfSignerWeight ((1U << 16U) + 3U)
|
||||
#define sfTransferFee ((1U << 16U) + 4U)
|
||||
#define sfTradingFee ((1U << 16U) + 5U)
|
||||
#define sfDiscountedFee ((1U << 16U) + 6U)
|
||||
#define sfVersion ((1U << 16U) + 16U)
|
||||
#define sfHookStateChangeCount ((1U << 16U) + 17U)
|
||||
#define sfHookEmitCount ((1U << 16U) + 18U)
|
||||
#define sfHookExecutionIndex ((1U << 16U) + 19U)
|
||||
#define sfHookApiVersion ((1U << 16U) + 20U)
|
||||
#define sfHookStateScale ((1U << 16U) + 21U)
|
||||
#define sfLedgerFixType ((1U << 16U) + 22U)
|
||||
#define sfHookExportCount ((1U << 16U) + 98U)
|
||||
#define sfEntropyCount ((1U << 16U) + 99U)
|
||||
#define sfEntropyDenominator ((1U << 16U) + 100U)
|
||||
#define sfExportCount ((1U << 16U) + 101U)
|
||||
#define sfNetworkID ((2U << 16U) + 1U)
|
||||
#define sfFlags ((2U << 16U) + 2U)
|
||||
#define sfSourceTag ((2U << 16U) + 3U)
|
||||
@@ -29,6 +42,7 @@
|
||||
#define sfWalletSize ((2U << 16U) + 12U)
|
||||
#define sfOwnerCount ((2U << 16U) + 13U)
|
||||
#define sfDestinationTag ((2U << 16U) + 14U)
|
||||
#define sfLastUpdateTime ((2U << 16U) + 15U)
|
||||
#define sfHighQualityIn ((2U << 16U) + 16U)
|
||||
#define sfHighQualityOut ((2U << 16U) + 17U)
|
||||
#define sfLowQualityIn ((2U << 16U) + 18U)
|
||||
@@ -60,7 +74,15 @@
|
||||
#define sfBurnedNFTokens ((2U << 16U) + 44U)
|
||||
#define sfHookStateCount ((2U << 16U) + 45U)
|
||||
#define sfEmitGeneration ((2U << 16U) + 46U)
|
||||
#define sfLockCount ((2U << 16U) + 47U)
|
||||
#define sfVoteWeight ((2U << 16U) + 48U)
|
||||
#define sfLockCount ((2U << 16U) + 49U)
|
||||
#define sfFirstNFTokenSequence ((2U << 16U) + 50U)
|
||||
#define sfOracleDocumentID ((2U << 16U) + 51U)
|
||||
#define sfStartTime ((2U << 16U) + 93U)
|
||||
#define sfRepeatCount ((2U << 16U) + 94U)
|
||||
#define sfDelaySeconds ((2U << 16U) + 95U)
|
||||
#define sfXahauActivationLgrSeq ((2U << 16U) + 96U)
|
||||
#define sfImportSequence ((2U << 16U) + 97U)
|
||||
#define sfRewardTime ((2U << 16U) + 98U)
|
||||
#define sfRewardLgrFirst ((2U << 16U) + 99U)
|
||||
#define sfRewardLgrLast ((2U << 16U) + 100U)
|
||||
@@ -80,12 +102,26 @@
|
||||
#define sfHookInstructionCount ((3U << 16U) + 17U)
|
||||
#define sfHookReturnCode ((3U << 16U) + 18U)
|
||||
#define sfReferenceCount ((3U << 16U) + 19U)
|
||||
#define sfXChainClaimID ((3U << 16U) + 20U)
|
||||
#define sfXChainAccountCreateCount ((3U << 16U) + 21U)
|
||||
#define sfXChainAccountClaimCount ((3U << 16U) + 22U)
|
||||
#define sfAssetPrice ((3U << 16U) + 23U)
|
||||
#define sfMaximumAmount ((3U << 16U) + 24U)
|
||||
#define sfOutstandingAmount ((3U << 16U) + 25U)
|
||||
#define sfMPTAmount ((3U << 16U) + 26U)
|
||||
#define sfIssuerNode ((3U << 16U) + 27U)
|
||||
#define sfSubjectNode ((3U << 16U) + 28U)
|
||||
#define sfExportNode ((3U << 16U) + 29U)
|
||||
#define sfTouchCount ((3U << 16U) + 97U)
|
||||
#define sfAccountIndex ((3U << 16U) + 98U)
|
||||
#define sfAccountCount ((3U << 16U) + 99U)
|
||||
#define sfRewardAccumulator ((3U << 16U) + 100U)
|
||||
#define sfEmailHash ((4U << 16U) + 1U)
|
||||
#define sfTakerPaysCurrency ((10U << 16U) + 1U)
|
||||
#define sfTakerPaysIssuer ((10U << 16U) + 2U)
|
||||
#define sfTakerGetsCurrency ((10U << 16U) + 3U)
|
||||
#define sfTakerGetsIssuer ((10U << 16U) + 4U)
|
||||
#define sfTakerPaysCurrency ((17U << 16U) + 1U)
|
||||
#define sfTakerPaysIssuer ((17U << 16U) + 2U)
|
||||
#define sfTakerGetsCurrency ((17U << 16U) + 3U)
|
||||
#define sfTakerGetsIssuer ((17U << 16U) + 4U)
|
||||
#define sfMPTokenIssuanceID ((21U << 16U) + 1U)
|
||||
#define sfLedgerHash ((5U << 16U) + 1U)
|
||||
#define sfParentHash ((5U << 16U) + 2U)
|
||||
#define sfTransactionHash ((5U << 16U) + 3U)
|
||||
@@ -99,6 +135,8 @@
|
||||
#define sfEmitParentTxnID ((5U << 16U) + 11U)
|
||||
#define sfEmitNonce ((5U << 16U) + 12U)
|
||||
#define sfEmitHookHash ((5U << 16U) + 13U)
|
||||
#define sfObjectID ((5U << 16U) + 14U)
|
||||
#define sfAMMID ((5U << 16U) + 15U)
|
||||
#define sfBookDirectory ((5U << 16U) + 16U)
|
||||
#define sfInvoiceID ((5U << 16U) + 17U)
|
||||
#define sfNickname ((5U << 16U) + 18U)
|
||||
@@ -120,6 +158,17 @@
|
||||
#define sfOfferID ((5U << 16U) + 34U)
|
||||
#define sfEscrowID ((5U << 16U) + 35U)
|
||||
#define sfURITokenID ((5U << 16U) + 36U)
|
||||
#define sfDomainID ((5U << 16U) + 37U)
|
||||
#define sfExportSignatureHash ((5U << 16U) + 38U)
|
||||
#define sfExportCommitteeHash ((5U << 16U) + 39U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
#define sfCron ((5U << 16U) + 95U)
|
||||
#define sfHookCanEmit ((5U << 16U) + 96U)
|
||||
#define sfEmittedTxnID ((5U << 16U) + 97U)
|
||||
#define sfGovernanceMarks ((5U << 16U) + 98U)
|
||||
#define sfGovernanceFlags ((5U << 16U) + 99U)
|
||||
#define sfNumber ((9U << 16U) + 1U)
|
||||
#define sfAmount ((6U << 16U) + 1U)
|
||||
#define sfBalance ((6U << 16U) + 2U)
|
||||
#define sfLimitAmount ((6U << 16U) + 3U)
|
||||
@@ -130,12 +179,26 @@
|
||||
#define sfFee ((6U << 16U) + 8U)
|
||||
#define sfSendMax ((6U << 16U) + 9U)
|
||||
#define sfDeliverMin ((6U << 16U) + 10U)
|
||||
#define sfAmount2 ((6U << 16U) + 11U)
|
||||
#define sfBidMin ((6U << 16U) + 12U)
|
||||
#define sfBidMax ((6U << 16U) + 13U)
|
||||
#define sfMinimumOffer ((6U << 16U) + 16U)
|
||||
#define sfRippleEscrow ((6U << 16U) + 17U)
|
||||
#define sfDeliveredAmount ((6U << 16U) + 18U)
|
||||
#define sfNFTokenBrokerFee ((6U << 16U) + 19U)
|
||||
#define sfHookCallbackFee ((6U << 16U) + 20U)
|
||||
#define sfLockedBalance ((6U << 16U) + 21U)
|
||||
#define sfBaseFeeDrops ((6U << 16U) + 22U)
|
||||
#define sfReserveBaseDrops ((6U << 16U) + 23U)
|
||||
#define sfReserveIncrementDrops ((6U << 16U) + 24U)
|
||||
#define sfLPTokenOut ((6U << 16U) + 25U)
|
||||
#define sfLPTokenIn ((6U << 16U) + 26U)
|
||||
#define sfEPrice ((6U << 16U) + 27U)
|
||||
#define sfPrice ((6U << 16U) + 28U)
|
||||
#define sfSignatureReward ((6U << 16U) + 29U)
|
||||
#define sfMinAccountCreateAmount ((6U << 16U) + 30U)
|
||||
#define sfLPTokenBalance ((6U << 16U) + 31U)
|
||||
#define sfTrustLineRewardAccumulator ((6U << 16U) + 99U)
|
||||
#define sfPublicKey ((7U << 16U) + 1U)
|
||||
#define sfMessageKey ((7U << 16U) + 2U)
|
||||
#define sfSigningPubKey ((7U << 16U) + 3U)
|
||||
@@ -161,6 +224,18 @@
|
||||
#define sfHookParameterName ((7U << 16U) + 24U)
|
||||
#define sfHookParameterValue ((7U << 16U) + 25U)
|
||||
#define sfBlob ((7U << 16U) + 26U)
|
||||
#define sfDIDDocument ((7U << 16U) + 27U)
|
||||
#define sfData ((7U << 16U) + 28U)
|
||||
#define sfAssetClass ((7U << 16U) + 29U)
|
||||
#define sfProvider ((7U << 16U) + 30U)
|
||||
#define sfMPTokenMetadata ((7U << 16U) + 31U)
|
||||
#define sfCredentialType ((7U << 16U) + 32U)
|
||||
#define sfEntropyContributors ((7U << 16U) + 33U)
|
||||
#define sfExportCommittee ((7U << 16U) + 34U)
|
||||
#define sfExportContributors ((7U << 16U) + 35U)
|
||||
#define sfHookName ((7U << 16U) + 97U)
|
||||
#define sfRemarkValue ((7U << 16U) + 98U)
|
||||
#define sfRemarkName ((7U << 16U) + 99U)
|
||||
#define sfAccount ((8U << 16U) + 1U)
|
||||
#define sfOwner ((8U << 16U) + 2U)
|
||||
#define sfDestination ((8U << 16U) + 3U)
|
||||
@@ -170,13 +245,32 @@
|
||||
#define sfRegularKey ((8U << 16U) + 8U)
|
||||
#define sfNFTokenMinter ((8U << 16U) + 9U)
|
||||
#define sfEmitCallback ((8U << 16U) + 10U)
|
||||
#define sfHolder ((8U << 16U) + 11U)
|
||||
#define sfHookAccount ((8U << 16U) + 16U)
|
||||
#define sfOtherChainSource ((8U << 16U) + 18U)
|
||||
#define sfOtherChainDestination ((8U << 16U) + 19U)
|
||||
#define sfAttestationSignerAccount ((8U << 16U) + 20U)
|
||||
#define sfAttestationRewardAccount ((8U << 16U) + 21U)
|
||||
#define sfLockingChainDoor ((8U << 16U) + 22U)
|
||||
#define sfIssuingChainDoor ((8U << 16U) + 23U)
|
||||
#define sfSubject ((8U << 16U) + 24U)
|
||||
#define sfInform ((8U << 16U) + 99U)
|
||||
#define sfIndexes ((19U << 16U) + 1U)
|
||||
#define sfHashes ((19U << 16U) + 2U)
|
||||
#define sfAmendments ((19U << 16U) + 3U)
|
||||
#define sfNFTokenOffers ((19U << 16U) + 4U)
|
||||
#define sfHookNamespaces ((19U << 16U) + 5U)
|
||||
#define sfCredentialIDs ((19U << 16U) + 6U)
|
||||
#define sfURITokenIDs ((19U << 16U) + 99U)
|
||||
#define sfPaths ((18U << 16U) + 1U)
|
||||
#define sfBaseAsset ((26U << 16U) + 1U)
|
||||
#define sfQuoteAsset ((26U << 16U) + 2U)
|
||||
#define sfLockingChainIssue ((24U << 16U) + 1U)
|
||||
#define sfIssuingChainIssue ((24U << 16U) + 2U)
|
||||
#define sfAsset ((24U << 16U) + 3U)
|
||||
#define sfAsset2 ((24U << 16U) + 4U)
|
||||
#define sfClaimCurrency ((24U << 16U) + 5U)
|
||||
#define sfXChainBridge ((25U << 16U) + 1U)
|
||||
#define sfTransactionMetaData ((14U << 16U) + 2U)
|
||||
#define sfCreatedNode ((14U << 16U) + 3U)
|
||||
#define sfDeletedNode ((14U << 16U) + 4U)
|
||||
@@ -191,13 +285,32 @@
|
||||
#define sfEmitDetails ((14U << 16U) + 13U)
|
||||
#define sfHook ((14U << 16U) + 14U)
|
||||
#define sfSigner ((14U << 16U) + 16U)
|
||||
#define sfExportSigner ((14U << 16U) + 17U)
|
||||
#define sfMajority ((14U << 16U) + 18U)
|
||||
#define sfDisabledValidator ((14U << 16U) + 19U)
|
||||
#define sfEmittedTxn ((14U << 16U) + 20U)
|
||||
#define sfHookExecution ((14U << 16U) + 21U)
|
||||
#define sfHookDefinition ((14U << 16U) + 22U)
|
||||
#define sfHookParameter ((14U << 16U) + 23U)
|
||||
#define sfHookGrant ((14U << 16U) + 24U)
|
||||
#define sfVoteEntry ((14U << 16U) + 25U)
|
||||
#define sfAuctionSlot ((14U << 16U) + 26U)
|
||||
#define sfAuthAccount ((14U << 16U) + 27U)
|
||||
#define sfXChainClaimProofSig ((14U << 16U) + 28U)
|
||||
#define sfXChainCreateAccountProofSig ((14U << 16U) + 29U)
|
||||
#define sfXChainClaimAttestationCollectionElement ((14U << 16U) + 30U)
|
||||
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
|
||||
#define sfPriceData ((14U << 16U) + 32U)
|
||||
#define sfCredential ((14U << 16U) + 33U)
|
||||
#define sfExportedTxn ((14U << 16U) + 90U)
|
||||
#define sfAmountEntry ((14U << 16U) + 91U)
|
||||
#define sfMintURIToken ((14U << 16U) + 92U)
|
||||
#define sfHookEmission ((14U << 16U) + 93U)
|
||||
#define sfImportVLKey ((14U << 16U) + 94U)
|
||||
#define sfActiveValidator ((14U << 16U) + 95U)
|
||||
#define sfGenesisMint ((14U << 16U) + 96U)
|
||||
#define sfRemark ((14U << 16U) + 97U)
|
||||
#define sfHighReward ((14U << 16U) + 98U)
|
||||
#define sfLowReward ((14U << 16U) + 99U)
|
||||
#define sfSigners ((15U << 16U) + 3U)
|
||||
#define sfSignerEntries ((15U << 16U) + 4U)
|
||||
#define sfTemplate ((15U << 16U) + 5U)
|
||||
@@ -207,9 +320,23 @@
|
||||
#define sfMemos ((15U << 16U) + 9U)
|
||||
#define sfNFTokens ((15U << 16U) + 10U)
|
||||
#define sfHooks ((15U << 16U) + 11U)
|
||||
#define sfVoteSlots ((15U << 16U) + 12U)
|
||||
#define sfMajorities ((15U << 16U) + 16U)
|
||||
#define sfDisabledValidators ((15U << 16U) + 17U)
|
||||
#define sfHookExecutions ((15U << 16U) + 18U)
|
||||
#define sfHookParameters ((15U << 16U) + 19U)
|
||||
#define sfHookGrants ((15U << 16U) + 20U)
|
||||
#define sfActiveValidators ((15U << 16U) + 95U)
|
||||
#define sfXChainClaimAttestations ((15U << 16U) + 21U)
|
||||
#define sfXChainCreateAccountAttestations ((15U << 16U) + 22U)
|
||||
#define sfExportSigners ((15U << 16U) + 23U)
|
||||
#define sfPriceDataSeries ((15U << 16U) + 24U)
|
||||
#define sfAuthAccounts ((15U << 16U) + 25U)
|
||||
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
|
||||
#define sfUnauthorizeCredentials ((15U << 16U) + 27U)
|
||||
#define sfAcceptedCredentials ((15U << 16U) + 28U)
|
||||
#define sfAmounts ((15U << 16U) + 92U)
|
||||
#define sfHookEmissions ((15U << 16U) + 93U)
|
||||
#define sfImportVLKeys ((15U << 16U) + 94U)
|
||||
#define sfActiveValidators ((15U << 16U) + 95U)
|
||||
#define sfGenesisMints ((15U << 16U) + 96U)
|
||||
#define sfRemarks ((15U << 16U) + 97U)
|
||||
|
||||
@@ -56,6 +56,10 @@ enum ltURI_TOKEN {
|
||||
enum remarks {
|
||||
lsfImmutable = 1,
|
||||
};
|
||||
enum ltEXPORT_LATCH {
|
||||
lsfExportXpopSeen = 0x00000001,
|
||||
lsfExportCanceled = 0x00000002,
|
||||
};
|
||||
enum ltMPTOKEN_ISSUANCE {
|
||||
lsfMPTLocked = 0x00000001,
|
||||
lsfMPTCanLock = 0x00000002,
|
||||
|
||||
@@ -145,6 +145,10 @@ int out_len = 0;\
|
||||
#define SUB_OFFSET(x) ((int32_t)(x >> 32))
|
||||
#define SUB_LENGTH(x) ((int32_t)(x & 0xFFFFFFFFULL))
|
||||
|
||||
#define ENTROPY_TIER(x) (((uint64_t)(x) >> 32U) & 0xFFU)
|
||||
#define ENTROPY_COUNT(x) (((uint64_t)(x) >> 16U) & 0xFFFFU)
|
||||
#define ENTROPY_DENOMINATOR(x) ((uint64_t)(x) & 0xFFFFU)
|
||||
|
||||
#define BUFFER_EQUAL_20(buf1, buf2)\
|
||||
(\
|
||||
*(((uint64_t*)(buf1)) + 0) == *(((uint64_t*)(buf2)) + 0) &&\
|
||||
@@ -378,4 +382,3 @@ int out_len = 0;\
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#define sfUNLModifyDisabling ((16U << 16U) + 17U)
|
||||
#define sfHookResult ((16U << 16U) + 18U)
|
||||
#define sfWasLockingChainSend ((16U << 16U) + 19U)
|
||||
#define sfSidecarType ((16U << 16U) + 20U)
|
||||
#define sfEntropyTier ((16U << 16U) + 21U)
|
||||
#define sfLedgerEntryType ((1U << 16U) + 1U)
|
||||
#define sfTransactionType ((1U << 16U) + 2U)
|
||||
#define sfSignerWeight ((1U << 16U) + 3U)
|
||||
@@ -22,6 +24,10 @@
|
||||
#define sfHookApiVersion ((1U << 16U) + 20U)
|
||||
#define sfHookStateScale ((1U << 16U) + 21U)
|
||||
#define sfLedgerFixType ((1U << 16U) + 22U)
|
||||
#define sfHookExportCount ((1U << 16U) + 98U)
|
||||
#define sfEntropyCount ((1U << 16U) + 99U)
|
||||
#define sfEntropyDenominator ((1U << 16U) + 100U)
|
||||
#define sfExportCount ((1U << 16U) + 101U)
|
||||
#define sfNetworkID ((2U << 16U) + 1U)
|
||||
#define sfFlags ((2U << 16U) + 2U)
|
||||
#define sfSourceTag ((2U << 16U) + 3U)
|
||||
@@ -105,6 +111,7 @@
|
||||
#define sfMPTAmount ((3U << 16U) + 26U)
|
||||
#define sfIssuerNode ((3U << 16U) + 27U)
|
||||
#define sfSubjectNode ((3U << 16U) + 28U)
|
||||
#define sfExportNode ((3U << 16U) + 29U)
|
||||
#define sfTouchCount ((3U << 16U) + 97U)
|
||||
#define sfAccountIndex ((3U << 16U) + 98U)
|
||||
#define sfAccountCount ((3U << 16U) + 99U)
|
||||
@@ -152,6 +159,8 @@
|
||||
#define sfEscrowID ((5U << 16U) + 35U)
|
||||
#define sfURITokenID ((5U << 16U) + 36U)
|
||||
#define sfDomainID ((5U << 16U) + 37U)
|
||||
#define sfExportSignatureHash ((5U << 16U) + 38U)
|
||||
#define sfExportCommitteeHash ((5U << 16U) + 39U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
#define sfCron ((5U << 16U) + 95U)
|
||||
@@ -221,6 +230,9 @@
|
||||
#define sfProvider ((7U << 16U) + 30U)
|
||||
#define sfMPTokenMetadata ((7U << 16U) + 31U)
|
||||
#define sfCredentialType ((7U << 16U) + 32U)
|
||||
#define sfEntropyContributors ((7U << 16U) + 33U)
|
||||
#define sfExportCommittee ((7U << 16U) + 34U)
|
||||
#define sfExportContributors ((7U << 16U) + 35U)
|
||||
#define sfHookName ((7U << 16U) + 97U)
|
||||
#define sfRemarkValue ((7U << 16U) + 98U)
|
||||
#define sfRemarkName ((7U << 16U) + 99U)
|
||||
@@ -273,6 +285,7 @@
|
||||
#define sfEmitDetails ((14U << 16U) + 13U)
|
||||
#define sfHook ((14U << 16U) + 14U)
|
||||
#define sfSigner ((14U << 16U) + 16U)
|
||||
#define sfExportSigner ((14U << 16U) + 17U)
|
||||
#define sfMajority ((14U << 16U) + 18U)
|
||||
#define sfDisabledValidator ((14U << 16U) + 19U)
|
||||
#define sfEmittedTxn ((14U << 16U) + 20U)
|
||||
@@ -288,6 +301,7 @@
|
||||
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
|
||||
#define sfPriceData ((14U << 16U) + 32U)
|
||||
#define sfCredential ((14U << 16U) + 33U)
|
||||
#define sfExportedTxn ((14U << 16U) + 90U)
|
||||
#define sfAmountEntry ((14U << 16U) + 91U)
|
||||
#define sfMintURIToken ((14U << 16U) + 92U)
|
||||
#define sfHookEmission ((14U << 16U) + 93U)
|
||||
@@ -314,6 +328,7 @@
|
||||
#define sfHookGrants ((15U << 16U) + 20U)
|
||||
#define sfXChainClaimAttestations ((15U << 16U) + 21U)
|
||||
#define sfXChainCreateAccountAttestations ((15U << 16U) + 22U)
|
||||
#define sfExportSigners ((15U << 16U) + 23U)
|
||||
#define sfPriceDataSeries ((15U << 16U) + 24U)
|
||||
#define sfAuthAccounts ((15U << 16U) + 25U)
|
||||
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#define ttNFTOKEN_MODIFY 70
|
||||
#define ttPERMISSIONED_DOMAIN_SET 71
|
||||
#define ttPERMISSIONED_DOMAIN_DELETE 72
|
||||
#define ttEXPORT 91
|
||||
#define ttCRON 92
|
||||
#define ttCRON_SET 93
|
||||
#define ttREMARKS_SET 94
|
||||
@@ -74,3 +75,5 @@
|
||||
#define ttUNL_MODIFY 102
|
||||
#define ttEMIT_FAILURE 103
|
||||
#define ttUNL_REPORT 104
|
||||
#define ttCONSENSUS_ENTROPY 105
|
||||
#define ttEXPORT_SIGNATURES 106
|
||||
|
||||
@@ -115,3 +115,10 @@ enum AMMClawbackFlags : uint32_t {
|
||||
enum BridgeModifyFlags : uint32_t {
|
||||
tfClearAccountCreateAmount = 0x00010000,
|
||||
};
|
||||
|
||||
enum ExportFlags : uint32_t {
|
||||
// Lifecycle control: erase the named latch and forfeit any later callback.
|
||||
tfExportEraseLatch = 0x00010000,
|
||||
// Committee control: erase the named immutable committee object.
|
||||
tfExportEraseCommittee = 0x00020000,
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include <boost/beast/core/string.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -166,8 +165,6 @@ private:
|
||||
beast::severities::Severity thresh_;
|
||||
File file_;
|
||||
bool silent_ = false;
|
||||
using Transform = std::function<std::string(std::string const&)>;
|
||||
Transform transform_;
|
||||
|
||||
public:
|
||||
Logs(beast::severities::Severity level);
|
||||
@@ -206,28 +203,6 @@ public:
|
||||
std::string const& text,
|
||||
bool console);
|
||||
|
||||
/** Rewrite test log lines. Not synchronized.
|
||||
*
|
||||
* Test-only. Installed once for the life of Logs. Empty is a null check.
|
||||
*/
|
||||
void
|
||||
setTransform(Transform fn)
|
||||
{
|
||||
transform_ = std::move(fn);
|
||||
}
|
||||
|
||||
bool
|
||||
hasTransform() const
|
||||
{
|
||||
return static_cast<bool>(transform_);
|
||||
}
|
||||
|
||||
std::string
|
||||
applyTransform(std::string const& text) const
|
||||
{
|
||||
return transform_ ? transform_(text) : text;
|
||||
}
|
||||
|
||||
std::string
|
||||
rotate();
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#define uint256 std::string
|
||||
#define featureHooksUpdate1 "1"
|
||||
#define featureHooksUpdate2 "1"
|
||||
#define featureExport "1"
|
||||
#define featureConsensusEntropy "1"
|
||||
#define fix20250131 "1"
|
||||
#define fixGuardDepth32 "1"
|
||||
namespace hook_api {
|
||||
@@ -384,7 +386,10 @@ enum class hook_return_code : int64_t {
|
||||
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state
|
||||
// entires in the combined hook chains
|
||||
TOO_MANY_NAMESPACES = -45
|
||||
TOO_MANY_NAMESPACES = -45,
|
||||
EXPORT_FAILURE = -46,
|
||||
TOO_MANY_EXPORTED_TXN = -47,
|
||||
TOO_LITTLE_ENTROPY = -48,
|
||||
};
|
||||
|
||||
enum class ExitType : uint8_t {
|
||||
@@ -398,6 +403,7 @@ const uint16_t max_state_modifications = 256;
|
||||
const uint8_t max_slots = 255;
|
||||
const uint8_t max_nonce = 255;
|
||||
const uint8_t max_emit = 255;
|
||||
const uint8_t max_export = 2;
|
||||
const uint8_t max_params = 16;
|
||||
const double fee_base_multiplier = 1.1f;
|
||||
|
||||
@@ -438,10 +444,6 @@ getImportWhitelist(Rules const& rules)
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef I32
|
||||
#undef I64
|
||||
|
||||
enum GuardRulesVersion : uint64_t {
|
||||
GuardRuleFix20250131 = 0x00000001,
|
||||
GuardRuleDepth32 = 0x00000002,
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
Canonical Hook artifact classification and QuickJS deployment envelope.
|
||||
|
||||
Legacy C Hooks remain raw WebAssembly. QuickJS bytecode is never deployed
|
||||
raw: it is carried in a fixed-width, big-endian XQJS envelope whose exact
|
||||
bytes participate in HookHash.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef XRPL_HOOK_HOOKARTIFACT_H_INCLUDED
|
||||
#define XRPL_HOOK_HOOKARTIFACT_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Expected.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace hook::artifact {
|
||||
|
||||
inline constexpr std::size_t identitySize = 32;
|
||||
inline constexpr std::size_t quickJSHeaderSize = 80;
|
||||
inline constexpr std::size_t maxCreateCodeSize = 65'535;
|
||||
inline constexpr std::array<std::uint8_t, 4> quickJSMagic = {
|
||||
'X',
|
||||
'Q',
|
||||
'J',
|
||||
'S'};
|
||||
inline constexpr std::uint8_t quickJSEnvelopeVersion = 1;
|
||||
inline constexpr std::uint8_t quickJSBytecodeKind = 1;
|
||||
|
||||
using Identity = std::array<std::uint8_t, identitySize>;
|
||||
|
||||
enum class Kind : std::uint8_t {
|
||||
legacyWasm,
|
||||
quickJSBytecode,
|
||||
};
|
||||
|
||||
enum class Error : std::uint8_t {
|
||||
tooLarge,
|
||||
tooShort,
|
||||
unknownMagic,
|
||||
truncatedQuickJSHeader,
|
||||
unsupportedEnvelopeVersion,
|
||||
unsupportedArtifactKind,
|
||||
nonCanonicalHeaderSize,
|
||||
nonZeroReserved,
|
||||
emptyPayload,
|
||||
lengthMismatch,
|
||||
zeroBytecodeABI,
|
||||
zeroRuntimeProfile,
|
||||
};
|
||||
|
||||
struct View
|
||||
{
|
||||
Kind kind;
|
||||
std::uint16_t hookApiVersion;
|
||||
Identity bytecodeABI;
|
||||
Identity runtimeProfile;
|
||||
ripple::Slice payload;
|
||||
};
|
||||
|
||||
/** Parse either a legacy raw-Wasm Hook or a canonical QuickJS artifact.
|
||||
|
||||
The returned slices retain no ownership and must not outlive `code`.
|
||||
*/
|
||||
ripple::Expected<View, Error>
|
||||
parse(ripple::Slice code) noexcept;
|
||||
|
||||
std::string_view
|
||||
toString(Error error) noexcept;
|
||||
|
||||
extern Identity const quickJSBytecodeABI;
|
||||
extern Identity const quickJSRuntimeProfile;
|
||||
extern Identity const quickJSProviderSHA256;
|
||||
extern std::size_t const quickJSProviderSize;
|
||||
extern std::uint16_t const quickJSHookApiVersion;
|
||||
extern std::uint64_t const quickJSInitializationFuel;
|
||||
extern std::uint64_t const quickJSInvocationFuel;
|
||||
extern std::string_view const quickJSHostWorkMeter;
|
||||
extern std::uint64_t const quickJSHostWorkBudget;
|
||||
extern std::uint64_t const quickJSHostWorkBasePerCall;
|
||||
extern std::uint64_t const quickJSHostWorkPerAddressedByte;
|
||||
extern std::string_view const quickJSHostAdapterPolicy;
|
||||
extern std::uint32_t const quickJSHeapBytes;
|
||||
extern std::uint32_t const quickJSStackBytes;
|
||||
extern std::uint32_t const quickJSSerializedObjectMaxBytes;
|
||||
extern std::uint32_t const quickJSSerializedObjectMaxFields;
|
||||
extern std::uint32_t const quickJSSerializedObjectMaxScopes;
|
||||
extern std::uint32_t const quickJSSerializedObjectMaxDepth;
|
||||
extern std::uint32_t const quickJSProviderMemoryMinimumPages;
|
||||
extern std::uint32_t const quickJSProviderMemoryMaximumPages;
|
||||
extern std::uint32_t const quickJSProviderWasmStackBytes;
|
||||
|
||||
bool
|
||||
isCurrentQuickJS(View const& artifact) noexcept;
|
||||
|
||||
} // namespace hook::artifact
|
||||
|
||||
#endif // XRPL_HOOK_HOOKARTIFACT_H_INCLUDED
|
||||
@@ -91,6 +91,83 @@
|
||||
|
||||
#define UNSIGNED_TYPE(T) std::make_unsigned_t<T>
|
||||
|
||||
#define DECLARE_HOOK_FUNCTION(R, F, ...) \
|
||||
std::variant<UNSIGNED_TYPE(R), hook_api::hook_return_code> F( \
|
||||
hook::HookContext& hookCtx, \
|
||||
WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \
|
||||
COMMA __VA_ARGS__)); \
|
||||
extern WasmEdge_Result WasmFunction##F( \
|
||||
void* data_ptr, \
|
||||
const WasmEdge_CallingFrameContext* frameCtx, \
|
||||
const WasmEdge_Value* in, \
|
||||
WasmEdge_Value* out); \
|
||||
extern WasmEdge_ValType WasmFunctionParams##F[]; \
|
||||
extern WasmEdge_ValType WasmFunctionResult##F[]; \
|
||||
extern WasmEdge_FunctionTypeContext* WasmFunctionType##F; \
|
||||
extern WasmEdge_String WasmFunctionName##F;
|
||||
|
||||
#define DEFINE_HOOK_FUNCTION(R, F, ...) \
|
||||
WasmEdge_Result hook_api::WasmFunction##F( \
|
||||
void* data_ptr, \
|
||||
const WasmEdge_CallingFrameContext* frameCtx, \
|
||||
const WasmEdge_Value* in, \
|
||||
WasmEdge_Value* out) \
|
||||
{ \
|
||||
__VA_OPT__(int _stack = 0;) \
|
||||
__VA_OPT__(FOR_VARS(VAR_ASSIGN, 2, __VA_ARGS__);) \
|
||||
hook::HookContext* hookCtx = \
|
||||
reinterpret_cast<hook::HookContext*>(data_ptr); \
|
||||
auto const& return_code = hook_api::F( \
|
||||
*hookCtx, \
|
||||
*const_cast<WasmEdge_CallingFrameContext*>(frameCtx) \
|
||||
__VA_OPT__(COMMA STRIP_TYPES(__VA_ARGS__))); \
|
||||
if (std::holds_alternative<hook_api::hook_return_code>(return_code) && \
|
||||
(std::get<hook_api::hook_return_code>(return_code) == \
|
||||
RC_ROLLBACK || \
|
||||
std::get<hook_api::hook_return_code>(return_code) == RC_ACCEPT)) \
|
||||
return WasmEdge_Result_Terminate; \
|
||||
out[0] = RET_ASSIGN( \
|
||||
R, \
|
||||
std::holds_alternative<UNSIGNED_TYPE(R)>(return_code) \
|
||||
? std::get<UNSIGNED_TYPE(R)>(return_code) \
|
||||
: R(std::get<hook_api::hook_return_code>(return_code))); \
|
||||
return WasmEdge_Result_Success; \
|
||||
}; \
|
||||
WasmEdge_ValType hook_api::WasmFunctionParams##F[] = { \
|
||||
__VA_OPT__(FOR_VARS(WASM_VAL_TYPE, 0, __VA_ARGS__))}; \
|
||||
WasmEdge_ValType hook_api::WasmFunctionResult##F[1] = { \
|
||||
WASM_VAL_TYPE(R, dummy)}; \
|
||||
WasmEdge_FunctionTypeContext* hook_api::WasmFunctionType##F = \
|
||||
WasmEdge_FunctionTypeCreate( \
|
||||
WasmFunctionParams##F, \
|
||||
VA_NARGS(NULL __VA_OPT__(, __VA_ARGS__)), \
|
||||
WasmFunctionResult##F, \
|
||||
1); \
|
||||
WasmEdge_String hook_api::WasmFunctionName##F = \
|
||||
WasmEdge_StringCreateByCString(#F); \
|
||||
std::variant<UNSIGNED_TYPE(R), hook_api::hook_return_code> hook_api::F( \
|
||||
hook::HookContext& hookCtx, \
|
||||
WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \
|
||||
COMMA __VA_ARGS__))
|
||||
|
||||
#define HOOK_SETUP() \
|
||||
using enum hook_api::hook_return_code; \
|
||||
try \
|
||||
{ \
|
||||
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
|
||||
[[maybe_unused]] auto& view = applyCtx.view(); \
|
||||
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
|
||||
[[maybe_unused]] WasmEdge_MemoryInstanceContext* memoryCtx = \
|
||||
WasmEdge_CallingFrameGetMemoryInstance(&frameCtx, 0); \
|
||||
[[maybe_unused]] unsigned char* memory = \
|
||||
WasmEdge_MemoryInstanceGetPointer(memoryCtx, 0, 0); \
|
||||
[[maybe_unused]] const uint64_t memory_length = \
|
||||
WasmEdge_MemoryInstanceGetPageSize(memoryCtx) * \
|
||||
WasmEdge_kPageSize; \
|
||||
[[maybe_unused]] auto& api = hookCtx.api(); \
|
||||
if (!memoryCtx || !memory || !memory_length) \
|
||||
return INTERNAL_ERROR;
|
||||
|
||||
#define HOOK_TEARDOWN() \
|
||||
} \
|
||||
catch (const std::exception& e) \
|
||||
@@ -101,6 +178,54 @@
|
||||
return INTERNAL_ERROR; \
|
||||
}
|
||||
|
||||
#define WRITE_WASM_MEMORY( \
|
||||
bytes_written, \
|
||||
guest_dst_ptr, \
|
||||
guest_dst_len, \
|
||||
host_src_ptr, \
|
||||
host_src_len, \
|
||||
host_memory_ptr, \
|
||||
guest_memory_length) \
|
||||
{ \
|
||||
int64_t bytes_to_write = std::min( \
|
||||
static_cast<int64_t>(host_src_len), \
|
||||
static_cast<int64_t>(guest_dst_len)); \
|
||||
if (guest_dst_ptr + bytes_to_write > guest_memory_length) \
|
||||
{ \
|
||||
JLOG(j.warn()) << "HookError[" << HC_ACC() << "]: " << __func__ \
|
||||
<< " tried to retreive blob of " << host_src_len \
|
||||
<< " bytes past end of wasm memory"; \
|
||||
return OUT_OF_BOUNDS; \
|
||||
} \
|
||||
if (!WasmEdge_ResultOK(WasmEdge_MemoryInstanceSetData( \
|
||||
memoryCtx, \
|
||||
reinterpret_cast<const uint8_t*>(host_src_ptr), \
|
||||
guest_dst_ptr, \
|
||||
bytes_to_write))) \
|
||||
return INTERNAL_ERROR; \
|
||||
bytes_written += bytes_to_write; \
|
||||
}
|
||||
|
||||
#define WRITE_WASM_MEMORY_AND_RETURN( \
|
||||
guest_dst_ptr, \
|
||||
guest_dst_len, \
|
||||
host_src_ptr, \
|
||||
host_src_len, \
|
||||
host_memory_ptr, \
|
||||
guest_memory_length) \
|
||||
{ \
|
||||
uint64_t bytes_written = 0; \
|
||||
WRITE_WASM_MEMORY( \
|
||||
bytes_written, \
|
||||
guest_dst_ptr, \
|
||||
guest_dst_len, \
|
||||
host_src_ptr, \
|
||||
host_src_len, \
|
||||
host_memory_ptr, \
|
||||
guest_memory_length); \
|
||||
return bytes_written; \
|
||||
}
|
||||
|
||||
// ptr = pointer inside the wasm memory space
|
||||
#define NOT_IN_BOUNDS(ptr, len, memory_length) \
|
||||
((static_cast<uint64_t>(ptr) >= static_cast<uint64_t>(memory_length)) || \
|
||||
|
||||
@@ -372,3 +372,33 @@ HOOK_API_DEFINITION(
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
featureHooksUpdate2)
|
||||
|
||||
// int64_t xport_reserve(uint32_t count);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xport_reserve, (uint32_t),
|
||||
featureExport)
|
||||
|
||||
// int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t committee_hash_ptr, uint32_t committee_hash_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xport, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
featureExport)
|
||||
|
||||
// int64_t xport_cancel(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xport_cancel, (uint32_t, uint32_t, uint32_t),
|
||||
featureExport)
|
||||
|
||||
// int64_t entropy_cr_dice(uint32_t sides, uint32_t min_tier);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, entropy_cr_dice, (uint32_t, uint32_t),
|
||||
featureConsensusEntropy)
|
||||
|
||||
// int64_t entropy_cr_random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, entropy_cr_random, (uint32_t, uint32_t, uint32_t),
|
||||
featureConsensusEntropy)
|
||||
|
||||
// int64_t entropy_cr_status(void);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, entropy_cr_status, (),
|
||||
featureConsensusEntropy)
|
||||
|
||||
2
include/xrpl/proto/.clang-format
Normal file
2
include/xrpl/proto/.clang-format
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
DisableFormat: true
|
||||
@@ -27,6 +27,7 @@ enum MessageType
|
||||
mtREPLAY_DELTA_RESPONSE = 60;
|
||||
mtHAVE_TRANSACTIONS = 63;
|
||||
mtTRANSACTIONS = 64;
|
||||
mtEXPORT_SHARES = 65;
|
||||
}
|
||||
|
||||
// token, iterations, target, challenge = issue demand for proof of work
|
||||
@@ -36,6 +37,7 @@ enum MessageType
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/* Provides the current ephemeral key for a validator. */
|
||||
//@@start opaque-self-verifying-overlay-artifact-precedent
|
||||
message TMManifest
|
||||
{
|
||||
// A Manifest object in the Ripple serialization format.
|
||||
@@ -49,6 +51,7 @@ message TMManifests
|
||||
// The manifests sent when a peer first connects to another peer are `history`.
|
||||
optional bool history = 2 [deprecated=true];
|
||||
}
|
||||
//@@end opaque-self-verifying-overlay-artifact-precedent
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -118,6 +121,15 @@ message TMTransactions
|
||||
repeated TMTransaction transactions = 1;
|
||||
}
|
||||
|
||||
// Canonical post-validation Export signature contributions. Receivers enforce
|
||||
// ExportLimits caps for entry count, each frame, aggregate frame payload, and
|
||||
// encoded protobuf size. The encoded-size cap includes protobuf tags and
|
||||
// length prefixes but excludes the overlay message header.
|
||||
message TMExportShares
|
||||
{
|
||||
repeated bytes shares = 1;
|
||||
}
|
||||
|
||||
|
||||
enum NodeStatus
|
||||
{
|
||||
@@ -153,7 +165,11 @@ message TMStatusChange
|
||||
message TMProposeSet
|
||||
{
|
||||
required uint32 proposeSeq = 1;
|
||||
required bytes currentTxHash = 2; // the hash of the ledger we are proposing
|
||||
// Proposed transaction-set identity. Legacy/plain proposals carry the
|
||||
// tx-set hash directly; ConsensusExtensions proposals carry a serialized
|
||||
// ExtendedPosition whose first field is that tx-set hash, followed by
|
||||
// signed RNG/Export sidecar fields.
|
||||
required bytes currentTxHash = 2;
|
||||
required bytes nodePubKey = 3;
|
||||
required uint32 closeTime = 4;
|
||||
required bytes signature = 5; // signature of above fields
|
||||
@@ -166,6 +182,16 @@ message TMProposeSet
|
||||
|
||||
// Number of hops traveled
|
||||
optional uint32 hops = 12 [deprecated=true];
|
||||
|
||||
//@@start proposal-export-share-container-binding
|
||||
// Canonical post-validation ExportShare frames authored by the proposing
|
||||
// validator and re-advertised for anti-entropy. The proposal's
|
||||
// ExtendedPosition includes a digest of this repeated field, so the exact
|
||||
// frames are covered by the proposal signature. Each frame carries its
|
||||
// version, owner, origin transaction, origin ledger sequence and hash,
|
||||
// committee position, signing key, and multisign signature.
|
||||
repeated bytes exportSignatures = 13;
|
||||
//@@end proposal-export-share-container-binding
|
||||
}
|
||||
|
||||
enum TxSetStatus
|
||||
@@ -384,4 +410,3 @@ message TMHaveTransactions
|
||||
{
|
||||
repeated bytes hashes = 1;
|
||||
}
|
||||
|
||||
|
||||
62
include/xrpl/protocol/EntropyTier.h
Normal file
62
include/xrpl/protocol/EntropyTier.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#ifndef RIPPLE_PROTOCOL_ENTROPY_TIER_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_ENTROPY_TIER_H_INCLUDED
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/// Which gate the ledger's entropy passed. Stored in sfEntropyTier (UINT8)
|
||||
/// on the ttCONSENSUS_ENTROPY pseudo-transaction and the ConsensusEntropy
|
||||
/// ledger entry.
|
||||
///
|
||||
/// EntropyCount says how many validators contributed; EntropyDenominator says
|
||||
/// how many active validators were in the ledger-anchored view for that
|
||||
/// non-fallback result; EntropyTier says which gate the result passed. Fallback
|
||||
/// entropy carries count=0/denominator=0 because no validator-derived
|
||||
/// denominator was accepted. Tier values are strength-ordered so consumers can
|
||||
/// gate with a numeric comparison (tier >= required).
|
||||
///
|
||||
/// RESIDUAL BIAS — applies to fallback, participant_aligned, and
|
||||
/// validator_quorum. This is a commit/reveal scheme: a validator can withhold
|
||||
/// its reveal until after observing peers' reveals, choosing between two
|
||||
/// outcomes (its contribution in vs. out) — up to one bit of influence per
|
||||
/// withholder, and colluding withholders near a threshold can instead force a
|
||||
/// downgrade to a lower tier. These tiers bound and *label* manipulation (it is
|
||||
/// observable and limited); they are NOT bias-resistant against a colluding
|
||||
/// validator minority. A hook that requires validator_full fails closed if any
|
||||
/// active validator withholds, trading availability for no selective-
|
||||
/// withholding slack.
|
||||
enum EntropyTier : std::uint8_t {
|
||||
/// No usable entropy (reserved; a fresh ConsensusEntropy entry should
|
||||
/// always carry one of the tiers below).
|
||||
entropyTierNone = 0,
|
||||
|
||||
/// Consensus-bound deterministic fallback: derived from already-agreed
|
||||
/// round inputs (parent ledger hash, base tx set hash, sequence) under
|
||||
/// HashPrefix::entropyFallback when no agreed reveal set reaches either
|
||||
/// participant_aligned or validator_quorum. Unpredictable in practice but
|
||||
/// user-influenceable via transaction submission — never suitable for
|
||||
/// value-bearing outcomes.
|
||||
entropyTierConsensusFallback = 1,
|
||||
|
||||
/// Participant-aligned sub-quorum entropy: the agreed reveal set aligned at
|
||||
/// the tier-2 participant threshold — below the 80% validator quorum but at
|
||||
/// or above the equivocation-intersection floor over the original
|
||||
/// (pre-nUNL)
|
||||
/// view. Weaker than validator_quorum; opt-in for hooks via min_tier.
|
||||
entropyTierParticipantAligned = 2,
|
||||
|
||||
/// Validator commit/reveal entropy whose sidecar set passed the
|
||||
/// active-validator-view quorum alignment gate.
|
||||
entropyTierValidatorQuorum = 3,
|
||||
|
||||
/// Validator commit/reveal entropy with reveals from every validator in
|
||||
/// the ledger-anchored active view. Any missing active validator downgrades
|
||||
/// the tier, so hooks can require this to fail closed on selective
|
||||
/// withholding.
|
||||
entropyTierValidatorFull = 4,
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
131
include/xrpl/protocol/ExportCommittee.h
Normal file
131
include/xrpl/protocol/ExportCommittee.h
Normal file
@@ -0,0 +1,131 @@
|
||||
#ifndef RIPPLE_PROTOCOL_EXPORTCOMMITTEE_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_EXPORTCOMMITTEE_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** One canonical, network-neutral Export validator committee. */
|
||||
struct ExportCommitteeProfile
|
||||
{
|
||||
std::vector<PublicKey> members;
|
||||
std::size_t quorum;
|
||||
|
||||
std::optional<std::uint16_t>
|
||||
position(PublicKey const& master) const
|
||||
{
|
||||
auto const it =
|
||||
std::lower_bound(members.begin(), members.end(), master);
|
||||
if (it == members.end() || *it != master)
|
||||
return std::nullopt;
|
||||
return static_cast<std::uint16_t>(std::distance(members.begin(), it));
|
||||
}
|
||||
};
|
||||
|
||||
/** Parse a strictly sorted sequence of compressed validator master keys. */
|
||||
inline std::optional<ExportCommitteeProfile>
|
||||
resolveExportCommittee(Slice roster)
|
||||
{
|
||||
constexpr std::size_t publicKeyBytes = 33;
|
||||
if (roster.empty() || roster.size() % publicKeyBytes != 0)
|
||||
return std::nullopt;
|
||||
|
||||
auto const count = roster.size() / publicKeyBytes;
|
||||
if (count == 0 || count > ExportLimits::maxCommitteeMembers)
|
||||
return std::nullopt;
|
||||
|
||||
std::vector<PublicKey> members;
|
||||
members.reserve(count);
|
||||
for (std::size_t offset = 0; offset < roster.size();
|
||||
offset += publicKeyBytes)
|
||||
{
|
||||
Slice const encoded{roster.data() + offset, publicKeyBytes};
|
||||
if (!publicKeyType(encoded))
|
||||
return std::nullopt;
|
||||
|
||||
PublicKey member{encoded};
|
||||
if (!members.empty() && !(members.back() < member))
|
||||
return std::nullopt;
|
||||
members.push_back(std::move(member));
|
||||
}
|
||||
|
||||
return ExportCommitteeProfile{
|
||||
std::move(members), ExportLimits::committeeQuorumThreshold(count)};
|
||||
}
|
||||
|
||||
inline Blob
|
||||
serializeExportCommittee(std::vector<PublicKey> members)
|
||||
{
|
||||
if (members.empty() || members.size() > ExportLimits::maxCommitteeMembers)
|
||||
return {};
|
||||
|
||||
std::sort(members.begin(), members.end());
|
||||
if (std::adjacent_find(members.begin(), members.end()) != members.end())
|
||||
return {};
|
||||
|
||||
Blob roster;
|
||||
roster.reserve(members.size() * 33);
|
||||
for (auto const& member : members)
|
||||
roster.insert(roster.end(), member.begin(), member.end());
|
||||
return roster;
|
||||
}
|
||||
|
||||
/** Canonicalize an unordered transaction roster into strict stored form. */
|
||||
inline std::optional<Blob>
|
||||
canonicalizeExportCommittee(Slice roster)
|
||||
{
|
||||
constexpr std::size_t publicKeyBytes = 33;
|
||||
if (roster.empty() || roster.size() % publicKeyBytes != 0)
|
||||
return std::nullopt;
|
||||
|
||||
auto const count = roster.size() / publicKeyBytes;
|
||||
if (count == 0 || count > ExportLimits::maxCommitteeMembers)
|
||||
return std::nullopt;
|
||||
|
||||
std::vector<PublicKey> members;
|
||||
members.reserve(count);
|
||||
for (std::size_t offset = 0; offset < roster.size();
|
||||
offset += publicKeyBytes)
|
||||
{
|
||||
Slice const encoded{roster.data() + offset, publicKeyBytes};
|
||||
if (!publicKeyType(encoded))
|
||||
return std::nullopt;
|
||||
members.emplace_back(encoded);
|
||||
}
|
||||
|
||||
auto canonical = serializeExportCommittee(std::move(members));
|
||||
if (canonical.empty())
|
||||
return std::nullopt;
|
||||
return canonical;
|
||||
}
|
||||
|
||||
/** Content identity of one already-validated canonical roster. */
|
||||
inline uint256
|
||||
exportCommitteeHash(Slice roster)
|
||||
{
|
||||
auto const profile = resolveExportCommittee(roster);
|
||||
if (!profile)
|
||||
return {};
|
||||
|
||||
Serializer serialized;
|
||||
serialized.add32(HashPrefix::exportCommittee);
|
||||
serialized.add32(static_cast<std::uint32_t>(profile->members.size()));
|
||||
serialized.addRaw(roster);
|
||||
return serialized.getSHA512Half();
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
119
include/xrpl/protocol/ExportLimits.h
Normal file
119
include/xrpl/protocol/ExportLimits.h
Normal file
@@ -0,0 +1,119 @@
|
||||
#ifndef RIPPLE_PROTOCOL_EXPORT_LIMITS_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_EXPORT_LIMITS_H_INCLUDED
|
||||
|
||||
#include <xrpl/protocol/ValidatorBitset.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// Export system caps.
|
||||
//
|
||||
// These limits bound the DoS surface of the export signature system:
|
||||
// - Each selected validator signs once after exact source validation
|
||||
// - Pending shares are re-advertised through bounded proposal/relay batches
|
||||
// - Inbound signature processing involves crypto verification per sig
|
||||
// - Durable and per-message caps bound scans, crypto, and sidecar leaves
|
||||
struct ExportLimits
|
||||
{
|
||||
// Ordinary XRPL multisigning accepts at most 32 signers. An account that
|
||||
// reserves a separate operator signer must select fewer validators.
|
||||
static constexpr std::size_t maxCommitteeMembers = 32;
|
||||
static constexpr std::size_t maxCommitteeRosterBytes =
|
||||
maxCommitteeMembers * 33;
|
||||
static constexpr std::size_t maxCommitteeContributorBytes =
|
||||
validatorBitsetBytes(maxCommitteeMembers);
|
||||
|
||||
// Export-specific serialized payload bounds. The release target includes
|
||||
// the canonical origin Memo appended after validation; the witness wraps
|
||||
// that target plus the accepted committee signatures.
|
||||
static constexpr std::size_t maxExportReleaseTargetBytes = 2'048;
|
||||
static constexpr std::size_t maxExportWitnessBytes = 8'192;
|
||||
|
||||
// V1 witnesses require the standard 80% quorum of the intent-selected
|
||||
// committee, rounded up. The intent selects members but cannot lower this
|
||||
// threshold. Split quotient/remainder arithmetic avoids overflow while
|
||||
// preserving ceil(memberCount * 0.8).
|
||||
static constexpr std::size_t
|
||||
committeeQuorumThreshold(std::size_t memberCount)
|
||||
{
|
||||
auto const quotient = memberCount / 5;
|
||||
auto const remainder = memberCount % 5;
|
||||
return quotient * 4 + (remainder * 4 + 4) / 5;
|
||||
}
|
||||
|
||||
// Maximum exports a single hook execution may produce. Hook API ABI
|
||||
// constant hook_api::max_export must stay equal.
|
||||
static constexpr std::uint8_t maxExportsPerHook = 2;
|
||||
|
||||
// Maximum Export intents admitted in one ledger and maximum live latches
|
||||
// owned by one account.
|
||||
static constexpr std::uint8_t maxPendingExports = 8;
|
||||
|
||||
// Global pending-latch and per-validation scan bound. Witnessed or canceled
|
||||
// latches release this signing-work slot while their account owner count
|
||||
// and reserve continue to bound retained state. This is a provisional
|
||||
// activation tuning value.
|
||||
static constexpr std::uint16_t maxLiveExportLatches = 64;
|
||||
|
||||
// Maximum admission window requested through the mandatory outer
|
||||
// LastLedgerSequence. This bounds how long an Export may remain queued
|
||||
// before entering a ledger; it does not bound post-validation release.
|
||||
static constexpr std::uint32_t maxAdmissionWindowLedgers = 5;
|
||||
|
||||
// Fixed source-ledger window for post-validation share publication and
|
||||
// witness materialization, measured from the ledger that admits the
|
||||
// intent. This is a provisional activation tuning value. Keeping it
|
||||
// separate from the outer LastLedgerSequence prevents queue delay from
|
||||
// consuming the publication window.
|
||||
static constexpr std::uint32_t maxPublicationLedgers = 5;
|
||||
|
||||
// A fully-canonical secp256k1 signature is at most 72 bytes; Ed25519 is 64.
|
||||
static constexpr std::size_t maxCanonicalExportSignatureBytes = 72;
|
||||
|
||||
// Export-signature sidecar leaves encode an origin, committee position,
|
||||
// signing key, and signature in an STObject envelope. Keep this comfortably
|
||||
// above the canonical encoding while bounding peer-supplied leaf bytes
|
||||
// before parse/hash work.
|
||||
static constexpr std::size_t maxExportSignatureSidecarBytes = 256;
|
||||
|
||||
// Post-validation relay framing. Values are deliberately conservative
|
||||
// local tuning knobs and require measurement before activation; changing
|
||||
// them does not change the canonical per-share format.
|
||||
// version + AccountID + 2 hashes + ledger sequence + committee position +
|
||||
// compressed public key + one-byte VL prefix + maximum signature.
|
||||
static constexpr std::size_t maxSerializedExportShareBytes =
|
||||
1 + 20 + 32 + 4 + 32 + 2 + 33 + 1 + maxCanonicalExportSignatureBytes;
|
||||
static constexpr std::size_t maxExportSharesPerRelay = 32;
|
||||
static constexpr std::size_t maxExportShareRelayPayloadBytes =
|
||||
maxSerializedExportShareBytes * maxExportSharesPerRelay;
|
||||
// Protobuf repeated-bytes framing is one tag byte plus a two-byte varint
|
||||
// length for every maximum-size frame. This excludes the overlay header.
|
||||
static constexpr std::size_t maxExportShareRelayMessageBytes =
|
||||
maxExportShareRelayPayloadBytes + 3 * maxExportSharesPerRelay;
|
||||
|
||||
// Provisional activation fee schedule. These values intentionally price
|
||||
// Export intents above ordinary transactions until production measurements
|
||||
// can replace the conservative work and storage estimates.
|
||||
//
|
||||
// Witness bytes are estimated from the exact serialized target size, a
|
||||
// fixed allowance for the witness envelope/origin stamp/contributor mask,
|
||||
// and one bounded signer entry for every selected member. The accepted
|
||||
// sidecar witness retains every valid committee contribution, not merely
|
||||
// the minimum quorum subset.
|
||||
static constexpr std::uint64_t feeWitnessFixedAllowanceBytes = 384;
|
||||
static constexpr std::uint64_t feeWitnessSignerAllowanceBytes = 128;
|
||||
static constexpr std::uint64_t feeSharePublicationRounds =
|
||||
maxPublicationLedgers + 1;
|
||||
static constexpr std::uint64_t feeWorkUnitsPerSharePublication = 1;
|
||||
static constexpr std::uint64_t feeWorkUnitsPerWitnessSignature = 2;
|
||||
static constexpr std::uint64_t feeWitnessChunkBytes = 256;
|
||||
static constexpr std::uint64_t feeWorkUnitsPerWitnessChunk = 1;
|
||||
static constexpr std::uint64_t feePermanentWitnessByteDrops = 1;
|
||||
static constexpr std::uint64_t feeSurchargeRoundDrops = 1'000;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
104
include/xrpl/protocol/ExportOriginMemo.h
Normal file
104
include/xrpl/protocol/ExportOriginMemo.h
Normal file
@@ -0,0 +1,104 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_EXPORTORIGINMEMO_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_EXPORTORIGINMEMO_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Expected.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace ripple::ExportOriginMemo {
|
||||
|
||||
inline constexpr std::string_view memoType = "xahau/export";
|
||||
inline constexpr std::uint8_t version = 1;
|
||||
inline constexpr std::uint8_t allowedFlags = 0;
|
||||
inline constexpr std::size_t identityBytes = 42;
|
||||
inline constexpr std::size_t releaseBytes = 78;
|
||||
|
||||
struct Origin
|
||||
{
|
||||
std::uint32_t sourceDomain;
|
||||
std::uint32_t targetDomain;
|
||||
uint256 transactionHash;
|
||||
|
||||
bool
|
||||
operator==(Origin const&) const = default;
|
||||
};
|
||||
|
||||
struct Anchor
|
||||
{
|
||||
LedgerIndex ledgerSequence;
|
||||
uint256 ledgerHash;
|
||||
|
||||
bool
|
||||
operator==(Anchor const&) const = default;
|
||||
};
|
||||
|
||||
struct Stamp
|
||||
{
|
||||
Origin origin;
|
||||
std::optional<Anchor> anchor;
|
||||
|
||||
bool
|
||||
operator==(Stamp const&) const = default;
|
||||
};
|
||||
|
||||
enum class Error {
|
||||
reservedMemoPresent,
|
||||
reservedMemoMissing,
|
||||
reservedMemoPosition,
|
||||
malformedMemo,
|
||||
unsupportedVersion,
|
||||
unsupportedFlags,
|
||||
localChecks
|
||||
};
|
||||
|
||||
/** Return true when any Memo reserves the Export protocol MemoType. */
|
||||
bool
|
||||
hasReservedMemo(STTx const& tx);
|
||||
|
||||
/** Append the canonical identity projection as the final Memo.
|
||||
|
||||
Existing Memos remain byte-identical and in their original order. This
|
||||
function does not normalize transaction authorization fields; callers must
|
||||
supply the canonical unsigned multisign base.
|
||||
*/
|
||||
Expected<STTx, Error>
|
||||
identityForm(STTx const& base, Origin const& origin);
|
||||
|
||||
/** Append the canonical release projection as the final Memo. */
|
||||
Expected<STTx, Error>
|
||||
releaseForm(STTx const& base, Origin const& origin, Anchor const& anchor);
|
||||
|
||||
/** Parse one canonical final Export Memo. */
|
||||
Expected<Stamp, Error>
|
||||
parse(STTx const& tx);
|
||||
|
||||
/** Replace a canonical release projection with its identity projection. */
|
||||
Expected<STTx, Error>
|
||||
projectIdentity(STTx const& stamped);
|
||||
|
||||
} // namespace ripple::ExportOriginMemo
|
||||
|
||||
#endif
|
||||
142
include/xrpl/protocol/ExportShare.h
Normal file
142
include/xrpl/protocol/ExportShare.h
Normal file
@@ -0,0 +1,142 @@
|
||||
#ifndef RIPPLE_PROTOCOL_EXPORTSHARE_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_EXPORTSHARE_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Canonical post-validation Export signature contribution.
|
||||
|
||||
The multisign signature authenticates the derived destination transaction.
|
||||
The remaining fields are bounded lookup and release context which live
|
||||
admission checks against the origin latch and local validated chain.
|
||||
*/
|
||||
struct ExportShare
|
||||
{
|
||||
static constexpr std::uint8_t currentVersion = 1;
|
||||
|
||||
std::uint8_t version{currentVersion};
|
||||
AccountID owner;
|
||||
uint256 originTxn;
|
||||
LedgerIndex originLedgerSeq{0};
|
||||
uint256 originLedgerHash;
|
||||
std::uint16_t committeePosition{0};
|
||||
PublicKey signingKey;
|
||||
Buffer signature;
|
||||
|
||||
bool
|
||||
hasCanonicalSignature() const
|
||||
{
|
||||
auto const keyType = publicKeyType(signingKey);
|
||||
if (!keyType)
|
||||
return false;
|
||||
|
||||
auto const sig = Slice{signature.data(), signature.size()};
|
||||
if (*keyType == KeyType::secp256k1)
|
||||
{
|
||||
auto const canonicality = ecdsaCanonicality(sig);
|
||||
return canonicality &&
|
||||
*canonicality == ECDSACanonicality::fullyCanonical;
|
||||
}
|
||||
|
||||
// Full Ed25519 canonicality is enforced by cryptographic admission.
|
||||
// The framing layer can still reject every impossible wire length.
|
||||
return *keyType == KeyType::ed25519 && signature.size() == 64;
|
||||
}
|
||||
|
||||
bool
|
||||
validShape() const
|
||||
{
|
||||
return version == currentVersion && owner != beast::zero &&
|
||||
!originTxn.isZero() && originLedgerSeq != 0 &&
|
||||
!originLedgerHash.isZero() &&
|
||||
committeePosition < ExportLimits::maxCommitteeMembers &&
|
||||
hasCanonicalSignature();
|
||||
}
|
||||
|
||||
Serializer
|
||||
serialize() const
|
||||
{
|
||||
if (!validShape())
|
||||
throw std::invalid_argument("invalid ExportShare shape");
|
||||
|
||||
Serializer result;
|
||||
result.add8(version);
|
||||
result.addBitString(owner);
|
||||
result.addBitString(originTxn);
|
||||
result.add32(originLedgerSeq);
|
||||
result.addBitString(originLedgerHash);
|
||||
result.add16(committeePosition);
|
||||
result.addRaw(signingKey.slice());
|
||||
result.addVL(signature);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint256
|
||||
wireHash() const
|
||||
{
|
||||
// Raw-wire suppression only. Routing context such as committeePosition
|
||||
// is checked against validated state before relay and is not
|
||||
// authenticated by the destination multisignature.
|
||||
auto const bytes = serialize();
|
||||
return sha512Half(bytes.slice());
|
||||
}
|
||||
|
||||
static std::optional<ExportShare>
|
||||
parse(Slice bytes)
|
||||
{
|
||||
if (bytes.empty() ||
|
||||
bytes.size() > ExportLimits::maxSerializedExportShareBytes)
|
||||
return std::nullopt;
|
||||
|
||||
try
|
||||
{
|
||||
SerialIter sit{bytes};
|
||||
auto const version = sit.get8();
|
||||
auto const owner = sit.getBitString<160, detail::AccountIDTag>();
|
||||
auto const originTxn = sit.get256();
|
||||
auto const originLedgerSeq = sit.get32();
|
||||
auto const originLedgerHash = sit.get256();
|
||||
auto const committeePosition = sit.get16();
|
||||
auto const keySlice = sit.getSlice(33);
|
||||
if (!publicKeyType(keySlice))
|
||||
return std::nullopt;
|
||||
PublicKey const signingKey{keySlice};
|
||||
auto signature = sit.getVLBuffer();
|
||||
if (!sit.empty())
|
||||
return std::nullopt;
|
||||
|
||||
ExportShare result{
|
||||
version,
|
||||
owner,
|
||||
originTxn,
|
||||
originLedgerSeq,
|
||||
originLedgerHash,
|
||||
committeePosition,
|
||||
signingKey,
|
||||
std::move(signature)};
|
||||
if (!result.validShape())
|
||||
return std::nullopt;
|
||||
return result;
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -96,6 +96,24 @@ enum class HashPrefix : std::uint32_t {
|
||||
|
||||
/** Credentials signature */
|
||||
credential = detail::make_hash_prefix('C', 'R', 'D'),
|
||||
|
||||
/** consensus extension sidecar object */
|
||||
sidecar = detail::make_hash_prefix('S', 'C', 'R'),
|
||||
|
||||
/** consensus diagnostic observed participant set */
|
||||
observedParticipants = detail::make_hash_prefix('O', 'B', 'P'),
|
||||
|
||||
/** consensus-bound fallback entropy digest (Tier 1: derived from
|
||||
already-agreed round inputs when no agreed reveal set reaches an
|
||||
accepted validator-participant tier; never to be confused with
|
||||
validator entropy) */
|
||||
entropyFallback = detail::make_hash_prefix('E', 'F', 'B'),
|
||||
|
||||
/** consensus entropy transaction-ordering salt */
|
||||
entropyTxnOrder = detail::make_hash_prefix('E', 'T', 'O'),
|
||||
|
||||
/** immutable Export committee roster */
|
||||
exportCommittee = detail::make_hash_prefix('E', 'C', 'M'),
|
||||
};
|
||||
|
||||
template <class Hasher>
|
||||
|
||||
@@ -454,6 +454,15 @@ getVLInfo(Json::Value const& xpop, beast::Journal const& j)
|
||||
<< "Import: unl blob was not valid json (after base64 decoding)";
|
||||
return {};
|
||||
}
|
||||
auto const isNonNegativeUInt = [](Json::Value const& value) {
|
||||
return value.isUInt() || (value.isInt() && value.asInt() >= 0);
|
||||
};
|
||||
if (!list.isMember(jss::sequence) ||
|
||||
!isNonNegativeUInt(list[jss::sequence]))
|
||||
{
|
||||
JLOG(j.warn()) << "Import: unl blob sequence was missing or negative";
|
||||
return {};
|
||||
}
|
||||
auto const sequence = list[jss::sequence].asUInt();
|
||||
auto const m = deserializeManifest(base64_decode(
|
||||
xpop[jss::validation][jss::unl][jss::manifest].asString()));
|
||||
|
||||
@@ -62,6 +62,22 @@ emittedDir() noexcept;
|
||||
Keylet
|
||||
emittedTxn(uint256 const& id) noexcept;
|
||||
|
||||
/** Locate an Export latch by its permanent source issuance identity.
|
||||
|
||||
The source Export transaction ID is content-addressed and cannot be reused,
|
||||
including after AccountDelete.
|
||||
*/
|
||||
Keylet
|
||||
exportLatch(AccountID const& account, uint256 const& originTxnHash) noexcept;
|
||||
|
||||
/** Locate an immutable Export committee owned by an account. */
|
||||
Keylet
|
||||
exportCommittee(AccountID const& account, uint256 const& digest) noexcept;
|
||||
|
||||
/** The fixed global directory of Export latches awaiting witness work. */
|
||||
Keylet const&
|
||||
pendingExports() noexcept;
|
||||
|
||||
Keylet
|
||||
hookDefinition(uint256 const& hash) noexcept;
|
||||
|
||||
@@ -118,6 +134,10 @@ negativeUNL() noexcept;
|
||||
Keylet const&
|
||||
UNLReport() noexcept;
|
||||
|
||||
/** The (fixed) index of the object containing consensus-derived entropy. */
|
||||
Keylet const&
|
||||
consensusEntropy() noexcept;
|
||||
|
||||
/** The beginning of an order book */
|
||||
struct book_t
|
||||
{
|
||||
|
||||
@@ -186,6 +186,10 @@ enum LedgerSpecificFlags {
|
||||
// remarks
|
||||
lsfImmutable = 1,
|
||||
|
||||
// ltEXPORT_LATCH
|
||||
lsfExportXpopSeen = 0x00000001,
|
||||
lsfExportCanceled = 0x00000002,
|
||||
|
||||
// ltMPTOKEN_ISSUANCE
|
||||
lsfMPTLocked = 0x00000001, // Also used in ltMPTOKEN
|
||||
lsfMPTCanLock = 0x00000002,
|
||||
|
||||
@@ -43,6 +43,19 @@ std::size_t constexpr txMinSizeBytes = 10;
|
||||
/** Largest legal byte size of a transaction. */
|
||||
std::size_t constexpr txMaxSizeBytes = megabytes(1);
|
||||
|
||||
/** Largest network ID whose transactions retain the legacy canonical form.
|
||||
|
||||
Transactions on these networks must omit sfNetworkID. Transactions on
|
||||
networks with larger IDs must include sfNetworkID matching the network.
|
||||
*/
|
||||
std::uint32_t constexpr maxNetworkIDWithoutTxField = 1024;
|
||||
|
||||
constexpr bool
|
||||
requiresTxNetworkID(std::uint32_t networkID)
|
||||
{
|
||||
return networkID > maxNetworkIDWithoutTxField;
|
||||
}
|
||||
|
||||
/** The maximum number of unfunded offers to delete at once */
|
||||
std::size_t constexpr unfundedOfferRemoveLimit = 1000;
|
||||
|
||||
|
||||
21
include/xrpl/protocol/SidecarType.h
Normal file
21
include/xrpl/protocol/SidecarType.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef RIPPLE_PROTOCOL_SIDECAR_TYPE_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_SIDECAR_TYPE_H_INCLUDED
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/// Discriminator for sidecar set entries (SHAMap leaves used for
|
||||
/// consensus extension data: RNG commit/reveal, export signatures).
|
||||
///
|
||||
/// Stored in sfSidecarType (UINT8) on each STObject entry.
|
||||
/// Makes sidecar sets self-describing — no content-sniffing needed.
|
||||
enum SidecarType : std::uint8_t {
|
||||
sidecarRngCommit = 1,
|
||||
sidecarRngReveal = 2,
|
||||
sidecarExportSig = 3,
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -68,7 +68,10 @@ enum TELcodes : TERUnderlyingType {
|
||||
telNON_LOCAL_EMITTED_TXN,
|
||||
telIMPORT_VL_KEY_NOT_RECOGNISED,
|
||||
telCAN_NOT_QUEUE_IMPORT,
|
||||
// Keep test-only sentinels before appended branch-local TEL codes so their
|
||||
// numeric values do not move.
|
||||
telENV_RPC_FAILED,
|
||||
telEXPORT_LATCH_REQUIRED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -363,6 +366,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecARRAY_TOO_LARGE = 197,
|
||||
tecLOCKED = 198,
|
||||
tecBAD_CREDENTIALS = 199,
|
||||
tecEXPORT_COMMITTEE_UNAVAILABLE = 201,
|
||||
tecLAST_POSSIBLE_ENTRY = 255,
|
||||
};
|
||||
|
||||
|
||||
@@ -274,6 +274,17 @@ enum BridgeModifyFlags : uint32_t {
|
||||
tfClearAccountCreateAmount = 0x00010000,
|
||||
};
|
||||
constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount);
|
||||
|
||||
// Export flags:
|
||||
enum ExportFlags : uint32_t {
|
||||
// Lifecycle control: erase the named latch and forfeit any later callback.
|
||||
tfExportEraseLatch = 0x00010000,
|
||||
// Committee control: erase the named immutable committee object.
|
||||
tfExportEraseCommittee = 0x00020000,
|
||||
};
|
||||
constexpr std::uint32_t tfExportMask =
|
||||
~(tfUniversal | tfExportEraseLatch | tfExportEraseCommittee);
|
||||
|
||||
// clang-format on
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -176,7 +176,6 @@ private:
|
||||
std::optional<STAmount> mDelivered;
|
||||
std::optional<STArray> mHookExecutions;
|
||||
std::optional<STArray> mHookEmissions;
|
||||
|
||||
STArray mNodes;
|
||||
};
|
||||
|
||||
|
||||
158
include/xrpl/protocol/ValidatorBitset.h
Normal file
158
include/xrpl/protocol/ValidatorBitset.h
Normal file
@@ -0,0 +1,158 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright 2026 Xahau
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_VALIDATORBITSET_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_VALIDATORBITSET_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ValidatedValidatorBitset
|
||||
{
|
||||
Blob bitset_;
|
||||
std::size_t memberCount_ = 0;
|
||||
std::size_t selected_ = 0;
|
||||
|
||||
ValidatedValidatorBitset(
|
||||
Slice bitset,
|
||||
std::size_t memberCount,
|
||||
std::size_t selected)
|
||||
: bitset_(bitset.begin(), bitset.end())
|
||||
, memberCount_(memberCount)
|
||||
, selected_(selected)
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
ValidatedValidatorBitset(ValidatedValidatorBitset const&) = default;
|
||||
ValidatedValidatorBitset&
|
||||
operator=(ValidatedValidatorBitset const&) = default;
|
||||
|
||||
ValidatedValidatorBitset(ValidatedValidatorBitset&& other) noexcept
|
||||
: bitset_(std::move(other.bitset_))
|
||||
, memberCount_(other.memberCount_)
|
||||
, selected_(other.selected_)
|
||||
{
|
||||
other.bitset_.clear();
|
||||
other.memberCount_ = 0;
|
||||
other.selected_ = 0;
|
||||
}
|
||||
|
||||
ValidatedValidatorBitset&
|
||||
operator=(ValidatedValidatorBitset&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
bitset_ = std::move(other.bitset_);
|
||||
memberCount_ = other.memberCount_;
|
||||
selected_ = other.selected_;
|
||||
|
||||
other.bitset_.clear();
|
||||
other.memberCount_ = 0;
|
||||
other.selected_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/** Construct an owning, validated validator bitset. */
|
||||
static std::optional<ValidatedValidatorBitset>
|
||||
make(Slice bitset, std::size_t memberCount);
|
||||
|
||||
std::size_t
|
||||
selected() const
|
||||
{
|
||||
return selected_;
|
||||
}
|
||||
|
||||
bool
|
||||
contains(std::size_t position) const
|
||||
{
|
||||
if (position >= memberCount_)
|
||||
return false;
|
||||
|
||||
auto const byte = position / 8;
|
||||
return (bitset_[byte] &
|
||||
static_cast<std::uint8_t>(1u << (position % 8))) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
constexpr std::size_t
|
||||
validatorBitsetBytes(std::size_t memberCount)
|
||||
{
|
||||
return memberCount / 8 + (memberCount % 8 != 0);
|
||||
}
|
||||
|
||||
/** Validate an LSB-first bitset over a fixed canonical validator universe.
|
||||
|
||||
A valid bitset has exactly enough bytes for the universe and leaves every
|
||||
unused high bit in the final byte clear. The returned population can be
|
||||
used by a caller's profile-specific admission and quorum rules.
|
||||
*/
|
||||
inline std::optional<ValidatedValidatorBitset>
|
||||
validateValidatorBitset(Slice bitset, std::size_t memberCount)
|
||||
{
|
||||
return ValidatedValidatorBitset::make(bitset, memberCount);
|
||||
}
|
||||
|
||||
inline std::optional<ValidatedValidatorBitset>
|
||||
ValidatedValidatorBitset::make(Slice bitset, std::size_t memberCount)
|
||||
{
|
||||
if (bitset.size() != validatorBitsetBytes(memberCount))
|
||||
return std::nullopt;
|
||||
|
||||
auto const remainder = memberCount % 8;
|
||||
if (remainder != 0)
|
||||
{
|
||||
auto const allowed = static_cast<std::uint8_t>((1u << remainder) - 1u);
|
||||
if ((bitset[bitset.size() - 1] & static_cast<std::uint8_t>(~allowed)) !=
|
||||
0)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t selected = 0;
|
||||
for (auto const byte : bitset)
|
||||
selected += std::popcount(byte);
|
||||
|
||||
return ValidatedValidatorBitset{bitset, memberCount, selected};
|
||||
}
|
||||
|
||||
template <class Predicate>
|
||||
Blob
|
||||
makeValidatorBitset(std::size_t memberCount, Predicate&& selected)
|
||||
{
|
||||
Blob bitset(validatorBitsetBytes(memberCount), 0);
|
||||
for (std::size_t i = 0; i < memberCount; ++i)
|
||||
{
|
||||
if (selected(i))
|
||||
bitset[i / 8] |= static_cast<std::uint8_t>(1u << (i % 8));
|
||||
}
|
||||
return bitset;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -34,7 +34,6 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(JSHooks, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
|
||||
@@ -66,6 +65,9 @@ XRPL_FEATURE(AMM, Supported::no, VoteBehavior::DefaultNo
|
||||
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(NegativeUNLActiveViewCap, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Export, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConsensusEntropy, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -223,6 +223,23 @@ LEDGER_ENTRY(ltURI_TOKEN, 0x0055, URIToken, uri_token, ({
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** The ledger object which stores consensus-derived entropy.
|
||||
|
||||
\note This is a singleton: only one such object exists in the ledger.
|
||||
|
||||
\sa keylet::consensusEntropy
|
||||
*/
|
||||
LEDGER_ENTRY_DUPLICATE(ltCONSENSUS_ENTROPY, 0x0058, ConsensusEntropy, consensus_entropy, ({
|
||||
{sfDigest, soeREQUIRED},
|
||||
{sfEntropyCount, soeREQUIRED},
|
||||
{sfEntropyDenominator, soeREQUIRED},
|
||||
{sfEntropyContributors, soeREQUIRED},
|
||||
{sfEntropyTier, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** A ledger object which describes an account.
|
||||
|
||||
\sa keylet::account
|
||||
@@ -257,11 +274,14 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
|
||||
{sfImportSequence, soeOPTIONAL},
|
||||
{sfGovernanceFlags, soeOPTIONAL},
|
||||
{sfGovernanceMarks, soeOPTIONAL},
|
||||
//@@start account-index-optional-accountroot-field
|
||||
{sfAccountIndex, soeOPTIONAL},
|
||||
//@@end account-index-optional-accountroot-field
|
||||
{sfTouchCount, soeOPTIONAL},
|
||||
{sfHookStateScale, soeOPTIONAL},
|
||||
{sfCron, soeOPTIONAL},
|
||||
{sfAMMID, soeOPTIONAL},
|
||||
{sfExportCount, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object which contains a list of object identifiers.
|
||||
@@ -277,6 +297,7 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, directory, ({
|
||||
{sfTakerGetsIssuer, soeOPTIONAL}, // order book directories
|
||||
{sfExchangeRate, soeOPTIONAL}, // order book directories
|
||||
{sfReferenceCount, soeOPTIONAL}, // for hook state directories
|
||||
{sfExportCount, soeOPTIONAL}, // live latches on Export root
|
||||
{sfIndexes, soeREQUIRED},
|
||||
{sfRootIndex, soeREQUIRED},
|
||||
{sfIndexNext, soeOPTIONAL},
|
||||
@@ -417,7 +438,9 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({
|
||||
{sfReserveBaseDrops, soeOPTIONAL},
|
||||
{sfReserveIncrementDrops, soeOPTIONAL},
|
||||
{sfXahauActivationLgrSeq, soeOPTIONAL},
|
||||
//@@start account-count-optional-fees-field
|
||||
{sfAccountCount, soeOPTIONAL},
|
||||
//@@end account-count-optional-fees-field
|
||||
{sfNetworkID, soeOPTIONAL},
|
||||
{sfPreviousTxnID, soeOPTIONAL},
|
||||
{sfPreviousTxnLgrSeq, soeOPTIONAL},
|
||||
@@ -592,6 +615,37 @@ LEDGER_ENTRY(ltDID, 0x008D, DID, did, ({
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
//@@start export-latch-ledger-entry
|
||||
/** A live lifecycle latch for an Export intent.
|
||||
|
||||
Created when an Export intent is admitted. It records witness and
|
||||
proof-of-execution arrival in either order and is erased once both facts
|
||||
exist. Account-owned (pays reserve).
|
||||
sfDigest stores the canonical target signing-intent hash.
|
||||
|
||||
\sa keylet::exportLatch
|
||||
*/
|
||||
LEDGER_ENTRY(ltEXPORT_LATCH, 0x5374, ExportLatch, export_latch, ({
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfTicketSequence, soeREQUIRED},
|
||||
{sfTransactionHash, soeOPTIONAL},
|
||||
{sfDigest, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfExportCommitteeHash, soeREQUIRED},
|
||||
{sfExportSignatureHash, soeOPTIONAL},
|
||||
{sfLastLedgerSequence, soeOPTIONAL},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfExportNode, soeOPTIONAL},
|
||||
}))
|
||||
//@@end export-latch-ledger-entry
|
||||
|
||||
/** An immutable, account-owned Export validator committee. */
|
||||
LEDGER_ENTRY(ltEXPORT_COMMITTEE, 0x4563, ExportCommittee, export_committee, ({
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfExportCommitteeHash, soeREQUIRED},
|
||||
{sfExportCommittee, soeREQUIRED},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
}))
|
||||
|
||||
#undef EXPAND
|
||||
#undef LEDGER_ENTRY_DUPLICATE
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ TYPED_SFIELD(sfTickSize, UINT8, 16)
|
||||
TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17)
|
||||
TYPED_SFIELD(sfHookResult, UINT8, 18)
|
||||
TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19)
|
||||
TYPED_SFIELD(sfSidecarType, UINT8, 20)
|
||||
TYPED_SFIELD(sfEntropyTier, UINT8, 21)
|
||||
|
||||
// 16-bit integers (common)
|
||||
TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::sMD_Never)
|
||||
@@ -59,6 +61,10 @@ TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
|
||||
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
|
||||
TYPED_SFIELD(sfHookStateScale, UINT16, 21)
|
||||
TYPED_SFIELD(sfLedgerFixType, UINT16, 22)
|
||||
TYPED_SFIELD(sfHookExportCount, UINT16, 98)
|
||||
TYPED_SFIELD(sfEntropyCount, UINT16, 99)
|
||||
TYPED_SFIELD(sfEntropyDenominator, UINT16, 100)
|
||||
TYPED_SFIELD(sfExportCount, UINT16, 101)
|
||||
|
||||
// 32-bit integers (common)
|
||||
TYPED_SFIELD(sfNetworkID, UINT32, 1)
|
||||
@@ -153,6 +159,7 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie
|
||||
TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default)
|
||||
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
|
||||
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
||||
TYPED_SFIELD(sfExportNode, UINT64, 29)
|
||||
TYPED_SFIELD(sfTouchCount, UINT64, 97)
|
||||
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
|
||||
TYPED_SFIELD(sfAccountCount, UINT64, 99)
|
||||
@@ -210,6 +217,8 @@ TYPED_SFIELD(sfOfferID, UINT256, 34)
|
||||
TYPED_SFIELD(sfEscrowID, UINT256, 35)
|
||||
TYPED_SFIELD(sfURITokenID, UINT256, 36)
|
||||
TYPED_SFIELD(sfDomainID, UINT256, 37)
|
||||
TYPED_SFIELD(sfExportSignatureHash, UINT256, 38)
|
||||
TYPED_SFIELD(sfExportCommitteeHash, UINT256, 39)
|
||||
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
|
||||
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
|
||||
TYPED_SFIELD(sfCron, UINT256, 95)
|
||||
@@ -293,6 +302,9 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
|
||||
TYPED_SFIELD(sfProvider, VL, 30)
|
||||
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
|
||||
TYPED_SFIELD(sfCredentialType, VL, 32)
|
||||
TYPED_SFIELD(sfEntropyContributors, VL, 33)
|
||||
TYPED_SFIELD(sfExportCommittee, VL, 34)
|
||||
TYPED_SFIELD(sfExportContributors, VL, 35)
|
||||
TYPED_SFIELD(sfHookName, VL, 97)
|
||||
TYPED_SFIELD(sfRemarkValue, VL, 98)
|
||||
TYPED_SFIELD(sfRemarkName, VL, 99)
|
||||
@@ -365,7 +377,7 @@ UNTYPED_SFIELD(sfHook, OBJECT, 14)
|
||||
|
||||
// inner object (uncommon)
|
||||
UNTYPED_SFIELD(sfSigner, OBJECT, 16)
|
||||
// 17 unused
|
||||
UNTYPED_SFIELD(sfExportSigner, OBJECT, 17)
|
||||
UNTYPED_SFIELD(sfMajority, OBJECT, 18)
|
||||
UNTYPED_SFIELD(sfDisabledValidator, OBJECT, 19)
|
||||
UNTYPED_SFIELD(sfEmittedTxn, OBJECT, 20)
|
||||
@@ -382,6 +394,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
|
||||
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
|
||||
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
|
||||
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
|
||||
UNTYPED_SFIELD(sfExportedTxn, OBJECT, 90)
|
||||
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
|
||||
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
|
||||
UNTYPED_SFIELD(sfHookEmission, OBJECT, 93)
|
||||
@@ -415,7 +428,7 @@ UNTYPED_SFIELD(sfHookParameters, ARRAY, 19)
|
||||
UNTYPED_SFIELD(sfHookGrants, ARRAY, 20)
|
||||
UNTYPED_SFIELD(sfXChainClaimAttestations, ARRAY, 21)
|
||||
UNTYPED_SFIELD(sfXChainCreateAccountAttestations, ARRAY, 22)
|
||||
// 23 unused
|
||||
UNTYPED_SFIELD(sfExportSigners, ARRAY, 23)
|
||||
UNTYPED_SFIELD(sfPriceDataSeries, ARRAY, 24)
|
||||
UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
|
||||
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
|
||||
|
||||
@@ -500,6 +500,17 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
|
||||
{sfDomainID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
//@@start export-transaction-types
|
||||
/* User-submittable Export. Its mutually exclusive forms create an intent,
|
||||
control an existing latch, or manage an immutable committee object. */
|
||||
TRANSACTION(ttEXPORT, 91, Export, ({
|
||||
{sfExportedTxn, soeOPTIONAL},
|
||||
{sfTransactionHash, soeOPTIONAL},
|
||||
{sfExportCommitteeHash, soeOPTIONAL},
|
||||
{sfExportCommittee, soeOPTIONAL},
|
||||
}))
|
||||
//@@end export-transaction-types
|
||||
|
||||
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
|
||||
TRANSACTION(ttCRON, 92, Cron, ({
|
||||
{sfOwner, soeREQUIRED},
|
||||
@@ -606,3 +617,20 @@ TRANSACTION(ttUNL_REPORT, 104, UNLReport, ({
|
||||
{sfActiveValidator, soeOPTIONAL},
|
||||
{sfImportVLKey, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
TRANSACTION(ttCONSENSUS_ENTROPY, 105, ConsensusEntropy, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfDigest, soeREQUIRED},
|
||||
{sfEntropyCount, soeREQUIRED},
|
||||
{sfEntropyDenominator, soeREQUIRED},
|
||||
{sfEntropyContributors, soeREQUIRED},
|
||||
{sfEntropyTier, soeREQUIRED},
|
||||
}))
|
||||
|
||||
TRANSACTION(ttEXPORT_SIGNATURES, 106, ExportSignatures, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
{sfExportedTxn, soeREQUIRED},
|
||||
{sfExportContributors, soeREQUIRED},
|
||||
{sfExportSigners, soeREQUIRED},
|
||||
}))
|
||||
|
||||
@@ -531,6 +531,9 @@ JSS(open_ledger_fee); // out: TxQ
|
||||
JSS(open_ledger_level); // out: TxQ
|
||||
JSS(oracles); // in: get_aggregate_price
|
||||
JSS(oracle_document_id); // in: get_aggregate_price
|
||||
JSS(origin_ledger_hash); // out: NetworkOPs
|
||||
JSS(origin_ledger_seq); // out: NetworkOPs
|
||||
JSS(origin_txid); // out: NetworkOPs
|
||||
JSS(owner); // in: LedgerEntry, out: NetworkOPs
|
||||
JSS(owner_funds); // in/out: Ledger, NetworkOPs, AcceptedLedgerTx
|
||||
JSS(page_index);
|
||||
@@ -652,6 +655,7 @@ JSS(state_now); // in: Subscribe
|
||||
JSS(status); // error
|
||||
JSS(stop); // in: LedgerCleaner
|
||||
JSS(stop_history_tx_only); // in: Unsubscribe, stop history tx stream
|
||||
JSS(stream); // out: NetworkOPs
|
||||
JSS(streams); // in: Subscribe, Unsubscribe
|
||||
JSS(strict); // in: AccountCurrencies, AccountInfo
|
||||
JSS(sub_index); // in: LedgerEntry
|
||||
@@ -733,6 +737,7 @@ JSS(TRANSACTION_FLAGS); // out: RPC server_definitions
|
||||
JSS(TRANSACTION_FLAGS_INDICES); // out: RPC server_definitions
|
||||
JSS(type_hex); // out: STPathSet
|
||||
JSS(unl); // out: UnlList
|
||||
JSS(committee_position); // out: NetworkOPs
|
||||
JSS(unlimited); // out: Connection.h
|
||||
JSS(uptime); // out: GetCounts
|
||||
JSS(uri); // out: ValidatorSites
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
@@ -351,9 +352,18 @@ Logs::format(
|
||||
|
||||
if (useLocalTime)
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto local = date::make_zoned(date::current_zone(), now);
|
||||
output = date::format(fmt, local);
|
||||
try
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto local = date::make_zoned(date::current_zone(), now);
|
||||
output = date::format(fmt, local);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// Enhanced logging should not make startup fatal if tzdb lookup is
|
||||
// unavailable or misconfigured. Fall back to UTC formatting.
|
||||
output = date::format(fmt, std::chrono::system_clock::now());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -56,9 +56,9 @@ getVersionString()
|
||||
(versionString == std::string("0.") + std::string("0.0") ||
|
||||
versionString == std::string("0.0.0+DEBUG")))
|
||||
{
|
||||
std::string y = std::string(__DATE__ + 7);
|
||||
std::string y = std::string(&__DATE__[7]);
|
||||
std::string d = std::string(
|
||||
__DATE__ + 4 + (__DATE__[4] == ' ' ? 1 : 0),
|
||||
&__DATE__[4 + (__DATE__[4] == ' ' ? 1 : 0)],
|
||||
__DATE__[4] == ' ' ? 1 : 2);
|
||||
std::string m;
|
||||
switch (__DATE__[0])
|
||||
|
||||
211
src/libxrpl/protocol/ExportOriginMemo.cpp
Normal file
211
src/libxrpl/protocol/ExportOriginMemo.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/protocol/ExportOriginMemo.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
namespace ripple::ExportOriginMemo {
|
||||
namespace {
|
||||
|
||||
Blob const&
|
||||
memoTypeBytes()
|
||||
{
|
||||
static Blob const value(memoType.begin(), memoType.end());
|
||||
return value;
|
||||
}
|
||||
|
||||
bool
|
||||
isReserved(STObject const& memo)
|
||||
{
|
||||
return memo.getFName() == sfMemo && memo.isFieldPresent(sfMemoType) &&
|
||||
memo.getFieldVL(sfMemoType) == memoTypeBytes();
|
||||
}
|
||||
|
||||
STObject
|
||||
cloneObject(STTx const& tx)
|
||||
{
|
||||
Serializer serializer;
|
||||
tx.add(serializer);
|
||||
SerialIter sit{serializer.slice()};
|
||||
STObject result{sfGeneric};
|
||||
result.set(sit);
|
||||
return result;
|
||||
}
|
||||
|
||||
Blob
|
||||
serialize(Origin const& origin, std::optional<Anchor> const& anchor)
|
||||
{
|
||||
//@@start export-origin-memo-wire-format
|
||||
Serializer serializer;
|
||||
serializer.add8(version);
|
||||
serializer.add8(allowedFlags);
|
||||
serializer.add32(origin.sourceDomain);
|
||||
serializer.add32(origin.targetDomain);
|
||||
serializer.addBitString(origin.transactionHash);
|
||||
if (anchor)
|
||||
{
|
||||
serializer.add32(anchor->ledgerSequence);
|
||||
serializer.addBitString(anchor->ledgerHash);
|
||||
}
|
||||
return serializer.getData();
|
||||
//@@end export-origin-memo-wire-format
|
||||
}
|
||||
|
||||
Expected<STTx, Error>
|
||||
append(
|
||||
STTx const& base,
|
||||
Origin const& origin,
|
||||
std::optional<Anchor> const& anchor)
|
||||
{
|
||||
if (hasReservedMemo(base))
|
||||
return Unexpected(Error::reservedMemoPresent);
|
||||
|
||||
//@@start export-origin-memo-canonical-append
|
||||
auto object = cloneObject(base);
|
||||
STArray memos = object.isFieldPresent(sfMemos)
|
||||
? object.getFieldArray(sfMemos)
|
||||
: STArray{sfMemos};
|
||||
|
||||
STObject memo{sfMemo};
|
||||
memo.setFieldVL(sfMemoType, memoTypeBytes());
|
||||
memo.setFieldVL(sfMemoData, serialize(origin, anchor));
|
||||
memos.emplace_back(std::move(memo));
|
||||
object.setFieldArray(sfMemos, memos);
|
||||
|
||||
STTx result{std::move(object)};
|
||||
std::string reason;
|
||||
if (!passesLocalChecks(result, reason))
|
||||
return Unexpected(Error::localChecks);
|
||||
return result;
|
||||
//@@end export-origin-memo-canonical-append
|
||||
}
|
||||
|
||||
Expected<Stamp, Error>
|
||||
parseData(Blob const& data)
|
||||
{
|
||||
if (data.size() != identityBytes && data.size() != releaseBytes)
|
||||
return Unexpected(Error::malformedMemo);
|
||||
|
||||
try
|
||||
{
|
||||
SerialIter sit{makeSlice(data)};
|
||||
if (sit.get8() != version)
|
||||
return Unexpected(Error::unsupportedVersion);
|
||||
if (sit.get8() != allowedFlags)
|
||||
return Unexpected(Error::unsupportedFlags);
|
||||
|
||||
Stamp stamp{
|
||||
.origin =
|
||||
Origin{
|
||||
.sourceDomain = sit.get32(),
|
||||
.targetDomain = sit.get32(),
|
||||
.transactionHash = sit.get256()},
|
||||
.anchor = std::nullopt};
|
||||
|
||||
if (data.size() == releaseBytes)
|
||||
stamp.anchor = Anchor{sit.get32(), sit.get256()};
|
||||
if (!sit.empty())
|
||||
return Unexpected(Error::malformedMemo);
|
||||
return stamp;
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return Unexpected(Error::malformedMemo);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
hasReservedMemo(STTx const& tx)
|
||||
{
|
||||
if (!tx.isFieldPresent(sfMemos))
|
||||
return false;
|
||||
|
||||
auto const& memos = tx.getFieldArray(sfMemos);
|
||||
return std::any_of(memos.begin(), memos.end(), isReserved);
|
||||
}
|
||||
|
||||
Expected<STTx, Error>
|
||||
identityForm(STTx const& base, Origin const& origin)
|
||||
{
|
||||
return append(base, origin, std::nullopt);
|
||||
}
|
||||
|
||||
Expected<STTx, Error>
|
||||
releaseForm(STTx const& base, Origin const& origin, Anchor const& anchor)
|
||||
{
|
||||
return append(base, origin, anchor);
|
||||
}
|
||||
|
||||
Expected<Stamp, Error>
|
||||
parse(STTx const& tx)
|
||||
{
|
||||
std::string reason;
|
||||
if (!passesLocalChecks(tx, reason))
|
||||
return Unexpected(Error::localChecks);
|
||||
if (!tx.isFieldPresent(sfMemos))
|
||||
return Unexpected(Error::reservedMemoMissing);
|
||||
|
||||
auto const& memos = tx.getFieldArray(sfMemos);
|
||||
auto found = memos.end();
|
||||
for (auto it = memos.begin(); it != memos.end(); ++it)
|
||||
{
|
||||
if (!isReserved(*it))
|
||||
continue;
|
||||
if (found != memos.end())
|
||||
return Unexpected(Error::reservedMemoPosition);
|
||||
found = it;
|
||||
}
|
||||
|
||||
if (found == memos.end())
|
||||
return Unexpected(Error::reservedMemoMissing);
|
||||
if (found != std::prev(memos.end()))
|
||||
return Unexpected(Error::reservedMemoPosition);
|
||||
if (found->isFieldPresent(sfMemoFormat) ||
|
||||
!found->isFieldPresent(sfMemoData))
|
||||
return Unexpected(Error::malformedMemo);
|
||||
|
||||
return parseData(found->getFieldVL(sfMemoData));
|
||||
}
|
||||
|
||||
Expected<STTx, Error>
|
||||
projectIdentity(STTx const& stamped)
|
||||
{
|
||||
auto parsed = parse(stamped);
|
||||
if (!parsed)
|
||||
return Unexpected(parsed.error());
|
||||
|
||||
auto object = cloneObject(stamped);
|
||||
auto memos = object.getFieldArray(sfMemos);
|
||||
memos.erase(std::prev(memos.end()));
|
||||
if (memos.empty())
|
||||
object.delField(sfMemos);
|
||||
else
|
||||
object.setFieldArray(sfMemos, memos);
|
||||
|
||||
return identityForm(STTx{std::move(object)}, parsed.value().origin);
|
||||
}
|
||||
|
||||
} // namespace ripple::ExportOriginMemo
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user