mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 19:57:09 +00:00
Compare commits
770
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ac456d29 | ||
|
|
29dc65e5ba | ||
|
|
13751d6199 | ||
|
|
4d5ca17f72 | ||
|
|
9e4d5904fb | ||
|
|
6c21f9d5f0 | ||
|
|
106ca672c9 | ||
|
|
56a7822107 | ||
|
|
ea36944b5c | ||
|
|
3fee098758 | ||
|
|
113a12c4e3 | ||
|
|
2635c260a4 | ||
|
|
4c325570b8 | ||
|
|
b92ef011bf | ||
|
|
4d4c5aa43a | ||
|
|
6211fcb039 | ||
|
|
9e23d7520e | ||
|
|
a91b7136b7 | ||
|
|
440b488210 | ||
|
|
25a31b4a20 | ||
|
|
bacd1fac81 | ||
|
|
a686dbd3a0 | ||
|
|
edf24309ad | ||
|
|
355c232951 | ||
|
|
23146b264d | ||
|
|
63ed0e4aa2 | ||
|
|
5d128dc1b4 | ||
|
|
4c097b590a | ||
|
|
8d4f0ae661 | ||
|
|
99a2f03137 | ||
|
|
1b105b6a99 | ||
|
|
945eb98f96 | ||
|
|
c6d15aac93 | ||
|
|
04a1d8a7b1 | ||
|
|
bc5b90282a | ||
|
|
64974242d2 | ||
|
|
e02e9cb1d7 | ||
|
|
6e934f0557 | ||
|
|
47babd73aa | ||
|
|
476a5bf76a | ||
|
|
c15b69fd9d | ||
|
|
540e04bcc9 | ||
|
|
12f9200a2a | ||
|
|
2a2c110e7e | ||
|
|
e0201c7365 | ||
|
|
2002e13e54 | ||
|
|
bac9e1df65 | ||
|
|
9308e5a191 | ||
|
|
a9488e39f2 | ||
|
|
4db2f4ea4c | ||
|
|
7bf9b931fb | ||
|
|
1e8097d175 | ||
|
|
95bfc258e9 | ||
|
|
786e317a62 | ||
|
|
d0ec00473c | ||
|
|
459196778b | ||
|
|
8a7e3896c3 | ||
|
|
9ead7aadeb | ||
|
|
b1730b8381 | ||
|
|
f980837abc | ||
|
|
474be6384d | ||
|
|
dec1da2ec8 | ||
|
|
63eafad824 | ||
|
|
61b739a5c7 | ||
|
|
fa831d40f7 | ||
|
|
d1450a774b | ||
|
|
9ed50bb71c | ||
|
|
65b39982f0 | ||
|
|
e8236cad86 | ||
|
|
56cb021a60 | ||
|
|
6be897804b | ||
|
|
04c24c3edf | ||
|
|
3ef17dac6c | ||
|
|
e84206020f | ||
|
|
fa7d1ae56c | ||
|
|
29fff4eeb7 | ||
|
|
4601bf6aec | ||
|
|
df0a779fed | ||
|
|
12a18040db | ||
|
|
38aab0700e | ||
|
|
f74e6833fd | ||
|
|
0d3c1d7967 | ||
|
|
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 |
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -5,6 +5,18 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
verify-hook-sdk-abi:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check shipped SDK against runtime ABI and constants
|
||||
run: clang++ -std=c++17 -Iinclude -fsyntax-only hook/tests/sdk_abi.cpp
|
||||
|
||||
- name: Compile entropy calls using shipped C headers
|
||||
run: clang -std=c11 -Werror -Wno-pointer-to-int-cast -fsyntax-only hook/tests/entropy_api.c
|
||||
|
||||
verify-generated-headers:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -16,6 +28,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
|
||||
|
||||
@@ -26,7 +26,7 @@ Loop: xrpld.app xrpld.nodestore
|
||||
xrpld.app > xrpld.nodestore
|
||||
|
||||
Loop: xrpld.app xrpld.overlay
|
||||
xrpld.overlay ~= xrpld.app
|
||||
xrpld.overlay == xrpld.app
|
||||
|
||||
Loop: xrpld.app xrpld.peerfinder
|
||||
xrpld.app > xrpld.peerfinder
|
||||
|
||||
@@ -12,6 +12,7 @@ libxrpl.server > xrpl.basics
|
||||
libxrpl.server > xrpl.json
|
||||
libxrpl.server > xrpl.protocol
|
||||
libxrpl.server > xrpl.server
|
||||
test.app > test.shamap
|
||||
test.app > test.toplevel
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
@@ -21,6 +22,7 @@ test.app > xrpld.ledger
|
||||
test.app > xrpld.nodestore
|
||||
test.app > xrpld.overlay
|
||||
test.app > xrpld.rpc
|
||||
test.app > xrpld.shamap
|
||||
test.app > xrpl.hook
|
||||
test.app > xrpl.json
|
||||
test.app > xrpl.protocol
|
||||
@@ -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
|
||||
|
||||
+9
-3
@@ -21,7 +21,11 @@ endif()
|
||||
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
|
||||
@@ -29,7 +33,8 @@ if(Git_FOUND)
|
||||
if(gch)
|
||||
set(GIT_COMMIT_HASH "${gch}")
|
||||
message(STATUS gch: ${GIT_COMMIT_HASH})
|
||||
add_definitions(-DGIT_COMMIT_HASH="${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
|
||||
@@ -37,7 +42,8 @@ if(Git_FOUND)
|
||||
if(gb)
|
||||
set(GIT_BRANCH "${gb}")
|
||||
message(STATUS gb: ${GIT_BRANCH})
|
||||
add_definitions(-DGIT_BRANCH="${GIT_BRANCH}")
|
||||
set_property(SOURCE ${git_info_sources} APPEND PROPERTY
|
||||
COMPILE_DEFINITIONS "GIT_BRANCH=\"${GIT_BRANCH}\"")
|
||||
endif()
|
||||
endif() #git
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
@@ -180,6 +183,21 @@ if(xrpld)
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test/*.cpp"
|
||||
)
|
||||
target_sources(rippled PRIVATE ${sources})
|
||||
|
||||
set(HOOKS_TEST_DIR "" CACHE PATH "External hook Env-test directory")
|
||||
if(NOT HOOKS_TEST_DIR AND DEFINED ENV{HOOKS_TEST_DIR})
|
||||
set(HOOKS_TEST_DIR "$ENV{HOOKS_TEST_DIR}")
|
||||
endif()
|
||||
if(HOOKS_TEST_DIR)
|
||||
file(GLOB_RECURSE hook_test_sources CONFIGURE_DEPENDS
|
||||
"${HOOKS_TEST_DIR}/*_test.cpp"
|
||||
)
|
||||
if(hook_test_sources)
|
||||
message(STATUS "Including external hook Env tests from ${HOOKS_TEST_DIR}")
|
||||
target_sources(rippled PRIVATE ${hook_test_sources})
|
||||
target_include_directories(rippled PRIVATE "${HOOKS_TEST_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(rippled
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -47,5 +47,9 @@
|
||||
#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 LATER_STRONG_HOOK -49
|
||||
#define HOOK_ERROR_CODES
|
||||
#endif //HOOK_ERROR_CODES
|
||||
@@ -339,6 +339,67 @@ prepare(
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
|
||||
extern int64_t
|
||||
xport_reserve(uint32_t count);
|
||||
|
||||
// callback_fee_drops: 0 omits third-party delivery permission; otherwise the
|
||||
// exact Import fee authorized by the emitted Export intent, in native drops.
|
||||
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,
|
||||
uint64_t callback_fee_drops);
|
||||
|
||||
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.
|
||||
|
||||
In strong execution, flags=0 requires the last eligible strong Hook.
|
||||
Otherwise: LATER_STRONG_HOOK before drawing or changing output.
|
||||
ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO permits later strong Hooks
|
||||
only on the installed account of the drawing Hook.
|
||||
ENTROPY_ALLOW_ANY_STRONG_VETO permits them on any account, including
|
||||
the same account. Both bits mean ANY; unknown bits are invalid.
|
||||
Allowed Hooks retain their ordinary veto authority. An opt-in must
|
||||
account for downstream code and whoever can replace it.
|
||||
|
||||
Draws return TOO_LITTLE_ENTROPY if the visible input is missing,
|
||||
stale, or below min_tier, independently of composition flags.
|
||||
Closed execution requires current-ledger input; open-ledger and
|
||||
simulate execution can use previous-ledger input and are provisional
|
||||
previews. Final ordered execution may see different entropy.
|
||||
*/
|
||||
extern int64_t
|
||||
entropy_cr_dice(uint32_t sides, uint32_t min_tier, uint32_t flags);
|
||||
|
||||
extern int64_t
|
||||
entropy_cr_random(
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t min_tier,
|
||||
uint32_t flags);
|
||||
|
||||
extern int64_t
|
||||
entropy_cr_status(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -41,6 +41,44 @@ 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:]]+xport[[:space:]]*\(/) {
|
||||
print "// callback_fee_drops: 0 omits third-party delivery permission; otherwise the";
|
||||
print "// exact Import fee authorized by the emitted Export intent, in native drops.";
|
||||
}
|
||||
|
||||
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 " In strong execution, flags=0 requires the last eligible strong Hook.";
|
||||
print " Otherwise: LATER_STRONG_HOOK before drawing or changing output.";
|
||||
print " ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO permits later strong Hooks";
|
||||
print " only on the installed account of the drawing Hook.";
|
||||
print " ENTROPY_ALLOW_ANY_STRONG_VETO permits them on any account, including";
|
||||
print " the same account. Both bits mean ANY; unknown bits are invalid.";
|
||||
print " Allowed Hooks retain their ordinary veto authority. An opt-in must";
|
||||
print " account for downstream code and whoever can replace it.";
|
||||
print "";
|
||||
print " Draws return TOO_LITTLE_ENTROPY if the visible input is missing,";
|
||||
print " stale, or below min_tier, independently of composition flags.";
|
||||
print " Closed execution requires current-ledger input; open-ledger and";
|
||||
print " simulate execution can use previous-ledger input and are provisional";
|
||||
print " previews. Final ordered execution may see different entropy.";
|
||||
print "*/";
|
||||
}
|
||||
|
||||
# printf("\n");
|
||||
|
||||
|
||||
@@ -348,5 +348,24 @@ util_verify(
|
||||
uint32_t kread_len);
|
||||
|
||||
extern int64_t xpop_slot(uint32_t, uint32_t);
|
||||
|
||||
extern int64_t
|
||||
xport_reserve(uint32_t count);
|
||||
|
||||
// callback_fee_drops: 0 omits third-party delivery permission; otherwise the
|
||||
// exact Import fee authorized by the emitted Export intent, in native drops.
|
||||
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,
|
||||
uint64_t callback_fee_drops);
|
||||
|
||||
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,18 @@
|
||||
#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 sfManifestID ((5U << 16U) + 91U)
|
||||
#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 +180,27 @@
|
||||
#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 sfExportCallbackFee ((6U << 16U) + 32U)
|
||||
#define sfTrustLineRewardAccumulator ((6U << 16U) + 99U)
|
||||
#define sfPublicKey ((7U << 16U) + 1U)
|
||||
#define sfMessageKey ((7U << 16U) + 2U)
|
||||
#define sfSigningPubKey ((7U << 16U) + 3U)
|
||||
@@ -161,6 +226,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 +247,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 +287,33 @@
|
||||
#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) + 89U)
|
||||
#define sfManifest ((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 +323,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,
|
||||
|
||||
+9
-2
@@ -145,6 +145,15 @@ 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)
|
||||
|
||||
// Zero flags require the last eligible strong Hook. ANY includes SAME_ACCOUNT;
|
||||
// both bits mean ANY. Allowed later Hooks retain ordinary veto authority.
|
||||
#define ENTROPY_ALLOW_ANY_STRONG_VETO (1U << 0)
|
||||
#define ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO (1U << 1)
|
||||
|
||||
#define BUFFER_EQUAL_20(buf1, buf2)\
|
||||
(\
|
||||
*(((uint64_t*)(buf1)) + 0) == *(((uint64_t*)(buf2)) + 0) &&\
|
||||
@@ -377,5 +386,3 @@ int out_len = 0;\
|
||||
#define amDELIVEREDAMOUNT 18U
|
||||
|
||||
#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 sfManifestID ((5U << 16U) + 91U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
@@ -190,6 +199,7 @@
|
||||
#define sfSignatureReward ((6U << 16U) + 29U)
|
||||
#define sfMinAccountCreateAmount ((6U << 16U) + 30U)
|
||||
#define sfLPTokenBalance ((6U << 16U) + 31U)
|
||||
#define sfExportCallbackFee ((6U << 16U) + 32U)
|
||||
#define sfTrustLineRewardAccumulator ((6U << 16U) + 99U)
|
||||
#define sfPublicKey ((7U << 16U) + 1U)
|
||||
#define sfMessageKey ((7U << 16U) + 2U)
|
||||
@@ -222,6 +232,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)
|
||||
@@ -274,6 +287,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)
|
||||
@@ -289,6 +303,7 @@
|
||||
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
|
||||
#define sfPriceData ((14U << 16U) + 32U)
|
||||
#define sfCredential ((14U << 16U) + 33U)
|
||||
#define sfExportedTxn ((14U << 16U) + 89U)
|
||||
#define sfManifest ((14U << 16U) + 90U)
|
||||
#define sfAmountEntry ((14U << 16U) + 91U)
|
||||
#define sfMintURIToken ((14U << 16U) + 92U)
|
||||
@@ -316,6 +331,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)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Compile against the shipped SDK without hand-declaring host imports.
|
||||
#include "../hookapi.h"
|
||||
|
||||
_Static_assert(ENTROPY_ALLOW_ANY_STRONG_VETO == 1, "ANY flag value");
|
||||
_Static_assert(
|
||||
ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO == 2,
|
||||
"SAME_ACCOUNT flag value");
|
||||
_Static_assert(LATER_STRONG_HOOK == -49, "composition refusal code");
|
||||
|
||||
int64_t
|
||||
hook(uint32_t reserved)
|
||||
{
|
||||
_g(1, 1);
|
||||
int64_t status = entropy_cr_status();
|
||||
if (status < 0)
|
||||
return rollback(0, 0, status);
|
||||
|
||||
uint8_t bytes[32];
|
||||
for (uint32_t flags = 0; GUARD(4), flags < 4; ++flags)
|
||||
{
|
||||
int64_t draw = entropy_cr_dice(1, 3, flags);
|
||||
if (draw != 0)
|
||||
return rollback(0, 0, draw);
|
||||
int64_t count = entropy_cr_random(SBUF(bytes), 3, flags);
|
||||
if (count != sizeof(bytes))
|
||||
return rollback(0, 0, count);
|
||||
}
|
||||
return accept(0, 0, ENTROPY_TIER(status));
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Compile-only check: shipped SDK declarations must match the runtime ABI,
|
||||
// not just the prototype comments used by generate_extern.sh.
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
// Use the dependency-free guard-checker view of the host enum definitions.
|
||||
#define GUARD_CHECKER_BUILD 1
|
||||
#include <xrpl/hook/Enum.h>
|
||||
|
||||
constexpr auto hostAnyStrongVeto = hook_api::ENTROPY_ALLOW_ANY_STRONG_VETO;
|
||||
constexpr auto hostSameAccountStrongVeto =
|
||||
hook_api::ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO;
|
||||
constexpr auto hostLaterStrongHook =
|
||||
static_cast<int64_t>(hook_api::hook_return_code::LATER_STRONG_HOOK);
|
||||
|
||||
#include "../hookapi.h"
|
||||
|
||||
static_assert(ENTROPY_ALLOW_ANY_STRONG_VETO == hostAnyStrongVeto);
|
||||
static_assert(
|
||||
ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO == hostSameAccountStrongVeto);
|
||||
static_assert(LATER_STRONG_HOOK == hostLaterStrongHook);
|
||||
|
||||
#define HOOK_API_DEFINITION( \
|
||||
RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \
|
||||
static_assert( \
|
||||
std::is_same_v<decltype(&FUNCTION_NAME), RETURN_TYPE(*) PARAMS_TUPLE>, \
|
||||
"Hook SDK/runtime ABI mismatch: " #FUNCTION_NAME);
|
||||
#include <xrpl/hook/hook_api.macro>
|
||||
#undef HOOK_API_DEFINITION
|
||||
@@ -61,6 +61,7 @@
|
||||
#define ttNFTOKEN_MODIFY 70
|
||||
#define ttPERMISSIONED_DOMAIN_SET 71
|
||||
#define ttPERMISSIONED_DOMAIN_DELETE 72
|
||||
#define ttEXPORT 90
|
||||
#define ttMANIFEST_SET 91
|
||||
#define ttCRON 92
|
||||
#define ttCRON_SET 93
|
||||
@@ -75,3 +76,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,
|
||||
};
|
||||
@@ -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 {
|
||||
@@ -385,9 +387,16 @@ 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 256 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,
|
||||
LATER_STRONG_HOOK = -49,
|
||||
};
|
||||
|
||||
inline constexpr uint32_t ENTROPY_ALLOW_ANY_STRONG_VETO = 1U << 0;
|
||||
inline constexpr uint32_t ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO = 1U << 1;
|
||||
|
||||
enum class ExitType : uint8_t {
|
||||
UNSET = 0,
|
||||
WASM_ERROR = 1,
|
||||
@@ -399,6 +408,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;
|
||||
|
||||
@@ -416,6 +426,7 @@ getImportWhitelist(Rules const& rules)
|
||||
#undef HOOK_API_DEFINITION
|
||||
|
||||
#define int64_t 0x7EU
|
||||
#define uint64_t 0x7EU
|
||||
#define int32_t 0x7FU
|
||||
#define uint32_t 0x7FU
|
||||
|
||||
@@ -432,6 +443,7 @@ getImportWhitelist(Rules const& rules)
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef HOOK_WRAP_PARAMS
|
||||
#undef int64_t
|
||||
#undef uint64_t
|
||||
#undef int32_t
|
||||
#undef uint32_t
|
||||
#pragma pop_macro("HOOK_API_DEFINITION")
|
||||
@@ -439,10 +451,6 @@ getImportWhitelist(Rules const& rules)
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef I32
|
||||
#undef I64
|
||||
|
||||
enum GuardRulesVersion : uint64_t {
|
||||
GuardRuleFix20250131 = 0x00000001,
|
||||
GuardRuleDepth32 = 0x00000002,
|
||||
|
||||
@@ -372,3 +372,41 @@ 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, uint64_t callback_fee_drops);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xport, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint64_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)
|
||||
|
||||
// flags=0 requires terminal strong execution; otherwise LATER_STRONG_HOOK
|
||||
// is returned before generating randomness or changing output/counter state.
|
||||
// ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO (bit 1) permits later strong Hooks
|
||||
// only on the drawing Hook's account. ENTROPY_ALLOW_ANY_STRONG_VETO (bit 0)
|
||||
// permits them on any account, including the same account. Both bits mean ANY.
|
||||
// Allowed Hooks retain ordinary vetoes. Unknown bits are INVALID_ARGUMENT. All modes
|
||||
// enforce min_tier/freshness. The drawing Hook can still fail; fair settlement
|
||||
// requires committed inputs and must prevent retries selecting a new outcome.
|
||||
// int64_t entropy_cr_dice(uint32_t sides, uint32_t min_tier, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, entropy_cr_dice, (uint32_t, uint32_t, uint32_t),
|
||||
featureConsensusEntropy)
|
||||
|
||||
// int64_t entropy_cr_random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, entropy_cr_random, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
featureConsensusEntropy)
|
||||
|
||||
// int64_t entropy_cr_status(void);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, entropy_cr_status, (),
|
||||
featureConsensusEntropy)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#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.
|
||||
///
|
||||
/// 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 input 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -365,6 +368,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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -70,7 +70,8 @@ XRPL_FEATURE(XChainBridge, Supported::no, VoteBehavior::DefaultNo
|
||||
XRPL_FEATURE(AMM, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo)
|
||||
// replaced to HookOnV2_1
|
||||
XRPL_FEATURE(Export, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConsensusEntropy, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(HookOnV2, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes)
|
||||
|
||||
@@ -257,11 +257,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},
|
||||
{sfManifestID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
@@ -278,6 +281,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},
|
||||
@@ -418,7 +422,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},
|
||||
@@ -593,6 +599,39 @@ 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},
|
||||
{sfExportCallbackFee, soeOPTIONAL},
|
||||
{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},
|
||||
}))
|
||||
|
||||
LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
@@ -615,4 +654,3 @@ LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
|
||||
|
||||
#undef EXPAND
|
||||
#undef LEDGER_ENTRY_DUPLICATE
|
||||
|
||||
@@ -42,6 +42,8 @@ TYPED_SFIELD(sfTickSize, UINT8, 16)
|
||||
TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17)
|
||||
TYPED_SFIELD(sfHookResult, UINT8, 18)
|
||||
TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19)
|
||||
TYPED_SFIELD(sfSidecarType, UINT8, 20)
|
||||
TYPED_SFIELD(sfEntropyTier, UINT8, 21)
|
||||
|
||||
// 16-bit integers (common)
|
||||
TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::sMD_Never)
|
||||
@@ -59,6 +61,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(sfManifestID, UINT256, 91)
|
||||
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
|
||||
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
|
||||
@@ -258,6 +267,7 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28)
|
||||
TYPED_SFIELD(sfSignatureReward, AMOUNT, 29)
|
||||
TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30)
|
||||
TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31)
|
||||
TYPED_SFIELD(sfExportCallbackFee, AMOUNT, 32)
|
||||
TYPED_SFIELD(sfTrustLineRewardAccumulator,AMOUNT, 99)
|
||||
|
||||
// variable length (common)
|
||||
@@ -294,6 +304,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)
|
||||
@@ -366,7 +379,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)
|
||||
@@ -383,6 +396,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, 89)
|
||||
UNTYPED_SFIELD(sfManifest, OBJECT, 90)
|
||||
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
|
||||
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
|
||||
@@ -417,7 +431,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, 90, Export, ({
|
||||
{sfExportedTxn, soeOPTIONAL},
|
||||
{sfExportCallbackFee, soeOPTIONAL},
|
||||
{sfTransactionHash, soeOPTIONAL},
|
||||
{sfExportCommitteeHash, soeOPTIONAL},
|
||||
{sfExportCommittee, soeOPTIONAL},
|
||||
}))
|
||||
//@@end export-transaction-types
|
||||
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
|
||||
makes the SOTemplate reject the format at construction. */
|
||||
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
|
||||
@@ -612,3 +623,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},
|
||||
}))
|
||||
@@ -534,6 +534,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);
|
||||
@@ -655,6 +658,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
|
||||
@@ -736,6 +740,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])
|
||||
|
||||
@@ -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
|
||||
@@ -74,6 +74,9 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
HOOK_DEFINITION = 'D',
|
||||
EMITTED_TXN = 'E',
|
||||
EMITTED_DIR = 'F',
|
||||
EXPORT_LATCH = 0x5374, // St
|
||||
EXPORT_COMMITTEE = 0x4563, // Ec
|
||||
EXPORT_PENDING_DIR = 0x4570, // Ep
|
||||
NFTOKEN_OFFER = 'q',
|
||||
NFTOKEN_BUY_OFFERS = 'h',
|
||||
NFTOKEN_SELL_OFFERS = 'i',
|
||||
@@ -189,6 +192,34 @@ emittedTxn(uint256 const& id) noexcept
|
||||
return {ltEMITTED_TXN, indexHash(LedgerNameSpace::EMITTED_TXN, id)};
|
||||
}
|
||||
|
||||
//@@start export-origin-keylet
|
||||
Keylet
|
||||
exportLatch(AccountID const& account, uint256 const& originTxnHash) noexcept
|
||||
{
|
||||
return {
|
||||
ltEXPORT_LATCH,
|
||||
indexHash(LedgerNameSpace::EXPORT_LATCH, account, originTxnHash)};
|
||||
}
|
||||
//@@end export-origin-keylet
|
||||
|
||||
Keylet
|
||||
exportCommittee(AccountID const& account, uint256 const& digest) noexcept
|
||||
{
|
||||
return {
|
||||
ltEXPORT_COMMITTEE,
|
||||
indexHash(LedgerNameSpace::EXPORT_COMMITTEE, account, digest)};
|
||||
}
|
||||
|
||||
//@@start export-pending-directory-keylet
|
||||
Keylet const&
|
||||
pendingExports() noexcept
|
||||
{
|
||||
static Keylet const ret{
|
||||
ltDIR_NODE, indexHash(LedgerNameSpace::EXPORT_PENDING_DIR)};
|
||||
return ret;
|
||||
}
|
||||
//@@end export-pending-directory-keylet
|
||||
|
||||
Keylet
|
||||
hook(AccountID const& id) noexcept
|
||||
{
|
||||
|
||||
@@ -53,6 +53,13 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfTxnSignature, soeREQUIRED},
|
||||
});
|
||||
|
||||
add(sfExportSigner.jsonName,
|
||||
sfExportSigner.getCode(),
|
||||
{
|
||||
{sfSigningPubKey, soeREQUIRED},
|
||||
{sfTxnSignature, soeREQUIRED},
|
||||
});
|
||||
|
||||
add(sfMajority.jsonName,
|
||||
sfMajority.getCode(),
|
||||
{
|
||||
@@ -78,6 +85,7 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfHookExecutionIndex, soeREQUIRED},
|
||||
{sfHookStateChangeCount, soeREQUIRED},
|
||||
{sfHookEmitCount, soeREQUIRED},
|
||||
{sfHookExportCount, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL}});
|
||||
|
||||
add(sfHookEmission.jsonName,
|
||||
|
||||
@@ -684,7 +684,8 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
|
||||
tt == ttCONSENSUS_ENTROPY || tt == ttEXPORT_SIGNATURES;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -124,6 +124,7 @@ transResults()
|
||||
MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."),
|
||||
MAKE_ERROR(tecLOCKED, "Fund is locked."),
|
||||
MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."),
|
||||
MAKE_ERROR(tecEXPORT_COMMITTEE_UNAVAILABLE, "Export committee is unavailable in the parent validator view."),
|
||||
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
@@ -174,6 +175,7 @@ transResults()
|
||||
MAKE_ERROR(telIMPORT_VL_KEY_NOT_RECOGNISED, "Import vl key was not recognized."),
|
||||
MAKE_ERROR(telCAN_NOT_QUEUE_IMPORT, "Import transaction was not able to be directly applied and cannot be queued."),
|
||||
MAKE_ERROR(telENV_RPC_FAILED, "Unit test RPC failure."),
|
||||
MAKE_ERROR(telEXPORT_LATCH_REQUIRED, "The imported Export callback has no matching Export latch."),
|
||||
|
||||
MAKE_ERROR(temMALFORMED, "Malformed transaction."),
|
||||
MAKE_ERROR(temBAD_AMM_TOKENS, "Malformed: Invalid LPTokens."),
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpl/protocol/ExportCommittee.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
@@ -469,6 +470,40 @@ public:
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testExportCommitteeCleanup(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
testcase("Export committee cleanup");
|
||||
|
||||
Env env{*this, features | featureExport};
|
||||
Account const alice("alice");
|
||||
Account const becky("becky");
|
||||
env.fund(XRP(10000), alice, becky);
|
||||
env.close();
|
||||
|
||||
auto const roster = serializeExportCommittee({alice.pk()});
|
||||
auto const digest = exportCommitteeHash(makeSlice(roster));
|
||||
Json::Value setup;
|
||||
setup[jss::TransactionType] = "Export";
|
||||
setup[jss::Account] = alice.human();
|
||||
setup[sfExportCommittee.jsonName] = strHex(roster);
|
||||
env(setup);
|
||||
env.close();
|
||||
|
||||
auto const committeeKey = keylet::exportCommittee(alice.id(), digest);
|
||||
BEAST_EXPECT(env.closed()->exists(committeeKey));
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
auto const acctDelFee{drops(env.current()->fees().increment)};
|
||||
env(acctdelete(alice, becky), fee(acctDelFee));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.closed()->exists(keylet::account(alice.id())));
|
||||
BEAST_EXPECT(!env.closed()->exists(committeeKey));
|
||||
}
|
||||
|
||||
void
|
||||
testResurrection(FeatureBitset features)
|
||||
{
|
||||
@@ -1279,6 +1314,7 @@ public:
|
||||
testBasics(features);
|
||||
testDirectories(features);
|
||||
testOwnedTypes(features);
|
||||
testExportCommitteeCleanup(features);
|
||||
testResurrection(features);
|
||||
testAmendmentEnable(features);
|
||||
testTooManyOffers(features);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,292 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/ValidatorKeys.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpl/protocol/ExportCommittee.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/ExportOriginMemo.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
struct ExportFee_test : public beast::unit_test::suite
|
||||
{
|
||||
static std::unique_ptr<Config>
|
||||
exportTestConfig()
|
||||
{
|
||||
auto cfg = jtx::envconfig(jtx::validator, "");
|
||||
cfg->NETWORK_ID = 21337;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void
|
||||
seedUNLReportLedger(jtx::Env& env, PublicKey const& masterKey)
|
||||
{
|
||||
env.app().openLedger().modify(
|
||||
[&](OpenView& view, beast::Journal) -> bool {
|
||||
STTx tx(ttUNL_REPORT, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, env.current()->seq());
|
||||
auto active = std::make_unique<STObject>(sfActiveValidator);
|
||||
active->setFieldVL(sfPublicKey, masterKey);
|
||||
obj.set(std::move(active));
|
||||
});
|
||||
auto const txID = tx.getTransactionID();
|
||||
auto serialized = std::make_shared<Serializer>();
|
||||
tx.add(*serialized);
|
||||
env.app().getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
view.rawTxInsert(txID, std::move(serialized), nullptr);
|
||||
return true;
|
||||
});
|
||||
|
||||
BEAST_EXPECT(env.close(
|
||||
env.now() + std::chrono::seconds{5}, std::chrono::milliseconds{0}));
|
||||
BEAST_EXPECT(env.le(keylet::UNLReport()));
|
||||
}
|
||||
|
||||
static STObject
|
||||
exportedPayment(
|
||||
AccountID const& source,
|
||||
AccountID const& destination,
|
||||
LedgerIndex const first,
|
||||
LedgerIndex const last)
|
||||
{
|
||||
STObject target(sfExportedTxn);
|
||||
target.setFieldU16(sfTransactionType, ttPAYMENT);
|
||||
target.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
target.setFieldU32(sfSequence, 0);
|
||||
target.setFieldU32(sfTicketSequence, 1);
|
||||
target.setFieldU32(sfNetworkID, 1);
|
||||
target.setFieldU32(sfFirstLedgerSequence, first);
|
||||
target.setFieldU32(sfLastLedgerSequence, last);
|
||||
target.setFieldAmount(sfAmount, XRPAmount{1'000'000});
|
||||
target.setFieldAmount(sfFee, XRPAmount{10});
|
||||
target.setFieldVL(sfSigningPubKey, Blob{});
|
||||
target.setAccountID(sfAccount, source);
|
||||
target.setAccountID(sfDestination, destination);
|
||||
return target;
|
||||
}
|
||||
|
||||
static Json::Value
|
||||
exportIntent(
|
||||
jtx::Account const& account,
|
||||
STObject const& target,
|
||||
LedgerIndex const last,
|
||||
Blob const& committee)
|
||||
{
|
||||
Json::Value tx;
|
||||
tx[jss::TransactionType] = jss::Export;
|
||||
tx[jss::Account] = account.human();
|
||||
tx[jss::LastLedgerSequence] = last;
|
||||
tx[sfExportedTxn.jsonName] = target.getJson(JsonOptions::none);
|
||||
tx[sfExportCommitteeHash.jsonName] =
|
||||
to_string(exportCommitteeHash(makeSlice(committee)));
|
||||
tx[sfExportCommittee.jsonName] = strHex(committee);
|
||||
return tx;
|
||||
}
|
||||
|
||||
static void
|
||||
addHookParameterPayload(STObject& target, std::size_t count)
|
||||
{
|
||||
STArray parameters(sfHookParameters);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
STObject parameter(sfHookParameter);
|
||||
parameter.setFieldVL(
|
||||
sfHookParameterName, Blob{static_cast<std::uint8_t>(i + 1)});
|
||||
parameter.setFieldVL(sfHookParameterValue, Blob(256, 0xA5));
|
||||
parameters.emplace_back(std::move(parameter));
|
||||
}
|
||||
target.setFieldArray(sfHookParameters, std::move(parameters));
|
||||
}
|
||||
|
||||
static XRPAmount
|
||||
expectedFee(ReadView const& view, STTx const& tx, std::size_t committeeSize)
|
||||
{
|
||||
Serializer target;
|
||||
tx.peekAtField(sfExportedTxn).downcast<STObject>().add(target);
|
||||
|
||||
auto const witnessBytes = target.size() +
|
||||
ExportLimits::feeWitnessFixedAllowanceBytes +
|
||||
committeeSize * ExportLimits::feeWitnessSignerAllowanceBytes;
|
||||
auto const workUnits = committeeSize *
|
||||
ExportLimits::feeSharePublicationRounds *
|
||||
ExportLimits::feeWorkUnitsPerSharePublication +
|
||||
committeeSize * ExportLimits::feeWorkUnitsPerWitnessSignature +
|
||||
((witnessBytes + ExportLimits::feeWitnessChunkBytes - 1) /
|
||||
ExportLimits::feeWitnessChunkBytes) *
|
||||
ExportLimits::feeWorkUnitsPerWitnessChunk;
|
||||
auto const unrounded = workUnits * view.fees().base.drops() +
|
||||
witnessBytes * ExportLimits::feePermanentWitnessByteDrops;
|
||||
auto const surcharge =
|
||||
((unrounded + ExportLimits::feeSurchargeRoundDrops - 1) /
|
||||
ExportLimits::feeSurchargeRoundDrops) *
|
||||
ExportLimits::feeSurchargeRoundDrops;
|
||||
return view.fees().base +
|
||||
XRPAmount{static_cast<std::int64_t>(surcharge)};
|
||||
}
|
||||
|
||||
void
|
||||
testIntentFeeAndEnforcement(FeatureBitset const& features)
|
||||
{
|
||||
testcase("Export intent fee prices publication and witness work");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{*this, exportTestConfig(), features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
env.fund(XRP(10'000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const& validatorKeys = env.app().getValidatorKeys();
|
||||
BEAST_EXPECT(validatorKeys.keys);
|
||||
if (!validatorKeys.keys)
|
||||
return;
|
||||
auto const committee =
|
||||
serializeExportCommittee({validatorKeys.keys->masterPublicKey});
|
||||
seedUNLReportLedger(env, validatorKeys.keys->masterPublicKey);
|
||||
|
||||
auto const sequence = env.current()->seq();
|
||||
auto const last = sequence + ExportLimits::maxAdmissionWindowLedgers;
|
||||
auto const target =
|
||||
exportedPayment(alice.id(), carol.id(), sequence + 1, last);
|
||||
auto const intent = exportIntent(alice, target, last, committee);
|
||||
auto const prepared = env.jt(intent);
|
||||
auto const required = calculateBaseFee(*env.current(), *prepared.stx);
|
||||
auto const expected = expectedFee(*env.current(), *prepared.stx, 1);
|
||||
|
||||
BEAST_EXPECT(required == expected);
|
||||
// Any positive surcharge rounds to at least one provisional fee unit.
|
||||
BEAST_EXPECT(
|
||||
required >= env.current()->fees().base +
|
||||
XRPAmount{static_cast<std::int64_t>(
|
||||
ExportLimits::feeSurchargeRoundDrops)});
|
||||
|
||||
env(intent, fee(required - drops(1)), ter(telINSUF_FEE_P));
|
||||
env(intent, fee(required), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto oversizedTarget = exportedPayment(
|
||||
alice.id(), carol.id(), env.current()->seq() + 1, last + 1);
|
||||
addHookParameterPayload(oversizedTarget, 8);
|
||||
auto projectedTarget = oversizedTarget;
|
||||
auto const projected = ExportOriginMemo::releaseForm(
|
||||
STTx{std::move(projectedTarget)},
|
||||
ExportOriginMemo::Origin{21337, 1, uint256{}},
|
||||
ExportOriginMemo::Anchor{0, uint256{}});
|
||||
BEAST_EXPECT(projected);
|
||||
if (!projected)
|
||||
return;
|
||||
Serializer projectedBytes;
|
||||
projected.value().add(projectedBytes);
|
||||
BEAST_EXPECT(
|
||||
projectedBytes.size() > ExportLimits::maxExportReleaseTargetBytes);
|
||||
auto const oversizedIntent =
|
||||
exportIntent(alice, oversizedTarget, last + 1, committee);
|
||||
auto const oversizedFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(oversizedIntent).stx);
|
||||
env(oversizedIntent, fee(oversizedFee), ter(temMALFORMED));
|
||||
}
|
||||
|
||||
void
|
||||
testFeeShape(FeatureBitset const& features)
|
||||
{
|
||||
testcase("Export fee scales by committee while admin stays ordinary");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{*this, exportTestConfig(), features};
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
env.fund(XRP(10'000), alice, carol);
|
||||
env.close();
|
||||
|
||||
std::vector<PublicKey> members;
|
||||
members.reserve(ExportLimits::maxCommitteeMembers);
|
||||
for (std::size_t i = 0; i < ExportLimits::maxCommitteeMembers; ++i)
|
||||
members.push_back(randomKeyPair(KeyType::secp256k1).first);
|
||||
auto const largeCommittee = serializeExportCommittee(members);
|
||||
auto const oneMember = serializeExportCommittee({members.front()});
|
||||
|
||||
Json::Value setup;
|
||||
setup[jss::TransactionType] = jss::Export;
|
||||
setup[jss::Account] = alice.human();
|
||||
setup[sfExportCommittee.jsonName] = strHex(largeCommittee);
|
||||
auto const setupFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(setup).stx);
|
||||
BEAST_EXPECT(setupFee == env.current()->fees().base);
|
||||
|
||||
Json::Value oneMemberSetup = setup;
|
||||
oneMemberSetup[sfExportCommittee.jsonName] = strHex(oneMember);
|
||||
env(oneMemberSetup, fee(env.current()->fees().base), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const sequence = env.current()->seq();
|
||||
auto const last = sequence + ExportLimits::maxAdmissionWindowLedgers;
|
||||
auto const target =
|
||||
exportedPayment(alice.id(), carol.id(), sequence + 1, last);
|
||||
auto const smallIntent = exportIntent(alice, target, last, oneMember);
|
||||
auto const largeIntent =
|
||||
exportIntent(alice, target, last, largeCommittee);
|
||||
auto const smallFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(smallIntent).stx);
|
||||
auto const largeFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(largeIntent).stx);
|
||||
|
||||
auto residentIntent = smallIntent;
|
||||
residentIntent.removeMember(sfExportCommittee.jsonName);
|
||||
auto const residentFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(residentIntent).stx);
|
||||
|
||||
BEAST_EXPECT(largeFee > smallFee);
|
||||
BEAST_EXPECT(residentFee == smallFee);
|
||||
BEAST_EXPECT(
|
||||
largeFee ==
|
||||
expectedFee(
|
||||
*env.current(),
|
||||
*env.jt(largeIntent).stx,
|
||||
ExportLimits::maxCommitteeMembers));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
auto const features = jtx::supported_amendments() | featureExport;
|
||||
testIntentFeeAndEnforcement(features);
|
||||
testFeeShape(features);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportFee, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,455 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES WHATSOEVER
|
||||
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
|
||||
#include <xrpld/ledger/Sandbox.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
struct ExportLatch_test : beast::unit_test::suite
|
||||
{
|
||||
static std::shared_ptr<SLE>
|
||||
makeLatch(AccountID const& account, std::uint32_t ordinal)
|
||||
{
|
||||
uint256 const origin{ordinal + 1};
|
||||
auto latch =
|
||||
std::make_shared<SLE>(keylet::exportLatch(account, origin));
|
||||
latch->setAccountID(sfAccount, account);
|
||||
latch->setFieldU32(sfTicketSequence, 50'000 + ordinal);
|
||||
latch->setFieldH256(sfTransactionHash, origin);
|
||||
latch->setFieldH256(sfDigest, uint256{10'000 + ordinal});
|
||||
latch->setFieldU32(sfLedgerSequence, 4'000'000 + ordinal);
|
||||
latch->setFieldH256(sfExportCommitteeHash, uint256{20'000 + ordinal});
|
||||
return latch;
|
||||
}
|
||||
|
||||
void
|
||||
testDirectoryLifecycle()
|
||||
{
|
||||
testcase("enhanced latch directory lifecycle");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
Env env{*this};
|
||||
env.fund(XRP(10'000), alice);
|
||||
env(ticket::create(alice, dirNodeMaxEntries));
|
||||
env.close();
|
||||
|
||||
beast::Journal j{beast::Journal::getNullSink()};
|
||||
Sandbox sb{env.closed().get(), tapNONE};
|
||||
std::vector<Keylet> latches;
|
||||
latches.reserve(dirNodeMaxEntries + 1);
|
||||
|
||||
for (std::uint32_t i = 0; i <= dirNodeMaxEntries; ++i)
|
||||
{
|
||||
auto latch = makeLatch(alice.id(), i);
|
||||
latches.emplace_back(keylet::unchecked(latch->key()));
|
||||
auto const ter =
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, latch, j);
|
||||
if (!BEAST_EXPECTS(
|
||||
isTesSuccess(ter),
|
||||
"ordinal=" + std::to_string(i) + " ter=" + transToken(ter)))
|
||||
return;
|
||||
}
|
||||
|
||||
auto const account = sb.read(keylet::account(alice.id()));
|
||||
auto const pending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(account);
|
||||
BEAST_EXPECT(pending);
|
||||
if (!account || !pending)
|
||||
return;
|
||||
BEAST_EXPECT(account->getFieldU16(sfExportCount) == 33);
|
||||
BEAST_EXPECT(account->getFieldU32(sfOwnerCount) == 65);
|
||||
BEAST_EXPECT(pending->getFieldU16(sfExportCount) == 33);
|
||||
BEAST_EXPECT(pending->getFieldV256(sfIndexes).size() == 32);
|
||||
|
||||
auto const first = sb.read(latches.front());
|
||||
auto const last = sb.read(latches.back());
|
||||
BEAST_EXPECT(first && first->getFieldU64(sfOwnerNode) == 1);
|
||||
BEAST_EXPECT(first && first->isFieldPresent(sfExportNode));
|
||||
BEAST_EXPECT(first && first->getFieldU64(sfExportNode) == 0);
|
||||
BEAST_EXPECT(last && last->getFieldU64(sfOwnerNode) == 2);
|
||||
BEAST_EXPECT(last && last->getFieldU64(sfExportNode) == 1);
|
||||
if (!first || !last)
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::removePendingExportLink(sb, latches.front(), j)));
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::removePendingExportLink(sb, latches.back(), j)));
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::removePendingExportLink(sb, latches.back(), j)));
|
||||
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::eraseExportLatch(sb, sb, latches.front(), j)));
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::eraseExportLatch(sb, sb, latches[1], j)));
|
||||
|
||||
auto const afterAccount = sb.read(keylet::account(alice.id()));
|
||||
auto const afterPending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(afterAccount->getFieldU16(sfExportCount) == 31);
|
||||
BEAST_EXPECT(afterAccount->getFieldU32(sfOwnerCount) == 63);
|
||||
BEAST_EXPECT(afterPending->getFieldU16(sfExportCount) == 30);
|
||||
|
||||
Sandbox reopened{&sb};
|
||||
std::set<uint256> recovered;
|
||||
forEachItem(
|
||||
reopened,
|
||||
keylet::pendingExports(),
|
||||
[&](std::shared_ptr<SLE const> const& sle) {
|
||||
recovered.insert(sle->key());
|
||||
});
|
||||
BEAST_EXPECT(recovered.size() == 30);
|
||||
std::set<uint256> expected;
|
||||
for (std::size_t i = 2; i + 1 < latches.size(); ++i)
|
||||
expected.insert(latches[i].key);
|
||||
BEAST_EXPECT(recovered == expected);
|
||||
|
||||
for (std::size_t i = 2; i < latches.size(); ++i)
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::eraseExportLatch(sb, sb, latches[i], j)));
|
||||
|
||||
auto const finalAccount = sb.read(keylet::account(alice.id()));
|
||||
auto const finalPending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(finalAccount->getFieldU16(sfExportCount) == 0);
|
||||
BEAST_EXPECT(finalAccount->getFieldU32(sfOwnerCount) == 32);
|
||||
BEAST_EXPECT(finalPending);
|
||||
BEAST_EXPECT(finalPending->getFieldU16(sfExportCount) == 0);
|
||||
BEAST_EXPECT(finalPending->getFieldV256(sfIndexes).empty());
|
||||
BEAST_EXPECT(finalPending->getFieldU64(sfIndexNext) == 0);
|
||||
BEAST_EXPECT(finalPending->getFieldU64(sfIndexPrevious) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testWitnessedLatchReleasesGlobalPendingCap()
|
||||
{
|
||||
testcase("witnessed latch releases global pending cap");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
Env env{*this};
|
||||
env.fund(XRP(10'000), alice);
|
||||
env.close();
|
||||
|
||||
beast::Journal j{beast::Journal::getNullSink()};
|
||||
Sandbox sb{env.closed().get(), tapNONE};
|
||||
std::vector<Keylet> latches;
|
||||
latches.reserve(ExportLimits::maxLiveExportLatches);
|
||||
|
||||
for (std::uint32_t i = 0; i < ExportLimits::maxLiveExportLatches; ++i)
|
||||
{
|
||||
auto latch = makeLatch(alice.id(), i);
|
||||
latches.emplace_back(keylet::unchecked(latch->key()));
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, latch, j)));
|
||||
}
|
||||
|
||||
auto const accountAtCap = sb.read(keylet::account(alice.id()));
|
||||
auto const rootAtCap = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(accountAtCap);
|
||||
BEAST_EXPECT(rootAtCap);
|
||||
if (!accountAtCap || !rootAtCap)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
rootAtCap->getFieldU16(sfExportCount) ==
|
||||
ExportLimits::maxLiveExportLatches);
|
||||
auto const ownerCountAtCap = accountAtCap->getFieldU32(sfOwnerCount);
|
||||
|
||||
auto replacement =
|
||||
makeLatch(alice.id(), ExportLimits::maxLiveExportLatches);
|
||||
auto const replacementKey = keylet::unchecked(replacement->key());
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, replacement, j) ==
|
||||
tecDIR_FULL);
|
||||
auto const accountAfterRejection = sb.read(keylet::account(alice.id()));
|
||||
auto const rootAfterRejection = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(accountAfterRejection);
|
||||
BEAST_EXPECT(rootAfterRejection);
|
||||
if (!accountAfterRejection || !rootAfterRejection)
|
||||
return;
|
||||
BEAST_EXPECT(!sb.exists(replacementKey));
|
||||
BEAST_EXPECT(
|
||||
accountAfterRejection->getFieldU16(sfExportCount) ==
|
||||
ExportLimits::maxLiveExportLatches);
|
||||
BEAST_EXPECT(
|
||||
accountAfterRejection->getFieldU32(sfOwnerCount) ==
|
||||
ownerCountAtCap);
|
||||
BEAST_EXPECT(
|
||||
rootAfterRejection->getFieldU16(sfExportCount) ==
|
||||
ExportLimits::maxLiveExportLatches);
|
||||
|
||||
uint256 const witnessHash{99'999};
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::recordExportWitness(
|
||||
sb, sb, latches.front(), witnessHash, j)));
|
||||
|
||||
auto const witnessed = sb.read(latches.front());
|
||||
auto const accountAfterWitness = sb.read(keylet::account(alice.id()));
|
||||
auto const rootAfterWitness = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(witnessed);
|
||||
BEAST_EXPECT(accountAfterWitness);
|
||||
BEAST_EXPECT(rootAfterWitness);
|
||||
if (!witnessed || !accountAfterWitness || !rootAfterWitness)
|
||||
return;
|
||||
BEAST_EXPECT(!witnessed->isFieldPresent(sfExportNode));
|
||||
BEAST_EXPECT(
|
||||
witnessed->getFieldH256(sfExportSignatureHash) == witnessHash);
|
||||
BEAST_EXPECT(
|
||||
accountAfterWitness->getFieldU16(sfExportCount) ==
|
||||
ExportLimits::maxLiveExportLatches);
|
||||
BEAST_EXPECT(
|
||||
accountAfterWitness->getFieldU32(sfOwnerCount) == ownerCountAtCap);
|
||||
BEAST_EXPECT(
|
||||
rootAfterWitness->getFieldU16(sfExportCount) ==
|
||||
ExportLimits::maxLiveExportLatches - 1);
|
||||
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, replacement, j)));
|
||||
BEAST_EXPECT(
|
||||
sb.read(keylet::pendingExports())->getFieldU16(sfExportCount) ==
|
||||
ExportLimits::maxLiveExportLatches);
|
||||
BEAST_EXPECT(sb.exists(latches.front()));
|
||||
}
|
||||
|
||||
void
|
||||
testRetainedLatchAcceptsBothFactOrders()
|
||||
{
|
||||
testcase("retained latch accepts XPOP and witness in either order");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
Env env{*this};
|
||||
env.fund(XRP(10'000), alice);
|
||||
env.close();
|
||||
|
||||
beast::Journal j{beast::Journal::getNullSink()};
|
||||
Sandbox sb{env.closed().get(), tapNONE};
|
||||
auto const baselineAccount = sb.read(keylet::account(alice.id()));
|
||||
BEAST_EXPECT(baselineAccount);
|
||||
if (!baselineAccount)
|
||||
return;
|
||||
auto const baselineExportCount =
|
||||
ExportLedgerOps::exportLatchCount(*baselineAccount);
|
||||
auto const baselineOwnerCount =
|
||||
baselineAccount->getFieldU32(sfOwnerCount);
|
||||
auto const baselineReserve =
|
||||
sb.fees().accountReserve(baselineOwnerCount);
|
||||
|
||||
auto xpopFirst = makeLatch(alice.id(), 0);
|
||||
auto const xpopFirstKey = keylet::unchecked(xpopFirst->key());
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, xpopFirst, j)));
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
|
||||
sb,
|
||||
sb,
|
||||
alice.id(),
|
||||
xpopFirst->getFieldH256(sfTransactionHash),
|
||||
false,
|
||||
j)));
|
||||
|
||||
auto const canceled = sb.read(xpopFirstKey);
|
||||
BEAST_EXPECT(canceled);
|
||||
if (!canceled)
|
||||
return;
|
||||
auto const canceledFlags = canceled->isFieldPresent(sfFlags)
|
||||
? canceled->getFieldU32(sfFlags)
|
||||
: std::uint32_t{0};
|
||||
BEAST_EXPECT((canceledFlags & lsfExportCanceled) != 0);
|
||||
BEAST_EXPECT(!canceled->isFieldPresent(sfExportNode));
|
||||
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::recordExportXpop(sb, sb, xpopFirstKey, j)));
|
||||
auto const afterXpop = sb.read(xpopFirstKey);
|
||||
BEAST_EXPECT(afterXpop);
|
||||
if (!afterXpop)
|
||||
return;
|
||||
auto const xpopFlags = afterXpop->getFieldU32(sfFlags);
|
||||
BEAST_EXPECT((xpopFlags & lsfExportCanceled) != 0);
|
||||
BEAST_EXPECT((xpopFlags & lsfExportXpopSeen) != 0);
|
||||
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::recordExportWitness(
|
||||
sb, sb, xpopFirstKey, uint256{99'999}, j)));
|
||||
BEAST_EXPECT(!sb.exists(xpopFirstKey));
|
||||
auto const accountAfterWitness = sb.read(keylet::account(alice.id()));
|
||||
auto const rootAfterWitness = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(accountAfterWitness);
|
||||
BEAST_EXPECT(rootAfterWitness);
|
||||
if (!accountAfterWitness || !rootAfterWitness)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*accountAfterWitness) ==
|
||||
baselineExportCount);
|
||||
BEAST_EXPECT(
|
||||
accountAfterWitness->getFieldU32(sfOwnerCount) ==
|
||||
baselineOwnerCount);
|
||||
BEAST_EXPECT(
|
||||
sb.fees().accountReserve(accountAfterWitness->getFieldU32(
|
||||
sfOwnerCount)) == baselineReserve);
|
||||
BEAST_EXPECT(ExportLedgerOps::exportLatchCount(*rootAfterWitness) == 0);
|
||||
|
||||
auto witnessFirst = makeLatch(alice.id(), 1);
|
||||
auto const witnessFirstKey = keylet::unchecked(witnessFirst->key());
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::insertPendingExportLatch(
|
||||
sb, sb, witnessFirst, j)));
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
|
||||
sb,
|
||||
sb,
|
||||
alice.id(),
|
||||
witnessFirst->getFieldH256(sfTransactionHash),
|
||||
false,
|
||||
j)));
|
||||
|
||||
// The accepted witness may already have been materialized before
|
||||
// cancellation even when consensus orders its application afterward.
|
||||
uint256 const witnessHash{100'000};
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::recordExportWitness(
|
||||
sb, sb, witnessFirstKey, witnessHash, j)));
|
||||
auto const afterWitness = sb.read(witnessFirstKey);
|
||||
BEAST_EXPECT(afterWitness);
|
||||
if (!afterWitness)
|
||||
return;
|
||||
auto const witnessFlags = afterWitness->getFieldU32(sfFlags);
|
||||
BEAST_EXPECT((witnessFlags & lsfExportCanceled) != 0);
|
||||
BEAST_EXPECT(
|
||||
afterWitness->getFieldH256(sfExportSignatureHash) == witnessHash);
|
||||
BEAST_EXPECT(!afterWitness->isFieldPresent(sfExportNode));
|
||||
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::recordExportXpop(sb, sb, witnessFirstKey, j)));
|
||||
BEAST_EXPECT(!sb.exists(witnessFirstKey));
|
||||
auto const accountAfterXpop = sb.read(keylet::account(alice.id()));
|
||||
auto const rootAfterXpop = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(accountAfterXpop);
|
||||
BEAST_EXPECT(rootAfterXpop);
|
||||
if (!accountAfterXpop || !rootAfterXpop)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*accountAfterXpop) ==
|
||||
baselineExportCount);
|
||||
BEAST_EXPECT(
|
||||
accountAfterXpop->getFieldU32(sfOwnerCount) == baselineOwnerCount);
|
||||
BEAST_EXPECT(
|
||||
sb.fees().accountReserve(accountAfterXpop->getFieldU32(
|
||||
sfOwnerCount)) == baselineReserve);
|
||||
BEAST_EXPECT(ExportLedgerOps::exportLatchCount(*rootAfterXpop) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testExplicitEraseAndExpiryRetention()
|
||||
{
|
||||
testcase("explicit erase and expiry retention");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
Env env{*this};
|
||||
env.fund(XRP(10'000), alice);
|
||||
env.close();
|
||||
|
||||
beast::Journal j{beast::Journal::getNullSink()};
|
||||
Sandbox sb{env.closed().get(), tapNONE};
|
||||
auto const baselineAccount = sb.read(keylet::account(alice.id()));
|
||||
auto const baselinePending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(baselineAccount);
|
||||
if (!baselineAccount)
|
||||
return;
|
||||
auto const baselineExportCount =
|
||||
ExportLedgerOps::exportLatchCount(*baselineAccount);
|
||||
auto const baselineOwnerCount =
|
||||
baselineAccount->getFieldU32(sfOwnerCount);
|
||||
auto const baselinePendingCount = baselinePending
|
||||
? ExportLedgerOps::exportLatchCount(*baselinePending)
|
||||
: std::uint16_t{0};
|
||||
|
||||
auto erased = makeLatch(alice.id(), 0);
|
||||
auto const erasedOrigin = erased->getFieldH256(sfTransactionHash);
|
||||
auto const erasedKey = keylet::exportLatch(alice.id(), erasedOrigin);
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, erased, j)));
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
|
||||
sb, sb, alice.id(), erasedOrigin, true, j)));
|
||||
BEAST_EXPECT(!sb.exists(erasedKey));
|
||||
|
||||
auto const afterEraseAccount = sb.read(keylet::account(alice.id()));
|
||||
auto const afterErasePending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(afterEraseAccount);
|
||||
BEAST_EXPECT(afterErasePending);
|
||||
if (!afterEraseAccount || !afterErasePending)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*afterEraseAccount) ==
|
||||
baselineExportCount);
|
||||
BEAST_EXPECT(
|
||||
afterEraseAccount->getFieldU32(sfOwnerCount) == baselineOwnerCount);
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*afterErasePending) ==
|
||||
baselinePendingCount);
|
||||
|
||||
auto expired = makeLatch(alice.id(), 1);
|
||||
expired->setFieldU32(sfLastLedgerSequence, 100);
|
||||
auto const expiredOrigin = expired->getFieldH256(sfTransactionHash);
|
||||
auto const expiredKey = keylet::exportLatch(alice.id(), expiredOrigin);
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, expired, j)));
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::pruneExpiredExportLatches(sb, sb, 101, j)));
|
||||
|
||||
auto const retained = sb.read(expiredKey);
|
||||
auto const afterExpiryAccount = sb.read(keylet::account(alice.id()));
|
||||
auto const afterExpiryPending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(retained);
|
||||
BEAST_EXPECT(afterExpiryAccount);
|
||||
BEAST_EXPECT(afterExpiryPending);
|
||||
if (!retained || !afterExpiryAccount || !afterExpiryPending)
|
||||
return;
|
||||
BEAST_EXPECT(!retained->isFieldPresent(sfExportNode));
|
||||
BEAST_EXPECT((retained->getFieldU32(sfFlags) & lsfExportCanceled) != 0);
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*afterExpiryAccount) ==
|
||||
baselineExportCount + 1);
|
||||
BEAST_EXPECT(
|
||||
afterExpiryAccount->getFieldU32(sfOwnerCount) ==
|
||||
baselineOwnerCount + 1);
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*afterExpiryPending) ==
|
||||
baselinePendingCount);
|
||||
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
|
||||
sb, sb, alice.id(), expiredOrigin, true, j)));
|
||||
BEAST_EXPECT(!sb.exists(expiredKey));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDirectoryLifecycle();
|
||||
testWitnessedLatchReleasesGlobalPendingCap();
|
||||
testRetainedLatchAcceptsBothFactOrders();
|
||||
testExplicitEraseAndExpiryRetention();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportLatch, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,881 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/ExportOriginMemo.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
makeHash(char const* label)
|
||||
{
|
||||
return sha512Half(Slice(label, std::strlen(label)));
|
||||
}
|
||||
|
||||
STTx
|
||||
makeSTTx(STObject const& obj)
|
||||
{
|
||||
Serializer s;
|
||||
obj.add(s);
|
||||
SerialIter sit{s.slice()};
|
||||
return STTx{std::ref(sit)};
|
||||
}
|
||||
|
||||
STTx
|
||||
makeExportedPayment(AccountID const& src, AccountID const& dst)
|
||||
{
|
||||
STObject obj(sfExportedTxn);
|
||||
obj.setFieldU16(sfTransactionType, ttPAYMENT);
|
||||
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldU32(sfTicketSequence, 1);
|
||||
obj.setFieldU32(sfFirstLedgerSequence, 2);
|
||||
obj.setFieldU32(sfLastLedgerSequence, 6);
|
||||
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
|
||||
obj.setFieldAmount(sfFee, XRPAmount{10});
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
obj.setAccountID(sfAccount, src);
|
||||
obj.setAccountID(sfDestination, dst);
|
||||
return makeSTTx(obj);
|
||||
}
|
||||
|
||||
std::optional<STTx>
|
||||
makeExportedPaymentWithSize(
|
||||
AccountID const& src,
|
||||
AccountID const& dst,
|
||||
std::size_t const serializedSize)
|
||||
{
|
||||
auto const base = makeExportedPayment(src, dst);
|
||||
for (std::size_t payloadBytes = 0; payloadBytes <= serializedSize;
|
||||
++payloadBytes)
|
||||
{
|
||||
auto candidate = base;
|
||||
STArray memos(sfMemos);
|
||||
STObject memo(sfMemo);
|
||||
memo.setFieldVL(sfMemoData, Blob(payloadBytes, 0xCC));
|
||||
memos.emplace_back(std::move(memo));
|
||||
candidate.setFieldArray(sfMemos, std::move(memos));
|
||||
|
||||
auto const size = candidate.getSerializer().size();
|
||||
if (size == serializedSize)
|
||||
return candidate;
|
||||
if (size > serializedSize)
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
STTx
|
||||
makeExportedPaymentChannelClaim(
|
||||
AccountID const& src,
|
||||
Blob const& channelSignature)
|
||||
{
|
||||
STObject obj(sfExportedTxn);
|
||||
obj.setFieldU16(sfTransactionType, ttPAYCHAN_CLAIM);
|
||||
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldU32(sfTicketSequence, 1);
|
||||
obj.setFieldU32(sfFirstLedgerSequence, 2);
|
||||
obj.setFieldU32(sfLastLedgerSequence, 6);
|
||||
obj.setFieldAmount(sfFee, XRPAmount{10});
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
obj.setAccountID(sfAccount, src);
|
||||
obj.setFieldH256(sfChannel, makeHash("payment-channel"));
|
||||
obj.setFieldVL(sfSignature, channelSignature);
|
||||
return makeSTTx(obj);
|
||||
}
|
||||
|
||||
ExportResultBuilder::PositionedSignatureSnapshot
|
||||
positionedSignatures(ExportResultBuilder::SignatureSnapshot const& signatures)
|
||||
{
|
||||
ExportResultBuilder::PositionedSignatureSnapshot result;
|
||||
std::uint16_t position = 0;
|
||||
for (auto const& [key, signature] : signatures)
|
||||
{
|
||||
result.emplace(
|
||||
position++,
|
||||
ExportResultBuilder::PositionedSignature{key, signature});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::pair<PublicKey, SecretKey>
|
||||
deterministicKeyPair(char const* label)
|
||||
{
|
||||
return generateKeyPair(KeyType::secp256k1, generateSeed(label));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ExportResultBuilder_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
testAssemblesMultiSignedTransaction()
|
||||
{
|
||||
testcase("assembles multisigned transaction");
|
||||
|
||||
auto const signerA = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signerB = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(signerA.first), calcAccountID(signerB.first));
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
signatures.emplace(
|
||||
signerA.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signerA.first, signerA.second));
|
||||
signatures.emplace(
|
||||
signerB.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signerB.first, signerB.second));
|
||||
|
||||
auto const multiSigned =
|
||||
ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, signatures);
|
||||
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
|
||||
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
|
||||
|
||||
auto const& signers = multiSigned.getFieldArray(sfSigners);
|
||||
BEAST_EXPECT(signers.size() == 2);
|
||||
if (signers.size() == 2)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
signers[0].getAccountID(sfAccount) <
|
||||
signers[1].getAccountID(sfAccount));
|
||||
}
|
||||
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
auto const pkVL = signer.getFieldVL(sfSigningPubKey);
|
||||
PublicKey const pk{makeSlice(pkVL)};
|
||||
auto const sigVL = signer.getFieldVL(sfTxnSignature);
|
||||
auto const signerAcctID = signer.getAccountID(sfAccount);
|
||||
auto const sigData = buildMultiSigningData(innerTx, signerAcctID);
|
||||
BEAST_EXPECT(verify(pk, sigData.slice(), makeSlice(sigVL)));
|
||||
}
|
||||
|
||||
auto const signedTxHash =
|
||||
multiSigned.getHash(HashPrefix::transactionID);
|
||||
|
||||
Serializer serialized;
|
||||
multiSigned.add(serialized);
|
||||
SerialIter sit(serialized.slice());
|
||||
STTx signedTx{std::ref(sit)};
|
||||
BEAST_EXPECT(signedTx.getTransactionID() == signedTxHash);
|
||||
}
|
||||
|
||||
void
|
||||
testBuildMultiSignedExportedTxnDirect()
|
||||
{
|
||||
testcase("builds multisigned exported transaction directly");
|
||||
|
||||
auto const signerA = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signerB = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(signerA.first), calcAccountID(dst.first));
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
signatures.emplace(signerB.first, Buffer{});
|
||||
signatures.emplace(
|
||||
signerA.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signerA.first, signerA.second));
|
||||
|
||||
auto multiSigned = ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, signatures);
|
||||
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
|
||||
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
|
||||
|
||||
auto const& signers = multiSigned.getFieldArray(sfSigners);
|
||||
BEAST_EXPECT(signers.size() == 1);
|
||||
if (signers.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
signers[0].getAccountID(sfAccount) ==
|
||||
calcAccountID(signerA.first));
|
||||
BEAST_EXPECT(
|
||||
makeSlice(signers[0].getFieldVL(sfSigningPubKey)) ==
|
||||
signerA.first.slice());
|
||||
}
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot none;
|
||||
auto unsignedMulti =
|
||||
ExportResultBuilder::buildMultiSignedExportedTxn(innerTx, none);
|
||||
BEAST_EXPECT(unsignedMulti.getFieldVL(sfSigningPubKey).empty());
|
||||
BEAST_EXPECT(!unsignedMulti.isFieldPresent(sfSigners));
|
||||
}
|
||||
|
||||
void
|
||||
testExportIntentHashIgnoresSignerSubset()
|
||||
{
|
||||
testcase("export intent hash ignores signer subset");
|
||||
|
||||
auto const src = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const otherDst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signerA = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signerB = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signerC = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot subsetAB;
|
||||
subsetAB.emplace(
|
||||
signerA.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signerA.first, signerA.second));
|
||||
subsetAB.emplace(
|
||||
signerB.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signerB.first, signerB.second));
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot subsetBC;
|
||||
subsetBC.emplace(
|
||||
signerB.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signerB.first, signerB.second));
|
||||
subsetBC.emplace(
|
||||
signerC.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signerC.first, signerC.second));
|
||||
|
||||
auto const signedAB =
|
||||
makeSTTx(ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, subsetAB));
|
||||
auto const signedBC =
|
||||
makeSTTx(ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, subsetBC));
|
||||
|
||||
BEAST_EXPECT(
|
||||
signedAB.getTransactionID() != signedBC.getTransactionID());
|
||||
auto const intentHash = ExportResultBuilder::exportIntentHash(innerTx);
|
||||
BEAST_EXPECT(
|
||||
ExportResultBuilder::exportIntentHash(signedAB) == intentHash);
|
||||
BEAST_EXPECT(
|
||||
ExportResultBuilder::exportIntentHash(signedBC) == intentHash);
|
||||
|
||||
auto singleAuthorized = innerTx;
|
||||
singleAuthorized.setFieldVL(sfSigningPubKey, signerA.first.slice());
|
||||
singleAuthorized.setFieldVL(sfTxnSignature, Blob{1, 2, 3});
|
||||
BEAST_EXPECT(
|
||||
ExportResultBuilder::exportIntentHash(singleAuthorized) ==
|
||||
intentHash);
|
||||
|
||||
auto const otherIntent = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(otherDst.first));
|
||||
BEAST_EXPECT(
|
||||
ExportResultBuilder::exportIntentHash(otherIntent) != intentHash);
|
||||
|
||||
Blob const claimSignatureA{1, 2, 3};
|
||||
auto const claimA = makeExportedPaymentChannelClaim(
|
||||
calcAccountID(src.first), claimSignatureA);
|
||||
auto const claimB = makeExportedPaymentChannelClaim(
|
||||
calcAccountID(src.first), Blob{4, 5, 6});
|
||||
BEAST_EXPECT(
|
||||
ExportResultBuilder::exportIntentHash(claimA) ==
|
||||
ExportResultBuilder::exportIntentHash(claimB));
|
||||
|
||||
auto const normalizedClaim =
|
||||
ExportResultBuilder::buildMultiSignedExportedTxn(claimA, {});
|
||||
BEAST_EXPECT(
|
||||
normalizedClaim.getFieldVL(sfSignature) == claimSignatureA);
|
||||
}
|
||||
|
||||
void
|
||||
testCapsSignerArray()
|
||||
{
|
||||
testcase("caps exported signer array");
|
||||
|
||||
auto const src = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
while (signatures.size() < STTx::maxMultiSigners() + 4)
|
||||
{
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
signatures.emplace(
|
||||
signer.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signer.first, signer.second));
|
||||
}
|
||||
|
||||
auto const multiSigned =
|
||||
ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, signatures);
|
||||
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
|
||||
|
||||
if (multiSigned.isFieldPresent(sfSigners))
|
||||
{
|
||||
auto const& signers = multiSigned.getFieldArray(sfSigners);
|
||||
BEAST_EXPECT(signers.size() == STTx::maxMultiSigners());
|
||||
for (std::size_t i = 1; i < signers.size(); ++i)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
signers[i - 1].getAccountID(sfAccount) <
|
||||
signers[i].getAccountID(sfAccount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSignatureWitnessRoundTrip()
|
||||
{
|
||||
testcase("signature witness round trip");
|
||||
|
||||
auto const src = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
auto const exportTxHash = makeHash("outer-export-witness");
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
while (signatures.size() < STTx::maxMultiSigners())
|
||||
{
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
signatures.emplace(
|
||||
signer.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signer.first, signer.second));
|
||||
}
|
||||
|
||||
auto const positioned = positionedSignatures(signatures);
|
||||
Blob const contributors(
|
||||
STTx::maxMultiSigners() / 8, std::uint8_t{0xFF});
|
||||
auto witness = ExportResultBuilder::buildSignatureWitness(
|
||||
exportTxHash, innerTx, positioned, STTx::maxMultiSigners(), 654);
|
||||
BEAST_EXPECT(witness.getTxnType() == ttEXPORT_SIGNATURES);
|
||||
BEAST_EXPECT(witness.getFieldU32(sfLedgerSequence) == 654);
|
||||
BEAST_EXPECT(witness.getFieldH256(sfTransactionHash) == exportTxHash);
|
||||
BEAST_EXPECT(witness.getFieldVL(sfExportContributors) == contributors);
|
||||
BEAST_EXPECT(!witness.isFieldPresent(sfSigners));
|
||||
|
||||
auto const& exported =
|
||||
witness.peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
BEAST_EXPECT(exported.getFieldU16(sfTransactionType) == ttPAYMENT);
|
||||
BEAST_EXPECT(!exported.isFieldPresent(sfSigners));
|
||||
BEAST_EXPECT(
|
||||
witness.getFieldArray(sfExportSigners).size() ==
|
||||
STTx::maxMultiSigners());
|
||||
|
||||
auto decoded = ExportResultBuilder::signaturesFromWitness(witness);
|
||||
BEAST_EXPECT(decoded);
|
||||
if (decoded)
|
||||
{
|
||||
BEAST_EXPECT(decoded->size() == STTx::maxMultiSigners());
|
||||
std::uint16_t expectedPosition = 0;
|
||||
for (auto const& [position, signature] : *decoded)
|
||||
{
|
||||
BEAST_EXPECT(position == expectedPosition++);
|
||||
auto const it = signatures.find(signature.signingKey);
|
||||
BEAST_EXPECT(it != signatures.end());
|
||||
if (it != signatures.end())
|
||||
BEAST_EXPECT(it->second == signature.signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSparseSignatureWitnessRoundTrip()
|
||||
{
|
||||
testcase("sparse cross-byte signature witness round trip");
|
||||
|
||||
auto const src = deterministicKeyPair("d-attrib-source");
|
||||
auto const dst = deterministicKeyPair("d-attrib-destination");
|
||||
auto const signer0 = deterministicKeyPair("d-attrib-signer-0");
|
||||
auto const signer7 = deterministicKeyPair("d-attrib-signer-7");
|
||||
auto const signer9 = deterministicKeyPair("d-attrib-signer-9");
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
|
||||
ExportResultBuilder::PositionedSignatureSnapshot signatures;
|
||||
for (auto const& [position, signer] : std::array{
|
||||
std::pair{std::uint16_t{0}, std::cref(signer0)},
|
||||
std::pair{std::uint16_t{7}, std::cref(signer7)},
|
||||
std::pair{std::uint16_t{9}, std::cref(signer9)}})
|
||||
{
|
||||
signatures.emplace(
|
||||
position,
|
||||
ExportResultBuilder::PositionedSignature{
|
||||
signer.get().first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signer.get().first, signer.get().second)});
|
||||
}
|
||||
|
||||
auto const witness = ExportResultBuilder::buildSignatureWitness(
|
||||
makeHash("d-attrib-export"), innerTx, signatures, 10, 654);
|
||||
Blob const expectedContributors{0x81, 0x02};
|
||||
BEAST_EXPECT(
|
||||
witness.getFieldVL(sfExportContributors) == expectedContributors);
|
||||
|
||||
auto const serialized = witness.getSerializer();
|
||||
BEAST_EXPECT(
|
||||
strHex(serialized.slice()) ==
|
||||
"12006A2400000000260000028E5316E18499CA512AC0B68C103C249FF20029D4"
|
||||
"6CEFCCD8E6882A481768546DCBF368400000000000000073007023028102811400"
|
||||
"00000000000000000000000000000000000000E0591200002280000000240000"
|
||||
"0000201A00000002201B000000062029000000016140000000000F424068400000"
|
||||
"000000000A7300811431C972A33313AC474C4996CF7463C8D5648AA1B283143A9"
|
||||
"D59046AB0269024A1C12118FB57F514A891BEE1F017E011732102006E9F20ADB30"
|
||||
"B695D059FEA08BF342FA6BD712433B769FD8D1A40098F4780F674463044022033"
|
||||
"A39198530AD920FB0F21A041E4CFD2C9509C085B541BD1058F2B71F8D3F01A02"
|
||||
"206B203D44035D8D653F5F146CF05EA769FB60ABC6246D3EC42CD5B14C541B583"
|
||||
"3E1E0117321030D2368AFD1505BCFDCF91815E073871917B2E276CF1999E0B781"
|
||||
"E9EC681B68D774473045022100824DDF6B8F117F53F11B35EFACC608676B3A0C2"
|
||||
"19591A01E446BFAE747DAE6AC02205955CB02DAA217DE0E3617E0B75E66FE5D09"
|
||||
"6326C4E754DDC46DFCEB992CC394E1E0117321028B8C98F69CFF73B9D66CC108"
|
||||
"8161E10E9B31AE2C075BADC4656C27D10EDE23D774473045022100C4F6FC146A"
|
||||
"2406E20E527572957265FB9EAA3C2F22D40D3B5924880D8E70797602202E54B5"
|
||||
"C0FDDD58D45909F7B47FB5B90E9DE3DD57A1B167E4FDA10CF48E2BFD0FE1F1");
|
||||
BEAST_EXPECT(
|
||||
to_string(witness.getTransactionID()) ==
|
||||
"17BEE8F0DF68D5226CAA778C9DF1B768618AC36ADB0CF6A49A62248173357EF8");
|
||||
SerialIter iter{serialized.slice()};
|
||||
STTx roundTripped{std::ref(iter)};
|
||||
BEAST_EXPECT(
|
||||
roundTripped.getFieldVL(sfExportContributors) ==
|
||||
expectedContributors);
|
||||
BEAST_EXPECT(
|
||||
roundTripped.getTransactionID() == witness.getTransactionID());
|
||||
|
||||
auto const decoded =
|
||||
ExportResultBuilder::signaturesFromWitness(roundTripped);
|
||||
BEAST_EXPECT(decoded);
|
||||
if (decoded)
|
||||
{
|
||||
BEAST_EXPECT(decoded->size() == signatures.size());
|
||||
auto expected = signatures.begin();
|
||||
for (auto const& [position, signature] : *decoded)
|
||||
{
|
||||
BEAST_EXPECT(expected != signatures.end());
|
||||
if (expected == signatures.end())
|
||||
break;
|
||||
BEAST_EXPECT(position == expected->first);
|
||||
BEAST_EXPECT(
|
||||
signature.signingKey == expected->second.signingKey);
|
||||
BEAST_EXPECT(signature.signature == expected->second.signature);
|
||||
++expected;
|
||||
}
|
||||
BEAST_EXPECT(expected == signatures.end());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDestinationSignerOrderIsIndependent()
|
||||
{
|
||||
testcase("destination signer order is independent of witness order");
|
||||
|
||||
auto const src = deterministicKeyPair("d-attrib-order-source");
|
||||
auto const dst = deterministicKeyPair("d-attrib-order-destination");
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
std::array signers{
|
||||
deterministicKeyPair("d-attrib-order-signer-0"),
|
||||
deterministicKeyPair("d-attrib-order-signer-1"),
|
||||
deterministicKeyPair("d-attrib-order-signer-2")};
|
||||
std::sort(
|
||||
signers.begin(),
|
||||
signers.end(),
|
||||
[](auto const& lhs, auto const& rhs) {
|
||||
return calcAccountID(lhs.first) < calcAccountID(rhs.first);
|
||||
});
|
||||
|
||||
ExportResultBuilder::PositionedSignatureSnapshot positioned;
|
||||
std::array<std::uint16_t, 3> const positions{0, 7, 9};
|
||||
for (std::size_t i = 0; i < signers.size(); ++i)
|
||||
{
|
||||
auto const& signer = signers[signers.size() - i - 1];
|
||||
positioned.emplace(
|
||||
positions[i],
|
||||
ExportResultBuilder::PositionedSignature{
|
||||
signer.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signer.first, signer.second)});
|
||||
}
|
||||
|
||||
auto const witness = ExportResultBuilder::buildSignatureWitness(
|
||||
makeHash("d-attrib-order-export"), innerTx, positioned, 10, 654);
|
||||
auto const& witnessEntries = witness.getFieldArray(sfExportSigners);
|
||||
BEAST_EXPECT(witnessEntries.size() == signers.size());
|
||||
if (witnessEntries.size() == signers.size())
|
||||
{
|
||||
for (std::size_t i = 0; i < witnessEntries.size(); ++i)
|
||||
{
|
||||
auto const key = PublicKey{
|
||||
makeSlice(witnessEntries[i].getFieldVL(sfSigningPubKey))};
|
||||
BEAST_EXPECT(
|
||||
calcAccountID(key) ==
|
||||
calcAccountID(signers[signers.size() - i - 1].first));
|
||||
}
|
||||
}
|
||||
|
||||
auto const decoded =
|
||||
ExportResultBuilder::signaturesFromWitness(witness);
|
||||
BEAST_EXPECT(decoded);
|
||||
if (!decoded)
|
||||
return;
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot destinationSignatures;
|
||||
for (auto const& [_, signature] : *decoded)
|
||||
destinationSignatures.emplace(
|
||||
signature.signingKey, signature.signature);
|
||||
auto const assembled = ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, destinationSignatures);
|
||||
auto const& destinationEntries = assembled.getFieldArray(sfSigners);
|
||||
BEAST_EXPECT(destinationEntries.size() == signers.size());
|
||||
if (destinationEntries.size() == signers.size())
|
||||
{
|
||||
for (std::size_t i = 0; i < destinationEntries.size(); ++i)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
destinationEntries[i].getAccountID(sfAccount) ==
|
||||
calcAccountID(signers[i].first));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsMalformedWitnessSigners()
|
||||
{
|
||||
testcase("signature witness rejects malformed signer entries");
|
||||
|
||||
auto const src = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signer0 = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signer1 = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
auto const exportTxHash = makeHash("bad-witness-signer");
|
||||
|
||||
ExportResultBuilder::PositionedSignatureSnapshot signatures;
|
||||
signatures.emplace(
|
||||
0,
|
||||
ExportResultBuilder::PositionedSignature{
|
||||
signer0.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signer0.first, signer0.second)});
|
||||
signatures.emplace(
|
||||
1,
|
||||
ExportResultBuilder::PositionedSignature{
|
||||
signer1.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signer1.first, signer1.second)});
|
||||
|
||||
auto const witness = ExportResultBuilder::buildSignatureWitness(
|
||||
exportTxHash, innerTx, signatures, 2, 654);
|
||||
BEAST_EXPECT(ExportResultBuilder::signaturesFromWitness(witness));
|
||||
|
||||
using Mutator = void (*)(STArray&);
|
||||
struct MalformedCase
|
||||
{
|
||||
char const* name;
|
||||
Mutator mutate;
|
||||
};
|
||||
std::array<MalformedCase, 8> const malformedCases{{
|
||||
{"missing public key",
|
||||
[](STArray& entries) { entries[0].delField(sfSigningPubKey); }},
|
||||
{"missing signature",
|
||||
[](STArray& entries) { entries[0].delField(sfTxnSignature); }},
|
||||
{"extra account",
|
||||
[](STArray& entries) {
|
||||
entries[0].setAccountID(sfAccount, AccountID{});
|
||||
}},
|
||||
{"wrong entry type",
|
||||
[](STArray& entries) { entries[0].setFName(sfSigner); }},
|
||||
{"malformed public key",
|
||||
[](STArray& entries) {
|
||||
entries[0].setFieldVL(sfSigningPubKey, Blob{0x02});
|
||||
}},
|
||||
{"empty signature",
|
||||
[](STArray& entries) {
|
||||
entries[0].setFieldVL(sfTxnSignature, Blob{});
|
||||
}},
|
||||
{"oversized signature",
|
||||
[](STArray& entries) {
|
||||
entries[0].setFieldVL(
|
||||
sfTxnSignature,
|
||||
Blob(
|
||||
ExportLimits::maxCanonicalExportSignatureBytes + 1,
|
||||
0x30));
|
||||
}},
|
||||
{"duplicate key",
|
||||
[](STArray& entries) {
|
||||
entries[1].setFieldVL(
|
||||
sfSigningPubKey, entries[0].getFieldVL(sfSigningPubKey));
|
||||
}},
|
||||
}};
|
||||
|
||||
for (auto const& malformedCase : malformedCases)
|
||||
{
|
||||
auto malformed = witness;
|
||||
auto& entries = malformed.peekFieldArray(sfExportSigners);
|
||||
malformedCase.mutate(entries);
|
||||
BEAST_EXPECTS(
|
||||
!ExportResultBuilder::signaturesFromWitness(malformed),
|
||||
malformedCase.name);
|
||||
}
|
||||
|
||||
auto malformedBitmap = ExportResultBuilder::buildSignatureWitness(
|
||||
exportTxHash, innerTx, signatures, 2, 654);
|
||||
malformedBitmap.setFieldVL(sfExportContributors, Blob{0x00});
|
||||
BEAST_EXPECT(
|
||||
!ExportResultBuilder::signaturesFromWitness(malformedBitmap));
|
||||
|
||||
ExportResultBuilder::PositionedSignatureSnapshot oversized;
|
||||
for (std::uint16_t position = 0;
|
||||
position <= ExportLimits::maxCommitteeMembers;
|
||||
++position)
|
||||
{
|
||||
auto const key = randomKeyPair(KeyType::secp256k1).first;
|
||||
oversized.emplace(
|
||||
position,
|
||||
ExportResultBuilder::PositionedSignature{key, Buffer{0x01}});
|
||||
}
|
||||
except([&] {
|
||||
ExportResultBuilder::buildSignatureWitness(
|
||||
exportTxHash, innerTx, oversized, oversized.size(), 654);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testSerializedSizeInventory()
|
||||
{
|
||||
//@@start export-serialized-size-inventory
|
||||
testcase("serialized size inventory");
|
||||
|
||||
auto const src = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto innerTx = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
|
||||
innerTx.setFieldArray(sfMemos, STArray(sfMemos, 1));
|
||||
STObject memo{sfMemo};
|
||||
memo.setFieldVL(
|
||||
sfMemoType,
|
||||
Blob{
|
||||
'x',
|
||||
'a',
|
||||
'h',
|
||||
'a',
|
||||
'u',
|
||||
'-',
|
||||
'e',
|
||||
'x',
|
||||
'p',
|
||||
'o',
|
||||
'r',
|
||||
't',
|
||||
'-',
|
||||
'v',
|
||||
'1'});
|
||||
memo.setFieldVL(sfMemoData, Blob(45, 0xA5));
|
||||
innerTx.peekFieldArray(sfMemos).emplace_back(std::move(memo));
|
||||
|
||||
std::array<std::uint8_t, 72> signatureBytes;
|
||||
signatureBytes.fill(0x5A);
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
while (signatures.size() < STTx::maxMultiSigners())
|
||||
{
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
signatures.emplace(
|
||||
signer.first,
|
||||
Buffer{signatureBytes.data(), signatureBytes.size()});
|
||||
}
|
||||
|
||||
auto const multiSigned =
|
||||
ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, signatures);
|
||||
auto const positioned = positionedSignatures(signatures);
|
||||
auto const witness = ExportResultBuilder::buildSignatureWitness(
|
||||
makeHash("size-inventory-export"),
|
||||
innerTx,
|
||||
positioned,
|
||||
STTx::maxMultiSigners(),
|
||||
654);
|
||||
auto const release = ExportOriginMemo::releaseForm(
|
||||
innerTx,
|
||||
ExportOriginMemo::Origin{21337, 0, makeHash("size-origin")},
|
||||
ExportOriginMemo::Anchor{653, makeHash("size-ledger")});
|
||||
BEAST_EXPECT(release);
|
||||
if (!release)
|
||||
return;
|
||||
auto const releaseWitness = ExportResultBuilder::buildSignatureWitness(
|
||||
makeHash("size-inventory-export"),
|
||||
release.value(),
|
||||
positioned,
|
||||
STTx::maxMultiSigners(),
|
||||
654);
|
||||
|
||||
auto const innerBytes = innerTx.getSerializer().size();
|
||||
auto const multiSignedBytes = multiSigned.getSerializer().size();
|
||||
auto const selfContainedWitnessBytes = witness.getSerializer().size();
|
||||
auto const releaseWitnessBytes = releaseWitness.getSerializer().size();
|
||||
auto const pricedWitnessBytes = innerBytes +
|
||||
ExportLimits::feeWitnessFixedAllowanceBytes +
|
||||
STTx::maxMultiSigners() *
|
||||
ExportLimits::feeWitnessSignerAllowanceBytes;
|
||||
constexpr std::size_t legacyShareBytes = 32 + 33 + 72;
|
||||
|
||||
log << "Export serialized-size inventory:\n"
|
||||
<< " unsigned target + issuance Memo: " << innerBytes << "\n"
|
||||
<< " 32-signer target: " << multiSignedBytes << "\n"
|
||||
<< " self-contained 32-signer witness: "
|
||||
<< selfContainedWitnessBytes << "\n"
|
||||
<< " stamped self-contained witness: " << releaseWitnessBytes
|
||||
<< "\n"
|
||||
<< " one legacy share blob: " << legacyShareBytes << "\n"
|
||||
<< " 32 legacy share blobs: "
|
||||
<< legacyShareBytes * STTx::maxMultiSigners() << std::endl;
|
||||
|
||||
BEAST_EXPECT(signatures.size() == STTx::maxMultiSigners());
|
||||
BEAST_EXPECT(!witness.isFieldPresent(sfSigners));
|
||||
auto const& exported =
|
||||
witness.peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
BEAST_EXPECT(!exported.isFieldPresent(sfSigners));
|
||||
BEAST_EXPECT(
|
||||
witness.getFieldArray(sfExportSigners).size() ==
|
||||
STTx::maxMultiSigners());
|
||||
Blob const contributors(
|
||||
STTx::maxMultiSigners() / 8, std::uint8_t{0xFF});
|
||||
BEAST_EXPECT(witness.getFieldVL(sfExportContributors) == contributors);
|
||||
BEAST_EXPECT(innerBytes == 163);
|
||||
BEAST_EXPECT(multiSignedBytes == 4453);
|
||||
BEAST_EXPECT(selfContainedWitnessBytes == 3839);
|
||||
BEAST_EXPECT(pricedWitnessBytes == 4643);
|
||||
BEAST_EXPECT(releaseWitnessBytes > selfContainedWitnessBytes);
|
||||
BEAST_EXPECT(pricedWitnessBytes >= releaseWitnessBytes);
|
||||
BEAST_EXPECT(pricedWitnessBytes <= ExportLimits::maxExportWitnessBytes);
|
||||
BEAST_EXPECT(legacyShareBytes * STTx::maxMultiSigners() == 4384);
|
||||
|
||||
STArray oversizedMemos(sfMemos);
|
||||
STObject oversizedMemo(sfMemo);
|
||||
oversizedMemo.setFieldVL(sfMemoData, Blob(6'000, 0xCC));
|
||||
oversizedMemos.emplace_back(std::move(oversizedMemo));
|
||||
|
||||
auto oversizedTarget = innerTx;
|
||||
oversizedTarget.setFieldArray(sfMemos, oversizedMemos);
|
||||
except([&] {
|
||||
ExportResultBuilder::buildSignatureWitness(
|
||||
makeHash("oversized-witness"),
|
||||
oversizedTarget,
|
||||
positioned,
|
||||
STTx::maxMultiSigners(),
|
||||
654);
|
||||
});
|
||||
|
||||
auto oversizedWitness = witness;
|
||||
auto& oversizedExported = const_cast<STObject&>(
|
||||
oversizedWitness.peekAtField(sfExportedTxn).downcast<STObject>());
|
||||
oversizedExported.setFieldArray(sfMemos, std::move(oversizedMemos));
|
||||
BEAST_EXPECT(
|
||||
!ExportResultBuilder::signaturesFromWitness(oversizedWitness));
|
||||
//@@end export-serialized-size-inventory
|
||||
}
|
||||
|
||||
void
|
||||
testReleaseTargetSizeBoundary()
|
||||
{
|
||||
testcase("release target serialized size boundary");
|
||||
|
||||
auto const src = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
auto const atLimit = makeExportedPaymentWithSize(
|
||||
calcAccountID(src.first),
|
||||
calcAccountID(dst.first),
|
||||
ExportLimits::maxExportReleaseTargetBytes);
|
||||
auto const overLimit = makeExportedPaymentWithSize(
|
||||
calcAccountID(src.first),
|
||||
calcAccountID(dst.first),
|
||||
ExportLimits::maxExportReleaseTargetBytes + 1);
|
||||
BEAST_EXPECT(atLimit);
|
||||
BEAST_EXPECT(overLimit);
|
||||
if (!atLimit || !overLimit)
|
||||
return;
|
||||
|
||||
ExportResultBuilder::PositionedSignatureSnapshot signatures;
|
||||
signatures.emplace(
|
||||
0,
|
||||
ExportResultBuilder::PositionedSignature{
|
||||
signer.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
*atLimit, signer.first, signer.second)});
|
||||
|
||||
auto const witness = ExportResultBuilder::buildSignatureWitness(
|
||||
makeHash("target-at-limit"), *atLimit, signatures, 1, 654);
|
||||
BEAST_EXPECT(ExportResultBuilder::signaturesFromWitness(witness));
|
||||
|
||||
except([&] {
|
||||
ExportResultBuilder::buildSignatureWitness(
|
||||
makeHash("target-over-limit"), *overLimit, signatures, 1, 654);
|
||||
});
|
||||
|
||||
auto oversizedWitness = witness;
|
||||
auto& embedded = const_cast<STObject&>(
|
||||
oversizedWitness.peekAtField(sfExportedTxn).downcast<STObject>());
|
||||
embedded.setFieldArray(sfMemos, overLimit->getFieldArray(sfMemos));
|
||||
Serializer embeddedBytes;
|
||||
embedded.add(embeddedBytes);
|
||||
BEAST_EXPECT(
|
||||
embeddedBytes.size() ==
|
||||
ExportLimits::maxExportReleaseTargetBytes + 1);
|
||||
BEAST_EXPECT(
|
||||
!ExportResultBuilder::signaturesFromWitness(oversizedWitness));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testAssemblesMultiSignedTransaction();
|
||||
testBuildMultiSignedExportedTxnDirect();
|
||||
testExportIntentHashIgnoresSignerSubset();
|
||||
testCapsSignerArray();
|
||||
testSignatureWitnessRoundTrip();
|
||||
testSparseSignatureWitnessRoundTrip();
|
||||
testDestinationSignerOrderIsIndependent();
|
||||
testRejectsMalformedWitnessSigners();
|
||||
testSerializedSizeInventory();
|
||||
testReleaseTargetSizeBoundary();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportResultBuilder, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,265 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 WHATSOEVER
|
||||
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/ExportSigCollector.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
origin(std::uint32_t value)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(value);
|
||||
return sha512Half(s.slice());
|
||||
}
|
||||
|
||||
PublicKey
|
||||
publicKey(char const* hex)
|
||||
{
|
||||
auto const raw = strUnHex(hex);
|
||||
return PublicKey{makeSlice(*raw)};
|
||||
}
|
||||
|
||||
Buffer
|
||||
signature(std::uint8_t value)
|
||||
{
|
||||
std::uint8_t bytes[] = {value, std::uint8_t(value + 1)};
|
||||
return Buffer{bytes, sizeof(bytes)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ExportSigCollector_test : public beast::unit_test::suite
|
||||
{
|
||||
PublicKey const keyA_ = publicKey(
|
||||
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C05"
|
||||
"2");
|
||||
PublicKey const keyB_ = randomKeyPair(KeyType::secp256k1).first;
|
||||
|
||||
static ExportSigCollector::Contribution
|
||||
contribution(
|
||||
ExportSigCollector::Position position,
|
||||
PublicKey const& key,
|
||||
std::uint8_t sig)
|
||||
{
|
||||
return {position, key, signature(sig)};
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testAdmissionAndConflict()
|
||||
{
|
||||
testcase("admission and absorbing conflict");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto const w = origin(1);
|
||||
BEAST_EXPECT(collector.registerOrigin(w, 9));
|
||||
auto first = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 1), 10);
|
||||
BEAST_EXPECT(first.result == ExportSigCollector::BeginResult::verify);
|
||||
BEAST_EXPECT(first.ticket.has_value());
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
if (!first.ticket)
|
||||
return;
|
||||
|
||||
auto accepted =
|
||||
collector.admitContribution(std::move(*first.ticket), true, 10);
|
||||
BEAST_EXPECT(
|
||||
accepted.result == ExportSigCollector::AdmitResult::accepted);
|
||||
auto snapshot = collector.fullUnionSnapshot();
|
||||
BEAST_EXPECT(snapshot.at(w).size() == 1);
|
||||
BEAST_EXPECT(snapshot.at(w).front().position == 7);
|
||||
|
||||
auto duplicate = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 1), 11);
|
||||
BEAST_EXPECT(
|
||||
duplicate.result == ExportSigCollector::BeginResult::duplicate);
|
||||
|
||||
auto second = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyB_, 2), 11);
|
||||
auto third = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 3), 11);
|
||||
BEAST_EXPECT(second.ticket.has_value());
|
||||
BEAST_EXPECT(third.result == ExportSigCollector::BeginResult::capacity);
|
||||
if (!second.ticket)
|
||||
return;
|
||||
|
||||
auto conflicted =
|
||||
collector.admitContribution(std::move(*second.ticket), true, 11);
|
||||
BEAST_EXPECT(
|
||||
conflicted.result == ExportSigCollector::AdmitResult::conflicted);
|
||||
BEAST_EXPECT(conflicted.priorContribution.has_value());
|
||||
BEAST_EXPECT(conflicted.conflictingContribution.has_value());
|
||||
BEAST_EXPECT(
|
||||
collector.positionStatus(w, 7) ==
|
||||
ExportSigCollector::PositionStatus::conflicted);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, contribution(7, keyA_, 4), 12)
|
||||
.result == ExportSigCollector::BeginResult::conflicted);
|
||||
}
|
||||
|
||||
void
|
||||
testReservationAndOriginLifecycle()
|
||||
{
|
||||
testcase("verification reservations and origin registration");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto const w = origin(2);
|
||||
BEAST_EXPECT(collector.registerOrigin(w, 19));
|
||||
auto invalid = collector.beginAttributedAdmission(
|
||||
w, contribution(3, keyA_, 10), 20);
|
||||
BEAST_EXPECT(invalid.ticket.has_value());
|
||||
if (!invalid.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*invalid.ticket), false, 20)
|
||||
.result == ExportSigCollector::AdmitResult::invalid);
|
||||
|
||||
auto valid = collector.beginAttributedAdmission(
|
||||
w, contribution(3, keyA_, 11), 20);
|
||||
BEAST_EXPECT(valid.ticket.has_value());
|
||||
if (!valid.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*valid.ticket), true, 20)
|
||||
.result == ExportSigCollector::AdmitResult::accepted);
|
||||
|
||||
auto other = collector.beginAttributedAdmission(
|
||||
w, contribution(4, keyB_, 12), 20);
|
||||
BEAST_EXPECT(other.ticket.has_value());
|
||||
if (!other.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*other.ticket), true, 20)
|
||||
.result == ExportSigCollector::AdmitResult::accepted);
|
||||
|
||||
BEAST_EXPECT(collector.registerOrigin(w, 21));
|
||||
BEAST_EXPECT(
|
||||
collector.positionStatus(w, 3) ==
|
||||
ExportSigCollector::PositionStatus::unique);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().at(w).size() == 2);
|
||||
|
||||
// Idempotent registration refreshes the stale-cleanup cursor.
|
||||
collector.cleanupStale(277);
|
||||
BEAST_EXPECT(!collector.fullUnionSnapshot().empty());
|
||||
collector.cleanupStale(278);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
}
|
||||
|
||||
void
|
||||
testMalformedBoundaries()
|
||||
{
|
||||
testcase("malformed contribution boundaries");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto good = contribution(0, keyA_, 1);
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(uint256{}, good).result ==
|
||||
ExportSigCollector::BeginResult::malformed);
|
||||
|
||||
auto const w = origin(3);
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollector::BeginResult::unknownOrigin);
|
||||
BEAST_EXPECT(collector.registerOrigin(w, 1));
|
||||
|
||||
good.position = ExportLimits::maxCommitteeMembers;
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollector::BeginResult::malformed);
|
||||
|
||||
good.position = 0;
|
||||
good.signature = Buffer{};
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollector::BeginResult::malformed);
|
||||
|
||||
std::vector<std::uint8_t> oversized(
|
||||
ExportLimits::maxCanonicalExportSignatureBytes + 1, 0xAB);
|
||||
good.signature = Buffer{oversized.data(), oversized.size()};
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollector::BeginResult::malformed);
|
||||
|
||||
good = contribution(5, keyA_, 5);
|
||||
auto abandoned = collector.beginAttributedAdmission(w, good, 2);
|
||||
BEAST_EXPECT(abandoned.ticket.has_value());
|
||||
if (!abandoned.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(collector.cancelAdmission(std::move(*abandoned.ticket)));
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 2).result ==
|
||||
ExportSigCollector::BeginResult::verify);
|
||||
|
||||
auto expired =
|
||||
collector.beginAttributedAdmission(w, contribution(6, keyA_, 6), 3);
|
||||
BEAST_EXPECT(expired.ticket.has_value());
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, contribution(6, keyB_, 7), 5)
|
||||
.result == ExportSigCollector::BeginResult::verify);
|
||||
|
||||
collector.clear(w);
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 7).result ==
|
||||
ExportSigCollector::BeginResult::unknownOrigin);
|
||||
BEAST_EXPECT(collector.registerOrigin(w, 7));
|
||||
}
|
||||
|
||||
void
|
||||
testOriginRegistrationBounds()
|
||||
{
|
||||
testcase("origin registration bounds");
|
||||
|
||||
ExportSigCollector collector;
|
||||
BEAST_EXPECT(!collector.registerOrigin(uint256{}, 1));
|
||||
BEAST_EXPECT(!collector.registerOrigin(origin(100), 0));
|
||||
|
||||
bool registeredAll = true;
|
||||
for (std::size_t i = 0; i < ExportSigCollector::maxTrackedOrigins; ++i)
|
||||
registeredAll =
|
||||
collector.registerOrigin(
|
||||
origin(static_cast<std::uint32_t>(i + 1'000)), 1) &&
|
||||
registeredAll;
|
||||
BEAST_EXPECT(registeredAll);
|
||||
BEAST_EXPECT(!collector.registerOrigin(origin(99'999), 1));
|
||||
BEAST_EXPECT(collector.registerOrigin(origin(1'000), 2));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testAdmissionAndConflict();
|
||||
testReservationAndOriginLifecycle();
|
||||
testMalformedBoundaries();
|
||||
testOriginRegistrationBounds();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportSigCollector, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,505 @@
|
||||
|
||||
// This file is generated by hookz build-test-hooks
|
||||
#ifndef EXPORT_TEST_WASM_INCLUDED
|
||||
#define EXPORT_TEST_WASM_INCLUDED
|
||||
#include <map>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
inline std::map<std::string, std::vector<uint8_t>> export_test_wasm = {
|
||||
/* ==== WASM: 0 ==== */
|
||||
{R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t committee_hash_ptr, uint32_t committee_hash_len, uint64_t callback_fee_drops);
|
||||
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);
|
||||
|
||||
uint8_t committee[32];
|
||||
ASSERT(otxn_param(SBUF(committee), "COMMITTEE", 9) == 32);
|
||||
|
||||
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];
|
||||
uint8_t callback_fee_bytes[8];
|
||||
uint64_t callback_fee = 0;
|
||||
if (otxn_param(SBUF(callback_fee_bytes), "CBFEE", 5) == 8)
|
||||
callback_fee =
|
||||
((uint64_t)callback_fee_bytes[0] << 56) |
|
||||
((uint64_t)callback_fee_bytes[1] << 48) |
|
||||
((uint64_t)callback_fee_bytes[2] << 40) |
|
||||
((uint64_t)callback_fee_bytes[3] << 32) |
|
||||
((uint64_t)callback_fee_bytes[4] << 24) |
|
||||
((uint64_t)callback_fee_bytes[5] << 16) |
|
||||
((uint64_t)callback_fee_bytes[6] << 8) |
|
||||
((uint64_t)callback_fee_bytes[7]);
|
||||
int64_t xport_result = xport(
|
||||
SBUF(hash), (uint32_t)tx, buf - tx, SBUF(committee), callback_fee);
|
||||
ASSERT(xport_result == 32);
|
||||
|
||||
return accept(0, 0, 0);
|
||||
}
|
||||
)[test.hook]",
|
||||
{
|
||||
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x30U,
|
||||
0x07U, 0x60U, 0x00U, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU,
|
||||
0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x02U, 0x7FU,
|
||||
0x7FU, 0x01U, 0x7FU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U,
|
||||
0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x07U, 0x7FU,
|
||||
0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x02U, 0x8BU,
|
||||
0x01U, 0x09U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U,
|
||||
0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U, 0x6EU,
|
||||
0x5FU, 0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU,
|
||||
0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x01U,
|
||||
0x03U, 0x65U, 0x6EU, 0x76U, 0x0DU, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U,
|
||||
0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x02U,
|
||||
0x03U, 0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U,
|
||||
0x61U, 0x63U, 0x6BU, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU,
|
||||
0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU,
|
||||
0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU,
|
||||
0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U,
|
||||
0x05U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U,
|
||||
0x65U, 0x72U, 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x00U, 0x03U, 0x65U,
|
||||
0x6EU, 0x76U, 0x05U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x00U, 0x06U,
|
||||
0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U,
|
||||
0x08U, 0x01U, 0x7FU, 0x01U, 0x41U, 0x80U, 0x80U, 0x04U, 0x0BU, 0x07U,
|
||||
0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x09U, 0x0AU,
|
||||
0xD8U, 0x04U, 0x01U, 0xD5U, 0x04U, 0x02U, 0x01U, 0x7EU, 0x02U, 0x7FU,
|
||||
0x23U, 0x00U, 0x41U, 0xA0U, 0x03U, 0x6BU, 0x22U, 0x00U, 0x24U, 0x00U,
|
||||
0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x00U, 0x1AU, 0x02U, 0x7EU, 0x10U,
|
||||
0x01U, 0x42U, 0x00U, 0x52U, 0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U,
|
||||
0x42U, 0x00U, 0x10U, 0x02U, 0x0CU, 0x01U, 0x0BU, 0x41U, 0x01U, 0x10U,
|
||||
0x03U, 0x42U, 0x01U, 0x52U, 0x04U, 0x40U, 0x41U, 0xD9U, 0x80U, 0x04U,
|
||||
0x41U, 0x16U, 0x42U, 0xDFU, 0x00U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x20U,
|
||||
0x00U, 0x41U, 0x80U, 0x03U, 0x6AU, 0x41U, 0x14U, 0x41U, 0x80U, 0x80U,
|
||||
0x04U, 0x41U, 0x03U, 0x10U, 0x05U, 0x42U, 0x14U, 0x52U, 0x04U, 0x40U,
|
||||
0x41U, 0xEFU, 0x80U, 0x04U, 0x41U, 0x0EU, 0x42U, 0xE3U, 0x00U, 0x10U,
|
||||
0x04U, 0x1AU, 0x0BU, 0x20U, 0x00U, 0x41U, 0xE0U, 0x02U, 0x6AU, 0x41U,
|
||||
0x14U, 0x10U, 0x06U, 0x42U, 0x14U, 0x52U, 0x04U, 0x40U, 0x41U, 0xFDU,
|
||||
0x80U, 0x04U, 0x41U, 0x1EU, 0x42U, 0xE6U, 0x00U, 0x10U, 0x04U, 0x1AU,
|
||||
0x0BU, 0x20U, 0x00U, 0x41U, 0xC0U, 0x02U, 0x6AU, 0x41U, 0x20U, 0x41U,
|
||||
0x84U, 0x80U, 0x04U, 0x41U, 0x09U, 0x10U, 0x05U, 0x42U, 0x20U, 0x52U,
|
||||
0x04U, 0x40U, 0x41U, 0xA7U, 0x80U, 0x04U, 0x41U, 0x32U, 0x42U, 0xE9U,
|
||||
0x00U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x10U, 0x07U, 0x21U, 0x01U, 0x20U,
|
||||
0x00U, 0x42U, 0x80U, 0x80U, 0x80U, 0xD0U, 0xB0U, 0x8EU, 0xC0U, 0xC0U,
|
||||
0x14U, 0x37U, 0x00U, 0x5DU, 0x20U, 0x00U, 0x42U, 0x80U, 0x80U, 0x80U,
|
||||
0x80U, 0xF0U, 0xC1U, 0x90U, 0xA0U, 0xE8U, 0x00U, 0x37U, 0x00U, 0x51U,
|
||||
0x20U, 0x00U, 0x42U, 0xA0U, 0xD2U, 0x80U, 0x80U, 0x80U, 0xA0U, 0xC0U,
|
||||
0xB0U, 0xC0U, 0x00U, 0x37U, 0x00U, 0x49U, 0x20U, 0x00U, 0x41U, 0xA0U,
|
||||
0x36U, 0x3BU, 0x00U, 0x43U, 0x20U, 0x00U, 0x41U, 0xA0U, 0x34U, 0x3BU,
|
||||
0x00U, 0x3DU, 0x20U, 0x00U, 0x41U, 0x00U, 0x36U, 0x00U, 0x39U, 0x20U,
|
||||
0x00U, 0x41U, 0x24U, 0x3AU, 0x00U, 0x38U, 0x20U, 0x00U, 0x42U, 0x92U,
|
||||
0x80U, 0x80U, 0x90U, 0x82U, 0x10U, 0x37U, 0x03U, 0x30U, 0x20U, 0x00U,
|
||||
0x20U, 0x00U, 0x29U, 0x03U, 0xE0U, 0x02U, 0x37U, 0x00U, 0x65U, 0x20U,
|
||||
0x00U, 0x20U, 0x00U, 0x29U, 0x03U, 0xE8U, 0x02U, 0x37U, 0x00U, 0x6DU,
|
||||
0x20U, 0x00U, 0x20U, 0x00U, 0x28U, 0x02U, 0xF0U, 0x02U, 0x36U, 0x00U,
|
||||
0x75U, 0x20U, 0x00U, 0x20U, 0x01U, 0xA7U, 0x22U, 0x03U, 0x41U, 0x05U,
|
||||
0x6AU, 0x22U, 0x02U, 0x3AU, 0x00U, 0x48U, 0x20U, 0x00U, 0x20U, 0x02U,
|
||||
0x41U, 0x08U, 0x76U, 0x3AU, 0x00U, 0x47U, 0x20U, 0x00U, 0x20U, 0x02U,
|
||||
0x41U, 0x10U, 0x76U, 0x3AU, 0x00U, 0x46U, 0x20U, 0x00U, 0x20U, 0x02U,
|
||||
0x41U, 0x18U, 0x76U, 0x3AU, 0x00U, 0x45U, 0x20U, 0x00U, 0x20U, 0x03U,
|
||||
0x41U, 0x01U, 0x6AU, 0x22U, 0x02U, 0x3AU, 0x00U, 0x42U, 0x20U, 0x00U,
|
||||
0x20U, 0x02U, 0x41U, 0x08U, 0x76U, 0x3AU, 0x00U, 0x41U, 0x20U, 0x00U,
|
||||
0x20U, 0x02U, 0x41U, 0x10U, 0x76U, 0x3AU, 0x00U, 0x40U, 0x20U, 0x00U,
|
||||
0x20U, 0x02U, 0x41U, 0x18U, 0x76U, 0x3AU, 0x00U, 0x3FU, 0x20U, 0x00U,
|
||||
0x41U, 0x83U, 0x29U, 0x3BU, 0x00U, 0x79U, 0x20U, 0x00U, 0x20U, 0x00U,
|
||||
0x29U, 0x03U, 0x80U, 0x03U, 0x37U, 0x00U, 0x7BU, 0x20U, 0x00U, 0x20U,
|
||||
0x00U, 0x29U, 0x03U, 0x88U, 0x03U, 0x37U, 0x00U, 0x83U, 0x01U, 0x20U,
|
||||
0x00U, 0x20U, 0x00U, 0x28U, 0x02U, 0x90U, 0x03U, 0x36U, 0x00U, 0x8BU,
|
||||
0x01U, 0x42U, 0x00U, 0x21U, 0x01U, 0x20U, 0x00U, 0x41U, 0x10U, 0x6AU,
|
||||
0x41U, 0x20U, 0x20U, 0x00U, 0x41U, 0x30U, 0x6AU, 0x41U, 0xDFU, 0x00U,
|
||||
0x20U, 0x00U, 0x41U, 0xC0U, 0x02U, 0x6AU, 0x41U, 0x20U, 0x20U, 0x00U,
|
||||
0x41U, 0x08U, 0x6AU, 0x41U, 0x08U, 0x41U, 0x8EU, 0x80U, 0x04U, 0x41U,
|
||||
0x05U, 0x10U, 0x05U, 0x42U, 0x08U, 0x51U, 0x04U, 0x7EU, 0x20U, 0x00U,
|
||||
0x29U, 0x00U, 0x08U, 0x22U, 0x01U, 0x42U, 0x38U, 0x86U, 0x20U, 0x01U,
|
||||
0x42U, 0x80U, 0xFEU, 0x03U, 0x83U, 0x42U, 0x28U, 0x86U, 0x84U, 0x20U,
|
||||
0x01U, 0x42U, 0x80U, 0x80U, 0xFCU, 0x07U, 0x83U, 0x42U, 0x18U, 0x86U,
|
||||
0x20U, 0x01U, 0x42U, 0x80U, 0x80U, 0x80U, 0xF8U, 0x0FU, 0x83U, 0x42U,
|
||||
0x08U, 0x86U, 0x84U, 0x84U, 0x20U, 0x01U, 0x42U, 0x08U, 0x88U, 0x42U,
|
||||
0x80U, 0x80U, 0x80U, 0xF8U, 0x0FU, 0x83U, 0x20U, 0x01U, 0x42U, 0x18U,
|
||||
0x88U, 0x42U, 0x80U, 0x80U, 0xFCU, 0x07U, 0x83U, 0x84U, 0x20U, 0x01U,
|
||||
0x42U, 0x28U, 0x88U, 0x42U, 0x80U, 0xFEU, 0x03U, 0x83U, 0x20U, 0x01U,
|
||||
0x42U, 0x38U, 0x88U, 0x84U, 0x84U, 0x84U, 0x05U, 0x20U, 0x01U, 0x0BU,
|
||||
0x10U, 0x08U, 0x42U, 0x20U, 0x52U, 0x04U, 0x40U, 0x41U, 0x94U, 0x80U,
|
||||
0x04U, 0x41U, 0x13U, 0x42U, 0x91U, 0x01U, 0x10U, 0x04U, 0x1AU, 0x0BU,
|
||||
0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x02U, 0x0BU, 0x21U,
|
||||
0x01U, 0x20U, 0x00U, 0x41U, 0xA0U, 0x03U, 0x6AU, 0x24U, 0x00U, 0x20U,
|
||||
0x01U, 0x0BU, 0x0BU, 0xA3U, 0x01U, 0x01U, 0x00U, 0x41U, 0x80U, 0x80U,
|
||||
0x04U, 0x0BU, 0x9AU, 0x01U, 0x44U, 0x53U, 0x54U, 0x00U, 0x43U, 0x4FU,
|
||||
0x4DU, 0x4DU, 0x49U, 0x54U, 0x54U, 0x45U, 0x45U, 0x00U, 0x43U, 0x42U,
|
||||
0x46U, 0x45U, 0x45U, 0x00U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU,
|
||||
0x72U, 0x65U, 0x73U, 0x75U, 0x6CU, 0x74U, 0x20U, 0x3DU, 0x3DU, 0x20U,
|
||||
0x33U, 0x32U, 0x00U, 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U,
|
||||
0x72U, 0x61U, 0x6DU, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x63U,
|
||||
0x6FU, 0x6DU, 0x6DU, 0x69U, 0x74U, 0x74U, 0x65U, 0x65U, 0x29U, 0x2CU,
|
||||
0x20U, 0x22U, 0x43U, 0x4FU, 0x4DU, 0x4DU, 0x49U, 0x54U, 0x54U, 0x45U,
|
||||
0x45U, 0x22U, 0x2CU, 0x20U, 0x39U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U,
|
||||
0x33U, 0x32U, 0x00U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU, 0x72U,
|
||||
0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x28U, 0x31U, 0x29U, 0x20U,
|
||||
0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x64U, 0x73U, 0x74U, 0x5FU, 0x6CU,
|
||||
0x65U, 0x6EU, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x68U,
|
||||
0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU,
|
||||
0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x61U, 0x63U, 0x63U,
|
||||
0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U,
|
||||
}},
|
||||
|
||||
/* ==== WASM: 1 ==== */
|
||||
{R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t committee_hash_ptr, uint32_t committee_hash_len, uint64_t callback_fee_drops);
|
||||
extern int64_t xport_reserve(uint32_t count);
|
||||
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
|
||||
extern int64_t otxn_type(void);
|
||||
extern int64_t ledger_seq(void);
|
||||
|
||||
#define SBUF(x) (uint32_t)(x), sizeof(x)
|
||||
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
|
||||
|
||||
#define ttPAYMENT 0
|
||||
#define tfCANONICAL 0x80000000UL
|
||||
|
||||
#define amAMOUNT 1
|
||||
#define amFEE 8
|
||||
#define atACCOUNT 1
|
||||
#define atDESTINATION 3
|
||||
|
||||
#define ENCODE_TT(buf_out, tt) \
|
||||
buf_out[0] = 0x12U; \
|
||||
buf_out[1] = (tt >> 8) & 0xFFU; \
|
||||
buf_out[2] = tt & 0xFFU; \
|
||||
buf_out += 3;
|
||||
|
||||
#define ENCODE_FLAGS(buf_out, flags) \
|
||||
buf_out[0] = 0x22U; \
|
||||
buf_out[1] = (flags >> 24) & 0xFFU; \
|
||||
buf_out[2] = (flags >> 16) & 0xFFU; \
|
||||
buf_out[3] = (flags >> 8) & 0xFFU; \
|
||||
buf_out[4] = flags & 0xFFU; \
|
||||
buf_out += 5;
|
||||
|
||||
#define ENCODE_SEQUENCE(buf_out, seq) \
|
||||
buf_out[0] = 0x24U; \
|
||||
buf_out[1] = (seq >> 24) & 0xFFU; \
|
||||
buf_out[2] = (seq >> 16) & 0xFFU; \
|
||||
buf_out[3] = (seq >> 8) & 0xFFU; \
|
||||
buf_out[4] = seq & 0xFFU; \
|
||||
buf_out += 5;
|
||||
|
||||
// sfNetworkID = UINT32 field 1 = 0x21
|
||||
#define ENCODE_NETWORK_ID(buf_out, id) \
|
||||
buf_out[0] = 0x21U; \
|
||||
buf_out[1] = (id >> 24) & 0xFFU; \
|
||||
buf_out[2] = (id >> 16) & 0xFFU; \
|
||||
buf_out[3] = (id >> 8) & 0xFFU; \
|
||||
buf_out[4] = id & 0xFFU; \
|
||||
buf_out += 5;
|
||||
|
||||
#define ENCODE_FLS(buf_out, fls) \
|
||||
buf_out[0] = 0x20U; \
|
||||
buf_out[1] = 0x1AU; \
|
||||
buf_out[2] = (fls >> 24) & 0xFFU; \
|
||||
buf_out[3] = (fls >> 16) & 0xFFU; \
|
||||
buf_out[4] = (fls >> 8) & 0xFFU; \
|
||||
buf_out[5] = fls & 0xFFU; \
|
||||
buf_out += 6;
|
||||
|
||||
#define ENCODE_LLS(buf_out, lls) \
|
||||
buf_out[0] = 0x20U; \
|
||||
buf_out[1] = 0x1BU; \
|
||||
buf_out[2] = (lls >> 24) & 0xFFU; \
|
||||
buf_out[3] = (lls >> 16) & 0xFFU; \
|
||||
buf_out[4] = (lls >> 8) & 0xFFU; \
|
||||
buf_out[5] = lls & 0xFFU; \
|
||||
buf_out += 6;
|
||||
|
||||
#define ENCODE_DROPS(buf_out, drops, amt_type) \
|
||||
buf_out[0] = 0x60U + amt_type; \
|
||||
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
|
||||
buf_out[2] = (drops >> 48) & 0xFFU; \
|
||||
buf_out[3] = (drops >> 40) & 0xFFU; \
|
||||
buf_out[4] = (drops >> 32) & 0xFFU; \
|
||||
buf_out[5] = (drops >> 24) & 0xFFU; \
|
||||
buf_out[6] = (drops >> 16) & 0xFFU; \
|
||||
buf_out[7] = (drops >> 8) & 0xFFU; \
|
||||
buf_out[8] = drops & 0xFFU; \
|
||||
buf_out += 9;
|
||||
|
||||
#define ENCODE_SIGNING_PUBKEY_EMPTY(buf_out) \
|
||||
buf_out[0] = 0x73U; \
|
||||
buf_out[1] = 0x00U; \
|
||||
buf_out += 2;
|
||||
|
||||
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
|
||||
buf_out[0] = 0x80U + acc_type; \
|
||||
buf_out[1] = 0x14U; \
|
||||
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
|
||||
buf_out += 22;
|
||||
|
||||
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
|
||||
if (otxn_type() != ttPAYMENT)
|
||||
return accept(0, 0, 0);
|
||||
|
||||
ASSERT(xport_reserve(1) == 1);
|
||||
|
||||
uint8_t dst[20];
|
||||
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
|
||||
ASSERT(dst_len == 20);
|
||||
|
||||
uint8_t acc[20];
|
||||
ASSERT(hook_account(SBUF(acc)) == 20);
|
||||
|
||||
uint8_t committee[32];
|
||||
ASSERT(otxn_param(SBUF(committee), "COMMITTEE", 9) == 32);
|
||||
|
||||
uint32_t cls = (uint32_t)ledger_seq();
|
||||
|
||||
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
|
||||
uint8_t* buf = tx;
|
||||
|
||||
ENCODE_TT(buf, ttPAYMENT);
|
||||
ENCODE_NETWORK_ID(buf, 21337); // must precede Sequence (canonical order)
|
||||
ENCODE_FLAGS(buf, tfCANONICAL);
|
||||
ENCODE_SEQUENCE(buf, 0);
|
||||
ENCODE_FLS(buf, cls + 1);
|
||||
ENCODE_LLS(buf, cls + 5);
|
||||
|
||||
uint64_t drops = 1000000;
|
||||
ENCODE_DROPS(buf, drops, amAMOUNT);
|
||||
ENCODE_DROPS(buf, 10, amFEE);
|
||||
|
||||
ENCODE_SIGNING_PUBKEY_EMPTY(buf);
|
||||
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
|
||||
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
|
||||
|
||||
uint8_t hash[32];
|
||||
int64_t xport_result = xport(
|
||||
SBUF(hash), (uint32_t)tx, buf - tx, SBUF(committee), 0);
|
||||
// xport should return EXPORT_FAILURE (-46), ASSERT will rollback
|
||||
ASSERT(xport_result == 32);
|
||||
|
||||
return accept(0, 0, 0);
|
||||
}
|
||||
)[test.hook]",
|
||||
{
|
||||
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x30U,
|
||||
0x07U, 0x60U, 0x00U, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU,
|
||||
0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x02U, 0x7FU,
|
||||
0x7FU, 0x01U, 0x7FU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U,
|
||||
0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x07U, 0x7FU,
|
||||
0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x02U, 0x8BU,
|
||||
0x01U, 0x09U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U,
|
||||
0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U, 0x6EU,
|
||||
0x5FU, 0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU,
|
||||
0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x01U,
|
||||
0x03U, 0x65U, 0x6EU, 0x76U, 0x0DU, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U,
|
||||
0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x02U,
|
||||
0x03U, 0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U,
|
||||
0x61U, 0x63U, 0x6BU, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU,
|
||||
0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU,
|
||||
0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU,
|
||||
0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U,
|
||||
0x05U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U,
|
||||
0x65U, 0x72U, 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x00U, 0x03U, 0x65U,
|
||||
0x6EU, 0x76U, 0x05U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x00U, 0x06U,
|
||||
0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U,
|
||||
0x08U, 0x01U, 0x7FU, 0x01U, 0x41U, 0x80U, 0x80U, 0x04U, 0x0BU, 0x07U,
|
||||
0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x09U, 0x0AU,
|
||||
0xE9U, 0x03U, 0x01U, 0xE6U, 0x03U, 0x02U, 0x02U, 0x7FU, 0x01U, 0x7EU,
|
||||
0x23U, 0x00U, 0x41U, 0x90U, 0x03U, 0x6BU, 0x22U, 0x00U, 0x24U, 0x00U,
|
||||
0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x00U, 0x1AU, 0x02U, 0x7EU, 0x10U,
|
||||
0x01U, 0x42U, 0x00U, 0x52U, 0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U,
|
||||
0x42U, 0x00U, 0x10U, 0x02U, 0x0CU, 0x01U, 0x0BU, 0x41U, 0x01U, 0x10U,
|
||||
0x03U, 0x42U, 0x01U, 0x52U, 0x04U, 0x40U, 0x41U, 0xD3U, 0x80U, 0x04U,
|
||||
0x41U, 0x16U, 0x42U, 0xE8U, 0x00U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x20U,
|
||||
0x00U, 0x41U, 0xF0U, 0x02U, 0x6AU, 0x41U, 0x14U, 0x41U, 0x80U, 0x80U,
|
||||
0x04U, 0x41U, 0x03U, 0x10U, 0x05U, 0x42U, 0x14U, 0x52U, 0x04U, 0x40U,
|
||||
0x41U, 0xE9U, 0x80U, 0x04U, 0x41U, 0x0EU, 0x42U, 0xECU, 0x00U, 0x10U,
|
||||
0x04U, 0x1AU, 0x0BU, 0x20U, 0x00U, 0x41U, 0xD0U, 0x02U, 0x6AU, 0x41U,
|
||||
0x14U, 0x10U, 0x06U, 0x42U, 0x14U, 0x52U, 0x04U, 0x40U, 0x41U, 0xF7U,
|
||||
0x80U, 0x04U, 0x41U, 0x1EU, 0x42U, 0xEFU, 0x00U, 0x10U, 0x04U, 0x1AU,
|
||||
0x0BU, 0x20U, 0x00U, 0x41U, 0xB0U, 0x02U, 0x6AU, 0x41U, 0x20U, 0x41U,
|
||||
0x84U, 0x80U, 0x04U, 0x41U, 0x09U, 0x10U, 0x05U, 0x42U, 0x20U, 0x52U,
|
||||
0x04U, 0x40U, 0x41U, 0xA1U, 0x80U, 0x04U, 0x41U, 0x32U, 0x42U, 0xF2U,
|
||||
0x00U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x10U, 0x07U, 0x21U, 0x03U, 0x20U,
|
||||
0x00U, 0x42U, 0x80U, 0x80U, 0x80U, 0xD0U, 0xB0U, 0x8EU, 0xC0U, 0xC0U,
|
||||
0x14U, 0x37U, 0x02U, 0x4CU, 0x20U, 0x00U, 0x42U, 0x80U, 0x80U, 0x80U,
|
||||
0x80U, 0xF0U, 0xC1U, 0x90U, 0xA0U, 0xE8U, 0x00U, 0x37U, 0x03U, 0x40U,
|
||||
0x20U, 0x00U, 0x41U, 0xE1U, 0x80U, 0x01U, 0x3BU, 0x01U, 0x3EU, 0x20U,
|
||||
0x00U, 0x41U, 0xA0U, 0x36U, 0x3BU, 0x01U, 0x38U, 0x20U, 0x00U, 0x41U,
|
||||
0xA0U, 0x34U, 0x3BU, 0x01U, 0x32U, 0x20U, 0x00U, 0x41U, 0x00U, 0x36U,
|
||||
0x01U, 0x2EU, 0x20U, 0x00U, 0x41U, 0x80U, 0xC8U, 0x00U, 0x3BU, 0x01U,
|
||||
0x2CU, 0x20U, 0x00U, 0x41U, 0xA2U, 0x80U, 0x02U, 0x36U, 0x02U, 0x28U,
|
||||
0x20U, 0x00U, 0x42U, 0x92U, 0x80U, 0x80U, 0x88U, 0x82U, 0x80U, 0xC0U,
|
||||
0xA9U, 0xD9U, 0x00U, 0x37U, 0x03U, 0x20U, 0x20U, 0x00U, 0x20U, 0x03U,
|
||||
0xA7U, 0x22U, 0x02U, 0x41U, 0x05U, 0x6AU, 0x22U, 0x01U, 0x3AU, 0x00U,
|
||||
0x3DU, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x08U, 0x76U, 0x3AU, 0x00U,
|
||||
0x3CU, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x10U, 0x76U, 0x3AU, 0x00U,
|
||||
0x3BU, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x18U, 0x76U, 0x3AU, 0x00U,
|
||||
0x3AU, 0x20U, 0x00U, 0x20U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x22U, 0x01U,
|
||||
0x3AU, 0x00U, 0x37U, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x08U, 0x76U,
|
||||
0x3AU, 0x00U, 0x36U, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x10U, 0x76U,
|
||||
0x3AU, 0x00U, 0x35U, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x18U, 0x76U,
|
||||
0x3AU, 0x00U, 0x34U, 0x20U, 0x00U, 0x41U, 0x83U, 0x29U, 0x3BU, 0x01U,
|
||||
0x68U, 0x20U, 0x00U, 0x20U, 0x00U, 0x28U, 0x02U, 0xE0U, 0x02U, 0x36U,
|
||||
0x02U, 0x64U, 0x20U, 0x00U, 0x20U, 0x00U, 0x29U, 0x03U, 0xD8U, 0x02U,
|
||||
0x37U, 0x02U, 0x5CU, 0x20U, 0x00U, 0x20U, 0x00U, 0x29U, 0x03U, 0xD0U,
|
||||
0x02U, 0x37U, 0x02U, 0x54U, 0x20U, 0x00U, 0x20U, 0x00U, 0x29U, 0x03U,
|
||||
0xF0U, 0x02U, 0x37U, 0x01U, 0x6AU, 0x20U, 0x00U, 0x20U, 0x00U, 0x29U,
|
||||
0x03U, 0xF8U, 0x02U, 0x37U, 0x01U, 0x72U, 0x20U, 0x00U, 0x20U, 0x00U,
|
||||
0x28U, 0x02U, 0x80U, 0x03U, 0x36U, 0x01U, 0x7AU, 0x20U, 0x00U, 0x41U,
|
||||
0x20U, 0x20U, 0x00U, 0x41U, 0x20U, 0x6AU, 0x41U, 0xDEU, 0x00U, 0x20U,
|
||||
0x00U, 0x41U, 0xB0U, 0x02U, 0x6AU, 0x41U, 0x20U, 0x42U, 0x00U, 0x10U,
|
||||
0x08U, 0x42U, 0x20U, 0x52U, 0x04U, 0x40U, 0x41U, 0x8EU, 0x80U, 0x04U,
|
||||
0x41U, 0x13U, 0x42U, 0x8CU, 0x01U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x41U,
|
||||
0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x02U, 0x0BU, 0x21U, 0x03U,
|
||||
0x20U, 0x00U, 0x41U, 0x90U, 0x03U, 0x6AU, 0x24U, 0x00U, 0x20U, 0x03U,
|
||||
0x0BU, 0x0BU, 0x9DU, 0x01U, 0x01U, 0x00U, 0x41U, 0x80U, 0x80U, 0x04U,
|
||||
0x0BU, 0x94U, 0x01U, 0x44U, 0x53U, 0x54U, 0x00U, 0x43U, 0x4FU, 0x4DU,
|
||||
0x4DU, 0x49U, 0x54U, 0x54U, 0x45U, 0x45U, 0x00U, 0x78U, 0x70U, 0x6FU,
|
||||
0x72U, 0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x75U, 0x6CU, 0x74U, 0x20U,
|
||||
0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U, 0x6FU, 0x74U, 0x78U, 0x6EU,
|
||||
0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU, 0x28U, 0x53U, 0x42U, 0x55U,
|
||||
0x46U, 0x28U, 0x63U, 0x6FU, 0x6DU, 0x6DU, 0x69U, 0x74U, 0x74U, 0x65U,
|
||||
0x65U, 0x29U, 0x2CU, 0x20U, 0x22U, 0x43U, 0x4FU, 0x4DU, 0x4DU, 0x49U,
|
||||
0x54U, 0x54U, 0x45U, 0x45U, 0x22U, 0x2CU, 0x20U, 0x39U, 0x29U, 0x20U,
|
||||
0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U, 0x78U, 0x70U, 0x6FU, 0x72U,
|
||||
0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x28U,
|
||||
0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x64U, 0x73U,
|
||||
0x74U, 0x5FU, 0x6CU, 0x65U, 0x6EU, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U,
|
||||
0x30U, 0x00U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U,
|
||||
0x6FU, 0x75U, 0x6EU, 0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U,
|
||||
0x61U, 0x63U, 0x63U, 0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U,
|
||||
0x30U,
|
||||
}},
|
||||
|
||||
};
|
||||
}
|
||||
} // namespace ripple
|
||||
#endif
|
||||
@@ -19,12 +19,19 @@
|
||||
#include <test/app/Import_json.h>
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/hook/HookAPI.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
#include <xrpld/app/tx/detail/Export.h>
|
||||
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/ExportOriginMemo.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
@@ -35,6 +42,64 @@ namespace test {
|
||||
class HookAPI_test : public beast::unit_test::suite
|
||||
{
|
||||
private:
|
||||
STTx
|
||||
makeSTTx(STObject const& obj)
|
||||
{
|
||||
Serializer s;
|
||||
obj.add(s);
|
||||
SerialIter sit{s.slice()};
|
||||
return STTx{std::ref(sit)};
|
||||
}
|
||||
|
||||
Blob
|
||||
serialize(STTx const& tx)
|
||||
{
|
||||
Serializer s;
|
||||
tx.add(s);
|
||||
return {s.begin(), s.end()};
|
||||
}
|
||||
|
||||
STTx
|
||||
makeExportedPayment(
|
||||
AccountID const& src,
|
||||
AccountID const& dst,
|
||||
std::optional<std::uint32_t> ticketSequence = 1)
|
||||
{
|
||||
STObject obj(sfExportedTxn);
|
||||
obj.setFieldU16(sfTransactionType, ttPAYMENT);
|
||||
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
if (ticketSequence)
|
||||
obj.setFieldU32(sfTicketSequence, *ticketSequence);
|
||||
obj.setFieldU32(sfFirstLedgerSequence, 2);
|
||||
obj.setFieldU32(sfLastLedgerSequence, 6);
|
||||
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
|
||||
obj.setFieldAmount(sfFee, XRPAmount{10});
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
obj.setAccountID(sfAccount, src);
|
||||
obj.setAccountID(sfDestination, dst);
|
||||
return makeSTTx(obj);
|
||||
}
|
||||
|
||||
STTx
|
||||
makeExportWrapper(AccountID const& account, STTx const& innerTx)
|
||||
{
|
||||
STObject obj(sfGeneric);
|
||||
obj.setFieldU16(sfTransactionType, ttEXPORT);
|
||||
obj.setAccountID(sfAccount, account);
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
obj.setFieldAmount(sfFee, XRPAmount{0});
|
||||
obj.setFieldU32(sfFirstLedgerSequence, 2);
|
||||
obj.setFieldU32(sfLastLedgerSequence, 6);
|
||||
|
||||
auto const innerBlob = serialize(innerTx);
|
||||
SerialIter sit{makeSlice(innerBlob)};
|
||||
obj.set(std::make_unique<STObject>(sit, sfExportedTxn));
|
||||
|
||||
return makeSTTx(obj);
|
||||
}
|
||||
|
||||
ApplyContext
|
||||
createApplyContext(jtx::Env& env, OpenView& ov, STTx const& tx)
|
||||
{
|
||||
@@ -147,7 +212,9 @@ public:
|
||||
BEAST_EXPECT(st.getAccountID(sfAccount) == alice.id());
|
||||
auto const seq = applyCtx.view().info().seq;
|
||||
BEAST_EXPECT(st.getFieldU32(sfFirstLedgerSequence) == seq + 1);
|
||||
BEAST_EXPECT(st.getFieldU32(sfLastLedgerSequence) == seq + 5);
|
||||
BEAST_EXPECT(
|
||||
st.getFieldU32(sfLastLedgerSequence) ==
|
||||
seq + ExportLimits::maxAdmissionWindowLedgers);
|
||||
BEAST_EXPECT(st.isFieldPresent(sfEmitDetails));
|
||||
|
||||
auto const result2 =
|
||||
@@ -177,7 +244,8 @@ public:
|
||||
obj[sfSequence] = 0;
|
||||
obj[sfSigningPubKey] = Slice{};
|
||||
obj[sfFirstLedgerSequence] = env.closed()->seq() + 1;
|
||||
obj[sfLastLedgerSequence] = env.closed()->seq() + 5;
|
||||
obj[sfLastLedgerSequence] =
|
||||
env.closed()->seq() + ExportLimits::maxAdmissionWindowLedgers;
|
||||
obj[sfFee] = env.closed()->fees().base;
|
||||
|
||||
auto& emitDetails = obj.peekFieldObject(sfEmitDetails);
|
||||
@@ -193,7 +261,8 @@ public:
|
||||
obj[sfSequence] = 0;
|
||||
obj[sfSigningPubKey] = Slice{};
|
||||
obj[sfFirstLedgerSequence] = env.closed()->seq() + 1;
|
||||
obj[sfLastLedgerSequence] = env.closed()->seq() + 5;
|
||||
obj[sfLastLedgerSequence] =
|
||||
env.closed()->seq() + ExportLimits::maxAdmissionWindowLedgers;
|
||||
obj[sfFee] = env.closed()->fees().base;
|
||||
STObject hookobj(sfHook);
|
||||
auto& hooks = obj.peekFieldArray(sfHooks);
|
||||
@@ -272,6 +341,35 @@ public:
|
||||
auto const result = api.emit(tx.getSerializer().slice());
|
||||
BEAST_EXPECT(result.error() == EMISSION_FAILURE);
|
||||
}
|
||||
{
|
||||
// Export wrappers require xport() so they cannot bypass its
|
||||
// committee check or per-Hook Export reservation.
|
||||
auto hookCtx = makeStubHookContext(
|
||||
applyCtx,
|
||||
alice.id(),
|
||||
alice.id(),
|
||||
{
|
||||
.expected_etxn_count = 3,
|
||||
});
|
||||
auto& api = hookCtx.api();
|
||||
auto const inner = makeExportedPayment(alice.id(), bob.id());
|
||||
auto wrapper = makeExportWrapper(alice.id(), inner);
|
||||
// No account-owned committee exists for this digest.
|
||||
wrapper.setFieldH256(sfExportCommitteeHash, uint256{1});
|
||||
for (int attempt = 0; attempt < 3; ++attempt)
|
||||
{
|
||||
auto const prepared =
|
||||
api.prepare(wrapper.getSerializer().slice());
|
||||
BEAST_EXPECT(prepared.has_value());
|
||||
if (!prepared)
|
||||
continue;
|
||||
auto const result =
|
||||
api.emit(Slice{prepared->data(), prepared->size()});
|
||||
BEAST_EXPECT(result.error() == EMISSION_FAILURE);
|
||||
}
|
||||
BEAST_EXPECT(hookCtx.result.emittedTxn.empty());
|
||||
BEAST_EXPECT(hookCtx.export_count == 0);
|
||||
}
|
||||
{
|
||||
// HookCanEmit (non-SetHook)
|
||||
auto hookCtx = makeStubHookContext(
|
||||
@@ -530,10 +628,12 @@ public:
|
||||
}
|
||||
{
|
||||
// Invalid sfLastLedgerSequence
|
||||
// (greater than current ledger seq + 5)
|
||||
// (greater than the export admission window)
|
||||
auto tx = emitInvokeTx;
|
||||
auto const currentSeq = applyCtx.view().info().seq;
|
||||
tx.setFieldU32(sfLastLedgerSequence, currentSeq + 6);
|
||||
tx.setFieldU32(
|
||||
sfLastLedgerSequence,
|
||||
currentSeq + ExportLimits::maxAdmissionWindowLedgers + 1);
|
||||
auto const result = api.emit(tx.getSerializer().slice());
|
||||
BEAST_EXPECT(result.error() == EMISSION_FAILURE);
|
||||
}
|
||||
@@ -732,6 +832,32 @@ public:
|
||||
else
|
||||
BEAST_EXPECT(result.value() == baseFee + memoSize);
|
||||
}
|
||||
{
|
||||
// Export's distributed-work fee must not be bypassed by the
|
||||
// pre-fixHookAPI20251128 generic fee path.
|
||||
auto const bob = Account{"bob"};
|
||||
auto const roster = serializeExportCommittee(
|
||||
{randomKeyPair(KeyType::secp256k1).first});
|
||||
auto const committeeHash = exportCommitteeHash(makeSlice(roster));
|
||||
auto committee = std::make_shared<SLE>(
|
||||
keylet::exportCommittee(alice.id(), committeeHash));
|
||||
committee->setAccountID(sfAccount, alice.id());
|
||||
committee->setFieldH256(sfExportCommitteeHash, committeeHash);
|
||||
committee->setFieldVL(sfExportCommittee, roster);
|
||||
committee->setFieldU64(sfOwnerNode, 0);
|
||||
applyCtx.view().insert(committee);
|
||||
|
||||
auto exportTx = makeExportWrapper(
|
||||
alice.id(), makeExportedPayment(alice.id(), bob.id()));
|
||||
exportTx.setFieldH256(sfExportCommitteeHash, committeeHash);
|
||||
auto const expected =
|
||||
Export::calculateBaseFee(applyCtx.view(), exportTx).drops();
|
||||
auto const result =
|
||||
api.etxn_fee_base(exportTx.getSerializer().slice());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(result.value() == expected);
|
||||
BEAST_EXPECT(result.value() > env.closed()->fees().base.drops());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -948,6 +1074,314 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_xport_reserve(FeatureBitset features)
|
||||
{
|
||||
testcase("Test xport_reserve");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace hook_api;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
Env env{*this, features};
|
||||
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
|
||||
OpenView ov{*env.current()};
|
||||
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
|
||||
auto const roster =
|
||||
serializeExportCommittee({randomKeyPair(KeyType::secp256k1).first});
|
||||
auto const committeeHash = exportCommitteeHash(makeSlice(roster));
|
||||
auto committee = std::make_shared<SLE>(
|
||||
keylet::exportCommittee(alice.id(), committeeHash));
|
||||
committee->setAccountID(sfAccount, alice.id());
|
||||
committee->setFieldH256(sfExportCommitteeHash, committeeHash);
|
||||
committee->setFieldVL(sfExportCommittee, roster);
|
||||
committee->setFieldU64(sfOwnerNode, 0);
|
||||
applyCtx.view().insert(committee);
|
||||
|
||||
{
|
||||
// ALREADY_SET
|
||||
StubHookContext stubCtx{.expected_export_count = 1};
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.xport_reserve(2);
|
||||
BEAST_EXPECT(result.error() == ALREADY_SET);
|
||||
}
|
||||
|
||||
{
|
||||
// TOO_SMALL
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.xport_reserve(0);
|
||||
BEAST_EXPECT(result.error() == TOO_SMALL);
|
||||
}
|
||||
|
||||
{
|
||||
// TOO_BIG
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.xport_reserve(hook_api::max_export + 1);
|
||||
BEAST_EXPECT(result.error() == TOO_BIG);
|
||||
}
|
||||
|
||||
{
|
||||
// SUCCESS
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.xport_reserve(2);
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(hookCtx.expected_export_count == 2);
|
||||
BEAST_EXPECT(hookCtx.expected_etxn_count == 2);
|
||||
}
|
||||
|
||||
{
|
||||
// xport_reserve composes with an earlier emit reservation.
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
BEAST_EXPECT(api.etxn_reserve(1).has_value());
|
||||
BEAST_EXPECT(api.xport_reserve(2).has_value());
|
||||
BEAST_EXPECT(hookCtx.expected_export_count == 2);
|
||||
BEAST_EXPECT(hookCtx.expected_etxn_count == 3);
|
||||
}
|
||||
|
||||
{
|
||||
// xport_reserve can fill the remaining emitted-txn budget.
|
||||
StubHookContext stubCtx{
|
||||
.expected_etxn_count =
|
||||
static_cast<int64_t>(hook_api::max_emit) -
|
||||
hook_api::max_export};
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
|
||||
auto& api = hookCtx.api();
|
||||
BEAST_EXPECT(api.xport_reserve(hook_api::max_export).has_value());
|
||||
BEAST_EXPECT(hookCtx.expected_export_count == hook_api::max_export);
|
||||
BEAST_EXPECT(hookCtx.expected_etxn_count == hook_api::max_emit);
|
||||
}
|
||||
|
||||
{
|
||||
// xport_reserve shares the emitted-txn budget and must fail
|
||||
// without partially setting the export reservation.
|
||||
StubHookContext stubCtx{.expected_etxn_count = hook_api::max_emit};
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.xport_reserve(1);
|
||||
BEAST_EXPECT(result.error() == TOO_BIG);
|
||||
BEAST_EXPECT(hookCtx.expected_export_count == -1);
|
||||
BEAST_EXPECT(hookCtx.expected_etxn_count == hook_api::max_emit);
|
||||
}
|
||||
|
||||
{
|
||||
// xport_reserve consumes the shared emitted-txn reservation slot,
|
||||
// so a later etxn_reserve cannot reset it.
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
BEAST_EXPECT(api.xport_reserve(1).has_value());
|
||||
BEAST_EXPECT(api.etxn_reserve(1).error() == ALREADY_SET);
|
||||
BEAST_EXPECT(hookCtx.expected_export_count == 1);
|
||||
BEAST_EXPECT(hookCtx.expected_etxn_count == 1);
|
||||
}
|
||||
|
||||
{
|
||||
// xport shares emit()'s emitted transaction queue and must not
|
||||
// append after normal emits fill the reserved budget.
|
||||
std::string reason;
|
||||
auto tx = std::make_shared<ripple::Transaction>(
|
||||
std::make_shared<ripple::STTx const>(invokeTx),
|
||||
reason,
|
||||
env.app());
|
||||
std::queue<std::shared_ptr<ripple::Transaction>> emittedTxn;
|
||||
emittedTxn.push(tx);
|
||||
auto hookCtx = makeStubHookContext(
|
||||
applyCtx,
|
||||
alice.id(),
|
||||
alice.id(),
|
||||
{
|
||||
.expected_etxn_count = 1,
|
||||
.expected_export_count = 1,
|
||||
.result = {.emittedTxn = emittedTxn},
|
||||
});
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.xport(Slice{}, committeeHash, 0);
|
||||
BEAST_EXPECT(result.error() == TOO_MANY_EMITTED_TXN);
|
||||
}
|
||||
|
||||
{
|
||||
// xport must enforce the emitted-txn generation cap before
|
||||
// constructing a generation-10 wrapper.
|
||||
auto const bob = Account{"bob"};
|
||||
auto const innerTx = makeExportedPayment(alice.id(), bob.id());
|
||||
auto const serialized = serialize(innerTx);
|
||||
StubHookContext stubCtx{
|
||||
.expected_etxn_count = 1,
|
||||
.expected_export_count = 1,
|
||||
.generation = 9,
|
||||
.burden = 1,
|
||||
.result = {.hookHash = uint256{3}},
|
||||
};
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.xport(
|
||||
Slice(serialized.data(), serialized.size()), committeeHash, 0);
|
||||
BEAST_EXPECT(result.error() == EXPORT_FAILURE);
|
||||
}
|
||||
|
||||
{
|
||||
// xport shares emit()'s burden accounting; overflow must fail
|
||||
// instead of silently substituting burden=1 in EmitDetails.
|
||||
auto const bob = Account{"bob"};
|
||||
auto const innerTx = makeExportedPayment(alice.id(), bob.id());
|
||||
auto const serialized = serialize(innerTx);
|
||||
StubHookContext stubCtx{
|
||||
.expected_etxn_count = 2,
|
||||
.expected_export_count = 1,
|
||||
.burden = std::numeric_limits<uint64_t>::max(),
|
||||
.result = {.hookHash = uint256{3}},
|
||||
};
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.xport(
|
||||
Slice(serialized.data(), serialized.size()), committeeHash, 0);
|
||||
BEAST_EXPECT(result.error() == FEE_TOO_LARGE);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_xport_cancel(FeatureBitset features)
|
||||
{
|
||||
testcase("Test xport_cancel");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace hook_api;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
std::uint32_t const importingTicket = 7;
|
||||
std::uint32_t const otherTicket = 8;
|
||||
uint256 const importingOrigin{1};
|
||||
uint256 const otherOrigin{2};
|
||||
auto importingTx =
|
||||
makeExportedPayment(alice.id(), bob.id(), importingTicket);
|
||||
auto otherTx = makeExportedPayment(alice.id(), bob.id(), otherTicket);
|
||||
auto const importingRelease = ExportOriginMemo::releaseForm(
|
||||
importingTx,
|
||||
ExportOriginMemo::Origin{21337, 0, importingOrigin},
|
||||
ExportOriginMemo::Anchor{1, uint256{99}});
|
||||
BEAST_EXPECT(importingRelease);
|
||||
if (!importingRelease)
|
||||
return;
|
||||
|
||||
auto xpopJson = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
xpopJson[jss::transaction][jss::blob] =
|
||||
strHex(serialize(importingRelease.value()));
|
||||
std::string const xpopStr = Json::FastWriter().write(xpopJson);
|
||||
STTx importTx = STTx(ttIMPORT, [&](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, alice.id());
|
||||
obj.setFieldVL(sfBlob, *strUnHex(strHex(xpopStr)));
|
||||
});
|
||||
|
||||
OpenView ov{*env.current()};
|
||||
ApplyContext applyCtx = createApplyContext(env, ov, importTx);
|
||||
auto insertLatch = [&](ApplyContext& ctx,
|
||||
STTx const& target,
|
||||
uint256 const& origin) {
|
||||
auto latch =
|
||||
std::make_shared<SLE>(keylet::exportLatch(alice.id(), origin));
|
||||
latch->setAccountID(sfAccount, alice.id());
|
||||
latch->setFieldU32(
|
||||
sfTicketSequence, target.getFieldU32(sfTicketSequence));
|
||||
latch->setFieldH256(sfTransactionHash, origin);
|
||||
latch->setFieldH256(
|
||||
sfDigest, ExportResultBuilder::exportIntentHash(target));
|
||||
latch->setFieldU32(sfLedgerSequence, ctx.view().info().seq);
|
||||
latch->setFieldH256(sfExportCommitteeHash, uint256{100});
|
||||
return ExportLedgerOps::insertPendingExportLatch(
|
||||
ctx.view(), ctx.rawView(), latch, env.journal);
|
||||
};
|
||||
BEAST_EXPECT(
|
||||
isTesSuccess(insertLatch(applyCtx, importingTx, importingOrigin)));
|
||||
BEAST_EXPECT(isTesSuccess(insertLatch(applyCtx, otherTx, otherOrigin)));
|
||||
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
auto const blocked = api.xport_cancel(importingOrigin, 0);
|
||||
BEAST_EXPECT(!blocked.has_value());
|
||||
BEAST_EXPECT(blocked.error() == PREREQUISITE_NOT_MET);
|
||||
BEAST_EXPECT(hookCtx.applyCtx.view().exists(
|
||||
keylet::exportLatch(alice.id(), importingOrigin)));
|
||||
|
||||
auto const invalidFlags = api.xport_cancel(otherOrigin, 0x00020000);
|
||||
BEAST_EXPECT(!invalidFlags.has_value());
|
||||
BEAST_EXPECT(invalidFlags.error() == INVALID_ARGUMENT);
|
||||
|
||||
auto const cancelled = api.xport_cancel(otherOrigin, 0);
|
||||
BEAST_EXPECT(cancelled.has_value());
|
||||
auto const canceledLatch = hookCtx.applyCtx.view().read(
|
||||
keylet::exportLatch(alice.id(), otherOrigin));
|
||||
BEAST_EXPECT(canceledLatch);
|
||||
BEAST_EXPECT(
|
||||
canceledLatch &&
|
||||
(canceledLatch->getFieldU32(sfFlags) & lsfExportCanceled) != 0);
|
||||
BEAST_EXPECT(
|
||||
canceledLatch && !canceledLatch->isFieldPresent(sfExportNode));
|
||||
|
||||
auto const erased = api.xport_cancel(otherOrigin, tfExportEraseLatch);
|
||||
BEAST_EXPECT(erased.has_value());
|
||||
BEAST_EXPECT(!hookCtx.applyCtx.view().exists(
|
||||
keylet::exportLatch(alice.id(), otherOrigin)));
|
||||
|
||||
auto exportTx = makeExportWrapper(alice.id(), importingTx);
|
||||
OpenView exportOv{*env.current()};
|
||||
ApplyContext exportApplyCtx =
|
||||
createApplyContext(env, exportOv, exportTx);
|
||||
std::uint32_t const exportOtherTicket = 9;
|
||||
auto exportOtherTx =
|
||||
makeExportedPayment(alice.id(), bob.id(), exportOtherTicket);
|
||||
auto const exportImportingOrigin = exportTx.getTransactionID();
|
||||
uint256 const exportOtherOrigin{4};
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
insertLatch(exportApplyCtx, importingTx, exportImportingOrigin)));
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
insertLatch(exportApplyCtx, exportOtherTx, exportOtherOrigin)));
|
||||
auto exportHookCtx =
|
||||
makeStubHookContext(exportApplyCtx, alice.id(), alice.id(), {});
|
||||
auto& exportApi = exportHookCtx.api();
|
||||
|
||||
auto const exportBlocked =
|
||||
exportApi.xport_cancel(exportImportingOrigin, 0);
|
||||
BEAST_EXPECT(!exportBlocked.has_value());
|
||||
BEAST_EXPECT(exportBlocked.error() == PREREQUISITE_NOT_MET);
|
||||
BEAST_EXPECT(exportHookCtx.applyCtx.view().exists(
|
||||
keylet::exportLatch(alice.id(), exportImportingOrigin)));
|
||||
|
||||
auto const exportCancelled =
|
||||
exportApi.xport_cancel(exportOtherOrigin, 0);
|
||||
BEAST_EXPECT(exportCancelled.has_value());
|
||||
auto const canceledExportLatch = exportHookCtx.applyCtx.view().read(
|
||||
keylet::exportLatch(alice.id(), exportOtherOrigin));
|
||||
BEAST_EXPECT(canceledExportLatch);
|
||||
BEAST_EXPECT(
|
||||
canceledExportLatch &&
|
||||
(canceledExportLatch->getFieldU32(sfFlags) & lsfExportCanceled) !=
|
||||
0);
|
||||
BEAST_EXPECT(
|
||||
canceledExportLatch &&
|
||||
!canceledExportLatch->isFieldPresent(sfExportNode));
|
||||
}
|
||||
|
||||
void
|
||||
test_fee_base(FeatureBitset features)
|
||||
{
|
||||
@@ -2668,6 +3102,30 @@ public:
|
||||
|
||||
// TODO: test INVALID_TXN
|
||||
|
||||
{
|
||||
// Auto-allocation must not choose the explicit destination slot.
|
||||
auto const xpopJson = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
std::string xpopStr = Json::FastWriter().write(xpopJson);
|
||||
STTx invokeTx = STTx(ttIMPORT, [&](STObject& obj) {
|
||||
obj.setFieldVL(sfBlob, *strUnHex(strHex(xpopStr)));
|
||||
});
|
||||
OpenView ov{*env.current()};
|
||||
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
auto const result = api.xpop_slot(0, 1);
|
||||
BEAST_EXPECT(result.has_value());
|
||||
if (result)
|
||||
{
|
||||
BEAST_EXPECT(result.value().first != result.value().second);
|
||||
BEAST_EXPECT(result.value().second == 1);
|
||||
BEAST_EXPECT(hookCtx.slot.count(result.value().first) == 1);
|
||||
BEAST_EXPECT(hookCtx.slot.count(result.value().second) == 1);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Success
|
||||
auto const xpopJson = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
@@ -3251,6 +3709,100 @@ public:
|
||||
INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
void
|
||||
test_slot_set_entropy(FeatureBitset features)
|
||||
{
|
||||
testcase(
|
||||
"Entropy has no state entry; slots cannot expose the seed pseudo");
|
||||
using namespace jtx;
|
||||
using namespace hook;
|
||||
Env env{*this, features | featureConsensusEntropy};
|
||||
Account const alice{"entropy-slot-alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
// Keep the ordinary transaction in the real unvalidated cache path;
|
||||
// validated lookup otherwise depends on optional test SQL history.
|
||||
env(pay(alice, Account::master, XRP(1)));
|
||||
auto const ordinary = env.tx();
|
||||
auto const retiredEntropyKey = sha512Half(std::uint16_t{'X'});
|
||||
auto const entropy = env.closed()->consensusEntropy();
|
||||
if (!BEAST_EXPECT(entropy != nullptr))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
entropy->getFieldU32(sfLedgerSequence) == env.closed()->seq());
|
||||
|
||||
auto bytes = [](LedgerEntryType type, uint256 const& key) {
|
||||
auto const t = static_cast<std::uint16_t>(type);
|
||||
Bytes out{
|
||||
static_cast<std::uint8_t>(t >> 8),
|
||||
static_cast<std::uint8_t>(t)};
|
||||
out.insert(out.end(), key.begin(), key.end());
|
||||
return out;
|
||||
};
|
||||
auto cache = [&](std::shared_ptr<STTx const> const& tx) {
|
||||
std::string reason;
|
||||
auto transaction =
|
||||
std::make_shared<Transaction>(tx, reason, env.app());
|
||||
env.app().getMasterTransaction().canonicalize(&transaction);
|
||||
auto const id = tx->getTransactionID();
|
||||
return Bytes{id.begin(), id.end()};
|
||||
};
|
||||
auto const ordinaryID = cache(ordinary);
|
||||
std::optional<Bytes> entropyID;
|
||||
for (auto const& [tx, meta] : env.closed()->txs)
|
||||
if (tx->getTxnType() == ttCONSENSUS_ENTROPY)
|
||||
entropyID = cache(tx);
|
||||
if (!BEAST_EXPECT(entropyID.has_value()))
|
||||
return;
|
||||
BEAST_EXPECT(ordinaryID.size() == 32 && entropyID->size() == 32);
|
||||
|
||||
auto check = [&](ReadView const& base) {
|
||||
OpenView ov{&base};
|
||||
STTx invokeTx{ttINVOKE, [](STObject&) {}};
|
||||
auto applyCtx = createApplyContext(env, ov, invokeTx);
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
auto const accountKey = keylet::account(alice.id());
|
||||
auto const accountBytes = bytes(accountKey.type, accountKey.key);
|
||||
auto const initial = api.slot_set(accountBytes, 1);
|
||||
if (!BEAST_EXPECT(initial && *initial == 1))
|
||||
return;
|
||||
auto const* preserved = api.slot(1).value();
|
||||
auto denied = [&](Bytes const& key, HookReturnCode error) {
|
||||
for (auto const destination : {0u, 1u})
|
||||
{
|
||||
auto const result = api.slot_set(key, destination);
|
||||
BEAST_EXPECT(!result && result.error() == error);
|
||||
// Denial must neither overwrite an existing slot nor
|
||||
// allocate an automatic slot containing the raw object.
|
||||
BEAST_EXPECT(api.slot(1).value() == preserved);
|
||||
auto const next = api.slot_set(accountBytes, 0);
|
||||
BEAST_EXPECT(next && *next == 2);
|
||||
if (next)
|
||||
BEAST_EXPECT(api.slot_clear(*next).has_value());
|
||||
}
|
||||
};
|
||||
for (auto const type : {ltANY, ltCHILD})
|
||||
denied(bytes(type, retiredEntropyKey), DOESNT_EXIST);
|
||||
// The entropy pseudo-transaction is another raw-digest carrier.
|
||||
denied(*entropyID, NOT_AUTHORIZED);
|
||||
for (auto const type : {ltACCOUNT_ROOT, ltANY, ltCHILD})
|
||||
{
|
||||
auto const result =
|
||||
api.slot_set(bytes(type, accountKey.key), 1);
|
||||
BEAST_EXPECT(result && *result == 1);
|
||||
}
|
||||
auto const transaction = api.slot_set(ordinaryID, 1);
|
||||
BEAST_EXPECT(transaction && *transaction == 1);
|
||||
BEAST_EXPECT(applyCtx.view().consensusEntropy() != nullptr);
|
||||
BEAST_EXPECT(
|
||||
!applyCtx.view().read(keylet::unchecked(retiredEntropyKey)));
|
||||
};
|
||||
check(*env.current());
|
||||
check(*env.closed());
|
||||
}
|
||||
|
||||
void
|
||||
test_slot_size(FeatureBitset features)
|
||||
{
|
||||
@@ -4721,6 +5273,8 @@ public:
|
||||
test_etxn_fee_base(features);
|
||||
test_etxn_nonce(features);
|
||||
test_etxn_reserve(features);
|
||||
test_xport_reserve(features);
|
||||
test_xport_cancel(features);
|
||||
test_fee_base(features);
|
||||
|
||||
test_otxn_field(features);
|
||||
@@ -4770,6 +5324,7 @@ public:
|
||||
test_slot_count(features);
|
||||
test_slot_float(features);
|
||||
test_slot_set(features);
|
||||
test_slot_set_entropy(features);
|
||||
test_slot_size(features);
|
||||
test_slot_subarray(features);
|
||||
test_slot_subfield(features);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <xrpld/app/tx/detail/Import.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/ledger/Dir.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -1560,6 +1561,25 @@ class Import_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(getVLInfo(*xpop, env.journal).has_value() == false);
|
||||
}
|
||||
|
||||
// Import: unl blob sequence was missing or negative
|
||||
{
|
||||
Json::Value tmpXpop = xpop;
|
||||
auto const data = base64_decode(
|
||||
tmpXpop[jss::validation][jss::unl][jss::blob].asString());
|
||||
Json::Value list;
|
||||
Json::Reader reader;
|
||||
BEAST_REQUIRE(reader.parse(data, list));
|
||||
list[jss::sequence] = -1;
|
||||
auto const listJson = writer.write(list);
|
||||
tmpXpop[jss::validation][jss::unl][jss::blob] =
|
||||
base64_encode(listJson);
|
||||
std::string strJson = writer.write(tmpXpop);
|
||||
Blob raw(strJson.begin(), strJson.end());
|
||||
auto const xpop = syntaxCheckXPOP(raw, env.journal);
|
||||
BEAST_REQUIRE(xpop);
|
||||
BEAST_EXPECT(getVLInfo(*xpop, env.journal).has_value() == false);
|
||||
}
|
||||
|
||||
// Import: valid unl blob
|
||||
{
|
||||
Json::Value tmpXpop = xpop;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/detail/PublishGap.h>
|
||||
#include <xrpld/app/ledger/detail/ValidatedLedgerWorkQueue.h>
|
||||
#include <xrpl/basics/RangeSet.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
@@ -253,6 +254,70 @@ class LedgerMaster_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(!lm.isPinned(seq));
|
||||
}
|
||||
|
||||
void
|
||||
testValidatedLedgerWorkQueue()
|
||||
{
|
||||
testcase("validated ledger work queue");
|
||||
|
||||
using detail::ValidatedLedgerWork;
|
||||
using detail::ValidatedLedgerWorkQueue;
|
||||
|
||||
ValidatedLedgerWorkQueue queue{2};
|
||||
ValidatedLedgerWork const a{10, uint256{1}};
|
||||
ValidatedLedgerWork const fork{10, uint256{2}};
|
||||
ValidatedLedgerWork const b{11, uint256{3}};
|
||||
|
||||
auto result = queue.enqueue(a);
|
||||
BEAST_EXPECT(result.inserted);
|
||||
BEAST_EXPECT(result.needsDrain);
|
||||
BEAST_EXPECT(!result.evicted);
|
||||
|
||||
result = queue.enqueue(a);
|
||||
BEAST_EXPECT(!result.inserted);
|
||||
BEAST_EXPECT(!result.needsDrain);
|
||||
BEAST_EXPECT(queue.size() == 1);
|
||||
|
||||
// Sequence alone is not identity: a different hash remains distinct.
|
||||
result = queue.enqueue(fork);
|
||||
BEAST_EXPECT(result.inserted);
|
||||
BEAST_EXPECT(!result.needsDrain);
|
||||
BEAST_EXPECT(queue.size() == 2);
|
||||
|
||||
// Saturation drops the oldest pending event and retains newer work.
|
||||
result = queue.enqueue(b);
|
||||
BEAST_EXPECT(result.inserted);
|
||||
BEAST_EXPECT(!result.needsDrain);
|
||||
BEAST_EXPECT(result.evicted && *result.evicted == a);
|
||||
|
||||
std::vector<ValidatedLedgerWork> drained;
|
||||
auto const count =
|
||||
queue.drain([&](ValidatedLedgerWork const& work) noexcept {
|
||||
// An exact duplicate cannot be queued while it is active.
|
||||
auto const duplicate = queue.enqueue(work);
|
||||
BEAST_EXPECT(!duplicate.inserted);
|
||||
drained.push_back(work);
|
||||
});
|
||||
|
||||
BEAST_EXPECT(count == 2);
|
||||
BEAST_EXPECT(drained.size() == 2);
|
||||
BEAST_EXPECT(drained[0] == fork);
|
||||
BEAST_EXPECT(drained[1] == b);
|
||||
BEAST_EXPECT(queue.size() == 0);
|
||||
BEAST_EXPECT(!queue.drainScheduled());
|
||||
|
||||
// A completed or failed-to-post drain can be scheduled again. A
|
||||
// failed post preserves the queued item, so the exact same identity
|
||||
// must be able to re-arm scheduling without inserting a duplicate.
|
||||
result = queue.enqueue(a);
|
||||
BEAST_EXPECT(result.needsDrain);
|
||||
queue.cancelDrain();
|
||||
BEAST_EXPECT(!queue.drainScheduled());
|
||||
result = queue.enqueue(a);
|
||||
BEAST_EXPECT(!result.inserted);
|
||||
BEAST_EXPECT(result.needsDrain);
|
||||
BEAST_EXPECT(queue.size() == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testSetPinnedRangesImmediateMerge()
|
||||
{
|
||||
@@ -334,6 +399,7 @@ public:
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
|
||||
testCanSkipPinnedGap();
|
||||
testValidatedLedgerWorkQueue();
|
||||
testWithFeats(all);
|
||||
testPinUnpinSymmetry();
|
||||
testSetPinnedRangesImmediateMerge();
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
#include <xrpld/overlay/PeerSet.h>
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
@@ -40,7 +42,7 @@ namespace test {
|
||||
struct LedgerReplay_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
run() override
|
||||
testReplay()
|
||||
{
|
||||
testcase("Replay ledger");
|
||||
|
||||
@@ -73,6 +75,73 @@ struct LedgerReplay_test : public beast::unit_test::suite
|
||||
|
||||
BEAST_EXPECT(replayed->info().hash == lastClosed->info().hash);
|
||||
}
|
||||
|
||||
void
|
||||
testConsensusEntropyReplay()
|
||||
{
|
||||
testcase("Replay ledger with consensus entropy");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
|
||||
Env env = [&] {
|
||||
auto c = jtx::envconfig();
|
||||
auto& sectionNode = c->section(ConfigSection::nodeDatabase());
|
||||
sectionNode.set("type", "memory");
|
||||
c->overwrite(SECTION_RELATIONAL_DB, "backend", "sqlite");
|
||||
return jtx::Env(
|
||||
*this,
|
||||
std::move(c),
|
||||
supported_amendments() | featureConsensusEntropy,
|
||||
nullptr);
|
||||
}();
|
||||
env.fund(XRP(100000), alice, bob);
|
||||
env.close();
|
||||
|
||||
LedgerMaster& ledgerMaster = env.app().getLedgerMaster();
|
||||
auto const lastClosed = ledgerMaster.getClosedLedger();
|
||||
auto const lastClosedParent =
|
||||
ledgerMaster.getLedgerByHash(lastClosed->info().parentHash);
|
||||
|
||||
LedgerReplay const replayData(lastClosedParent, lastClosed);
|
||||
auto const& orderedTxns = replayData.orderedTxns();
|
||||
BEAST_EXPECT(orderedTxns.size() > 1);
|
||||
if (orderedTxns.empty())
|
||||
return;
|
||||
|
||||
auto const& [index, entropyTx] = *orderedTxns.begin();
|
||||
BEAST_EXPECT(index == 0);
|
||||
BEAST_EXPECT(entropyTx->getTxnType() == ttCONSENSUS_ENTROPY);
|
||||
|
||||
auto const persisted =
|
||||
lastClosed->txRead(entropyTx->getTransactionID());
|
||||
BEAST_EXPECT(persisted.first);
|
||||
BEAST_EXPECT(
|
||||
persisted.second &&
|
||||
persisted.second->getFieldU32(sfTransactionIndex) == index);
|
||||
if (persisted.first)
|
||||
{
|
||||
Serializer persistedBytes;
|
||||
persisted.first->add(persistedBytes);
|
||||
Serializer replayBytes;
|
||||
entropyTx->add(replayBytes);
|
||||
BEAST_EXPECT(replayBytes.peekData() == persistedBytes.peekData());
|
||||
}
|
||||
|
||||
auto const replayed =
|
||||
buildLedger(replayData, tapNONE, env.app(), env.journal);
|
||||
|
||||
BEAST_EXPECT(replayed->info().hash == lastClosed->info().hash);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testReplay();
|
||||
testConsensusEntropyReplay();
|
||||
}
|
||||
};
|
||||
|
||||
enum class InboundLedgersBehavior {
|
||||
|
||||
@@ -1878,6 +1878,15 @@ class MPToken_test : public beast::unit_test::suite
|
||||
[jss::Amount] = mpt.getJson(JsonOptions::none);
|
||||
test(jv, sfAmounts.jsonName.c_str());
|
||||
}
|
||||
// Export callback fee authorization is a native-only amount.
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::Export;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[sfExportCallbackFee.jsonName] =
|
||||
mpt.getJson(JsonOptions::none);
|
||||
test(jv, sfExportCallbackFee.fieldName);
|
||||
}
|
||||
// ClaimReward
|
||||
{
|
||||
Json::Value jv = reward::claim(alice);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpl/basics/BasicConfig.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -97,11 +98,12 @@ public:
|
||||
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
|
||||
}
|
||||
|
||||
// any network up to and including networkid 1024 cannot support
|
||||
// NetworkID
|
||||
// Legacy networks cannot support NetworkID.
|
||||
{
|
||||
test::jtx::Env env{*this, makeNetworkConfig(1024)};
|
||||
BEAST_EXPECT(env.app().config().NETWORK_ID == 1024);
|
||||
test::jtx::Env env{
|
||||
*this, makeNetworkConfig(maxNetworkIDWithoutTxField)};
|
||||
BEAST_EXPECT(
|
||||
env.app().config().NETWORK_ID == maxNetworkIDWithoutTxField);
|
||||
|
||||
// try to submit a txn without network id, this should work
|
||||
Json::Value jv;
|
||||
@@ -110,18 +112,20 @@ public:
|
||||
runTx(env, jv, tesSUCCESS);
|
||||
|
||||
// now submit with a network id, this will fail
|
||||
jv[jss::NetworkID] = 1024;
|
||||
jv[jss::NetworkID] = maxNetworkIDWithoutTxField;
|
||||
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
|
||||
|
||||
jv[jss::NetworkID] = 1000;
|
||||
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
|
||||
}
|
||||
|
||||
// any network above networkid 1024 will produce an error if fed a txn
|
||||
// absent networkid
|
||||
// Any network above the legacy range rejects a txn absent NetworkID.
|
||||
{
|
||||
test::jtx::Env env{*this, makeNetworkConfig(1025)};
|
||||
BEAST_EXPECT(env.app().config().NETWORK_ID == 1025);
|
||||
test::jtx::Env env{
|
||||
*this, makeNetworkConfig(maxNetworkIDWithoutTxField + 1)};
|
||||
BEAST_EXPECT(
|
||||
env.app().config().NETWORK_ID ==
|
||||
maxNetworkIDWithoutTxField + 1);
|
||||
// try to submit a txn without network id, this should not work
|
||||
{
|
||||
env.fund(XRP(200), alice);
|
||||
@@ -150,11 +154,11 @@ public:
|
||||
jv[jss::NetworkID] = 0;
|
||||
runTx(env, jv, telWRONG_NETWORK);
|
||||
|
||||
jv[jss::NetworkID] = 1024;
|
||||
jv[jss::NetworkID] = maxNetworkIDWithoutTxField;
|
||||
runTx(env, jv, telWRONG_NETWORK);
|
||||
|
||||
// submit the correct network id
|
||||
jv[jss::NetworkID] = 1025;
|
||||
jv[jss::NetworkID] = maxNetworkIDWithoutTxField + 1;
|
||||
runTx(env, jv, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,17 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/detail/ApplyContext.h>
|
||||
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
|
||||
#include <xrpld/ledger/PaymentSandbox.h>
|
||||
#include <xrpld/ledger/Sandbox.h>
|
||||
#include <xrpl/protocol/EntropyTier.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -27,6 +35,27 @@ namespace test {
|
||||
|
||||
struct PseudoTx_test : public beast::unit_test::suite
|
||||
{
|
||||
STTx
|
||||
consensusEntropyTx(
|
||||
std::uint32_t seq,
|
||||
std::uint8_t tier,
|
||||
std::uint16_t count,
|
||||
std::uint16_t denominator,
|
||||
Blob contributors)
|
||||
{
|
||||
return STTx(ttCONSENSUS_ENTROPY, [&](auto& obj) {
|
||||
obj.setAccountID(sfAccount, AccountID());
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldAmount(sfFee, STAmount{});
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldH256(sfDigest, uint256(3));
|
||||
obj.setFieldU16(sfEntropyCount, count);
|
||||
obj.setFieldU16(sfEntropyDenominator, denominator);
|
||||
obj.setFieldVL(sfEntropyContributors, contributors);
|
||||
obj.setFieldU8(sfEntropyTier, tier);
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<STTx>
|
||||
getPseudoTxs(Rules const& rules, std::uint32_t seq)
|
||||
{
|
||||
@@ -56,6 +85,27 @@ struct PseudoTx_test : public beast::unit_test::suite
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
}));
|
||||
|
||||
res.emplace_back(consensusEntropyTx(
|
||||
seq, entropyTierValidatorQuorum, 1, 1, Blob{0x01}));
|
||||
|
||||
auto const secret = generateSecretKey(KeyType::secp256k1, randomSeed());
|
||||
auto const publicKey = derivePublicKey(KeyType::secp256k1, secret);
|
||||
ExportResultBuilder::PositionedSignatureSnapshot signatures;
|
||||
std::uint8_t const signatureBytes[] = {1, 2, 3};
|
||||
signatures.emplace(
|
||||
0,
|
||||
ExportResultBuilder::PositionedSignature{
|
||||
publicKey, Buffer{signatureBytes, sizeof(signatureBytes)}});
|
||||
auto const releaseTarget = STTx(ttPAYMENT, [&](auto& obj) {
|
||||
obj.setAccountID(sfAccount, AccountID(1));
|
||||
obj.setAccountID(sfDestination, AccountID(2));
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldAmount(sfFee, STAmount{});
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
});
|
||||
res.emplace_back(ExportResultBuilder::buildSignatureWitness(
|
||||
uint256(4), releaseTarget, signatures, 1, seq));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -92,7 +142,7 @@ struct PseudoTx_test : public beast::unit_test::suite
|
||||
[&](OpenView& view, beast::Journal j) {
|
||||
auto const result =
|
||||
ripple::apply(env.app(), view, stx, tapNONE, j);
|
||||
BEAST_EXPECT(!result.applied && result.ter == temINVALID);
|
||||
BEAST_EXPECT(!result.applied);
|
||||
return result.applied;
|
||||
});
|
||||
}
|
||||
@@ -109,6 +159,183 @@ struct PseudoTx_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
expectOpenLedgerResult(jtx::Env& env, STTx const& tx, TER expected)
|
||||
{
|
||||
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
|
||||
auto const result = ripple::apply(env.app(), view, tx, tapNONE, j);
|
||||
BEAST_EXPECT(result.ter == expected);
|
||||
BEAST_EXPECT(!result.applied);
|
||||
return result.applied;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testConsensusEntropyContributorMaskPreflight()
|
||||
{
|
||||
testcase("ConsensusEntropy contributor mask preflight");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, supported_amendments() | featureConsensusEntropy);
|
||||
auto const seq = env.closed()->seq() + 1;
|
||||
|
||||
expectOpenLedgerResult(
|
||||
env,
|
||||
consensusEntropyTx(seq, entropyTierConsensusFallback, 0, 0, Blob{}),
|
||||
temINVALID);
|
||||
expectOpenLedgerResult(
|
||||
env,
|
||||
consensusEntropyTx(
|
||||
seq, entropyTierValidatorQuorum, 2, 3, Blob{0x03}),
|
||||
temINVALID);
|
||||
|
||||
expectOpenLedgerResult(
|
||||
env,
|
||||
consensusEntropyTx(
|
||||
seq, entropyTierConsensusFallback, 1, 1, Blob{0x01}),
|
||||
temMALFORMED);
|
||||
expectOpenLedgerResult(
|
||||
env,
|
||||
consensusEntropyTx(
|
||||
seq, entropyTierValidatorQuorum, 1, 9, Blob{0x01}),
|
||||
temMALFORMED);
|
||||
expectOpenLedgerResult(
|
||||
env,
|
||||
consensusEntropyTx(
|
||||
seq, entropyTierValidatorQuorum, 1, 9, Blob{0x01, 0x02}),
|
||||
temMALFORMED);
|
||||
expectOpenLedgerResult(
|
||||
env,
|
||||
consensusEntropyTx(
|
||||
seq, entropyTierValidatorQuorum, 2, 3, Blob{0x01}),
|
||||
temMALFORMED);
|
||||
expectOpenLedgerResult(
|
||||
env,
|
||||
consensusEntropyTx(seq, entropyTierValidatorFull, 2, 3, Blob{0x03}),
|
||||
temMALFORMED);
|
||||
}
|
||||
|
||||
void
|
||||
testConsensusEntropyLedgerSequence()
|
||||
{
|
||||
testcase("ConsensusEntropy current ledger binding");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, supported_amendments() | featureConsensusEntropy);
|
||||
auto const parent = env.app().getLedgerMaster().getClosedLedger();
|
||||
auto const seq = parent->seq() + 1;
|
||||
|
||||
auto applyToNextLedger = [&](STTx const& tx, TER expected) {
|
||||
auto next = std::make_shared<Ledger>(
|
||||
*parent, env.app().timeKeeper().closeTime());
|
||||
OpenView accum(&*next);
|
||||
auto const result =
|
||||
ripple::apply(env.app(), accum, tx, tapNONE, env.journal);
|
||||
BEAST_EXPECT(result.ter == expected);
|
||||
BEAST_EXPECT(result.applied == isTesSuccess(expected));
|
||||
};
|
||||
|
||||
applyToNextLedger(
|
||||
consensusEntropyTx(
|
||||
seq + 1, entropyTierValidatorQuorum, 1, 1, Blob{0x01}),
|
||||
tefFAILURE);
|
||||
applyToNextLedger(
|
||||
consensusEntropyTx(
|
||||
seq, entropyTierValidatorQuorum, 1, 1, Blob{0x01}),
|
||||
tesSUCCESS);
|
||||
}
|
||||
|
||||
void
|
||||
testEntropyContextPublication()
|
||||
{
|
||||
testcase("Entropy context commit, discard, dry-run, and duplicate");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments() | featureConsensusEntropy};
|
||||
env.close();
|
||||
auto const parent = env.app().getLedgerMaster().getClosedLedger();
|
||||
auto next = std::make_shared<Ledger>(*parent, parent->info().closeTime);
|
||||
OpenView view{next.get()};
|
||||
BEAST_EXPECT(!view.consensusEntropy());
|
||||
auto const tx = consensusEntropyTx(
|
||||
view.seq(), entropyTierConsensusFallback, 0, 0, Blob{});
|
||||
auto const input = std::make_shared<STTx const>(tx);
|
||||
|
||||
// Staging is transaction-local and cannot contaminate the base or its
|
||||
// sibling snapshot when an attempt is discarded.
|
||||
OpenView sibling{view};
|
||||
ApplyContext staged{
|
||||
env.app(),
|
||||
view,
|
||||
tx,
|
||||
tesSUCCESS,
|
||||
view.fees().base,
|
||||
tapNONE,
|
||||
env.journal};
|
||||
staged.stageConsensusEntropy(input);
|
||||
BEAST_EXPECT(!view.consensusEntropy());
|
||||
staged.discard();
|
||||
// A synthetic successful apply after discard detects leaked staging.
|
||||
// This isolated view is never committed as a ledger.
|
||||
staged.apply(tesSUCCESS);
|
||||
BEAST_EXPECT(!view.consensusEntropy());
|
||||
BEAST_EXPECT(!sibling.consensusEntropy());
|
||||
|
||||
OpenView failedView{next.get()};
|
||||
ApplyContext failed{
|
||||
env.app(),
|
||||
failedView,
|
||||
tx,
|
||||
tesSUCCESS,
|
||||
failedView.fees().base,
|
||||
tapNONE,
|
||||
env.journal};
|
||||
failed.stageConsensusEntropy(input);
|
||||
failed.apply(tecINVARIANT_FAILED);
|
||||
BEAST_EXPECT(!failedView.consensusEntropy());
|
||||
|
||||
OpenView dryView{next.get()};
|
||||
auto const dry =
|
||||
ripple::apply(env.app(), dryView, tx, tapDRY_RUN, env.journal);
|
||||
BEAST_EXPECT(isTesSuccess(dry.ter));
|
||||
BEAST_EXPECT(!dryView.consensusEntropy());
|
||||
BEAST_EXPECT(dryView.txCount() == 0);
|
||||
|
||||
OpenView committed{next.get()};
|
||||
auto const result =
|
||||
ripple::apply(env.app(), committed, tx, tapNONE, env.journal);
|
||||
BEAST_EXPECT(result.applied && isTesSuccess(result.ter));
|
||||
auto const published = committed.consensusEntropy();
|
||||
if (!BEAST_EXPECT(published != nullptr))
|
||||
return;
|
||||
BEAST_EXPECT(published->getTransactionID() == tx.getTransactionID());
|
||||
BEAST_EXPECT(!sibling.consensusEntropy());
|
||||
|
||||
OpenView copied{committed};
|
||||
Sandbox sandbox{&copied, tapNONE};
|
||||
PaymentSandbox payment{&sandbox, tapNONE};
|
||||
BEAST_EXPECT(sandbox.consensusEntropy() == published);
|
||||
BEAST_EXPECT(payment.consensusEntropy() == published);
|
||||
// Discarding an ordinary transaction view leaves the ledger input.
|
||||
ApplyContext ordinary{
|
||||
env.app(),
|
||||
committed,
|
||||
tx,
|
||||
tesSUCCESS,
|
||||
committed.fees().base,
|
||||
tapNONE,
|
||||
env.journal};
|
||||
ordinary.discard();
|
||||
BEAST_EXPECT(ordinary.view().consensusEntropy() == published);
|
||||
|
||||
auto duplicate = consensusEntropyTx(
|
||||
committed.seq(), entropyTierValidatorQuorum, 1, 1, Blob{0x01});
|
||||
auto const rejected = ripple::apply(
|
||||
env.app(), committed, duplicate, tapNONE, env.journal);
|
||||
BEAST_EXPECT(rejected.ter == tefFAILURE && !rejected.applied);
|
||||
BEAST_EXPECT(committed.consensusEntropy() == published);
|
||||
BEAST_EXPECT(committed.txCount() == 1);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -119,6 +346,9 @@ struct PseudoTx_test : public beast::unit_test::suite
|
||||
testPrevented(all - featureXRPFees);
|
||||
testPrevented(all);
|
||||
testAllowed();
|
||||
testConsensusEntropyContributorMaskPreflight();
|
||||
testConsensusEntropyLedgerSequence();
|
||||
testEntropyContextPublication();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5688,6 +5688,26 @@ private:
|
||||
BEAST_EXPECT(!features[featurePermissionedDomains]);
|
||||
}
|
||||
|
||||
void
|
||||
testExportTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("export tsh");
|
||||
|
||||
// ttEXPORT is a wrapper/consensus-driven path, not a hook-dispatched
|
||||
// user transaction for triggered strong/weak hook execution.
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testExportSignaturesTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("export signatures tsh");
|
||||
|
||||
// Pseudo transaction consumed by ttEXPORT during ledger replay; it is
|
||||
// never a hook-dispatched originating transaction.
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testSetFeeTSH(FeatureBitset features)
|
||||
{
|
||||
@@ -6128,6 +6148,15 @@ private:
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testConsensusEntropyTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("consensus entropy tsh");
|
||||
|
||||
// pseudo transaction
|
||||
pass();
|
||||
}
|
||||
|
||||
// | otxn | tfBurnable | tsh | mint | burn | buy | sell | cancel
|
||||
// | O | false | O | N/A | S | N/A | S | S
|
||||
// | O | false | I | N/A | N | N/A | W | N/A
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/import.h>
|
||||
#include <test/jtx/xpop.h>
|
||||
#include <test/shamap/common.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/proof/LedgerProof.h>
|
||||
#include <xrpld/app/proof/ProofBuilder.h>
|
||||
#include <xrpld/app/proof/XPOPv1.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/protocol/Import.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
makeHash(char const* label)
|
||||
{
|
||||
return sha512Half(Slice(label, std::strlen(label)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct XPOP_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testBuildLedgerProof()
|
||||
{
|
||||
testcase("Build LedgerProof from a payment");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this};
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Submit a payment and close the ledger.
|
||||
env(pay(alice, bob, XRP(100)));
|
||||
env.close();
|
||||
|
||||
// Get the tx hash from the last closed ledger.
|
||||
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
|
||||
BEAST_EXPECT(lcl);
|
||||
|
||||
// Find a payment tx in the ledger.
|
||||
uint256 paymentHash;
|
||||
bool found = false;
|
||||
lcl->txMap().visitLeaves(
|
||||
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
if (!found)
|
||||
{
|
||||
paymentHash = item->key();
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
BEAST_EXPECT(found);
|
||||
|
||||
// Build the proof.
|
||||
auto const lp = proof::buildLedgerProof(*lcl, paymentHash);
|
||||
BEAST_EXPECT(lp.has_value());
|
||||
|
||||
if (lp)
|
||||
{
|
||||
// Verify header fields are populated.
|
||||
BEAST_EXPECT(lp->ledgerIndex > 0);
|
||||
BEAST_EXPECT(lp->totalCoins > 0);
|
||||
BEAST_EXPECT(lp->parentHash != uint256{});
|
||||
BEAST_EXPECT(lp->txRoot != uint256{});
|
||||
BEAST_EXPECT(lp->accountRoot != uint256{});
|
||||
|
||||
// Verify tx blob is non-empty.
|
||||
BEAST_EXPECT(!lp->txBlob.empty());
|
||||
BEAST_EXPECT(!lp->metaBlob.empty());
|
||||
|
||||
// Verify merkle proof exists and is valid.
|
||||
BEAST_EXPECT(lp->txProof.has_value());
|
||||
if (lp->txProof)
|
||||
{
|
||||
auto const computedRoot = lp->txProof->computeRoot();
|
||||
BEAST_EXPECT(computedRoot.has_value());
|
||||
if (computedRoot)
|
||||
BEAST_EXPECT(*computedRoot == lp->txRoot);
|
||||
}
|
||||
|
||||
// Verify ledger hash reconstruction.
|
||||
auto const computedHash = lp->computeLedgerHash();
|
||||
BEAST_EXPECT(computedHash == lcl->info().hash);
|
||||
}
|
||||
|
||||
auto missing = proof::buildLedgerProof(*lcl, makeHash("missing-tx"));
|
||||
BEAST_EXPECT(!missing);
|
||||
|
||||
auto const missingProof =
|
||||
proof::extractProofV1(lcl->txMap(), makeHash("missing-proof"));
|
||||
BEAST_EXPECT(!missingProof);
|
||||
}
|
||||
|
||||
void
|
||||
testProofBuilderEdgeCases()
|
||||
{
|
||||
testcase("ProofBuilder edge cases");
|
||||
|
||||
proof::MerkleProof empty;
|
||||
BEAST_EXPECT(!empty.computeRoot());
|
||||
BEAST_EXPECT(!empty.verify(makeHash("root")));
|
||||
BEAST_EXPECT(empty.toJsonV1().isNull());
|
||||
|
||||
proof::MerkleProof manual;
|
||||
manual.key = makeHash("manual-key");
|
||||
manual.leafHash = makeHash("manual-leaf");
|
||||
|
||||
proof::ProofNode leafParent;
|
||||
leafParent.targetBranch = 3;
|
||||
leafParent.isLeafParent = true;
|
||||
leafParent.branches[0] = makeHash("manual-sibling-0");
|
||||
manual.path.push_back(leafParent);
|
||||
|
||||
auto const computedRoot = manual.computeRoot();
|
||||
BEAST_EXPECT(computedRoot.has_value());
|
||||
if (computedRoot)
|
||||
BEAST_EXPECT(manual.verify(*computedRoot));
|
||||
|
||||
auto const proofJson = manual.toJsonV1();
|
||||
BEAST_EXPECT(proofJson.isArray());
|
||||
BEAST_EXPECT(proofJson.size() == 16);
|
||||
BEAST_EXPECT(proofJson[3].asString() == to_string(manual.leafHash));
|
||||
}
|
||||
|
||||
void
|
||||
testProofBuilderSyntheticTrie()
|
||||
{
|
||||
testcase("ProofBuilder synthetic trie collisions");
|
||||
|
||||
tests::TestNodeFamily f{beast::Journal{beast::Journal::getNullSink()}};
|
||||
SHAMap map{SHAMapType::TRANSACTION, f};
|
||||
|
||||
auto const keyA = uint256{
|
||||
"1000000000000000000000000000000000000000000000000000000000000001"};
|
||||
auto const keyB = uint256{
|
||||
"1800000000000000000000000000000000000000000000000000000000000002"};
|
||||
auto const keyC = uint256{
|
||||
"2000000000000000000000000000000000000000000000000000000000000003"};
|
||||
auto const keyD = uint256{
|
||||
"1f00000000000000000000000000000000000000000000000000000000000004"};
|
||||
|
||||
auto add = [&](uint256 const& key, Blob data) {
|
||||
return map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(key, makeSlice(data)));
|
||||
};
|
||||
auto payload = [](std::uint8_t first) {
|
||||
Blob data;
|
||||
data.reserve(12);
|
||||
for (std::uint8_t i = 0; i < 12; ++i)
|
||||
data.push_back(first + i);
|
||||
return data;
|
||||
};
|
||||
|
||||
BEAST_EXPECT(add(keyA, payload(0x01)));
|
||||
BEAST_EXPECT(add(keyB, payload(0x11)));
|
||||
BEAST_EXPECT(add(keyC, payload(0x21)));
|
||||
BEAST_EXPECT(add(keyD, payload(0x31)));
|
||||
map.invariants();
|
||||
|
||||
auto const proof = proof::extractProofV1(map, keyA);
|
||||
BEAST_EXPECT(proof.has_value());
|
||||
if (proof)
|
||||
{
|
||||
BEAST_EXPECT(proof->path.size() == 2);
|
||||
auto const computedRoot = proof->computeRoot();
|
||||
BEAST_EXPECT(computedRoot.has_value());
|
||||
if (computedRoot)
|
||||
BEAST_EXPECT(proof->verify(*computedRoot));
|
||||
|
||||
auto const json = proof->toJsonV1();
|
||||
BEAST_EXPECT(json.isArray());
|
||||
BEAST_EXPECT(json[proof->path.front().targetBranch].isArray());
|
||||
}
|
||||
|
||||
auto const nearMiss = uint256{
|
||||
"10000000000000000000000000000000000000000000000000000000000000ff"};
|
||||
BEAST_EXPECT(!proof::extractProofV1(map, nearMiss));
|
||||
}
|
||||
|
||||
void
|
||||
testBuildXPOPv1()
|
||||
{
|
||||
testcase("Build XPOP v1 JSON from a payment");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this};
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
env(pay(alice, bob, XRP(100)));
|
||||
env.close();
|
||||
|
||||
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
|
||||
BEAST_EXPECT(lcl);
|
||||
|
||||
// Find a tx.
|
||||
uint256 txHash;
|
||||
lcl->txMap().visitLeaves(
|
||||
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
txHash = item->key();
|
||||
});
|
||||
|
||||
// Build XPOP using the test helper.
|
||||
auto const xpopCtx = xpop::TestXPOPContext::create(3);
|
||||
auto const xpop = xpopCtx.buildXPOP(*lcl, txHash);
|
||||
BEAST_EXPECT(!xpop.isNull());
|
||||
|
||||
// Verify structure.
|
||||
BEAST_EXPECT(xpop.isMember(jss::ledger));
|
||||
BEAST_EXPECT(xpop.isMember(jss::transaction));
|
||||
BEAST_EXPECT(xpop.isMember(jss::validation));
|
||||
|
||||
// Ledger section.
|
||||
auto const& lgr = xpop[jss::ledger];
|
||||
BEAST_EXPECT(lgr.isMember(jss::index));
|
||||
BEAST_EXPECT(lgr.isMember(jss::coins));
|
||||
BEAST_EXPECT(lgr.isMember(jss::phash));
|
||||
BEAST_EXPECT(lgr.isMember(jss::txroot));
|
||||
BEAST_EXPECT(lgr.isMember(jss::acroot));
|
||||
BEAST_EXPECT(lgr.isMember(jss::close));
|
||||
BEAST_EXPECT(lgr.isMember(jss::pclose));
|
||||
BEAST_EXPECT(lgr.isMember(jss::cres));
|
||||
BEAST_EXPECT(lgr.isMember(jss::flags));
|
||||
|
||||
// Transaction section.
|
||||
auto const& txn = xpop[jss::transaction];
|
||||
BEAST_EXPECT(txn.isMember(jss::blob));
|
||||
BEAST_EXPECT(txn.isMember(jss::meta));
|
||||
BEAST_EXPECT(txn.isMember(jss::proof));
|
||||
BEAST_EXPECT(txn[jss::blob].asString().size() > 0);
|
||||
BEAST_EXPECT(txn[jss::meta].asString().size() > 0);
|
||||
|
||||
// Validation section.
|
||||
auto const& val = xpop[jss::validation];
|
||||
BEAST_EXPECT(val.isMember(jss::data));
|
||||
BEAST_EXPECT(val.isMember(jss::unl));
|
||||
BEAST_EXPECT(val[jss::data].size() == 3); // 3 validators
|
||||
|
||||
auto const& unl = val[jss::unl];
|
||||
BEAST_EXPECT(unl.isMember(jss::public_key));
|
||||
BEAST_EXPECT(unl.isMember(jss::manifest));
|
||||
BEAST_EXPECT(unl.isMember(jss::blob));
|
||||
BEAST_EXPECT(unl.isMember(jss::signature));
|
||||
BEAST_EXPECT(unl.isMember(jss::version));
|
||||
|
||||
auto const encoded = proof::xpopToHex(xpop);
|
||||
BEAST_EXPECT(!encoded.empty());
|
||||
BEAST_EXPECT(strUnHex(encoded).has_value());
|
||||
|
||||
auto const missing = proof::buildXPOPv1(
|
||||
*lcl,
|
||||
makeHash("missing-xpop-tx"),
|
||||
std::vector<proof::ValidatorKeys>{},
|
||||
xpopCtx.vlData);
|
||||
BEAST_EXPECT(missing.isNull());
|
||||
}
|
||||
|
||||
void
|
||||
testBuildXPOPv1WithoutMerkleProof()
|
||||
{
|
||||
testcase("Build XPOP v1 without merkle proof");
|
||||
|
||||
auto const xpopCtx = jtx::xpop::TestXPOPContext::create(0);
|
||||
|
||||
proof::LedgerProof lp;
|
||||
lp.ledgerIndex = 17;
|
||||
lp.totalCoins = 12345;
|
||||
lp.parentHash = makeHash("xpop-parent");
|
||||
lp.txRoot = makeHash("xpop-tx-root");
|
||||
lp.accountRoot = makeHash("xpop-account-root");
|
||||
lp.parentCloseTime = 100;
|
||||
lp.closeTime = 200;
|
||||
lp.closeTimeResolution = 10;
|
||||
lp.closeFlags = 1;
|
||||
lp.txBlob = Blob{0x12, 0x00, 0x00};
|
||||
lp.metaBlob = Blob{0x01, 0x02};
|
||||
|
||||
auto const xpop = proof::buildXPOPv1(
|
||||
lp, std::vector<proof::ValidatorKeys>{}, xpopCtx.vlData);
|
||||
BEAST_EXPECT(!xpop.isNull());
|
||||
BEAST_EXPECT(xpop[jss::transaction][jss::proof].isArray());
|
||||
BEAST_EXPECT(xpop[jss::transaction][jss::proof].size() == 0);
|
||||
BEAST_EXPECT(xpop[jss::validation][jss::data].size() == 0);
|
||||
|
||||
auto const encoded = proof::xpopToHex(xpop);
|
||||
BEAST_EXPECT(!encoded.empty());
|
||||
BEAST_EXPECT(strUnHex(encoded).has_value());
|
||||
}
|
||||
|
||||
void
|
||||
testMerkleProofVerification()
|
||||
{
|
||||
testcase("Merkle proof verifies against tx root");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this};
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// Multiple transactions to create a deeper trie.
|
||||
env(pay(alice, bob, XRP(10)));
|
||||
env(pay(bob, carol, XRP(5)));
|
||||
env(pay(carol, alice, XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
|
||||
BEAST_EXPECT(lcl);
|
||||
|
||||
// Verify proof for each transaction in the ledger.
|
||||
int proofCount = 0;
|
||||
lcl->txMap().visitLeaves(
|
||||
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
auto const lp = proof::buildLedgerProof(*lcl, item->key());
|
||||
BEAST_EXPECT(lp.has_value());
|
||||
|
||||
if (lp && lp->txProof)
|
||||
{
|
||||
// Proof must verify against the ledger's tx root.
|
||||
BEAST_EXPECT(lp->txProof->verify(lp->txRoot));
|
||||
|
||||
// JSON v1 serialization must round-trip.
|
||||
auto const json = lp->txProof->toJsonV1();
|
||||
BEAST_EXPECT(!json.isNull());
|
||||
BEAST_EXPECT(json.isArray());
|
||||
|
||||
++proofCount;
|
||||
}
|
||||
});
|
||||
|
||||
// We should have proven at least 3 transactions.
|
||||
BEAST_EXPECT(proofCount >= 3);
|
||||
}
|
||||
|
||||
void
|
||||
testImportWithGeneratedXPOP()
|
||||
{
|
||||
testcase("Import accepts dynamically generated XPOP");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create XPOP context (VL publisher + validators).
|
||||
auto const xpopCtx = xpop::TestXPOPContext::create(3);
|
||||
|
||||
// --- Source "network": generate a payment and build XPOP ---
|
||||
Env srcEnv{*this};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
srcEnv.fund(XRP(10000), alice, bob);
|
||||
srcEnv.close();
|
||||
|
||||
// Import requires: no sfNetworkID + sfOperationLimit = dest NETWORK_ID.
|
||||
Json::Value payTx;
|
||||
payTx[jss::TransactionType] = jss::Payment;
|
||||
payTx[jss::Account] = alice.human();
|
||||
payTx[jss::Destination] = bob.human();
|
||||
payTx[jss::Amount] = "100000000";
|
||||
payTx[sfOperationLimit.jsonName] = 21337;
|
||||
srcEnv(payTx, fee(XRP(1)));
|
||||
srcEnv.close();
|
||||
|
||||
// Find the tx hash and build the XPOP.
|
||||
auto const srcLcl = srcEnv.app().getLedgerMaster().getClosedLedger();
|
||||
BEAST_EXPECT(srcLcl);
|
||||
|
||||
uint256 paymentHash;
|
||||
srcLcl->txMap().visitLeaves(
|
||||
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
paymentHash = item->key();
|
||||
});
|
||||
|
||||
auto const xpopJson = xpopCtx.buildXPOP(*srcLcl, paymentHash);
|
||||
BEAST_EXPECT(!xpopJson.isNull());
|
||||
|
||||
// --- Destination "network": import the XPOP ---
|
||||
Env dstEnv{*this, xpopCtx.makeEnvConfig(21337)};
|
||||
|
||||
// Burn some XRP so B2M can credit.
|
||||
auto const master = Account("masterpassphrase");
|
||||
dstEnv(noop(master), fee(10'000'000'000), ter(tesSUCCESS));
|
||||
dstEnv.close();
|
||||
|
||||
Account const importAlice{"alice"};
|
||||
dstEnv.fund(XRP(1000), importAlice);
|
||||
dstEnv.close();
|
||||
|
||||
auto const feeDrops = dstEnv.current()->fees().base;
|
||||
|
||||
// Submit the import — should succeed (B2M path).
|
||||
dstEnv(
|
||||
import::import(importAlice, xpopJson),
|
||||
fee(feeDrops * 10),
|
||||
ter(tesSUCCESS));
|
||||
dstEnv.close();
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testBuildLedgerProof();
|
||||
testProofBuilderEdgeCases();
|
||||
testProofBuilderSyntheticTrie();
|
||||
testBuildXPOPv1();
|
||||
testBuildXPOPv1WithoutMerkleProof();
|
||||
testMerkleProofVerification();
|
||||
testImportWithGeneratedXPOP();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(XPOP, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,634 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/hook/detail/XportWrapperBuilder.h>
|
||||
#include <xrpl/basics/Expected.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/ExportOriginMemo.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
makeHash(char const* label)
|
||||
{
|
||||
return sha512Half(Slice(label, std::strlen(label)));
|
||||
}
|
||||
|
||||
STTx
|
||||
makeSTTx(STObject const& obj)
|
||||
{
|
||||
Serializer s;
|
||||
obj.add(s);
|
||||
SerialIter sit{s.slice()};
|
||||
return STTx{std::ref(sit)};
|
||||
}
|
||||
|
||||
Blob
|
||||
serialize(STTx const& tx)
|
||||
{
|
||||
Serializer s;
|
||||
tx.add(s);
|
||||
return {s.begin(), s.end()};
|
||||
}
|
||||
|
||||
STTx
|
||||
makeExportedPayment(
|
||||
AccountID const& src,
|
||||
AccountID const& dst,
|
||||
std::optional<std::uint32_t> networkID = std::nullopt,
|
||||
std::optional<std::uint32_t> ticketSequence = 1,
|
||||
std::uint32_t sequence = 0)
|
||||
{
|
||||
STObject obj(sfExportedTxn);
|
||||
obj.setFieldU16(sfTransactionType, ttPAYMENT);
|
||||
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
obj.setFieldU32(sfSequence, sequence);
|
||||
if (ticketSequence)
|
||||
obj.setFieldU32(sfTicketSequence, *ticketSequence);
|
||||
obj.setFieldU32(sfFirstLedgerSequence, 2);
|
||||
obj.setFieldU32(sfLastLedgerSequence, 6);
|
||||
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
|
||||
obj.setFieldAmount(sfFee, XRPAmount{10});
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
obj.setAccountID(sfAccount, src);
|
||||
obj.setAccountID(sfDestination, dst);
|
||||
if (networkID)
|
||||
obj.setFieldU32(sfNetworkID, *networkID);
|
||||
return makeSTTx(obj);
|
||||
}
|
||||
|
||||
STTx
|
||||
withMemo(STTx tx, Blob type, Blob data)
|
||||
{
|
||||
STArray memos = tx.isFieldPresent(sfMemos) ? tx.getFieldArray(sfMemos)
|
||||
: STArray{sfMemos};
|
||||
STObject memo{sfMemo};
|
||||
memo.setFieldVL(sfMemoType, std::move(type));
|
||||
memo.setFieldVL(sfMemoData, std::move(data));
|
||||
memos.emplace_back(std::move(memo));
|
||||
tx.setFieldArray(sfMemos, memos);
|
||||
return tx;
|
||||
}
|
||||
|
||||
beast::Journal
|
||||
nullJournal()
|
||||
{
|
||||
return beast::Journal{beast::Journal::getNullSink()};
|
||||
}
|
||||
|
||||
hook::XportWrapperBuilder::Input
|
||||
makeInput(
|
||||
Slice innerTxBlob,
|
||||
AccountID const& exporter,
|
||||
std::uint32_t networkID = 21337,
|
||||
hook::XportWrapperBuilder::NonceGenerator generateNonce =
|
||||
[] {
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
},
|
||||
hook::XportWrapperBuilder::FeeCalculator calculateFee =
|
||||
[](Slice const&) {
|
||||
return Expected<std::uint64_t, ::hook_api::hook_return_code>{12345};
|
||||
})
|
||||
{
|
||||
return hook::XportWrapperBuilder::Input{
|
||||
innerTxBlob,
|
||||
makeHash("committee"),
|
||||
exporter,
|
||||
networkID,
|
||||
10,
|
||||
makeHash("parent-tx"),
|
||||
makeHash("hook-hash"),
|
||||
true,
|
||||
3,
|
||||
7,
|
||||
std::move(generateNonce),
|
||||
std::move(calculateFee),
|
||||
nullJournal()};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class XportWrapperBuilder_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
testBuildsWrapper()
|
||||
{
|
||||
testcase("builds xport wrapper");
|
||||
|
||||
auto const exporter = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first));
|
||||
auto const serialized = serialize(innerTx);
|
||||
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first)));
|
||||
|
||||
BEAST_EXPECT(result);
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
auto const& wrapper = result->wrapperTx;
|
||||
BEAST_EXPECT(wrapper.getTransactionID() != innerTx.getTransactionID());
|
||||
BEAST_EXPECT(wrapper.getTxnType() == ttEXPORT);
|
||||
BEAST_EXPECT(
|
||||
wrapper.getAccountID(sfAccount) == calcAccountID(exporter.first));
|
||||
BEAST_EXPECT(wrapper.getFieldU32(sfSequence) == 0);
|
||||
BEAST_EXPECT(wrapper.getFieldU32(sfFirstLedgerSequence) == 11);
|
||||
BEAST_EXPECT(
|
||||
wrapper.getFieldU32(sfLastLedgerSequence) ==
|
||||
10 + ExportLimits::maxAdmissionWindowLedgers);
|
||||
BEAST_EXPECT(wrapper.getFieldAmount(sfFee) == STAmount{12345});
|
||||
BEAST_EXPECT(!wrapper.isFieldPresent(sfExportCallbackFee));
|
||||
BEAST_EXPECT(wrapper.getFieldVL(sfSigningPubKey).empty());
|
||||
BEAST_EXPECT(
|
||||
wrapper.getFieldH256(sfExportCommitteeHash) ==
|
||||
makeHash("committee"));
|
||||
|
||||
auto const& exported =
|
||||
wrapper.peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
Serializer exportedSer;
|
||||
exported.add(exportedSer);
|
||||
STTx parsedInner{SerialIter{exportedSer.slice()}};
|
||||
BEAST_EXPECT(
|
||||
parsedInner.getTransactionID() == innerTx.getTransactionID());
|
||||
|
||||
auto const& emitDetails =
|
||||
wrapper.peekAtField(sfEmitDetails).downcast<STObject>();
|
||||
BEAST_EXPECT(emitDetails.getFieldU32(sfEmitGeneration) == 3);
|
||||
BEAST_EXPECT(emitDetails.getFieldU64(sfEmitBurden) == 7);
|
||||
BEAST_EXPECT(
|
||||
emitDetails.getFieldH256(sfEmitParentTxnID) ==
|
||||
makeHash("parent-tx"));
|
||||
BEAST_EXPECT(
|
||||
emitDetails.getFieldH256(sfEmitNonce) == makeHash("nonce"));
|
||||
BEAST_EXPECT(
|
||||
emitDetails.getFieldH256(sfEmitHookHash) == makeHash("hook-hash"));
|
||||
BEAST_EXPECT(
|
||||
emitDetails.getAccountID(sfEmitCallback) ==
|
||||
calcAccountID(exporter.first));
|
||||
}
|
||||
|
||||
void
|
||||
testCallbackFee()
|
||||
{
|
||||
testcase(
|
||||
"exact callback fee is optional, bounded, and priced in wrapper");
|
||||
auto const exporter = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const inner = makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first));
|
||||
auto const blob = serialize(inner);
|
||||
auto input = makeInput(makeSlice(blob), calcAccountID(exporter.first));
|
||||
bool priced = false;
|
||||
input.calculateFee = [&](Slice const& bytes) {
|
||||
STTx const wrapper{SerialIter{bytes}};
|
||||
BEAST_EXPECT(
|
||||
wrapper.isFieldPresent(sfExportCallbackFee) ==
|
||||
(input.callbackFeeDrops != 0));
|
||||
if (input.callbackFeeDrops != 0)
|
||||
BEAST_EXPECT(
|
||||
wrapper.getFieldAmount(sfExportCallbackFee).xrp() ==
|
||||
XRPAmount{
|
||||
static_cast<std::int64_t>(input.callbackFeeDrops)});
|
||||
priced = true;
|
||||
return Expected<std::uint64_t, ::hook_api::hook_return_code>{12345};
|
||||
};
|
||||
for (auto amount :
|
||||
{std::uint64_t{0},
|
||||
std::uint64_t{1000},
|
||||
static_cast<std::uint64_t>(INITIAL_XRP.drops())})
|
||||
{
|
||||
input.callbackFeeDrops = amount;
|
||||
priced = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(input);
|
||||
BEAST_EXPECT(result);
|
||||
BEAST_EXPECT(priced);
|
||||
if (!result)
|
||||
continue;
|
||||
BEAST_EXPECT(
|
||||
result->wrapperTx.getFieldAmount(sfFee) == STAmount{12345});
|
||||
auto const& target = result->wrapperTx.peekAtField(sfExportedTxn)
|
||||
.downcast<STObject>();
|
||||
BEAST_EXPECT(!target.isFieldPresent(sfExportCallbackFee));
|
||||
}
|
||||
for (auto amount :
|
||||
{static_cast<std::uint64_t>(INITIAL_XRP.drops()) + 1,
|
||||
std::numeric_limits<std::uint64_t>::max()})
|
||||
{
|
||||
input.callbackFeeDrops = amount;
|
||||
priced = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(input);
|
||||
BEAST_EXPECT(!result);
|
||||
if (!result)
|
||||
BEAST_EXPECT(
|
||||
result.error() ==
|
||||
::hook_api::hook_return_code::INVALID_ARGUMENT);
|
||||
BEAST_EXPECT(!priced);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testBuildsWrapperWithoutCallback()
|
||||
{
|
||||
testcase("builds xport wrapper without callback");
|
||||
|
||||
auto const exporter = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first));
|
||||
auto const serialized = serialize(innerTx);
|
||||
|
||||
auto input = makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first));
|
||||
input.hasCallback = false;
|
||||
|
||||
auto const result = hook::XportWrapperBuilder::build(input);
|
||||
BEAST_EXPECT(result);
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
auto const& emitDetails =
|
||||
result->wrapperTx.peekAtField(sfEmitDetails).downcast<STObject>();
|
||||
BEAST_EXPECT(!emitDetails.isFieldPresent(sfEmitCallback));
|
||||
}
|
||||
|
||||
void
|
||||
testPreservesUserMemos()
|
||||
{
|
||||
testcase("preserves user memos");
|
||||
|
||||
auto const exporter = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = withMemo(
|
||||
makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first)),
|
||||
Blob{'u', 's', 'e', 'r'},
|
||||
Blob{1, 2, 3, 4});
|
||||
auto const serialized = serialize(innerTx);
|
||||
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first)));
|
||||
|
||||
BEAST_EXPECT(result);
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
auto const& exported =
|
||||
result->wrapperTx.peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
Serializer exportedSer;
|
||||
exported.add(exportedSer);
|
||||
STTx parsedInner{SerialIter{exportedSer.slice()}};
|
||||
BEAST_EXPECT(serialize(parsedInner) == serialized);
|
||||
BEAST_EXPECT(!ExportOriginMemo::hasReservedMemo(parsedInner));
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsInvalidInputs()
|
||||
{
|
||||
testcase("rejects invalid inputs");
|
||||
|
||||
auto const exporter = randomKeyPair(KeyType::secp256k1);
|
||||
auto const other = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first));
|
||||
auto const serialized = serialize(innerTx);
|
||||
|
||||
{
|
||||
auto input = makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first));
|
||||
input.committeeHash = {};
|
||||
auto const result = hook::XportWrapperBuilder::build(input);
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() ==
|
||||
::hook_api::hook_return_code::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
{
|
||||
Blob malformed{1, 2, 3};
|
||||
bool nonceCalled = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(malformed.data(), malformed.size()),
|
||||
calcAccountID(exporter.first),
|
||||
21337,
|
||||
[&nonceCalled] {
|
||||
nonceCalled = true;
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
}));
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
BEAST_EXPECT(!nonceCalled);
|
||||
}
|
||||
|
||||
auto expectSigningEnvelopeRejected = [&](STTx tx) {
|
||||
auto const blob = serialize(tx);
|
||||
bool nonceCalled = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(blob.data(), blob.size()),
|
||||
calcAccountID(exporter.first),
|
||||
21337,
|
||||
[&nonceCalled] {
|
||||
nonceCalled = true;
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
}));
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
BEAST_EXPECT(!nonceCalled);
|
||||
};
|
||||
|
||||
{
|
||||
auto signedInner = innerTx;
|
||||
signedInner.setFieldVL(sfSigningPubKey, exporter.first.slice());
|
||||
expectSigningEnvelopeRejected(std::move(signedInner));
|
||||
}
|
||||
|
||||
{
|
||||
auto signedInner = innerTx;
|
||||
signedInner.setFieldVL(sfTxnSignature, Blob{1, 2, 3});
|
||||
expectSigningEnvelopeRejected(std::move(signedInner));
|
||||
}
|
||||
|
||||
{
|
||||
auto signedInner = innerTx;
|
||||
STArray signers(sfSigners);
|
||||
STObject signer(sfSigner);
|
||||
signer.setAccountID(sfAccount, calcAccountID(exporter.first));
|
||||
signer.setFieldVL(sfSigningPubKey, exporter.first.slice());
|
||||
signer.setFieldVL(sfTxnSignature, Blob{1, 2, 3});
|
||||
signers.push_back(std::move(signer));
|
||||
signedInner.setFieldArray(sfSigners, signers);
|
||||
expectSigningEnvelopeRejected(std::move(signedInner));
|
||||
}
|
||||
|
||||
{
|
||||
bool nonceCalled = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(other.first),
|
||||
21337,
|
||||
[&nonceCalled] {
|
||||
nonceCalled = true;
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
}));
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
BEAST_EXPECT(!nonceCalled);
|
||||
}
|
||||
|
||||
{
|
||||
auto const networkTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first), 21337);
|
||||
auto const serializedNetwork = serialize(networkTx);
|
||||
bool nonceCalled = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(serializedNetwork.data(), serializedNetwork.size()),
|
||||
calcAccountID(exporter.first),
|
||||
21337,
|
||||
[&nonceCalled] {
|
||||
nonceCalled = true;
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
}));
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
BEAST_EXPECT(!nonceCalled);
|
||||
}
|
||||
|
||||
{
|
||||
bool nonceCalled = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first),
|
||||
0,
|
||||
[&nonceCalled] {
|
||||
nonceCalled = true;
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
}));
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
BEAST_EXPECT(!nonceCalled);
|
||||
}
|
||||
|
||||
{
|
||||
auto const noTicketTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first),
|
||||
calcAccountID(dst.first),
|
||||
std::nullopt,
|
||||
std::nullopt);
|
||||
auto const serializedNoTicket = serialize(noTicketTx);
|
||||
bool nonceCalled = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(serializedNoTicket.data(), serializedNoTicket.size()),
|
||||
calcAccountID(exporter.first),
|
||||
21337,
|
||||
[&nonceCalled] {
|
||||
nonceCalled = true;
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
}));
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
BEAST_EXPECT(!nonceCalled);
|
||||
}
|
||||
|
||||
{
|
||||
auto const sequencedTicketTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first),
|
||||
calcAccountID(dst.first),
|
||||
std::nullopt,
|
||||
1,
|
||||
9);
|
||||
auto const serializedSequencedTicket = serialize(sequencedTicketTx);
|
||||
bool nonceCalled = false;
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(
|
||||
serializedSequencedTicket.data(),
|
||||
serializedSequencedTicket.size()),
|
||||
calcAccountID(exporter.first),
|
||||
21337,
|
||||
[&nonceCalled] {
|
||||
nonceCalled = true;
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
}));
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
BEAST_EXPECT(!nonceCalled);
|
||||
}
|
||||
|
||||
{
|
||||
auto const reservedType = Blob(
|
||||
ExportOriginMemo::memoType.begin(),
|
||||
ExportOriginMemo::memoType.end());
|
||||
auto const reservedMemoTx = withMemo(
|
||||
innerTx, reservedType, Blob(ExportOriginMemo::identityBytes));
|
||||
expectSigningEnvelopeRejected(reservedMemoTx);
|
||||
}
|
||||
|
||||
{
|
||||
auto const fullMemoTx =
|
||||
withMemo(innerTx, Blob{'u', 's', 'e', 'r'}, Blob(930, 0x5A));
|
||||
std::string reason;
|
||||
BEAST_EXPECT(passesLocalChecks(fullMemoTx, reason));
|
||||
expectSigningEnvelopeRejected(fullMemoTx);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsMissingCallbacks()
|
||||
{
|
||||
testcase("rejects missing callbacks");
|
||||
|
||||
auto const exporter = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first));
|
||||
auto const serialized = serialize(innerTx);
|
||||
|
||||
{
|
||||
auto input = makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first));
|
||||
input.generateNonce = {};
|
||||
|
||||
auto const result = hook::XportWrapperBuilder::build(input);
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
{
|
||||
auto input = makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first));
|
||||
input.calculateFee = {};
|
||||
|
||||
auto const result = hook::XportWrapperBuilder::build(input);
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMapsNonceFailureToInternalError()
|
||||
{
|
||||
testcase("maps nonce failure to internal error");
|
||||
|
||||
auto const exporter = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first));
|
||||
auto const serialized = serialize(innerTx);
|
||||
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first),
|
||||
21337,
|
||||
[] {
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
Unexpected(::hook_api::hook_return_code::TOO_MANY_NONCES)};
|
||||
}));
|
||||
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsFeeFailure()
|
||||
{
|
||||
testcase("rejects fee failure");
|
||||
|
||||
auto const exporter = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(exporter.first), calcAccountID(dst.first));
|
||||
auto const serialized = serialize(innerTx);
|
||||
|
||||
auto const result = hook::XportWrapperBuilder::build(makeInput(
|
||||
Slice(serialized.data(), serialized.size()),
|
||||
calcAccountID(exporter.first),
|
||||
21337,
|
||||
[] {
|
||||
return Expected<uint256, ::hook_api::hook_return_code>{
|
||||
makeHash("nonce")};
|
||||
},
|
||||
[](Slice const&) {
|
||||
return Expected<std::uint64_t, ::hook_api::hook_return_code>{
|
||||
Unexpected(::hook_api::hook_return_code::EXPORT_FAILURE)};
|
||||
}));
|
||||
|
||||
BEAST_EXPECT(!result);
|
||||
BEAST_EXPECT(
|
||||
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testBuildsWrapper();
|
||||
testCallbackFee();
|
||||
testBuildsWrapperWithoutCallback();
|
||||
testPreservesUserMemos();
|
||||
testRejectsInvalidInputs();
|
||||
testRejectsMissingCallbacks();
|
||||
testMapsNonceFailureToInternalError();
|
||||
testRejectsFeeFailure();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(XportWrapperBuilder, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -22,6 +22,8 @@
|
||||
#include <xrpld/consensus/ConsensusProposal.h>
|
||||
#include <xrpl/beast/clock/manual_clock.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace ripple {
|
||||
@@ -36,10 +38,18 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static void
|
||||
enableSilentTracing(csf::Sim& sim)
|
||||
{
|
||||
sim.sink.silent(true);
|
||||
sim.sink.threshold(beast::severities::kTrace);
|
||||
}
|
||||
|
||||
void
|
||||
testShouldCloseLedger()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
testcase("should close ledger");
|
||||
|
||||
// Use default parameters
|
||||
ConsensusParms const p{};
|
||||
@@ -78,46 +88,102 @@ public:
|
||||
testCheckConsensus()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
testcase("check consensus");
|
||||
|
||||
// Use default parameterss
|
||||
ConsensusParms const p{};
|
||||
|
||||
///////////////
|
||||
// Disputes still in doubt
|
||||
//
|
||||
// Not enough time has elapsed
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(10, 2, 2, 0, 3s, 2s, p, true, journal_));
|
||||
checkConsensus(10, 2, 2, 0, 3s, 2s, false, p, true, journal_));
|
||||
|
||||
// If not enough peers have propsed, ensure
|
||||
// more time for proposals
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(10, 2, 2, 0, 3s, 4s, p, true, journal_));
|
||||
checkConsensus(10, 2, 2, 0, 3s, 4s, false, p, true, journal_));
|
||||
|
||||
// Enough time has elapsed and we all agree
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(10, 2, 2, 0, 3s, 10s, p, true, journal_));
|
||||
checkConsensus(10, 2, 2, 0, 3s, 10s, false, p, true, journal_));
|
||||
|
||||
// Enough time has elapsed and we don't yet agree
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(10, 2, 1, 0, 3s, 10s, p, true, journal_));
|
||||
checkConsensus(10, 2, 1, 0, 3s, 10s, false, p, true, journal_));
|
||||
|
||||
// Our peers have moved on
|
||||
// Enough time has elapsed and we all agree
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::MovedOn ==
|
||||
checkConsensus(10, 2, 1, 8, 3s, 10s, p, true, journal_));
|
||||
checkConsensus(10, 2, 1, 8, 3s, 10s, false, p, true, journal_));
|
||||
|
||||
// If no peers, don't agree until time has passed.
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(0, 0, 0, 0, 3s, 10s, p, true, journal_));
|
||||
checkConsensus(0, 0, 0, 0, 3s, 10s, false, p, true, journal_));
|
||||
|
||||
// Agree if no peers and enough time has passed.
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(0, 0, 0, 0, 3s, 16s, p, true, journal_));
|
||||
checkConsensus(0, 0, 0, 0, 3s, 16s, false, p, true, journal_));
|
||||
|
||||
// Expire if too much time has passed without agreement
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Expired ==
|
||||
checkConsensus(10, 8, 1, 0, 1s, 19s, false, p, true, journal_));
|
||||
|
||||
///////////////
|
||||
// Stalled
|
||||
//
|
||||
// Not enough time has elapsed
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(10, 2, 2, 0, 3s, 2s, true, p, true, journal_));
|
||||
|
||||
// If not enough peers have propsed, ensure
|
||||
// more time for proposals
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(10, 2, 2, 0, 3s, 4s, true, p, true, journal_));
|
||||
|
||||
// Enough time has elapsed and we all agree
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(10, 2, 2, 0, 3s, 10s, true, p, true, journal_));
|
||||
|
||||
// Enough time has elapsed and we don't yet agree, but there's nothing
|
||||
// left to dispute
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(10, 2, 1, 0, 3s, 10s, true, p, true, journal_));
|
||||
|
||||
// Our peers have moved on
|
||||
// Enough time has elapsed and we all agree, nothing left to dispute
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(10, 2, 1, 8, 3s, 10s, true, p, true, journal_));
|
||||
|
||||
// If no peers, don't agree until time has passed.
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::No ==
|
||||
checkConsensus(0, 0, 0, 0, 3s, 10s, true, p, true, journal_));
|
||||
|
||||
// Agree if no peers and enough time has passed.
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(0, 0, 0, 0, 3s, 16s, true, p, true, journal_));
|
||||
|
||||
// We are done if there's nothing left to dispute, no matter how much
|
||||
// time has passed
|
||||
BEAST_EXPECT(
|
||||
ConsensusState::Yes ==
|
||||
checkConsensus(10, 8, 1, 0, 1s, 19s, true, p, true, journal_));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -125,6 +191,7 @@ public:
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
using namespace csf;
|
||||
testcase("standalone");
|
||||
|
||||
Sim s;
|
||||
PeerGroup peers = s.createGroup(1);
|
||||
@@ -149,9 +216,12 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("peers agree");
|
||||
|
||||
//@@start peers-agree
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
PeerGroup peers = sim.createGroup(5);
|
||||
|
||||
// Connected trust and network graphs with single fixed delay
|
||||
@@ -179,6 +249,7 @@ public:
|
||||
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
|
||||
}
|
||||
}
|
||||
//@@end peers-agree
|
||||
}
|
||||
|
||||
void
|
||||
@@ -186,15 +257,18 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("slow peers");
|
||||
|
||||
// Several tests of a complete trust graph with a subset of peers
|
||||
// that have significantly longer network delays to the rest of the
|
||||
// network
|
||||
|
||||
//@@start slow-peer-scenario
|
||||
// Test when a slow peer doesn't delay a consensus quorum (4/5 agree)
|
||||
{
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
PeerGroup slow = sim.createGroup(1);
|
||||
PeerGroup fast = sim.createGroup(4);
|
||||
PeerGroup network = fast + slow;
|
||||
@@ -229,16 +303,18 @@ public:
|
||||
BEAST_EXPECT(
|
||||
peer->prevRoundTime == network[0]->prevRoundTime);
|
||||
|
||||
// Slow peer's transaction (Tx{0}) didn't make it in time
|
||||
BEAST_EXPECT(lcl.txs().find(Tx{0}) == lcl.txs().end());
|
||||
for (std::uint32_t i = 2; i < network.size(); ++i)
|
||||
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
|
||||
|
||||
// Tx 0 didn't make it
|
||||
// Tx 0 is still in the open transaction set for next round
|
||||
BEAST_EXPECT(
|
||||
peer->openTxs.find(Tx{0}) != peer->openTxs.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
//@@end slow-peer-scenario
|
||||
|
||||
// Test when the slow peers delay a consensus quorum (4/6 agree)
|
||||
{
|
||||
@@ -251,6 +327,7 @@ public:
|
||||
ConsensusParms const parms{};
|
||||
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
PeerGroup slow = sim.createGroup(2);
|
||||
PeerGroup fast = sim.createGroup(4);
|
||||
PeerGroup network = fast + slow;
|
||||
@@ -351,6 +428,7 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("close time disagree");
|
||||
|
||||
// This is a very specialized test to get ledgers to disagree on
|
||||
// the close time. It unfortunately assumes knowledge about current
|
||||
@@ -379,6 +457,7 @@ public:
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
|
||||
PeerGroup groupA = sim.createGroup(2);
|
||||
PeerGroup groupB = sim.createGroup(2);
|
||||
@@ -412,11 +491,59 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testBootstrapFastStart()
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("bootstrap fast start");
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
PeerGroup peers = sim.createGroup(4);
|
||||
peers.trustAndConnect(
|
||||
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
|
||||
|
||||
for (Peer* peer : peers)
|
||||
{
|
||||
peer->ce().testBootstrapFastStartEnabled_ = true;
|
||||
peer->targetLedgers =
|
||||
static_cast<int>(parms.bootstrapStableRoundsRequired);
|
||||
peer->start();
|
||||
|
||||
auto const json = peer->consensus.getJson(true);
|
||||
BEAST_EXPECT(json.isMember("bootstrap_fast_start"));
|
||||
BEAST_EXPECT(json["bootstrap_fast_start"].asBool());
|
||||
BEAST_EXPECT(
|
||||
json["previous_mseconds"].asInt() ==
|
||||
parms.bootstrapRoundTimeSeed.count());
|
||||
BEAST_EXPECT(json["bootstrap_stable_rounds"].asInt() == 0);
|
||||
}
|
||||
|
||||
sim.scheduler.step();
|
||||
|
||||
if (BEAST_EXPECT(sim.synchronized()))
|
||||
{
|
||||
for (Peer* peer : peers)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
peer->completedLedgers ==
|
||||
static_cast<int>(parms.bootstrapStableRoundsRequired));
|
||||
auto const json = peer->consensus.getJson(true);
|
||||
BEAST_EXPECT(!json.isMember("bootstrap_fast_start"));
|
||||
BEAST_EXPECT(peer->prevRoundTime < parms.ledgerIDLE_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWrongLCL()
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("wrong LCL");
|
||||
|
||||
// Specialized test to exercise a temporary fork in which some peers
|
||||
// are working on an incorrect prior ledger.
|
||||
|
||||
@@ -426,6 +553,7 @@ public:
|
||||
// the wrong LCL at different phases of consensus
|
||||
for (auto validationDelay : {0ms, parms.ledgerMIN_CLOSE})
|
||||
{
|
||||
//@@start wrong-lcl-scenario
|
||||
// Consider 10 peers:
|
||||
// 0 1 2 3 4 5 6 7 8 9
|
||||
// minority majorityA majorityB
|
||||
@@ -446,8 +574,10 @@ public:
|
||||
|
||||
// This topology can potentially fork with the above trust relations
|
||||
// but that is intended for this test.
|
||||
//@@end wrong-lcl-scenario
|
||||
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
|
||||
PeerGroup minority = sim.createGroup(2);
|
||||
PeerGroup majorityA = sim.createGroup(3);
|
||||
@@ -552,6 +682,7 @@ public:
|
||||
// after it is already in the establish phase of the next round.
|
||||
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
PeerGroup loner = sim.createGroup(1);
|
||||
PeerGroup friends = sim.createGroup(3);
|
||||
loner.trust(loner + friends);
|
||||
@@ -589,6 +720,7 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("consensus close time rounding");
|
||||
|
||||
// This is a specialized test engineered to yield ledgers with different
|
||||
// close times even though the peers believe they had close time
|
||||
@@ -596,6 +728,7 @@ public:
|
||||
ConsensusParms parms;
|
||||
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
|
||||
// This requires a group of 4 fast and 2 slow peers to create a
|
||||
// situation in which a subset of peers requires seeing additional
|
||||
@@ -604,9 +737,6 @@ public:
|
||||
PeerGroup fast = sim.createGroup(4);
|
||||
PeerGroup network = fast + slow;
|
||||
|
||||
for (Peer* peer : network)
|
||||
peer->consensusParms = parms;
|
||||
|
||||
// Connected trust graph
|
||||
network.trust(network);
|
||||
|
||||
@@ -692,6 +822,7 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("fork");
|
||||
|
||||
std::uint32_t numPeers = 10;
|
||||
// Vary overlap between two UNLs
|
||||
@@ -699,6 +830,7 @@ public:
|
||||
{
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
|
||||
std::uint32_t numA = (numPeers - overlap) / 2;
|
||||
std::uint32_t numB = numPeers - numA - overlap;
|
||||
@@ -729,9 +861,13 @@ public:
|
||||
}
|
||||
sim.run(1);
|
||||
|
||||
// Fork should not happen for 40% or greater overlap
|
||||
// Since the overlapped nodes have a UNL that is the union of the
|
||||
// two cliques, the maximum sized UNL list is the number of peers
|
||||
//@@start fork-threshold
|
||||
// Historical CSF topology regression: in this symmetric two-clique
|
||||
// setup, the overlapped nodes trust the union of both cliques and
|
||||
// the legacy simulator expects synchronization above the 40%
|
||||
// overlap boundary. This is not a general XRP LCP safety theorem;
|
||||
// modern consensus analysis has stricter heterogeneous-UNL
|
||||
// assumptions.
|
||||
if (overlap > 0.4 * numPeers)
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
else
|
||||
@@ -740,6 +876,7 @@ public:
|
||||
// One for cliqueA, one for cliqueB and one for nodes in both
|
||||
BEAST_EXPECT(sim.branches() <= 3);
|
||||
}
|
||||
//@@end fork-threshold
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,12 +885,14 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("hub network");
|
||||
|
||||
// Simulate a set of 5 validators that aren't directly connected but
|
||||
// rely on a single hub node for communication
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
PeerGroup validators = sim.createGroup(5);
|
||||
PeerGroup center = sim.createGroup(1);
|
||||
validators.trust(validators);
|
||||
@@ -835,6 +974,7 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("preferred by branch");
|
||||
|
||||
// Simulate network splits that are prevented from forking when using
|
||||
// preferred ledger by trie. This is a contrived example that involves
|
||||
@@ -868,6 +1008,7 @@ public:
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
|
||||
// Goes A->B->D
|
||||
PeerGroup groupABD = sim.createGroup(2);
|
||||
@@ -967,6 +1108,7 @@ public:
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("pause for laggards");
|
||||
|
||||
// Test that validators that jump ahead of the network slow
|
||||
// down.
|
||||
@@ -990,6 +1132,7 @@ public:
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
enableSilentTracing(sim);
|
||||
SimDuration delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
|
||||
|
||||
PeerGroup behind = sim.createGroup(3);
|
||||
@@ -1052,6 +1195,410 @@ public:
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
}
|
||||
|
||||
// RNG consensus tests in ConsensusRng_test.cpp
|
||||
|
||||
// MERGE NOTE (sync-2.5.0): upstream testDisputes() is already present
|
||||
// below with j/clog stalled() params from 86ef16dbeb. If upstream
|
||||
// auto-merges a duplicate, delete it — keep only this version.
|
||||
void
|
||||
testDisputes()
|
||||
{
|
||||
testcase("disputes");
|
||||
|
||||
using namespace csf;
|
||||
|
||||
// Test dispute objects directly
|
||||
using Dispute = DisputedTx<Tx, PeerID>;
|
||||
|
||||
Tx const txTrue{99};
|
||||
Tx const txFalse{98};
|
||||
Tx const txFollowingTrue{97};
|
||||
Tx const txFollowingFalse{96};
|
||||
int const numPeers = 100;
|
||||
ConsensusParms p;
|
||||
std::size_t peersUnchanged = 0;
|
||||
|
||||
auto logs = std::make_unique<Logs>(beast::severities::kError);
|
||||
auto j = logs->journal("Test");
|
||||
auto clog = std::make_unique<std::stringstream>();
|
||||
|
||||
// Three cases:
|
||||
// 1 proposing, initial vote yes
|
||||
// 2 proposing, initial vote no
|
||||
// 3 not proposing, initial vote doesn't matter after the first update,
|
||||
// use yes
|
||||
{
|
||||
Dispute proposingTrue{txTrue.id(), true, numPeers, journal_};
|
||||
Dispute proposingFalse{txFalse.id(), false, numPeers, journal_};
|
||||
Dispute followingTrue{
|
||||
txFollowingTrue.id(), true, numPeers, journal_};
|
||||
Dispute followingFalse{
|
||||
txFollowingFalse.id(), false, numPeers, journal_};
|
||||
BEAST_EXPECT(proposingTrue.ID() == 99);
|
||||
BEAST_EXPECT(proposingFalse.ID() == 98);
|
||||
BEAST_EXPECT(followingTrue.ID() == 97);
|
||||
BEAST_EXPECT(followingFalse.ID() == 96);
|
||||
|
||||
// Create an even split in the peer votes
|
||||
for (int i = 0; i < numPeers; ++i)
|
||||
{
|
||||
BEAST_EXPECT(proposingTrue.setVote(PeerID(i), i < 50));
|
||||
BEAST_EXPECT(proposingFalse.setVote(PeerID(i), i < 50));
|
||||
BEAST_EXPECT(followingTrue.setVote(PeerID(i), i < 50));
|
||||
BEAST_EXPECT(followingFalse.setVote(PeerID(i), i < 50));
|
||||
}
|
||||
// Switch the middle vote to match mine
|
||||
BEAST_EXPECT(proposingTrue.setVote(PeerID(50), true));
|
||||
BEAST_EXPECT(proposingFalse.setVote(PeerID(49), false));
|
||||
BEAST_EXPECT(followingTrue.setVote(PeerID(50), true));
|
||||
BEAST_EXPECT(followingFalse.setVote(PeerID(49), false));
|
||||
|
||||
// no changes yet
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(
|
||||
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(clog->str() == "");
|
||||
|
||||
// I'm in the majority, my vote should not change
|
||||
BEAST_EXPECT(!proposingTrue.updateVote(5, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(5, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(5, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(5, false, p));
|
||||
|
||||
BEAST_EXPECT(!proposingTrue.updateVote(10, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(10, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(10, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(10, false, p));
|
||||
|
||||
peersUnchanged = 2;
|
||||
BEAST_EXPECT(
|
||||
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(clog->str() == "");
|
||||
|
||||
// Right now, the vote is 51%. The requirement is about to jump to
|
||||
// 65%
|
||||
BEAST_EXPECT(proposingTrue.updateVote(55, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(55, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(55, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(55, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == false);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
// 16 validators change their vote to match my original vote
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
auto pTrue = PeerID(numPeers - i - 1);
|
||||
auto pFalse = PeerID(i);
|
||||
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
|
||||
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
|
||||
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
|
||||
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
|
||||
}
|
||||
// The vote should now be 66%, threshold is 65%
|
||||
BEAST_EXPECT(proposingTrue.updateVote(60, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(60, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(60, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(60, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
// Threshold jumps to 70%
|
||||
BEAST_EXPECT(proposingTrue.updateVote(86, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(86, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(86, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(86, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == false);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
// 5 more validators change their vote to match my original vote
|
||||
for (int i = 16; i < 21; ++i)
|
||||
{
|
||||
auto pTrue = PeerID(numPeers - i - 1);
|
||||
auto pFalse = PeerID(i);
|
||||
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
|
||||
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
|
||||
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
|
||||
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
|
||||
}
|
||||
|
||||
// The vote should now be 71%, threshold is 70%
|
||||
BEAST_EXPECT(proposingTrue.updateVote(90, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(90, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(90, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(90, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
// The vote should now be 71%, threshold is 70%
|
||||
BEAST_EXPECT(!proposingTrue.updateVote(150, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(150, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(150, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(150, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
// The vote should now be 71%, threshold is 70%
|
||||
BEAST_EXPECT(!proposingTrue.updateVote(190, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(190, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(190, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(190, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
peersUnchanged = 3;
|
||||
BEAST_EXPECT(
|
||||
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(clog->str() == "");
|
||||
|
||||
// Threshold jumps to 95%
|
||||
BEAST_EXPECT(proposingTrue.updateVote(220, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(220, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(220, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(220, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == false);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
// 25 more validators change their vote to match my original vote
|
||||
for (int i = 21; i < 46; ++i)
|
||||
{
|
||||
auto pTrue = PeerID(numPeers - i - 1);
|
||||
auto pFalse = PeerID(i);
|
||||
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
|
||||
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
|
||||
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
|
||||
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
|
||||
}
|
||||
|
||||
// The vote should now be 96%, threshold is 95%
|
||||
BEAST_EXPECT(proposingTrue.updateVote(250, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(250, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(250, false, p));
|
||||
BEAST_EXPECT(!followingFalse.updateVote(250, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
for (peersUnchanged = 0; peersUnchanged < 6; ++peersUnchanged)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(
|
||||
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
|
||||
BEAST_EXPECT(clog->str() == "");
|
||||
}
|
||||
|
||||
auto expectStalled = [this, &clog](
|
||||
int txid,
|
||||
bool ourVote,
|
||||
int ourTime,
|
||||
int peerTime,
|
||||
int support,
|
||||
std::uint32_t line) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
auto const s = clog->str();
|
||||
expect(s.find("stalled"), s, __FILE__, line);
|
||||
expect(
|
||||
s.starts_with("Transaction "s + std::to_string(txid)),
|
||||
s,
|
||||
__FILE__,
|
||||
line);
|
||||
expect(
|
||||
s.find("voting "s + (ourVote ? "YES" : "NO")) != s.npos,
|
||||
s,
|
||||
__FILE__,
|
||||
line);
|
||||
expect(
|
||||
s.find("for "s + std::to_string(ourTime) + " rounds."s) !=
|
||||
s.npos,
|
||||
s,
|
||||
__FILE__,
|
||||
line);
|
||||
expect(
|
||||
s.find(
|
||||
"votes in "s + std::to_string(peerTime) + " rounds.") !=
|
||||
s.npos,
|
||||
s,
|
||||
__FILE__,
|
||||
line);
|
||||
expect(
|
||||
s.ends_with(
|
||||
"has "s + std::to_string(support) + "% support. "s),
|
||||
s,
|
||||
__FILE__,
|
||||
line);
|
||||
clog = std::make_unique<std::stringstream>();
|
||||
};
|
||||
|
||||
for (int i = 0; i < 1; ++i)
|
||||
{
|
||||
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
|
||||
BEAST_EXPECT(
|
||||
!followingFalse.updateVote(250 + 10 * i, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
// true vote has changed recently, so not stalled
|
||||
BEAST_EXPECT(!proposingTrue.stalled(p, true, 0, j, clog));
|
||||
BEAST_EXPECT(clog->str() == "");
|
||||
// remaining votes have been unchanged in so long that we only
|
||||
// need to hit the second round at 95% to be stalled, regardless
|
||||
// of peers
|
||||
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
|
||||
expectStalled(98, false, 11, 0, 2, __LINE__);
|
||||
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
|
||||
expectStalled(97, true, 11, 0, 97, __LINE__);
|
||||
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
|
||||
expectStalled(96, false, 11, 0, 3, __LINE__);
|
||||
|
||||
// true vote has changed recently, so not stalled
|
||||
BEAST_EXPECT(
|
||||
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
|
||||
BEAST_EXPECTS(clog->str() == "", clog->str());
|
||||
// remaining votes have been unchanged in so long that we only
|
||||
// need to hit the second round at 95% to be stalled, regardless
|
||||
// of peers
|
||||
BEAST_EXPECT(
|
||||
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
|
||||
expectStalled(98, false, 11, 6, 2, __LINE__);
|
||||
BEAST_EXPECT(
|
||||
followingTrue.stalled(p, false, peersUnchanged, j, clog));
|
||||
expectStalled(97, true, 11, 6, 97, __LINE__);
|
||||
BEAST_EXPECT(
|
||||
followingFalse.stalled(p, false, peersUnchanged, j, clog));
|
||||
expectStalled(96, false, 11, 6, 3, __LINE__);
|
||||
}
|
||||
for (int i = 1; i < 3; ++i)
|
||||
{
|
||||
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
|
||||
BEAST_EXPECT(
|
||||
!followingFalse.updateVote(250 + 10 * i, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
// true vote changed 2 rounds ago, and peers are changing, so
|
||||
// not stalled
|
||||
BEAST_EXPECT(!proposingTrue.stalled(p, true, 0, j, clog));
|
||||
BEAST_EXPECTS(clog->str() == "", clog->str());
|
||||
// still stalled
|
||||
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
|
||||
expectStalled(98, false, 11 + i, 0, 2, __LINE__);
|
||||
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
|
||||
expectStalled(97, true, 11 + i, 0, 97, __LINE__);
|
||||
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
|
||||
expectStalled(96, false, 11 + i, 0, 3, __LINE__);
|
||||
|
||||
// true vote changed 2 rounds ago, and peers are NOT changing,
|
||||
// so stalled
|
||||
BEAST_EXPECT(
|
||||
proposingTrue.stalled(p, true, peersUnchanged, j, clog));
|
||||
expectStalled(99, true, 1 + i, 6, 97, __LINE__);
|
||||
// still stalled
|
||||
BEAST_EXPECT(
|
||||
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
|
||||
expectStalled(98, false, 11 + i, 6, 2, __LINE__);
|
||||
BEAST_EXPECT(
|
||||
followingTrue.stalled(p, false, peersUnchanged, j, clog));
|
||||
expectStalled(97, true, 11 + i, 6, 97, __LINE__);
|
||||
BEAST_EXPECT(
|
||||
followingFalse.stalled(p, false, peersUnchanged, j, clog));
|
||||
expectStalled(96, false, 11 + i, 6, 3, __LINE__);
|
||||
}
|
||||
for (int i = 3; i < 5; ++i)
|
||||
{
|
||||
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
|
||||
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
|
||||
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
|
||||
BEAST_EXPECT(
|
||||
!followingFalse.updateVote(250 + 10 * i, false, p));
|
||||
|
||||
BEAST_EXPECT(proposingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(proposingFalse.getOurVote() == false);
|
||||
BEAST_EXPECT(followingTrue.getOurVote() == true);
|
||||
BEAST_EXPECT(followingFalse.getOurVote() == false);
|
||||
|
||||
BEAST_EXPECT(proposingTrue.stalled(p, true, 0, j, clog));
|
||||
expectStalled(99, true, 1 + i, 0, 97, __LINE__);
|
||||
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
|
||||
expectStalled(98, false, 11 + i, 0, 2, __LINE__);
|
||||
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
|
||||
expectStalled(97, true, 11 + i, 0, 97, __LINE__);
|
||||
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
|
||||
expectStalled(96, false, 11 + i, 0, 3, __LINE__);
|
||||
|
||||
BEAST_EXPECT(
|
||||
proposingTrue.stalled(p, true, peersUnchanged, j, clog));
|
||||
expectStalled(99, true, 1 + i, 6, 97, __LINE__);
|
||||
BEAST_EXPECT(
|
||||
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
|
||||
expectStalled(98, false, 11 + i, 6, 2, __LINE__);
|
||||
BEAST_EXPECT(
|
||||
followingTrue.stalled(p, false, peersUnchanged, j, clog));
|
||||
expectStalled(97, true, 11 + i, 6, 97, __LINE__);
|
||||
BEAST_EXPECT(
|
||||
followingFalse.stalled(p, false, peersUnchanged, j, clog));
|
||||
expectStalled(96, false, 11 + i, 6, 3, __LINE__);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -1062,12 +1609,15 @@ public:
|
||||
testPeersAgree();
|
||||
testSlowPeers();
|
||||
testCloseTimeDisagree();
|
||||
testBootstrapFastStart();
|
||||
testWrongLCL();
|
||||
testConsensusCloseTimeRounding();
|
||||
testFork();
|
||||
testHubNetwork();
|
||||
testPreferredByBranch();
|
||||
testPauseForLaggards();
|
||||
// RNG consensus tests moved to ConsensusRng_test.cpp
|
||||
testDisputes();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,772 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/consensus/RCLCxPeerPos.h>
|
||||
#include <xrpld/consensus/ConsensusProposal.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class ExtendedPosition_test : public beast::unit_test::suite
|
||||
{
|
||||
// Generate deterministic test hashes
|
||||
static uint256
|
||||
makeHash(char const* label)
|
||||
{
|
||||
return sha512Half(Slice(label, std::strlen(label)));
|
||||
}
|
||||
|
||||
void
|
||||
testSerializationRoundTrip()
|
||||
{
|
||||
testcase("Serialization round-trip");
|
||||
|
||||
//@@start test-extended-position-legacy-compat
|
||||
// Empty position (legacy compat)
|
||||
{
|
||||
auto const txSet = makeHash("txset-a");
|
||||
ExtendedPosition pos{txSet};
|
||||
|
||||
Serializer s;
|
||||
pos.add(s);
|
||||
|
||||
// Should be exactly 32 bytes (no flags byte)
|
||||
BEAST_EXPECT(s.getDataLength() == 32);
|
||||
|
||||
SerialIter sit(s.slice());
|
||||
auto deserialized =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
|
||||
BEAST_EXPECT(deserialized.has_value());
|
||||
if (!deserialized)
|
||||
return;
|
||||
BEAST_EXPECT(deserialized->txSetHash == txSet);
|
||||
BEAST_EXPECT(!deserialized->myCommitment);
|
||||
BEAST_EXPECT(!deserialized->myReveal);
|
||||
BEAST_EXPECT(!deserialized->commitSetHash);
|
||||
BEAST_EXPECT(!deserialized->entropySetHash);
|
||||
BEAST_EXPECT(!deserialized->exportSigSetHash);
|
||||
BEAST_EXPECT(!deserialized->exportSignaturesHash);
|
||||
BEAST_EXPECT(!deserialized->observedParticipantsHash);
|
||||
}
|
||||
//@@end test-extended-position-legacy-compat
|
||||
|
||||
//@@start test-extended-position-export-root-wire-bit
|
||||
// Pin the existing Export root allocation independently of the
|
||||
// all-fields fixture: byte 32 is flags, and bit 0x10 carries exactly
|
||||
// one following uint256 exportSigSetHash.
|
||||
{
|
||||
auto const txSet = makeHash("txset-export-root-wire");
|
||||
auto const exportRoot = makeHash("export-root-wire");
|
||||
ExtendedPosition pos{txSet};
|
||||
pos.exportSigSetHash = exportRoot;
|
||||
|
||||
Serializer s;
|
||||
pos.add(s);
|
||||
BEAST_EXPECT(s.getDataLength() == 65);
|
||||
BEAST_EXPECT(s.peekData()[32] == 0x10);
|
||||
|
||||
SerialIter sit(s.slice());
|
||||
auto const deserialized =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
BEAST_EXPECT(deserialized);
|
||||
if (deserialized)
|
||||
{
|
||||
BEAST_EXPECT(deserialized->txSetHash == txSet);
|
||||
BEAST_EXPECT(deserialized->exportSigSetHash == exportRoot);
|
||||
BEAST_EXPECT(!deserialized->exportSignaturesHash);
|
||||
}
|
||||
}
|
||||
//@@end test-extended-position-export-root-wire-bit
|
||||
|
||||
// Position with commitment
|
||||
{
|
||||
auto const txSet = makeHash("txset-b");
|
||||
auto const commit = makeHash("commit-b");
|
||||
|
||||
ExtendedPosition pos{txSet};
|
||||
pos.myCommitment = commit;
|
||||
|
||||
Serializer s;
|
||||
pos.add(s);
|
||||
|
||||
// 32 (txSet) + 1 (flags) + 32 (commitment) = 65
|
||||
BEAST_EXPECT(s.getDataLength() == 65);
|
||||
|
||||
SerialIter sit(s.slice());
|
||||
auto deserialized =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
|
||||
BEAST_EXPECT(deserialized.has_value());
|
||||
if (!deserialized)
|
||||
return;
|
||||
BEAST_EXPECT(deserialized->txSetHash == txSet);
|
||||
BEAST_EXPECT(deserialized->myCommitment == commit);
|
||||
BEAST_EXPECT(!deserialized->myReveal);
|
||||
}
|
||||
|
||||
// Position with diagnostic participant hash only
|
||||
{
|
||||
auto const txSet = makeHash("txset-participants");
|
||||
auto const participants = makeHash("participants");
|
||||
|
||||
ExtendedPosition pos{txSet};
|
||||
pos.observedParticipantsHash = participants;
|
||||
|
||||
Serializer s;
|
||||
pos.add(s);
|
||||
|
||||
// 32 (txSet) + 1 (flags) + 32 (participant hash) = 65
|
||||
BEAST_EXPECT(s.getDataLength() == 65);
|
||||
|
||||
SerialIter sit(s.slice());
|
||||
auto deserialized =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
|
||||
BEAST_EXPECT(deserialized.has_value());
|
||||
if (!deserialized)
|
||||
return;
|
||||
BEAST_EXPECT(deserialized->txSetHash == txSet);
|
||||
BEAST_EXPECT(
|
||||
deserialized->observedParticipantsHash == participants);
|
||||
BEAST_EXPECT(!deserialized->myCommitment);
|
||||
BEAST_EXPECT(!deserialized->myReveal);
|
||||
}
|
||||
|
||||
// Position with all fields
|
||||
{
|
||||
auto const txSet = makeHash("txset-c");
|
||||
auto const commitSet = makeHash("commitset-c");
|
||||
auto const entropySet = makeHash("entropyset-c");
|
||||
auto const exportSigSet = makeHash("exportsigset-c");
|
||||
auto const exportSigs = makeHash("exportsigs-c");
|
||||
auto const participants = makeHash("participants-c");
|
||||
auto const commit = makeHash("commit-c");
|
||||
auto const reveal = makeHash("reveal-c");
|
||||
|
||||
ExtendedPosition pos{txSet};
|
||||
pos.commitSetHash = commitSet;
|
||||
pos.entropySetHash = entropySet;
|
||||
pos.exportSigSetHash = exportSigSet;
|
||||
pos.exportSignaturesHash = exportSigs;
|
||||
pos.observedParticipantsHash = participants;
|
||||
pos.myCommitment = commit;
|
||||
pos.myReveal = reveal;
|
||||
|
||||
Serializer s;
|
||||
pos.add(s);
|
||||
|
||||
// 32 + 1 + 7*32 = 257
|
||||
BEAST_EXPECT(s.getDataLength() == 257);
|
||||
|
||||
SerialIter sit(s.slice());
|
||||
auto deserialized =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
|
||||
BEAST_EXPECT(deserialized.has_value());
|
||||
if (!deserialized)
|
||||
return;
|
||||
BEAST_EXPECT(deserialized->txSetHash == txSet);
|
||||
BEAST_EXPECT(deserialized->commitSetHash == commitSet);
|
||||
BEAST_EXPECT(deserialized->entropySetHash == entropySet);
|
||||
BEAST_EXPECT(deserialized->exportSigSetHash == exportSigSet);
|
||||
BEAST_EXPECT(deserialized->exportSignaturesHash == exportSigs);
|
||||
BEAST_EXPECT(
|
||||
deserialized->observedParticipantsHash == participants);
|
||||
BEAST_EXPECT(deserialized->myCommitment == commit);
|
||||
BEAST_EXPECT(deserialized->myReveal == reveal);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSigningConsistency()
|
||||
{
|
||||
testcase("Signing hash consistency");
|
||||
|
||||
// The signing hash from ConsensusProposal::signingHash() must match
|
||||
// what a receiver would compute via the same function after
|
||||
// deserializing the ExtendedPosition from the wire.
|
||||
|
||||
auto const [pk, sk] = randomKeyPair(KeyType::secp256k1);
|
||||
auto const nodeId = calcNodeID(pk);
|
||||
auto const prevLedger = makeHash("prevledger");
|
||||
auto const closeTime =
|
||||
NetClock::time_point{NetClock::duration{1234567}};
|
||||
|
||||
//@@start test-extended-position-signing-binds-fields
|
||||
// Test with commitment (the case that was failing)
|
||||
{
|
||||
auto const txSet = makeHash("txset-sign");
|
||||
auto const commit = makeHash("commitment-sign");
|
||||
|
||||
ExtendedPosition pos{txSet};
|
||||
pos.myCommitment = commit;
|
||||
|
||||
using Proposal =
|
||||
ConsensusProposal<NodeID, uint256, ExtendedPosition>;
|
||||
|
||||
Proposal prop{
|
||||
prevLedger,
|
||||
Proposal::seqJoin,
|
||||
pos,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
nodeId};
|
||||
|
||||
// Sign it (same as propose() does)
|
||||
auto const signingHash = prop.signingHash();
|
||||
auto sig = signDigest(pk, sk, signingHash);
|
||||
|
||||
// Serialize position to wire format
|
||||
Serializer positionData;
|
||||
pos.add(positionData);
|
||||
auto const posSlice = positionData.slice();
|
||||
|
||||
// Deserialize (same as PeerImp::onMessage does)
|
||||
SerialIter sit(posSlice);
|
||||
auto const maybeReceivedPos =
|
||||
ExtendedPosition::fromSerialIter(sit, posSlice.size());
|
||||
|
||||
BEAST_EXPECT(maybeReceivedPos.has_value());
|
||||
if (!maybeReceivedPos)
|
||||
return;
|
||||
|
||||
// Reconstruct proposal on receiver side
|
||||
Proposal receivedProp{
|
||||
prevLedger,
|
||||
Proposal::seqJoin,
|
||||
*maybeReceivedPos,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
nodeId};
|
||||
|
||||
// The signing hash must match
|
||||
BEAST_EXPECT(receivedProp.signingHash() == signingHash);
|
||||
|
||||
// Verify signature (same as checkSign does)
|
||||
BEAST_EXPECT(
|
||||
verifyDigest(pk, receivedProp.signingHash(), sig, false));
|
||||
}
|
||||
//@@end test-extended-position-signing-binds-fields
|
||||
|
||||
// Test without commitment (legacy case)
|
||||
{
|
||||
auto const txSet = makeHash("txset-legacy");
|
||||
ExtendedPosition pos{txSet};
|
||||
|
||||
using Proposal =
|
||||
ConsensusProposal<NodeID, uint256, ExtendedPosition>;
|
||||
|
||||
Proposal prop{
|
||||
prevLedger,
|
||||
Proposal::seqJoin,
|
||||
pos,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
nodeId};
|
||||
|
||||
auto const signingHash = prop.signingHash();
|
||||
auto sig = signDigest(pk, sk, signingHash);
|
||||
|
||||
Serializer positionData;
|
||||
pos.add(positionData);
|
||||
|
||||
SerialIter sit(positionData.slice());
|
||||
auto const maybeReceivedPos = ExtendedPosition::fromSerialIter(
|
||||
sit, positionData.getDataLength());
|
||||
|
||||
BEAST_EXPECT(maybeReceivedPos.has_value());
|
||||
if (!maybeReceivedPos)
|
||||
return;
|
||||
|
||||
Proposal receivedProp{
|
||||
prevLedger,
|
||||
Proposal::seqJoin,
|
||||
*maybeReceivedPos,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
nodeId};
|
||||
|
||||
BEAST_EXPECT(receivedProp.signingHash() == signingHash);
|
||||
BEAST_EXPECT(
|
||||
verifyDigest(pk, receivedProp.signingHash(), sig, false));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSuppressionConsistency()
|
||||
{
|
||||
testcase("Suppression hash consistency");
|
||||
|
||||
// proposalUniqueId must produce the same result on sender and
|
||||
// receiver when given the same ExtendedPosition data.
|
||||
|
||||
auto const [pk, sk] = randomKeyPair(KeyType::secp256k1);
|
||||
auto const prevLedger = makeHash("prevledger-supp");
|
||||
auto const closeTime =
|
||||
NetClock::time_point{NetClock::duration{1234567}};
|
||||
std::uint32_t const proposeSeq = 0;
|
||||
|
||||
auto const txSet = makeHash("txset-supp");
|
||||
auto const commit = makeHash("commitment-supp");
|
||||
|
||||
ExtendedPosition pos{txSet};
|
||||
pos.myCommitment = commit;
|
||||
|
||||
// Sign (to get a real signature for suppression)
|
||||
using Proposal = ConsensusProposal<NodeID, uint256, ExtendedPosition>;
|
||||
Proposal prop{
|
||||
prevLedger,
|
||||
proposeSeq,
|
||||
pos,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
calcNodeID(pk)};
|
||||
|
||||
auto sig = signDigest(pk, sk, prop.signingHash());
|
||||
|
||||
// Sender computes suppression
|
||||
auto const senderSuppression =
|
||||
proposalUniqueId(pos, prevLedger, proposeSeq, closeTime, pk, sig);
|
||||
|
||||
// Simulate wire: serialize and deserialize
|
||||
Serializer positionData;
|
||||
pos.add(positionData);
|
||||
SerialIter sit(positionData.slice());
|
||||
auto const maybeReceivedPos =
|
||||
ExtendedPosition::fromSerialIter(sit, positionData.getDataLength());
|
||||
|
||||
BEAST_EXPECT(maybeReceivedPos.has_value());
|
||||
if (!maybeReceivedPos)
|
||||
return;
|
||||
|
||||
// Receiver computes suppression
|
||||
auto const receiverSuppression = proposalUniqueId(
|
||||
*maybeReceivedPos, prevLedger, proposeSeq, closeTime, pk, sig);
|
||||
|
||||
BEAST_EXPECT(senderSuppression == receiverSuppression);
|
||||
}
|
||||
|
||||
void
|
||||
testPeerPosition()
|
||||
{
|
||||
testcase("RCLCxPeerPos signed proposal wrapper");
|
||||
|
||||
auto const [pk, sk] = randomKeyPair(KeyType::secp256k1);
|
||||
auto const nodeId = calcNodeID(pk);
|
||||
auto const prevLedger = makeHash("prevledger-peer");
|
||||
auto const closeTime =
|
||||
NetClock::time_point{NetClock::duration{7654321}};
|
||||
|
||||
ExtendedPosition pos{makeHash("txset-peer")};
|
||||
pos.commitSetHash = makeHash("commitset-peer");
|
||||
pos.entropySetHash = makeHash("entropyset-peer");
|
||||
pos.exportSigSetHash = makeHash("exportsigset-peer");
|
||||
pos.exportSignaturesHash = makeHash("exportsigs-peer");
|
||||
pos.observedParticipantsHash = makeHash("participants-peer");
|
||||
pos.myCommitment = makeHash("commitment-peer");
|
||||
pos.myReveal = makeHash("reveal-peer");
|
||||
|
||||
using Proposal = ConsensusProposal<NodeID, uint256, ExtendedPosition>;
|
||||
Proposal prop{
|
||||
prevLedger,
|
||||
Proposal::seqJoin,
|
||||
pos,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
nodeId};
|
||||
|
||||
auto const sig = signDigest(pk, sk, prop.signingHash());
|
||||
auto const suppression = proposalUniqueId(
|
||||
pos, prevLedger, prop.proposeSeq(), closeTime, pk, sig);
|
||||
std::vector<std::string> exportSignatures{
|
||||
"export-sig-a", "export-sig-b"};
|
||||
|
||||
RCLCxPeerPos peer{
|
||||
pk,
|
||||
sig,
|
||||
suppression,
|
||||
Proposal{
|
||||
prevLedger,
|
||||
Proposal::seqJoin,
|
||||
pos,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
nodeId},
|
||||
exportSignatures};
|
||||
|
||||
BEAST_EXPECT(peer.checkSign());
|
||||
BEAST_EXPECT(peer.publicKey() == pk);
|
||||
BEAST_EXPECT(peer.signature().size() == sig.size());
|
||||
BEAST_EXPECT(peer.suppressionID() == suppression);
|
||||
BEAST_EXPECT(peer.proposal().position().txSetHash == pos.txSetHash);
|
||||
BEAST_EXPECT(peer.exportSignatures() == exportSignatures);
|
||||
BEAST_EXPECT(!peer.render().empty());
|
||||
|
||||
auto const json = peer.getJson();
|
||||
BEAST_EXPECT(json.isMember(jss::peer_id));
|
||||
BEAST_EXPECT(
|
||||
json[jss::peer_id].asString() ==
|
||||
toBase58(TokenType::NodePublic, pk));
|
||||
|
||||
auto badSig = sig;
|
||||
badSig.data()[badSig.size() - 1] ^= 0x01;
|
||||
RCLCxPeerPos badPeer{
|
||||
pk,
|
||||
badSig,
|
||||
suppression,
|
||||
Proposal{
|
||||
prevLedger,
|
||||
Proposal::seqJoin,
|
||||
pos,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
nodeId}};
|
||||
BEAST_EXPECT(!badPeer.checkSign());
|
||||
|
||||
auto mutated = pos;
|
||||
mutated.myReveal = makeHash("reveal-peer-mutated");
|
||||
BEAST_EXPECT(
|
||||
proposalUniqueId(
|
||||
mutated, prevLedger, prop.proposeSeq(), closeTime, pk, sig) !=
|
||||
suppression);
|
||||
|
||||
mutated = pos;
|
||||
//@@start test-extended-position-export-root-binds-proposal
|
||||
mutated.exportSigSetHash = makeHash("exportsigset-peer-mutated");
|
||||
BEAST_EXPECT(
|
||||
proposalUniqueId(
|
||||
mutated, prevLedger, prop.proposeSeq(), closeTime, pk, sig) !=
|
||||
suppression);
|
||||
Proposal mutatedRootProposal{
|
||||
prevLedger,
|
||||
Proposal::seqJoin,
|
||||
mutated,
|
||||
closeTime,
|
||||
NetClock::time_point{},
|
||||
nodeId};
|
||||
BEAST_EXPECT(mutatedRootProposal.signingHash() != prop.signingHash());
|
||||
BEAST_EXPECT(
|
||||
!verifyDigest(pk, mutatedRootProposal.signingHash(), sig, false));
|
||||
//@@end test-extended-position-export-root-binds-proposal
|
||||
|
||||
mutated = pos;
|
||||
mutated.exportSignaturesHash = makeHash("exportsigs-peer-mutated");
|
||||
BEAST_EXPECT(
|
||||
proposalUniqueId(
|
||||
mutated, prevLedger, prop.proposeSeq(), closeTime, pk, sig) !=
|
||||
suppression);
|
||||
|
||||
BEAST_EXPECT(
|
||||
proposalUniqueId(
|
||||
pos,
|
||||
makeHash("prevledger-peer-mutated"),
|
||||
prop.proposeSeq(),
|
||||
closeTime,
|
||||
pk,
|
||||
sig) != suppression);
|
||||
BEAST_EXPECT(
|
||||
proposalUniqueId(
|
||||
pos, prevLedger, prop.proposeSeq() + 1, closeTime, pk, sig) !=
|
||||
suppression);
|
||||
BEAST_EXPECT(
|
||||
proposalUniqueId(
|
||||
pos,
|
||||
prevLedger,
|
||||
prop.proposeSeq(),
|
||||
NetClock::time_point{NetClock::duration{7654322}},
|
||||
pk,
|
||||
sig) != suppression);
|
||||
|
||||
auto const [otherPk, unusedSk] = randomKeyPair(KeyType::secp256k1);
|
||||
(void)unusedSk;
|
||||
BEAST_EXPECT(
|
||||
proposalUniqueId(
|
||||
pos, prevLedger, prop.proposeSeq(), closeTime, otherPk, sig) !=
|
||||
suppression);
|
||||
}
|
||||
|
||||
void
|
||||
testMalformedPayload()
|
||||
{
|
||||
testcase("Malformed payload rejected");
|
||||
|
||||
//@@start test-extended-position-malformed-canonicality
|
||||
// Too short (< 32 bytes)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(0xDEADBEEF); // only 4 bytes
|
||||
SerialIter sit(s.slice());
|
||||
auto result =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
}
|
||||
|
||||
// Empty payload
|
||||
{
|
||||
Serializer s;
|
||||
SerialIter sit(s.slice());
|
||||
auto result = ExtendedPosition::fromSerialIter(sit, 0);
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
}
|
||||
|
||||
// Flags claim fields that aren't present (truncated)
|
||||
{
|
||||
auto const txSet = makeHash("txset-malformed");
|
||||
Serializer s;
|
||||
s.addBitString(txSet);
|
||||
// flags = 0x0F (all 4 fields), but no field data follows
|
||||
s.add8(0x0F);
|
||||
SerialIter sit(s.slice());
|
||||
auto result =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
}
|
||||
|
||||
// Flags claim 2 fields but only 1 field's worth of data
|
||||
{
|
||||
auto const txSet = makeHash("txset-malformed2");
|
||||
auto const commit = makeHash("commit-malformed2");
|
||||
Serializer s;
|
||||
s.addBitString(txSet);
|
||||
// flags = 0x03 (commitSetHash + entropySetHash), but only
|
||||
// provide commitSetHash data
|
||||
s.add8(0x03);
|
||||
s.addBitString(commit);
|
||||
SerialIter sit(s.slice());
|
||||
auto result =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
}
|
||||
|
||||
// Unknown flag bits above known extension fields (wire malleability)
|
||||
{
|
||||
auto const txSet = makeHash("txset-unkflags");
|
||||
Serializer s;
|
||||
s.addBitString(txSet);
|
||||
s.add8(0x81); // bit 7 is unknown, bit 0 = commitSetHash
|
||||
s.addBitString(makeHash("commitset-unkflags"));
|
||||
SerialIter sit(s.slice());
|
||||
auto result =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
}
|
||||
|
||||
// Size says extended payload, but no flag byte remains.
|
||||
{
|
||||
auto const txSet = makeHash("txset-missing-flags");
|
||||
Serializer s;
|
||||
s.addBitString(txSet);
|
||||
SerialIter sit(s.slice());
|
||||
auto result = ExtendedPosition::fromSerialIter(sit, 33);
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
}
|
||||
|
||||
// Zero flags are a non-canonical duplicate of the 32-byte legacy form.
|
||||
{
|
||||
auto const txSet = makeHash("txset-zero-flags");
|
||||
Serializer s;
|
||||
s.addBitString(txSet);
|
||||
s.add8(0);
|
||||
SerialIter sit(s.slice());
|
||||
auto result =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
}
|
||||
|
||||
// Trailing extra bytes after valid fields
|
||||
{
|
||||
auto const txSet = makeHash("txset-trailing");
|
||||
auto const commitSet = makeHash("commitset-trailing");
|
||||
Serializer s;
|
||||
s.addBitString(txSet);
|
||||
s.add8(0x01); // commitSetHash only
|
||||
s.addBitString(commitSet);
|
||||
s.add32(0xDEADBEEF); // 4 extra trailing bytes
|
||||
SerialIter sit(s.slice());
|
||||
auto result =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
}
|
||||
|
||||
// Valid flags with exactly the right amount of data (should succeed)
|
||||
{
|
||||
auto const txSet = makeHash("txset-ok");
|
||||
auto const commitSet = makeHash("commitset-ok");
|
||||
Serializer s;
|
||||
s.addBitString(txSet);
|
||||
s.add8(0x01); // commitSetHash only
|
||||
s.addBitString(commitSet);
|
||||
SerialIter sit(s.slice());
|
||||
auto result =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
if (result)
|
||||
{
|
||||
BEAST_EXPECT(result->txSetHash == txSet);
|
||||
BEAST_EXPECT(result->commitSetHash == commitSet);
|
||||
BEAST_EXPECT(!result->entropySetHash);
|
||||
}
|
||||
}
|
||||
//@@end test-extended-position-malformed-canonicality
|
||||
}
|
||||
|
||||
void
|
||||
testTxSetIdentity()
|
||||
{
|
||||
testcase("Tx-set identity is explicit");
|
||||
|
||||
auto const txSet = makeHash("txset-eq");
|
||||
auto const txSet2 = makeHash("txset-eq-2");
|
||||
|
||||
ExtendedPosition a{txSet};
|
||||
a.myCommitment = makeHash("commit1-eq");
|
||||
|
||||
ExtendedPosition b{txSet};
|
||||
b.myCommitment = makeHash("commit2-eq");
|
||||
|
||||
// Same txSetHash, different leaves -> same consensus tx-set key.
|
||||
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
|
||||
|
||||
// Same txSetHash, different commitSetHash -> same tx-set key
|
||||
// (sub-state quorum handles commitSetHash agreement)
|
||||
b.commitSetHash = makeHash("cs-eq");
|
||||
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
|
||||
|
||||
// Same txSetHash, different entropySetHash -> same tx-set key
|
||||
b.entropySetHash = makeHash("es-eq");
|
||||
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
|
||||
|
||||
// Same txSetHash, different export signature digest -> same tx-set key
|
||||
b.exportSignaturesHash = makeHash("export-sigs-eq");
|
||||
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
|
||||
|
||||
// Same txSetHash, different participant diagnostics -> same tx-set key
|
||||
b.observedParticipantsHash = makeHash("participants-eq");
|
||||
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
|
||||
|
||||
// Different txSetHash -> different tx-set key
|
||||
ExtendedPosition c{txSet2};
|
||||
BEAST_EXPECT(positionTxSetID(a) != positionTxSetID(c));
|
||||
|
||||
BEAST_EXPECT(positionTxSetID(a) == txSet);
|
||||
BEAST_EXPECT(positionTxSetID(a) != txSet2);
|
||||
|
||||
a.updateTxSet(txSet2);
|
||||
BEAST_EXPECT(positionTxSetID(a) == txSet2);
|
||||
BEAST_EXPECT(positionTxSetID(a) != positionTxSetID(b));
|
||||
}
|
||||
|
||||
void
|
||||
testExportSignatureDigest()
|
||||
{
|
||||
testcase("Export signature digest");
|
||||
|
||||
//@@start test-extended-position-export-signature-digest
|
||||
std::vector<std::string> blobs;
|
||||
blobs.emplace_back("txhash-pubkey-sig-a");
|
||||
blobs.emplace_back("txhash-pubkey-sig-b");
|
||||
|
||||
auto const digest = proposalExportSignaturesHash(blobs);
|
||||
BEAST_EXPECT(digest == proposalExportSignaturesHash(blobs));
|
||||
|
||||
auto reordered = blobs;
|
||||
std::swap(reordered[0], reordered[1]);
|
||||
BEAST_EXPECT(digest != proposalExportSignaturesHash(reordered));
|
||||
|
||||
auto mutated = blobs;
|
||||
mutated[1].push_back('x');
|
||||
BEAST_EXPECT(digest != proposalExportSignaturesHash(mutated));
|
||||
//@@end test-extended-position-export-signature-digest
|
||||
}
|
||||
|
||||
void
|
||||
testStringJsonAndHash()
|
||||
{
|
||||
testcase("String, JSON, and hash helpers");
|
||||
|
||||
auto const txSet = makeHash("txset-json");
|
||||
auto const commitSet = makeHash("commitset-json");
|
||||
auto const entropySet = makeHash("entropyset-json");
|
||||
auto const exportSigSet = makeHash("exportsigset-json");
|
||||
auto const exportSigs = makeHash("exportsigs-json");
|
||||
auto const participants = makeHash("participants-json");
|
||||
|
||||
ExtendedPosition pos{txSet};
|
||||
pos.commitSetHash = commitSet;
|
||||
pos.entropySetHash = entropySet;
|
||||
pos.exportSigSetHash = exportSigSet;
|
||||
pos.exportSignaturesHash = exportSigs;
|
||||
pos.observedParticipantsHash = participants;
|
||||
|
||||
BEAST_EXPECT(to_string(pos) == to_string(txSet));
|
||||
|
||||
std::ostringstream os;
|
||||
os << pos;
|
||||
BEAST_EXPECT(os.str() == to_string(txSet));
|
||||
|
||||
auto const json = pos.getJson();
|
||||
BEAST_EXPECT(json["tx_set"].asString() == to_string(txSet));
|
||||
BEAST_EXPECT(json["commit_set"].asString() == to_string(commitSet));
|
||||
BEAST_EXPECT(json["entropy_set"].asString() == to_string(entropySet));
|
||||
BEAST_EXPECT(
|
||||
json["export_sig_set"].asString() == to_string(exportSigSet));
|
||||
BEAST_EXPECT(
|
||||
json["export_signatures"].asString() == to_string(exportSigs));
|
||||
BEAST_EXPECT(
|
||||
json["observed_participants"].asString() ==
|
||||
to_string(participants));
|
||||
|
||||
auto const fullHash = sha512Half(pos);
|
||||
auto withDifferentLeaf = pos;
|
||||
withDifferentLeaf.myCommitment = makeHash("commitment-json");
|
||||
BEAST_EXPECT(fullHash != sha512Half(withDifferentLeaf));
|
||||
|
||||
auto sameTxDifferentSidecar = pos;
|
||||
sameTxDifferentSidecar.entropySetHash = makeHash("entropyset-other");
|
||||
BEAST_EXPECT(
|
||||
positionTxSetID(pos) == positionTxSetID(sameTxDifferentSidecar));
|
||||
BEAST_EXPECT(sha512Half(pos) != sha512Half(sameTxDifferentSidecar));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testSerializationRoundTrip();
|
||||
testSigningConsistency();
|
||||
testSuppressionConsistency();
|
||||
testPeerPosition();
|
||||
testMalformedPayload();
|
||||
testTxSetIdentity();
|
||||
testExportSignatureDigest();
|
||||
testStringJsonAndHash();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExtendedPosition, consensus, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -27,6 +27,8 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
@@ -546,8 +548,12 @@ struct NetworkHistory
|
||||
std::optional<int> numLedgers;
|
||||
};
|
||||
|
||||
NetworkHistory(beast::unit_test::suite& suite, Parameter const& p)
|
||||
: env(suite, jtx::supported_amendments() | featureNegativeUNL)
|
||||
NetworkHistory(
|
||||
beast::unit_test::suite& suite,
|
||||
Parameter const& p,
|
||||
FeatureBitset features = jtx::supported_amendments() |
|
||||
featureNegativeUNL)
|
||||
: env(suite, features | featureNegativeUNL)
|
||||
, param(p)
|
||||
, validations(env.app().getValidations())
|
||||
{
|
||||
@@ -1120,6 +1126,41 @@ class NegativeUNLVoteInternal_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFindAllCandidatesWithActiveViewCap()
|
||||
{
|
||||
testcase("Find All Candidates with active-view cap denominator");
|
||||
|
||||
jtx::Env env(*this);
|
||||
NegativeUNLVote vote(NodeID(0xA0), env.journal, env.app());
|
||||
|
||||
std::vector<NodeID> nodeIds;
|
||||
hash_set<NodeID> unl;
|
||||
hash_set<NodeID> negUnl;
|
||||
hash_map<NodeID, std::uint32_t> scoreTable;
|
||||
|
||||
for (std::size_t i = 0; i < 20; ++i)
|
||||
{
|
||||
nodeIds.emplace_back(i + 1);
|
||||
unl.insert(nodeIds.back());
|
||||
scoreTable[nodeIds.back()] =
|
||||
NegativeUNLVote::negativeUNLLowWaterMark - 1;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
negUnl.insert(nodeIds[i]);
|
||||
|
||||
auto const legacyCandidates =
|
||||
vote.findAllCandidates(unl, negUnl, scoreTable);
|
||||
BEAST_EXPECT(legacyCandidates.toDisableCandidates.size() == 17);
|
||||
BEAST_EXPECT(legacyCandidates.toReEnableCandidates.empty());
|
||||
|
||||
auto const activeViewCandidates =
|
||||
vote.findAllCandidates(unl, negUnl, scoreTable, 10);
|
||||
BEAST_EXPECT(activeViewCandidates.toDisableCandidates.empty());
|
||||
BEAST_EXPECT(activeViewCandidates.toReEnableCandidates.empty());
|
||||
}
|
||||
|
||||
void
|
||||
testFindAllCandidatesCombination()
|
||||
{
|
||||
@@ -1375,6 +1416,7 @@ class NegativeUNLVoteInternal_test : public beast::unit_test::suite
|
||||
testPickOneCandidate();
|
||||
testBuildScoreTableSpecialCases();
|
||||
testFindAllCandidates();
|
||||
testFindAllCandidatesWithActiveViewCap();
|
||||
testFindAllCandidatesCombination();
|
||||
testNewValidators();
|
||||
}
|
||||
@@ -1613,6 +1655,82 @@ class NegativeUNLVoteOffline_test : public beast::unit_test::suite
|
||||
|
||||
class NegativeUNLVoteMaxListed_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
seedUNLReport(NetworkHistory& history, std::size_t activeCount)
|
||||
{
|
||||
BEAST_EXPECT(activeCount <= history.UNLKeys.size());
|
||||
auto& ledger = history.history.back();
|
||||
OpenView accum(&*ledger);
|
||||
PublicKey const importKey = history.UNLKeys.back();
|
||||
|
||||
for (std::size_t i = 0; i < activeCount; ++i)
|
||||
{
|
||||
auto tx = unl::createUNLReportTx(
|
||||
ledger->seq(), importKey, history.UNLKeys[i]);
|
||||
BEAST_EXPECT(unl::applyAndTestResult(history.env, accum, tx, true));
|
||||
}
|
||||
|
||||
accum.apply(*ledger);
|
||||
|
||||
auto const sle = ledger->read(keylet::UNLReport());
|
||||
BEAST_EXPECT(sle);
|
||||
if (sle)
|
||||
{
|
||||
BEAST_EXPECT(sle->isFieldPresent(sfActiveValidators));
|
||||
if (sle->isFieldPresent(sfActiveValidators))
|
||||
BEAST_EXPECT(
|
||||
sle->getFieldArray(sfActiveValidators).size() ==
|
||||
activeCount);
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t
|
||||
voteCountWithUNLReport(FeatureBitset features, bool withReport = true)
|
||||
{
|
||||
NetworkHistory history = {*this, {20, 3, false, false, {}}, features};
|
||||
BEAST_EXPECT(history.goodHistory);
|
||||
if (!history.goodHistory)
|
||||
return 0;
|
||||
|
||||
if (withReport)
|
||||
seedUNLReport(history, 10);
|
||||
history.walkHistoryAndAddValidations(
|
||||
[&](std::shared_ptr<Ledger const> const&, std::size_t idx) -> bool {
|
||||
return idx == history.UNLNodeIDs.size() - 1;
|
||||
});
|
||||
|
||||
NegativeUNLVote vote(
|
||||
history.UNLNodeIDs.back(), history.env.journal, history.env.app());
|
||||
auto txSet = std::make_shared<SHAMap>(
|
||||
SHAMapType::TRANSACTION, history.env.app().getNodeFamily());
|
||||
vote.doVoting(
|
||||
history.lastLedger(),
|
||||
history.UNLKeySet,
|
||||
history.validations,
|
||||
txSet);
|
||||
|
||||
return unl::countTx(txSet);
|
||||
}
|
||||
|
||||
void
|
||||
testMaxListedPolicy()
|
||||
{
|
||||
testcase("Max listed policy");
|
||||
|
||||
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(0) == 0);
|
||||
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(1) == 1);
|
||||
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(4) == 1);
|
||||
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(5) == 2);
|
||||
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(8) == 2);
|
||||
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(9) == 3);
|
||||
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(32) == 8);
|
||||
|
||||
auto const max = std::numeric_limits<std::size_t>::max();
|
||||
BEAST_EXPECT(
|
||||
NegativeUNLVote::maxNegativeUNLListed(max) ==
|
||||
max / NegativeUNLVote::negativeUNLMaxListedDenominator + 1);
|
||||
}
|
||||
|
||||
void
|
||||
testDoVoting()
|
||||
{
|
||||
@@ -1637,10 +1755,47 @@ class NegativeUNLVoteMaxListed_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testActiveViewCap()
|
||||
{
|
||||
auto const legacyFeatures = (jtx::supported_amendments() -
|
||||
featureConsensusEntropy - featureExport) |
|
||||
featureNegativeUNL;
|
||||
|
||||
for (bool const entropy : {false, true})
|
||||
{
|
||||
for (bool const exportEnabled : {false, true})
|
||||
{
|
||||
testcase(
|
||||
std::string("Active-view cap: CE=") +
|
||||
(entropy ? "on" : "off") +
|
||||
", Export=" + (exportEnabled ? "on" : "off"));
|
||||
auto features = legacyFeatures;
|
||||
if (entropy)
|
||||
features.set(featureConsensusEntropy);
|
||||
if (exportEnabled)
|
||||
features.set(featureExport);
|
||||
|
||||
// Three validators are already disabled. A 20-validator
|
||||
// trusted UNL permits a fourth disable vote, whereas the
|
||||
// parent report's 10 active validators cap the list at three.
|
||||
BEAST_EXPECT(
|
||||
voteCountWithUNLReport(features) ==
|
||||
(entropy || exportEnabled ? 0 : 1));
|
||||
|
||||
// Without a parent report, all four feature combinations
|
||||
// retain the trusted-UNL denominator.
|
||||
BEAST_EXPECT(voteCountWithUNLReport(features, false) == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testMaxListedPolicy();
|
||||
testDoVoting();
|
||||
testActiveViewCap();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 AUTHORS BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/consensus/ProposalPrecheck.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/ExportShare.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
makeHash(char const* label)
|
||||
{
|
||||
return sha512Half(Slice(label, std::strlen(label)));
|
||||
}
|
||||
|
||||
void
|
||||
setPreviousLedger(protocol::TMProposeSet& set)
|
||||
{
|
||||
auto const prev = makeHash("proposal-precheck-prev");
|
||||
set.set_previousledger(prev.data(), prev.size());
|
||||
}
|
||||
|
||||
void
|
||||
setPosition(protocol::TMProposeSet& set, ExtendedPosition const& position)
|
||||
{
|
||||
Serializer s;
|
||||
position.add(s);
|
||||
set.set_currenttxhash(s.data(), s.size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ProposalPrecheck_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using enum detail::ProposalPrecheckResult;
|
||||
|
||||
//@@start test-peer-proposal-authentication
|
||||
testcase("cluster transport does not bypass proposal authentication");
|
||||
{
|
||||
BEAST_EXPECT(detail::proposalSignatureAccepted(false, true));
|
||||
BEAST_EXPECT(detail::proposalSignatureAccepted(true, true));
|
||||
BEAST_EXPECT(!detail::proposalSignatureAccepted(false, false));
|
||||
BEAST_EXPECT(!detail::proposalSignatureAccepted(true, false));
|
||||
}
|
||||
//@@end test-peer-proposal-authentication
|
||||
|
||||
//@@start test-proposal-precheck-legacy-ok
|
||||
testcase("legacy and extended ok");
|
||||
{
|
||||
protocol::TMProposeSet set;
|
||||
setPreviousLedger(set);
|
||||
ExtendedPosition position{makeHash("legacy-position")};
|
||||
setPosition(set, position);
|
||||
|
||||
auto const precheck =
|
||||
detail::checkProposalExtensions(set, false, false);
|
||||
BEAST_EXPECT(precheck.result == ok);
|
||||
BEAST_EXPECT(precheck.position);
|
||||
if (precheck.position)
|
||||
BEAST_EXPECT(
|
||||
precheck.position->txSetHash == position.txSetHash);
|
||||
}
|
||||
//@@end test-proposal-precheck-legacy-ok
|
||||
|
||||
testcase("feature predicates are lazy");
|
||||
{
|
||||
int entropyChecks = 0;
|
||||
int exportChecks = 0;
|
||||
auto const entropyEnabled = [&] {
|
||||
++entropyChecks;
|
||||
return false;
|
||||
};
|
||||
auto const exportEnabled = [&] {
|
||||
++exportChecks;
|
||||
return false;
|
||||
};
|
||||
|
||||
protocol::TMProposeSet plainSet;
|
||||
setPreviousLedger(plainSet);
|
||||
ExtendedPosition plain{makeHash("plain-lazy-position")};
|
||||
setPosition(plainSet, plain);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(
|
||||
plainSet, entropyEnabled, exportEnabled)
|
||||
.result == ok);
|
||||
BEAST_EXPECT(entropyChecks == 0);
|
||||
BEAST_EXPECT(exportChecks == 0);
|
||||
|
||||
protocol::TMProposeSet entropySet;
|
||||
setPreviousLedger(entropySet);
|
||||
ExtendedPosition entropy{makeHash("entropy-lazy-position")};
|
||||
entropy.myCommitment = makeHash("lazy-commitment");
|
||||
setPosition(entropySet, entropy);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(
|
||||
entropySet, entropyEnabled, exportEnabled)
|
||||
.result == entropyDisabled);
|
||||
BEAST_EXPECT(entropyChecks == 1);
|
||||
BEAST_EXPECT(exportChecks == 0);
|
||||
|
||||
protocol::TMProposeSet exportSet;
|
||||
setPreviousLedger(exportSet);
|
||||
ExtendedPosition exportPos{makeHash("export-lazy-position")};
|
||||
exportPos.exportSigSetHash = makeHash("lazy-export-sidecar");
|
||||
setPosition(exportSet, exportPos);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(
|
||||
exportSet, entropyEnabled, exportEnabled)
|
||||
.result == exportDisabled);
|
||||
BEAST_EXPECT(entropyChecks == 1);
|
||||
BEAST_EXPECT(exportChecks == 1);
|
||||
}
|
||||
|
||||
testcase("malformed hashes and extended payload");
|
||||
{
|
||||
protocol::TMProposeSet set;
|
||||
setPreviousLedger(set);
|
||||
set.set_currenttxhash("short", 5);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(set, true, true).result ==
|
||||
badHashes);
|
||||
|
||||
set.clear_currenttxhash();
|
||||
std::string malformed(uint256::size(), '\0');
|
||||
malformed.push_back(static_cast<char>(0x80));
|
||||
set.set_currenttxhash(malformed.data(), malformed.size());
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(set, true, true).result ==
|
||||
badPosition);
|
||||
|
||||
protocol::TMProposeSet badPrev;
|
||||
ExtendedPosition position{makeHash("bad-prev-position")};
|
||||
setPosition(badPrev, position);
|
||||
badPrev.set_previousledger("short", 5);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(badPrev, true, true).result ==
|
||||
badHashes);
|
||||
}
|
||||
|
||||
//@@start test-proposal-extension-feature-gating
|
||||
testcase("feature gating");
|
||||
{
|
||||
protocol::TMProposeSet entropySet;
|
||||
setPreviousLedger(entropySet);
|
||||
ExtendedPosition entropy{makeHash("entropy-position")};
|
||||
entropy.myCommitment = makeHash("commitment");
|
||||
setPosition(entropySet, entropy);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(entropySet, false, true)
|
||||
.result == entropyDisabled);
|
||||
|
||||
protocol::TMProposeSet observedSet;
|
||||
setPreviousLedger(observedSet);
|
||||
ExtendedPosition observed{makeHash("observed-position")};
|
||||
observed.observedParticipantsHash =
|
||||
makeHash("observed-participants");
|
||||
setPosition(observedSet, observed);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(observedSet, false, true)
|
||||
.result == ok);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(observedSet, true, false)
|
||||
.result == ok);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(observedSet, false, false)
|
||||
.result == extensionDiagnosticsDisabled);
|
||||
|
||||
protocol::TMProposeSet exportSet;
|
||||
setPreviousLedger(exportSet);
|
||||
ExtendedPosition exportPos{makeHash("export-position")};
|
||||
exportPos.exportSigSetHash = makeHash("export-sidecar");
|
||||
setPosition(exportSet, exportPos);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(exportSet, true, false)
|
||||
.result == exportDisabled);
|
||||
}
|
||||
//@@end test-proposal-extension-feature-gating
|
||||
|
||||
//@@start test-proposal-export-signature-binding
|
||||
testcase("export signature binding");
|
||||
{
|
||||
protocol::TMProposeSet tooMany;
|
||||
setPreviousLedger(tooMany);
|
||||
ExtendedPosition position{makeHash("too-many-position")};
|
||||
setPosition(tooMany, position);
|
||||
for (std::size_t i = 0; i <= ExportLimits::maxExportSharesPerRelay;
|
||||
++i)
|
||||
tooMany.add_exportsignatures("sig");
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(tooMany, true, true).result ==
|
||||
tooManyExportSignatures);
|
||||
|
||||
protocol::TMProposeSet unsignedSigs;
|
||||
setPreviousLedger(unsignedSigs);
|
||||
setPosition(unsignedSigs, position);
|
||||
unsignedSigs.add_exportsignatures("sig");
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(unsignedSigs, true, true)
|
||||
.result == unsignedExportSignatures);
|
||||
|
||||
protocol::TMProposeSet mismatch;
|
||||
setPreviousLedger(mismatch);
|
||||
ExtendedPosition mismatchPos{makeHash("mismatch-position")};
|
||||
mismatchPos.exportSignaturesHash =
|
||||
proposalExportSignaturesHash(std::vector<std::string>{"one"});
|
||||
setPosition(mismatch, mismatchPos);
|
||||
mismatch.add_exportsignatures("two");
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(mismatch, true, true).result ==
|
||||
exportSignaturesHashMismatch);
|
||||
|
||||
protocol::TMProposeSet missing;
|
||||
setPreviousLedger(missing);
|
||||
setPosition(missing, mismatchPos);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(missing, true, true).result ==
|
||||
missingExportSignatures);
|
||||
|
||||
protocol::TMProposeSet okSet;
|
||||
setPreviousLedger(okSet);
|
||||
std::vector<std::string> const sigs{"signed-export"};
|
||||
ExtendedPosition okPos{makeHash("export-ok-position")};
|
||||
okPos.exportSignaturesHash = proposalExportSignaturesHash(sigs);
|
||||
setPosition(okSet, okPos);
|
||||
okSet.add_exportsignatures(sigs.front());
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(okSet, true, true).result ==
|
||||
ok);
|
||||
|
||||
// A single oversized blob is rejected before its bytes are hashed,
|
||||
// even though the count is within the relay cap. This bounds the
|
||||
// pre-auth SHA512 work on the proposal ingress path.
|
||||
protocol::TMProposeSet oversized;
|
||||
setPreviousLedger(oversized);
|
||||
std::string const bigSig(
|
||||
ExportLimits::maxSerializedExportShareBytes + 1, 'x');
|
||||
std::vector<std::string> const bigSigs{bigSig};
|
||||
ExtendedPosition oversizedPos{
|
||||
makeHash("export-oversized-position")};
|
||||
oversizedPos.exportSignaturesHash =
|
||||
proposalExportSignaturesHash(bigSigs);
|
||||
setPosition(oversized, oversizedPos);
|
||||
oversized.add_exportsignatures(bigSig);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(oversized, true, true).result ==
|
||||
oversizedExportSignature);
|
||||
|
||||
// A maximum-size blob is still accepted.
|
||||
protocol::TMProposeSet maxSized;
|
||||
setPreviousLedger(maxSized);
|
||||
std::string const maxSig(
|
||||
ExportLimits::maxSerializedExportShareBytes, 'x');
|
||||
std::vector<std::string> const maxSigs{maxSig};
|
||||
ExtendedPosition maxPos{makeHash("export-maxsize-position")};
|
||||
maxPos.exportSignaturesHash = proposalExportSignaturesHash(maxSigs);
|
||||
setPosition(maxSized, maxPos);
|
||||
maxSized.add_exportsignatures(maxSig);
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(maxSized, true, true).result ==
|
||||
ok);
|
||||
|
||||
auto const [key, secret] = randomKeyPair(KeyType::ed25519);
|
||||
auto const signature = sign(key, secret, Slice{"share", 5});
|
||||
auto const share =
|
||||
ExportShare{
|
||||
ExportShare::currentVersion,
|
||||
calcAccountID(key),
|
||||
makeHash("export-origin"),
|
||||
42,
|
||||
makeHash("export-ledger"),
|
||||
3,
|
||||
key,
|
||||
signature}
|
||||
.serialize();
|
||||
protocol::TMProposeSet encodedShare;
|
||||
setPreviousLedger(encodedShare);
|
||||
std::vector<std::string> const frames{std::string{
|
||||
reinterpret_cast<char const*>(share.data()), share.size()}};
|
||||
ExtendedPosition sharePos{makeHash("export-share-position")};
|
||||
sharePos.exportSignaturesHash =
|
||||
proposalExportSignaturesHash(frames);
|
||||
setPosition(encodedShare, sharePos);
|
||||
encodedShare.add_exportsignatures(frames.front());
|
||||
BEAST_EXPECT(
|
||||
detail::checkProposalExtensions(encodedShare, true, true)
|
||||
.result == ok);
|
||||
}
|
||||
//@@end test-proposal-export-signature-binding
|
||||
|
||||
testcase("rejection diagnostics");
|
||||
{
|
||||
BEAST_EXPECT(!detail::proposalPrecheckRejection(ok));
|
||||
|
||||
auto const check = [&](detail::ProposalPrecheckResult result,
|
||||
char const* logMessage,
|
||||
char const* feeReason) {
|
||||
auto const rejection =
|
||||
detail::proposalPrecheckRejection(result);
|
||||
BEAST_EXPECT(rejection);
|
||||
if (rejection)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string_view{rejection->logMessage} == logMessage);
|
||||
BEAST_EXPECT(
|
||||
std::string_view{rejection->feeReason} == feeReason);
|
||||
}
|
||||
};
|
||||
|
||||
check(badHashes, "Proposal: malformed", "bad hashes");
|
||||
check(
|
||||
badPosition,
|
||||
"Proposal: malformed extended position",
|
||||
"bad proposal position");
|
||||
check(
|
||||
extensionDiagnosticsDisabled,
|
||||
"Proposal: extension diagnostics while consensus extensions "
|
||||
"disabled",
|
||||
"extension diagnostics disabled");
|
||||
check(
|
||||
entropyDisabled,
|
||||
"Proposal: entropy fields while featureConsensusEntropy "
|
||||
"disabled",
|
||||
"entropy fields disabled");
|
||||
check(
|
||||
exportDisabled,
|
||||
"Proposal: export fields while featureExport disabled",
|
||||
"export fields disabled");
|
||||
check(
|
||||
tooManyExportSignatures,
|
||||
"Proposal: too many export signatures",
|
||||
"too many export sigs");
|
||||
check(
|
||||
oversizedExportSignature,
|
||||
"Proposal: oversized export signature",
|
||||
"oversized export sig");
|
||||
check(
|
||||
unsignedExportSignatures,
|
||||
"Proposal: unsigned export signatures",
|
||||
"unsigned export sigs");
|
||||
check(
|
||||
exportSignaturesHashMismatch,
|
||||
"Proposal: export signatures hash mismatch",
|
||||
"export sig hash mismatch");
|
||||
check(
|
||||
missingExportSignatures,
|
||||
"Proposal: missing signed export signatures",
|
||||
"missing export sigs");
|
||||
BEAST_EXPECT(!detail::proposalPrecheckRejection(
|
||||
static_cast<detail::ProposalPrecheckResult>(255)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ProposalPrecheck, consensus, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -21,6 +21,9 @@
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
@@ -63,6 +66,27 @@ class JobQueue_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRejectMeasurementJobs()
|
||||
{
|
||||
testcase("measurement-only jobs are rejected without retaining work");
|
||||
jtx::Env env{*this};
|
||||
auto& queue = env.app().getJobQueue();
|
||||
auto ran = std::make_shared<std::atomic<bool>>(false);
|
||||
for (auto const type : {jtPEER, jtGENERIC})
|
||||
{
|
||||
BEAST_EXPECT(JobTypes::instance().get(type).special());
|
||||
BEAST_EXPECT(!queue.addJob(
|
||||
type, "RejectMeasurementJob", [ran] { *ran = true; }));
|
||||
BEAST_EXPECT(queue.getJobCountTotal(type) == 0);
|
||||
auto measurement = queue.makeLoadEvent(type, "MeasurementOnly");
|
||||
BEAST_EXPECT(measurement != nullptr);
|
||||
}
|
||||
queue.rendezvous();
|
||||
queue.stop();
|
||||
BEAST_EXPECT(!*ran);
|
||||
}
|
||||
|
||||
void
|
||||
testPostCoro()
|
||||
{
|
||||
@@ -154,6 +178,7 @@ public:
|
||||
run() override
|
||||
{
|
||||
testAddJob();
|
||||
testRejectMeasurementJobs();
|
||||
testPostCoro();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <test/csf/Histogram.h>
|
||||
#include <test/csf/Peer.h>
|
||||
#include <test/csf/PeerGroup.h>
|
||||
#include <test/csf/PeerTick.h>
|
||||
#include <test/csf/Proposal.h>
|
||||
#include <test/csf/Scheduler.h>
|
||||
#include <test/csf/Sim.h>
|
||||
|
||||
+1059
-14
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,14 @@
|
||||
#ifndef RIPPLE_TEST_CSF_PEERTICK_H_INCLUDED
|
||||
#define RIPPLE_TEST_CSF_PEERTICK_H_INCLUDED
|
||||
|
||||
#include <test/csf/Peer.h>
|
||||
#include <xrpld/consensus/ConsensusExtensionsTick.h>
|
||||
|
||||
template <class Ctx>
|
||||
ripple::ExtensionTickResult
|
||||
ripple::test::csf::Peer::Extensions::onTick(Ctx const& ctx)
|
||||
{
|
||||
return ripple::extensionsTick(*this, ctx);
|
||||
}
|
||||
|
||||
#endif
|
||||
+71
-4
@@ -23,17 +23,84 @@
|
||||
#include <test/csf/Validation.h>
|
||||
#include <test/csf/ledgers.h>
|
||||
#include <xrpld/consensus/ConsensusProposal.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/hash/hash_append.h>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace csf {
|
||||
/** Proposal is a position taken in the consensus process and is represented
|
||||
directly from the generic types.
|
||||
/** Position sidecar for CSF that can model RNG commit/reveal fields.
|
||||
|
||||
Core tx-set convergence remains keyed on txSetHash only, matching
|
||||
production's ExtendedPosition behavior.
|
||||
*/
|
||||
using Proposal = ConsensusProposal<PeerID, Ledger::ID, TxSet::ID>;
|
||||
struct RngPosition
|
||||
{
|
||||
TxSet::ID txSetHash{};
|
||||
std::optional<uint256> commitSetHash;
|
||||
std::optional<uint256> entropySetHash;
|
||||
std::optional<uint256> exportSigSetHash;
|
||||
std::optional<uint256> myCommitment;
|
||||
std::optional<uint256> myReveal;
|
||||
std::optional<Ledger::ID> exportSignatureOrigin;
|
||||
std::optional<uint256> myExportSignature;
|
||||
|
||||
RngPosition() = default;
|
||||
explicit RngPosition(TxSet::ID txSet) : txSetHash(txSet)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
updateTxSet(TxSet::ID txSet)
|
||||
{
|
||||
txSetHash = txSet;
|
||||
}
|
||||
};
|
||||
|
||||
inline std::string
|
||||
to_string(RngPosition const& pos)
|
||||
{
|
||||
return std::to_string(pos.txSetHash);
|
||||
}
|
||||
|
||||
inline std::ostream&
|
||||
operator<<(std::ostream& os, RngPosition const& pos)
|
||||
{
|
||||
return os << pos.txSetHash;
|
||||
}
|
||||
|
||||
template <class Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, RngPosition const& pos)
|
||||
{
|
||||
using beast::hash_append;
|
||||
auto appendOpt = [&](auto const& o) {
|
||||
hash_append(h, static_cast<std::uint8_t>(o.has_value() ? 1 : 0));
|
||||
if (o)
|
||||
hash_append(h, *o);
|
||||
};
|
||||
|
||||
hash_append(h, pos.txSetHash);
|
||||
appendOpt(pos.commitSetHash);
|
||||
appendOpt(pos.entropySetHash);
|
||||
appendOpt(pos.exportSigSetHash);
|
||||
appendOpt(pos.myCommitment);
|
||||
appendOpt(pos.myReveal);
|
||||
appendOpt(pos.exportSignatureOrigin);
|
||||
appendOpt(pos.myExportSignature);
|
||||
}
|
||||
|
||||
/** Proposal is a position taken in the consensus process.
|
||||
*/
|
||||
using Proposal = ConsensusProposal<PeerID, Ledger::ID, RngPosition>;
|
||||
using ProposalPosition = RngPosition;
|
||||
|
||||
} // namespace csf
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
#endif
|
||||
+13
-1
@@ -25,6 +25,7 @@
|
||||
#include <test/csf/Digraph.h>
|
||||
#include <test/csf/Peer.h>
|
||||
#include <test/csf/PeerGroup.h>
|
||||
#include <test/csf/PeerTick.h>
|
||||
#include <test/csf/Scheduler.h>
|
||||
#include <test/csf/SimTime.h>
|
||||
#include <test/csf/TrustGraph.h>
|
||||
@@ -41,6 +42,7 @@ namespace csf {
|
||||
class BasicSink : public beast::Journal::Sink
|
||||
{
|
||||
Scheduler::clock_type const& clock_;
|
||||
bool silent_ = false;
|
||||
|
||||
public:
|
||||
BasicSink(Scheduler::clock_type const& clock)
|
||||
@@ -48,11 +50,19 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
silent(bool value)
|
||||
{
|
||||
silent_ = value;
|
||||
}
|
||||
|
||||
void
|
||||
write(beast::severities::Severity level, std::string const& text) override
|
||||
{
|
||||
if (level < threshold())
|
||||
return;
|
||||
if (silent_)
|
||||
return;
|
||||
|
||||
std::cout << clock_.now().time_since_epoch().count() << " " << text
|
||||
<< std::endl;
|
||||
@@ -83,6 +93,7 @@ public:
|
||||
BasicNetwork<Peer*> net;
|
||||
TrustGraph<Peer*> trustGraph;
|
||||
CollectorRefs collectors;
|
||||
SidecarStore sidecarStore;
|
||||
|
||||
/** Create a simulation
|
||||
|
||||
@@ -119,7 +130,8 @@ public:
|
||||
net,
|
||||
trustGraph,
|
||||
collectors,
|
||||
j);
|
||||
j,
|
||||
sidecarStore);
|
||||
newPeers.emplace_back(&peers.back());
|
||||
}
|
||||
PeerGroup res{newPeers};
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
ID
|
||||
ID const&
|
||||
id() const
|
||||
{
|
||||
return id_;
|
||||
|
||||
@@ -98,7 +98,8 @@ LedgerOracle::accept(
|
||||
Ledger const& parent,
|
||||
TxSetType const& txs,
|
||||
NetClock::duration closeTimeResolution,
|
||||
NetClock::time_point const& consensusCloseTime)
|
||||
NetClock::time_point const& consensusCloseTime,
|
||||
std::optional<uint256> const& consensusExtensionEffect)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
Ledger::Instance next(*parent.instance_);
|
||||
@@ -114,6 +115,7 @@ LedgerOracle::accept(
|
||||
|
||||
next.parentCloseTime = parent.closeTime();
|
||||
next.parentID = parent.id();
|
||||
next.consensusExtensionEffect = consensusExtensionEffect;
|
||||
next.ancestors.push_back(parent.id());
|
||||
|
||||
auto it = instances_.left.find(next);
|
||||
|
||||
+15
-2
@@ -22,6 +22,7 @@
|
||||
#include <test/csf/Tx.h>
|
||||
#include <xrpld/consensus/LedgerTiming.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/comparators.h>
|
||||
#include <xrpl/basics/tagged_integer.h>
|
||||
@@ -100,6 +101,10 @@ private:
|
||||
//! Parent ledger close time
|
||||
NetClock::time_point parentCloseTime;
|
||||
|
||||
//! Synthetic accept-time consensus-extension effect. This models
|
||||
//! pseudo-transaction bytes that are not part of the agreed TxSet.
|
||||
std::optional<uint256> consensusExtensionEffect;
|
||||
|
||||
//! IDs of this ledgers ancestors. Since each ledger already has unique
|
||||
//! ancestors based on the parentID, this member is not needed for any
|
||||
//! of the operators below.
|
||||
@@ -115,7 +120,8 @@ private:
|
||||
closeTime,
|
||||
closeTimeAgree,
|
||||
parentID,
|
||||
parentCloseTime);
|
||||
parentCloseTime,
|
||||
consensusExtensionEffect);
|
||||
}
|
||||
|
||||
friend bool
|
||||
@@ -211,6 +217,12 @@ public:
|
||||
return instance_->txs;
|
||||
}
|
||||
|
||||
std::optional<uint256> const&
|
||||
consensusExtensionEffect() const
|
||||
{
|
||||
return instance_->consensusExtensionEffect;
|
||||
}
|
||||
|
||||
/** Determine whether ancestor is really an ancestor of this ledger */
|
||||
bool
|
||||
isAncestor(Ledger const& ancestor) const;
|
||||
@@ -275,7 +287,8 @@ public:
|
||||
Ledger const& curr,
|
||||
TxSetType const& txs,
|
||||
NetClock::duration closeTimeResolution,
|
||||
NetClock::time_point const& consensusCloseTime);
|
||||
NetClock::time_point const& consensusCloseTime,
|
||||
std::optional<uint256> const& consensusExtensionEffect = std::nullopt);
|
||||
|
||||
Ledger
|
||||
accept(Ledger const& curr, Tx tx)
|
||||
|
||||
+12
-1
@@ -82,7 +82,18 @@ supported_amendments()
|
||||
Throw<std::runtime_error>(
|
||||
"Unknown feature: " + s + " in supportedAmendments.");
|
||||
}
|
||||
return FeatureBitset(feats);
|
||||
//@@start rng-test-environment-gating
|
||||
// ConsensusEntropy is a supported amendment, but it is intentionally
|
||||
// excluded from the default jtx fixture set.
|
||||
//
|
||||
// Enabling it changes every closed ledger by injecting a deterministic
|
||||
// ttCONSENSUS_ENTROPY pseudo-transaction. Many legacy Env tests make
|
||||
// exact transaction-count / metadata-shape assertions and are not
|
||||
// trying to exercise ledger-wide entropy materialization. Keep those
|
||||
// default fixtures stable; tests that cover entropy opt in explicitly
|
||||
// with `supported_amendments() | featureConsensusEntropy`.
|
||||
return FeatureBitset(feats) - featureConsensusEntropy;
|
||||
//@@end rng-test-environment-gating
|
||||
}();
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ struct StubHookContext
|
||||
uint16_t emit_nonce_counter{0};
|
||||
uint16_t ledger_nonce_counter{0};
|
||||
int64_t expected_etxn_count{-1};
|
||||
int64_t expected_export_count{-1};
|
||||
std::map<ripple::uint256, bool> nonce_used{};
|
||||
uint32_t generation = 0;
|
||||
uint64_t burden = 0;
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -130,7 +131,8 @@ Env::close(
|
||||
// Go through the rpc interface unless we need to simulate
|
||||
// a specific consensus delay.
|
||||
if (consensusDelay)
|
||||
app().getOPs().acceptLedger(consensusDelay);
|
||||
app().getOPs().acceptLedger(
|
||||
consensusDelay, "Env::close(consensusDelay)");
|
||||
else
|
||||
{
|
||||
auto resp = rpc("ledger_accept");
|
||||
@@ -510,7 +512,7 @@ Env::acct_autofill(JTx& jt, Account const& account)
|
||||
jtx::fill_seq(jv, *current());
|
||||
|
||||
uint32_t networkID = app().config().NETWORK_ID;
|
||||
if (!jv.isMember(jss::NetworkID) && networkID > 1024)
|
||||
if (!jv.isMember(jss::NetworkID) && requiresTxNetworkID(networkID))
|
||||
jv[jss::NetworkID] = std::to_string(networkID);
|
||||
|
||||
// Must come last
|
||||
@@ -537,7 +539,7 @@ Env::autofill(JTx& jt)
|
||||
if (jt.fill_netid)
|
||||
{
|
||||
uint32_t networkID = app().config().NETWORK_ID;
|
||||
if (!jv.isMember(jss::NetworkID) && networkID > 1024)
|
||||
if (!jv.isMember(jss::NetworkID) && requiresTxNetworkID(networkID))
|
||||
jv[jss::NetworkID] = std::to_string(networkID);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ makeStubHookContext(
|
||||
.emit_nonce_counter = stubHookContext.emit_nonce_counter,
|
||||
.ledger_nonce_counter = stubHookContext.ledger_nonce_counter,
|
||||
.expected_etxn_count = stubHookContext.expected_etxn_count,
|
||||
.expected_export_count = stubHookContext.expected_export_count,
|
||||
.nonce_used = stubHookContext.nonce_used,
|
||||
.generation = stubHookContext.generation,
|
||||
.burden = stubHookContext.burden,
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
#ifndef RIPPLE_TEST_JTX_XPOP_H_INCLUDED
|
||||
#define RIPPLE_TEST_JTX_XPOP_H_INCLUDED
|
||||
|
||||
#include <test/jtx/Env.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/proof/LedgerProof.h>
|
||||
#include <xrpld/app/proof/XPOPv1.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
namespace xpop {
|
||||
|
||||
/// Build a manifest string (binary, not base64).
|
||||
inline std::string
|
||||
makeManifestRaw(
|
||||
PublicKey const& masterPub,
|
||||
SecretKey const& masterSec,
|
||||
PublicKey const& signingPub,
|
||||
SecretKey const& signingSec,
|
||||
int seq = 1)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = seq;
|
||||
st[sfPublicKey] = masterPub;
|
||||
st[sfSigningPubKey] = signingPub;
|
||||
|
||||
sign(st, HashPrefix::manifest, *publicKeyType(signingPub), signingSec);
|
||||
sign(
|
||||
st,
|
||||
HashPrefix::manifest,
|
||||
*publicKeyType(masterPub),
|
||||
masterSec,
|
||||
sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
return std::string(static_cast<char const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
/// A complete test validator with all keys and manifest.
|
||||
struct TestValidator
|
||||
{
|
||||
PublicKey masterPublic;
|
||||
SecretKey masterSecret;
|
||||
PublicKey signingPublic;
|
||||
SecretKey signingSecret;
|
||||
std::string manifestRaw;
|
||||
std::string manifestBase64;
|
||||
|
||||
static TestValidator
|
||||
create()
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
auto const mp = derivePublicKey(KeyType::ed25519, ms);
|
||||
auto const [sp, ss] = randomKeyPair(KeyType::secp256k1);
|
||||
auto raw = makeManifestRaw(mp, ms, sp, ss, 1);
|
||||
return {mp, ms, sp, ss, raw, base64_encode(raw)};
|
||||
}
|
||||
|
||||
proof::ValidatorKeys
|
||||
toValidatorKeys() const
|
||||
{
|
||||
return {
|
||||
masterPublic,
|
||||
masterSecret,
|
||||
signingPublic,
|
||||
signingSecret,
|
||||
manifestBase64};
|
||||
}
|
||||
};
|
||||
|
||||
/// A complete test VL publisher with keys and manifest.
|
||||
struct TestVLPublisher
|
||||
{
|
||||
PublicKey masterPublic;
|
||||
SecretKey masterSecret;
|
||||
PublicKey signingPublic;
|
||||
SecretKey signingSecret;
|
||||
std::string manifestBase64;
|
||||
|
||||
static TestVLPublisher
|
||||
create()
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
auto const mp = derivePublicKey(KeyType::ed25519, ms);
|
||||
auto const [sp, ss] = randomKeyPair(KeyType::secp256k1);
|
||||
return {
|
||||
mp, ms, sp, ss, base64_encode(makeManifestRaw(mp, ms, sp, ss, 1))};
|
||||
}
|
||||
|
||||
/// Build VL data for these validators.
|
||||
proof::VLData
|
||||
buildVLData(
|
||||
std::vector<TestValidator> const& validators,
|
||||
std::uint32_t sequence = 1,
|
||||
std::uint32_t expiration = 767784645) const
|
||||
{
|
||||
// Build the JSON blob
|
||||
std::string data = "{\"sequence\":" + std::to_string(sequence) +
|
||||
",\"expiration\":" + std::to_string(expiration) +
|
||||
",\"validators\":[";
|
||||
|
||||
for (std::size_t i = 0; i < validators.size(); ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
data += ",";
|
||||
data += "{\"validation_public_key\":\"" +
|
||||
strHex(validators[i].masterPublic) + "\",\"manifest\":\"" +
|
||||
validators[i].manifestBase64 + "\"}";
|
||||
}
|
||||
data += "]}";
|
||||
|
||||
auto const blob = base64_encode(data);
|
||||
auto const sig =
|
||||
strHex(sign(signingPublic, signingSecret, makeSlice(data)));
|
||||
|
||||
return proof::VLData{
|
||||
masterPublic, masterSecret, manifestBase64, blob, sig, 1};
|
||||
}
|
||||
};
|
||||
|
||||
/// Everything needed to build and import XPOPs in tests.
|
||||
struct TestXPOPContext
|
||||
{
|
||||
std::vector<TestValidator> validators;
|
||||
TestVLPublisher publisher;
|
||||
proof::VLData vlData;
|
||||
|
||||
static TestXPOPContext
|
||||
create(int validatorCount = 5)
|
||||
{
|
||||
auto pub = TestVLPublisher::create();
|
||||
std::vector<TestValidator> vals;
|
||||
for (int i = 0; i < validatorCount; ++i)
|
||||
vals.push_back(TestValidator::create());
|
||||
auto vl = pub.buildVLData(vals);
|
||||
return {std::move(vals), std::move(pub), std::move(vl)};
|
||||
}
|
||||
|
||||
/// Get the VL master public key hex for IMPORT_VL_KEYS config.
|
||||
std::string
|
||||
vlKeyHex() const
|
||||
{
|
||||
return strHex(publisher.masterPublic);
|
||||
}
|
||||
|
||||
/// Build an Env config with NETWORK_ID and IMPORT_VL_KEYS set.
|
||||
std::unique_ptr<Config>
|
||||
makeEnvConfig(std::uint32_t networkID = 21337) const
|
||||
{
|
||||
auto cfg = envconfig(jtx::validator, "");
|
||||
cfg->NETWORK_ID = networkID;
|
||||
auto const keyHex = vlKeyHex();
|
||||
auto const pkHex = strUnHex(keyHex);
|
||||
if (pkHex)
|
||||
cfg->IMPORT_VL_KEYS.emplace(keyHex, makeSlice(*pkHex));
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/// Build XPOP from a closed ledger for a specific tx.
|
||||
Json::Value
|
||||
buildXPOP(Ledger const& ledger, uint256 const& txHash) const
|
||||
{
|
||||
std::vector<proof::ValidatorKeys> valKeys;
|
||||
for (auto const& v : validators)
|
||||
valKeys.push_back(v.toValidatorKeys());
|
||||
return proof::buildXPOPv1(ledger, txHash, valKeys, vlData);
|
||||
}
|
||||
|
||||
/// Build XPOP from an Env's last closed ledger.
|
||||
Json::Value
|
||||
buildXPOP(Env& env, uint256 const& txHash) const
|
||||
{
|
||||
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!lcl)
|
||||
return {};
|
||||
return buildXPOP(*lcl, txHash);
|
||||
}
|
||||
};
|
||||
|
||||
/// Build a complete XPOP v1 JSON from an Env's last closed ledger.
|
||||
/// Creates fresh validator keys and VL publisher for each call.
|
||||
inline Json::Value
|
||||
buildTestXPOP(Env& env, uint256 const& txHash, int validatorCount = 5)
|
||||
{
|
||||
auto ctx = TestXPOPContext::create(validatorCount);
|
||||
return ctx.buildXPOP(env, txHash);
|
||||
}
|
||||
|
||||
/// Get the hex-encoded XPOP blob suitable for sfBlob in ttIMPORT.
|
||||
inline std::string
|
||||
buildTestXPOPHex(Env& env, uint256 const& txHash, int validatorCount = 5)
|
||||
{
|
||||
auto const xpop = buildTestXPOP(env, txHash, validatorCount);
|
||||
if (xpop.isNull())
|
||||
return {};
|
||||
return proof::xpopToHex(xpop);
|
||||
}
|
||||
|
||||
} // namespace xpop
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -24,11 +24,14 @@
|
||||
#include <xrpld/app/tx/detail/ApplyContext.h>
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/ExportCommittee.h>
|
||||
#include <xrpl/protocol/InnerObjectFormats.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Invariants_test : public beast::unit_test::suite
|
||||
@@ -1234,6 +1237,99 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
void
|
||||
testValidExportCommittee()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase << "ValidExportCommittee";
|
||||
|
||||
// A zero digest is never a valid content-addressed committee.
|
||||
doInvariantCheck(
|
||||
{{"Invariant failed: malformed or mutated Export committee"}},
|
||||
[](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto const roster = serializeExportCommittee({A2.pk()});
|
||||
auto const digest = exportCommitteeHash(makeSlice(roster));
|
||||
auto sle = std::make_shared<SLE>(
|
||||
keylet::exportCommittee(A1.id(), digest));
|
||||
sle->setAccountID(sfAccount, A1.id());
|
||||
sle->setFieldH256(sfExportCommitteeHash, uint256{});
|
||||
sle->setFieldVL(sfExportCommittee, roster);
|
||||
sle->setFieldU64(sfOwnerNode, 0);
|
||||
ac.view().insert(sle);
|
||||
return true;
|
||||
});
|
||||
|
||||
// The SLE key must be derived from its account and roster digest.
|
||||
doInvariantCheck(
|
||||
{{"Invariant failed: malformed or mutated Export committee"}},
|
||||
[](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto const roster = serializeExportCommittee({A2.pk()});
|
||||
auto const digest = exportCommitteeHash(makeSlice(roster));
|
||||
auto const otherRoster = serializeExportCommittee({A1.pk()});
|
||||
auto const otherDigest =
|
||||
exportCommitteeHash(makeSlice(otherRoster));
|
||||
auto sle = std::make_shared<SLE>(
|
||||
keylet::exportCommittee(A1.id(), otherDigest));
|
||||
sle->setAccountID(sfAccount, A1.id());
|
||||
sle->setFieldH256(sfExportCommitteeHash, digest);
|
||||
sle->setFieldVL(sfExportCommittee, roster);
|
||||
sle->setFieldU64(sfOwnerNode, 0);
|
||||
ac.view().insert(sle);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Valid keys in noncanonical roster order must not gain a second
|
||||
// content identity.
|
||||
doInvariantCheck(
|
||||
{{"Invariant failed: malformed or mutated Export committee"}},
|
||||
[](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto roster = serializeExportCommittee({A1.pk(), A2.pk()});
|
||||
std::rotate(
|
||||
roster.begin(),
|
||||
roster.begin() + A1.pk().size(),
|
||||
roster.end());
|
||||
auto const digest = exportCommitteeHash(makeSlice(roster));
|
||||
auto sle = std::make_shared<SLE>(
|
||||
keylet::exportCommittee(A1.id(), digest));
|
||||
sle->setAccountID(sfAccount, A1.id());
|
||||
sle->setFieldH256(sfExportCommitteeHash, digest);
|
||||
sle->setFieldVL(sfExportCommittee, roster);
|
||||
sle->setFieldU64(sfOwnerNode, 0);
|
||||
ac.view().insert(sle);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Once created, even a change that preserves the content-addressed
|
||||
// fields is an invariant violation.
|
||||
doInvariantCheck(
|
||||
{{"Invariant failed: malformed or mutated Export committee"}},
|
||||
[](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto const roster = serializeExportCommittee({A2.pk()});
|
||||
auto const digest = exportCommitteeHash(makeSlice(roster));
|
||||
auto sle =
|
||||
ac.view().peek(keylet::exportCommittee(A1.id(), digest));
|
||||
if (!sle)
|
||||
return false;
|
||||
sle->setFieldU64(
|
||||
sfOwnerNode, sle->getFieldU64(sfOwnerNode) + 1);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
[](Account const& A1, Account const& A2, Env& env) {
|
||||
Json::Value setup;
|
||||
setup[jss::TransactionType] = jss::Export;
|
||||
setup[jss::Account] = A1.human();
|
||||
setup[sfExportCommittee.jsonName] =
|
||||
strHex(serializeExportCommittee({A2.pk()}));
|
||||
env(setup);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testLockedBalance()
|
||||
{
|
||||
@@ -1288,6 +1384,7 @@ public:
|
||||
testValidNewAccountRoot();
|
||||
testNFTokenPageInvariants();
|
||||
testPermissionedDomainInvariants();
|
||||
testValidExportCommittee();
|
||||
testLockedBalance();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 WHATSOEVER
|
||||
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx/Env.h>
|
||||
#include <xrpld/overlay/Message.h>
|
||||
#include <xrpld/overlay/detail/ExportShareJob.h>
|
||||
#include <xrpld/overlay/detail/ProtocolMessage.h>
|
||||
#include <xrpld/overlay/detail/TrafficCount.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/ExportShare.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/beast/core/multi_buffer.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class ExportShareTransport_test : public beast::unit_test::suite
|
||||
{
|
||||
static ExportShare
|
||||
makeShare()
|
||||
{
|
||||
auto const [key, secret] = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signature = sign(key, secret, Slice{"export-share", 12});
|
||||
return ExportShare{
|
||||
ExportShare::currentVersion,
|
||||
calcAccountID(key),
|
||||
uint256{1},
|
||||
4'200'000,
|
||||
uint256{2},
|
||||
17,
|
||||
key,
|
||||
signature};
|
||||
}
|
||||
|
||||
struct Handler
|
||||
{
|
||||
bool seen = false;
|
||||
int shares = 0;
|
||||
|
||||
bool
|
||||
compressionEnabled() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
onMessageUnknown(std::uint16_t)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
onMessageBegin(
|
||||
std::uint16_t,
|
||||
std::shared_ptr<::google::protobuf::Message> const&,
|
||||
std::size_t,
|
||||
std::size_t,
|
||||
bool)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
onMessageEnd(
|
||||
std::uint16_t,
|
||||
std::shared_ptr<::google::protobuf::Message> const&)
|
||||
{
|
||||
}
|
||||
|
||||
template <class Message>
|
||||
void
|
||||
onMessage(std::shared_ptr<Message> const& message)
|
||||
{
|
||||
if constexpr (std::is_same_v<Message, protocol::TMExportShares>)
|
||||
{
|
||||
seen = true;
|
||||
shares = message->shares_size();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testBatchBounds()
|
||||
{
|
||||
testcase("ExportShare batch structural bounds");
|
||||
|
||||
auto const encoded = makeShare().serialize();
|
||||
protocol::TMExportShares batch;
|
||||
batch.add_shares(encoded.data(), encoded.size());
|
||||
|
||||
auto const parsed = detail::parseExportShareBatch(batch);
|
||||
BEAST_EXPECT(parsed && parsed->size() == 1);
|
||||
BEAST_EXPECT(
|
||||
batch.ByteSizeLong() <=
|
||||
ExportLimits::maxExportShareRelayMessageBytes);
|
||||
|
||||
protocol::TMExportShares maxBatch;
|
||||
for (std::size_t i = 0; i < ExportLimits::maxExportSharesPerRelay; ++i)
|
||||
maxBatch.add_shares(encoded.data(), encoded.size());
|
||||
auto const payloadBytes =
|
||||
encoded.size() * ExportLimits::maxExportSharesPerRelay;
|
||||
BEAST_EXPECT(
|
||||
payloadBytes <= ExportLimits::maxExportShareRelayPayloadBytes);
|
||||
BEAST_EXPECT(maxBatch.ByteSizeLong() > payloadBytes);
|
||||
BEAST_EXPECT(
|
||||
maxBatch.ByteSizeLong() <=
|
||||
ExportLimits::maxExportShareRelayMessageBytes);
|
||||
BEAST_EXPECT(detail::parseExportShareBatch(maxBatch));
|
||||
|
||||
protocol::TMExportShares empty;
|
||||
BEAST_EXPECT(!detail::parseExportShareBatch(empty));
|
||||
|
||||
protocol::TMExportShares tooMany;
|
||||
for (std::size_t i = 0; i <= ExportLimits::maxExportSharesPerRelay; ++i)
|
||||
tooMany.add_shares(encoded.data(), encoded.size());
|
||||
BEAST_EXPECT(!detail::parseExportShareBatch(tooMany));
|
||||
|
||||
protocol::TMExportShares oversized;
|
||||
oversized.add_shares(
|
||||
std::string(ExportLimits::maxSerializedExportShareBytes + 1, 'x'));
|
||||
BEAST_EXPECT(!detail::parseExportShareBatch(oversized));
|
||||
|
||||
protocol::TMExportShares malformed;
|
||||
malformed.add_shares("not-an-export-share");
|
||||
BEAST_EXPECT(!detail::parseExportShareBatch(malformed));
|
||||
}
|
||||
|
||||
void
|
||||
testReceiveJobDispatch()
|
||||
{
|
||||
testcase("ExportShare production posting boundary dispatches work");
|
||||
auto const type = detail::exportShareJobType;
|
||||
auto const& info = JobTypes::instance().get(type);
|
||||
// Fail before posting an undrainable special-category job: otherwise
|
||||
// the old bug would also hang the application's shutdown in this test.
|
||||
if (!BEAST_EXPECT(!info.special() && info.limit() > 0))
|
||||
return;
|
||||
|
||||
jtx::Env env{*this};
|
||||
auto& queue = env.app().getJobQueue();
|
||||
auto completed = std::make_shared<std::promise<std::thread::id>>();
|
||||
auto result = completed->get_future();
|
||||
auto payload = std::make_shared<ExportShare>(makeShare());
|
||||
std::weak_ptr<ExportShare> retained = payload;
|
||||
BEAST_EXPECT(detail::postExportShareJob(queue, [completed, payload] {
|
||||
// Exercise captured payload access on an actual worker.
|
||||
if (payload->validShape())
|
||||
completed->set_value(std::this_thread::get_id());
|
||||
}));
|
||||
payload.reset();
|
||||
using namespace std::chrono_literals;
|
||||
if (BEAST_EXPECT(result.wait_for(5s) == std::future_status::ready))
|
||||
BEAST_EXPECT(result.get() != std::this_thread::get_id());
|
||||
queue.rendezvous();
|
||||
BEAST_EXPECT(queue.getJobCountTotal(type) == 0);
|
||||
BEAST_EXPECT(retained.expired());
|
||||
|
||||
queue.stop();
|
||||
auto ranAfterStop = std::make_shared<std::atomic<bool>>(false);
|
||||
BEAST_EXPECT(!detail::postExportShareJob(
|
||||
queue, [ranAfterStop] { *ranAfterStop = true; }));
|
||||
BEAST_EXPECT(!*ranAfterStop);
|
||||
}
|
||||
|
||||
void
|
||||
testReceiveConcurrency()
|
||||
{
|
||||
testcase("ExportShare workers limit concurrency and drain queued work");
|
||||
auto const type = detail::exportShareJobType;
|
||||
if (!BEAST_EXPECT(JobTypes::instance().get(type).limit() == 2))
|
||||
return;
|
||||
|
||||
auto config = jtx::envconfig();
|
||||
config->FORCE_MULTI_THREAD = true;
|
||||
config->WORKERS = 4;
|
||||
jtx::Env env{*this, std::move(config)};
|
||||
auto& queue = env.app().getJobQueue();
|
||||
struct State
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
int started = 0;
|
||||
int active = 0;
|
||||
int peak = 0;
|
||||
int finished = 0;
|
||||
bool release = false;
|
||||
};
|
||||
auto state = std::make_shared<State>();
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
BEAST_EXPECT(detail::postExportShareJob(queue, [state] {
|
||||
std::unique_lock lock(state->mutex);
|
||||
++state->started;
|
||||
state->peak = std::max(state->peak, ++state->active);
|
||||
state->changed.notify_all();
|
||||
state->changed.wait(lock, [&] { return state->release; });
|
||||
--state->active;
|
||||
++state->finished;
|
||||
state->changed.notify_all();
|
||||
}));
|
||||
}
|
||||
using namespace std::chrono_literals;
|
||||
{
|
||||
std::unique_lock lock(state->mutex);
|
||||
BEAST_EXPECT(state->changed.wait_for(
|
||||
lock, 5s, [&] { return state->started >= 2; }));
|
||||
BEAST_EXPECT(state->started == 2);
|
||||
BEAST_EXPECT(queue.getJobCountTotal(type) == 3);
|
||||
BEAST_EXPECT(queue.getJobCount(type) == 1);
|
||||
// Release workers even if an expectation above failed.
|
||||
state->release = true;
|
||||
state->changed.notify_all();
|
||||
BEAST_EXPECT(state->changed.wait_for(
|
||||
lock, 5s, [&] { return state->finished == 3; }));
|
||||
BEAST_EXPECT(state->peak == 2);
|
||||
}
|
||||
queue.rendezvous();
|
||||
BEAST_EXPECT(queue.getJobCountTotal(type) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testDispatchAndTraffic()
|
||||
{
|
||||
testcase("ExportShare protocol dispatch and traffic category");
|
||||
|
||||
auto const encoded = makeShare().serialize();
|
||||
protocol::TMExportShares batch;
|
||||
batch.add_shares(encoded.data(), encoded.size());
|
||||
|
||||
BEAST_EXPECT(protocol::mtEXPORT_SHARES == 65);
|
||||
BEAST_EXPECT(
|
||||
protocolMessageName(protocol::mtEXPORT_SHARES) == "export_shares");
|
||||
BEAST_EXPECT(
|
||||
TrafficCount::categorize(batch, protocol::mtEXPORT_SHARES, true) ==
|
||||
TrafficCount::category::export_shares);
|
||||
|
||||
Message message{batch, protocol::mtEXPORT_SHARES};
|
||||
auto const& wire = message.getBuffer(compression::Compressed::Off);
|
||||
boost::beast::multi_buffer buffers;
|
||||
buffers.commit(boost::asio::buffer_copy(
|
||||
buffers.prepare(wire.size()), boost::asio::buffer(wire)));
|
||||
|
||||
Handler handler;
|
||||
std::size_t hint = 0;
|
||||
auto const [consumed, ec] =
|
||||
invokeProtocolMessage(buffers.data(), handler, hint);
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(consumed == wire.size());
|
||||
BEAST_EXPECT(handler.seen);
|
||||
BEAST_EXPECT(handler.shares == 1);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testBatchBounds();
|
||||
testDispatchAndTraffic();
|
||||
testReceiveJobDispatch();
|
||||
testReceiveConcurrency();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportShareTransport, overlay, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -49,6 +49,26 @@ public:
|
||||
BEAST_EXPECT(!isFeatureValue(headers, "feature4", "10"));
|
||||
BEAST_EXPECT(isFeatureValue(headers, "feature4", "1"));
|
||||
BEAST_EXPECT(!featureEnabled(headers, "v6"));
|
||||
|
||||
auto const capableRequest =
|
||||
makeFeaturesRequestHeader(false, false, false, false);
|
||||
headers.set("X-Protocol-Ctl", capableRequest);
|
||||
BEAST_EXPECT(featureEnabled(headers, FEATURE_CONSENSUS_ENTROPY));
|
||||
BEAST_EXPECT(featureEnabled(headers, FEATURE_EXPORT_SHARES));
|
||||
|
||||
http_request_type request;
|
||||
boost::beast::http::fields response;
|
||||
response.set(
|
||||
"X-Protocol-Ctl",
|
||||
makeFeaturesResponseHeader(request, false, false, false, false));
|
||||
BEAST_EXPECT(!featureEnabled(response, FEATURE_CONSENSUS_ENTROPY));
|
||||
BEAST_EXPECT(!featureEnabled(response, FEATURE_EXPORT_SHARES));
|
||||
request.set("X-Protocol-Ctl", capableRequest);
|
||||
response.set(
|
||||
"X-Protocol-Ctl",
|
||||
makeFeaturesResponseHeader(request, false, false, false, false));
|
||||
BEAST_EXPECT(featureEnabled(response, FEATURE_CONSENSUS_ENTROPY));
|
||||
BEAST_EXPECT(featureEnabled(response, FEATURE_EXPORT_SHARES));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -61,4 +81,4 @@ public:
|
||||
BEAST_DEFINE_TESTSUITE(handshake, ripple_data, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
} // namespace ripple
|
||||
@@ -18,11 +18,16 @@
|
||||
//==============================================================================
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/overlay/detail/OverlayImpl.h>
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpld/peerfinder/detail/SlotImp.h>
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/ExportShare.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -118,9 +123,18 @@ private:
|
||||
}
|
||||
~PeerTest() = default;
|
||||
|
||||
bool trustedCluster_ = false;
|
||||
|
||||
bool
|
||||
cluster() const override
|
||||
{
|
||||
return trustedCluster_;
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
run_++;
|
||||
}
|
||||
void
|
||||
send(std::shared_ptr<Message> const&) override
|
||||
@@ -137,11 +151,13 @@ private:
|
||||
{
|
||||
queueTx_ = 0;
|
||||
sendTx_ = 0;
|
||||
run_ = 0;
|
||||
sid_ = 0;
|
||||
}
|
||||
inline static std::size_t sid_ = 0;
|
||||
inline static std::uint16_t queueTx_ = 0;
|
||||
inline static std::uint16_t sendTx_ = 0;
|
||||
inline static std::uint16_t run_ = 0;
|
||||
};
|
||||
|
||||
std::uint16_t lid_{0};
|
||||
@@ -240,12 +256,241 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUnsignedImportClusterProof()
|
||||
{
|
||||
testcase("cluster signature trust does not skip unsigned Import proof");
|
||||
using namespace jtx;
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.app().getValidationPublicKey());
|
||||
env.app().getOPs().setMode(OperatingMode::FULL);
|
||||
env.app().getOPs().clearNeedNetworkLedger();
|
||||
|
||||
std::vector<std::shared_ptr<PeerTest>> peers;
|
||||
PeerTest::init();
|
||||
PeerTest::sid_ = 1;
|
||||
lid_ = 0;
|
||||
rid_ = 1;
|
||||
std::uint16_t disabled = 1;
|
||||
addPeer(env, peers, disabled);
|
||||
peers.front()->trustedCluster_ = true;
|
||||
auto const deliver = [&](std::shared_ptr<STTx const> const& tx) {
|
||||
auto packet = std::make_shared<protocol::TMTransaction>();
|
||||
auto const bytes = tx->getSerializer();
|
||||
packet->set_rawtransaction(bytes.data(), bytes.size());
|
||||
packet->set_status(protocol::tsNEW);
|
||||
packet->set_deferred(false);
|
||||
peers.front()->onMessage(packet);
|
||||
env.app().getJobQueue().rendezvous();
|
||||
};
|
||||
|
||||
auto const sequence = env.seq(alice);
|
||||
auto const balance = env.balance(alice);
|
||||
Json::Value malformed;
|
||||
malformed[jss::TransactionType] = jss::Import;
|
||||
malformed[jss::Account] = alice.human();
|
||||
malformed[jss::SigningPubKey] = "";
|
||||
malformed[jss::Blob] = "00";
|
||||
auto const candidate = env.jt(malformed, sig(none), fee(100));
|
||||
deliver(candidate.stx);
|
||||
BEAST_EXPECT(
|
||||
env.app().getHashRouter().getFlags(
|
||||
candidate.stx->getTransactionID()) &
|
||||
SF_BAD);
|
||||
BEAST_EXPECT(env.seq(alice) == sequence);
|
||||
BEAST_EXPECT(env.balance(alice) == balance);
|
||||
|
||||
// Ordinary signed cluster traffic still follows the normal path.
|
||||
auto const ordinary = env.jt(noop(alice), fee(100));
|
||||
deliver(ordinary.stx);
|
||||
BEAST_EXPECT(env.seq(alice) == sequence + 1);
|
||||
}
|
||||
|
||||
void
|
||||
testProtocolFeatureGate()
|
||||
{
|
||||
testcase("required protocol feature gate");
|
||||
jtx::Env env(*this);
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
std::vector<std::shared_ptr<PeerTest>> peers;
|
||||
PeerTest::init();
|
||||
lid_ = 0;
|
||||
rid_ = 1;
|
||||
|
||||
boost::beast::http::fields legacy;
|
||||
boost::beast::http::fields capable;
|
||||
capable.set(
|
||||
"X-Protocol-Ctl",
|
||||
makeFeaturesRequestHeader(false, false, false, false));
|
||||
|
||||
BEAST_EXPECT(!overlay.isProtocolFeatureRequired(
|
||||
ProtocolFeature::ConsensusEntropy));
|
||||
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
legacy, make_protocol(2, 2)));
|
||||
|
||||
// Leave a legacy peer in the active map while eviction is posted to its
|
||||
// strand. Proposal filtering must take effect synchronously with the
|
||||
// atomic requirement, without waiting for close().
|
||||
std::uint16_t disabled = 1;
|
||||
addPeer(env, peers, disabled);
|
||||
BEAST_EXPECT(PeerTest::run_ == 1);
|
||||
|
||||
overlay.requireProtocolFeature(ProtocolFeature::ConsensusEntropy);
|
||||
|
||||
BEAST_EXPECT(overlay.isProtocolFeatureRequired(
|
||||
ProtocolFeature::ConsensusEntropy));
|
||||
BEAST_EXPECT(
|
||||
overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
legacy, make_protocol(2, 2)) ==
|
||||
ProtocolFeature::ConsensusEntropy);
|
||||
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
capable, make_protocol(2, 2)));
|
||||
|
||||
protocol::TMProposeSet proposal;
|
||||
proposal.set_proposeseq(1);
|
||||
proposal.set_currenttxhash(std::string(32, '\0'));
|
||||
proposal.set_nodepubkey(std::string(33, '\0'));
|
||||
proposal.set_closetime(0);
|
||||
proposal.set_signature(std::string(64, '\0'));
|
||||
proposal.set_previousledger(std::string(32, '\0'));
|
||||
BEAST_EXPECT(proposal.IsInitialized());
|
||||
overlay.broadcast(proposal);
|
||||
BEAST_EXPECT(PeerTest::sendTx_ == 0);
|
||||
|
||||
// A capable peer admitted after the cutoff still runs and receives
|
||||
// proposals; a later legacy peer is inserted only for lifecycle cleanup
|
||||
// and is failed before run().
|
||||
disabled = 0;
|
||||
addPeer(env, peers, disabled);
|
||||
BEAST_EXPECT(PeerTest::run_ == 2);
|
||||
overlay.broadcast(proposal);
|
||||
BEAST_EXPECT(PeerTest::sendTx_ == 1);
|
||||
disabled = 1;
|
||||
addPeer(env, peers, disabled);
|
||||
BEAST_EXPECT(PeerTest::run_ == 2);
|
||||
|
||||
// Requiring an already-required feature is intentionally idempotent.
|
||||
overlay.requireProtocolFeature(ProtocolFeature::ConsensusEntropy);
|
||||
BEAST_EXPECT(overlay.isProtocolFeatureRequired(
|
||||
ProtocolFeature::ConsensusEntropy));
|
||||
}
|
||||
|
||||
void
|
||||
testProtocolFeatureGateFromLedger()
|
||||
{
|
||||
testcase("active ledger installs protocol feature gate");
|
||||
auto config = jtx::envconfig();
|
||||
config->features.insert(featureConsensusEntropy);
|
||||
config->features.insert(featureExport);
|
||||
jtx::Env env(*this, std::move(config));
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
BEAST_EXPECT(overlay.isProtocolFeatureRequired(
|
||||
ProtocolFeature::ConsensusEntropy));
|
||||
BEAST_EXPECT(
|
||||
overlay.isProtocolFeatureRequired(ProtocolFeature::ExportShares));
|
||||
}
|
||||
|
||||
void
|
||||
testGenericProtocolFeatureAdmission()
|
||||
{
|
||||
testcase("generic protocol feature admission");
|
||||
jtx::Env env(*this);
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
auto const capableCtl =
|
||||
makeFeaturesRequestHeader(false, false, false, false);
|
||||
boost::beast::http::fields empty;
|
||||
boost::beast::http::fields capable;
|
||||
capable.set("X-Protocol-Ctl", capableCtl);
|
||||
|
||||
overlay.requireProtocolFeature(ProtocolFeature::ExportShares);
|
||||
BEAST_EXPECT(
|
||||
overlay.isProtocolFeatureRequired(ProtocolFeature::ExportShares));
|
||||
BEAST_EXPECT(
|
||||
overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
empty, make_protocol(2, 2)) == ProtocolFeature::ExportShares);
|
||||
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
capable, make_protocol(2, 2)));
|
||||
|
||||
overlay.requireProtocolFeature(
|
||||
ProtocolFeature::ValidatorList2Propagation);
|
||||
BEAST_EXPECT(
|
||||
overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
empty, make_protocol(2, 1)) ==
|
||||
ProtocolFeature::ValidatorList2Propagation);
|
||||
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
capable, make_protocol(2, 2)));
|
||||
|
||||
overlay.requireProtocolFeature(ProtocolFeature::LedgerReplay);
|
||||
BEAST_EXPECT(
|
||||
overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
capable, make_protocol(2, 2)) == ProtocolFeature::LedgerReplay);
|
||||
boost::beast::http::fields withReplay;
|
||||
withReplay.set(
|
||||
"X-Protocol-Ctl", capableCtl + std::string("ledgerreplay=1;"));
|
||||
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
|
||||
withReplay, make_protocol(2, 2)));
|
||||
}
|
||||
|
||||
static protocol::TMExportShares
|
||||
makeExportShareBatch(uint256 const& sidecarHash)
|
||||
{
|
||||
auto const [key, secret] = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signature = sign(key, secret, Slice{"export-share", 12});
|
||||
ExportShare const share{
|
||||
ExportShare::currentVersion,
|
||||
calcAccountID(key),
|
||||
sidecarHash,
|
||||
4'200'000,
|
||||
uint256{2},
|
||||
17,
|
||||
key,
|
||||
signature};
|
||||
auto const encoded = share.serialize();
|
||||
protocol::TMExportShares batch;
|
||||
batch.add_shares(encoded.data(), encoded.size());
|
||||
return batch;
|
||||
}
|
||||
|
||||
void
|
||||
testExportSharesRelayCutoff()
|
||||
{
|
||||
testcase("export share relay respects protocol feature gate");
|
||||
jtx::Env env(*this);
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
std::vector<std::shared_ptr<PeerTest>> peers;
|
||||
PeerTest::init();
|
||||
lid_ = 0;
|
||||
rid_ = 1;
|
||||
|
||||
std::uint16_t disabled = 1;
|
||||
addPeer(env, peers, disabled);
|
||||
overlay.requireProtocolFeature(ProtocolFeature::ExportShares);
|
||||
|
||||
auto legacyBatch = makeExportShareBatch(uint256{1});
|
||||
overlay.relay(legacyBatch);
|
||||
BEAST_EXPECT(PeerTest::sendTx_ == 0);
|
||||
|
||||
disabled = 0;
|
||||
addPeer(env, peers, disabled);
|
||||
auto capableBatch = makeExportShareBatch(uint256{3});
|
||||
overlay.relay(capableBatch);
|
||||
BEAST_EXPECT(PeerTest::sendTx_ == 1);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
bool log = false;
|
||||
std::set<Peer::id_t> skip = {0, 1, 2, 3, 4};
|
||||
testConfig(log);
|
||||
testUnsignedImportClusterProof();
|
||||
testProtocolFeatureGate();
|
||||
testProtocolFeatureGateFromLedger();
|
||||
testGenericProtocolFeatureAdmission();
|
||||
testExportSharesRelayCutoff();
|
||||
// relay to all peers, no hash queue
|
||||
testRelay("feature disabled", false, 10, 0, 10, 25, 10, 0);
|
||||
// relay to nPeers - skip (10-5=5)
|
||||
@@ -283,4 +528,4 @@ private:
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(tx_reduce_relay, ripple_data, ripple);
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,270 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/basics/StringUtilities.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/ExportCommittee.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
makeOrigin(char const* label)
|
||||
{
|
||||
return sha512Half(Slice(label, std::strlen(label)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ExportKeylet_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
testOriginIdentity()
|
||||
{
|
||||
testcase("origin transaction identity");
|
||||
|
||||
AccountID const fixtureAccount{1};
|
||||
uint256 const fixtureOrigin{2};
|
||||
uint256 const expected{
|
||||
"04FEBC83B833D1A2EBFD1605BA3A0574F"
|
||||
"F0B0B8C9690ADBE35CD2F258984EB56"};
|
||||
BEAST_EXPECT(
|
||||
keylet::exportLatch(fixtureAccount, fixtureOrigin).key == expected);
|
||||
|
||||
auto const firstAccount =
|
||||
calcAccountID(randomKeyPair(KeyType::secp256k1).first);
|
||||
auto const secondAccount =
|
||||
calcAccountID(randomKeyPair(KeyType::secp256k1).first);
|
||||
auto const firstOrigin = makeOrigin("first-export-origin");
|
||||
auto const secondOrigin = makeOrigin("second-export-origin");
|
||||
|
||||
auto const key = keylet::exportLatch(firstAccount, firstOrigin);
|
||||
BEAST_EXPECT(key.type == ltEXPORT_LATCH);
|
||||
BEAST_EXPECT(
|
||||
key.key == keylet::exportLatch(firstAccount, firstOrigin).key);
|
||||
BEAST_EXPECT(
|
||||
key.key != keylet::exportLatch(firstAccount, secondOrigin).key);
|
||||
BEAST_EXPECT(
|
||||
key.key != keylet::exportLatch(secondAccount, firstOrigin).key);
|
||||
}
|
||||
|
||||
void
|
||||
testPendingDirectory()
|
||||
{
|
||||
testcase("pending Export directory");
|
||||
|
||||
uint256 const expected{
|
||||
"70D4A5FF7B4087C41414E71BD1FF2ABE"
|
||||
"5F49C380C158ACDCC76254279B176018"};
|
||||
auto const& pending = keylet::pendingExports();
|
||||
|
||||
BEAST_EXPECT(pending.type == ltDIR_NODE);
|
||||
BEAST_EXPECT(pending.key == expected);
|
||||
BEAST_EXPECT(&pending == &keylet::pendingExports());
|
||||
}
|
||||
|
||||
void
|
||||
testStructuralLimits()
|
||||
{
|
||||
testcase("structural Export limits");
|
||||
|
||||
BEAST_EXPECT(ExportLimits::maxCommitteeMembers == 32);
|
||||
BEAST_EXPECT(ExportLimits::maxCommitteeRosterBytes == 1056);
|
||||
BEAST_EXPECT(ExportLimits::maxCommitteeContributorBytes == 4);
|
||||
BEAST_EXPECT(
|
||||
ExportLimits::maxCommitteeMembers == STTx::maxMultiSigners());
|
||||
|
||||
std::array<std::size_t, 11> constexpr expected{
|
||||
0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 8};
|
||||
for (std::size_t members = 0; members < expected.size(); ++members)
|
||||
BEAST_EXPECT(
|
||||
ExportLimits::committeeQuorumThreshold(members) ==
|
||||
expected[members]);
|
||||
BEAST_EXPECT(ExportLimits::committeeQuorumThreshold(28) == 23);
|
||||
BEAST_EXPECT(ExportLimits::committeeQuorumThreshold(32) == 26);
|
||||
}
|
||||
|
||||
void
|
||||
testCommitteeProfile()
|
||||
{
|
||||
testcase("Export committee profile");
|
||||
|
||||
std::vector<PublicKey> masters;
|
||||
masters.reserve(28);
|
||||
for (std::size_t i = 0; i < 28; ++i)
|
||||
masters.push_back(randomKeyPair(KeyType::secp256k1).first);
|
||||
auto const roster = serializeExportCommittee(masters);
|
||||
|
||||
auto profile = resolveExportCommittee(makeSlice(roster));
|
||||
BEAST_EXPECT(profile.has_value());
|
||||
if (profile)
|
||||
{
|
||||
BEAST_EXPECT(profile->members.size() == 28);
|
||||
BEAST_EXPECT(profile->quorum == 23);
|
||||
BEAST_EXPECT(profile->position(profile->members.front()) == 0);
|
||||
BEAST_EXPECT(profile->position(profile->members.back()) == 27);
|
||||
}
|
||||
BEAST_EXPECT(!exportCommitteeHash(makeSlice(roster)).isZero());
|
||||
|
||||
Blob unordered;
|
||||
for (auto it = masters.rbegin(); it != masters.rend(); ++it)
|
||||
unordered.insert(unordered.end(), it->begin(), it->end());
|
||||
BEAST_EXPECT(!resolveExportCommittee(makeSlice(unordered)));
|
||||
auto const canonical =
|
||||
canonicalizeExportCommittee(makeSlice(unordered));
|
||||
BEAST_EXPECT(canonical.has_value());
|
||||
if (canonical)
|
||||
{
|
||||
BEAST_EXPECT(*canonical == roster);
|
||||
auto const unorderedProfile =
|
||||
resolveExportCommittee(makeSlice(*canonical));
|
||||
BEAST_EXPECT(unorderedProfile.has_value());
|
||||
if (unorderedProfile)
|
||||
BEAST_EXPECT(unorderedProfile->members == profile->members);
|
||||
BEAST_EXPECT(
|
||||
exportCommitteeHash(makeSlice(*canonical)) ==
|
||||
exportCommitteeHash(makeSlice(roster)));
|
||||
}
|
||||
|
||||
Serializer expectedDigest;
|
||||
expectedDigest.add32(HashPrefix::exportCommittee);
|
||||
expectedDigest.add32(static_cast<std::uint32_t>(masters.size()));
|
||||
expectedDigest.addRaw(makeSlice(roster));
|
||||
BEAST_EXPECT(
|
||||
exportCommitteeHash(makeSlice(roster)) ==
|
||||
expectedDigest.getSHA512Half());
|
||||
|
||||
BEAST_EXPECT(!resolveExportCommittee(Slice{}));
|
||||
|
||||
Blob malformed(34, 0);
|
||||
BEAST_EXPECT(!resolveExportCommittee(makeSlice(malformed)));
|
||||
|
||||
Blob duplicate;
|
||||
duplicate.insert(
|
||||
duplicate.end(), masters.front().begin(), masters.front().end());
|
||||
duplicate.insert(
|
||||
duplicate.end(), masters.front().begin(), masters.front().end());
|
||||
BEAST_EXPECT(!resolveExportCommittee(makeSlice(duplicate)));
|
||||
|
||||
std::vector<PublicKey> tooMany;
|
||||
for (std::size_t i = 0; i <= ExportLimits::maxCommitteeMembers; ++i)
|
||||
tooMany.push_back(randomKeyPair(KeyType::secp256k1).first);
|
||||
BEAST_EXPECT(serializeExportCommittee(std::move(tooMany)).empty());
|
||||
}
|
||||
|
||||
void
|
||||
testExportLatchFormat()
|
||||
{
|
||||
testcase("Export latch format");
|
||||
|
||||
BEAST_EXPECT(sfExportCount.fieldCode == field_code(STI_UINT16, 101));
|
||||
BEAST_EXPECT(sfExportNode.fieldCode == field_code(STI_UINT64, 29));
|
||||
BEAST_EXPECT(
|
||||
sfExportCommitteeHash.fieldCode == field_code(STI_UINT256, 39));
|
||||
BEAST_EXPECT(sfExportCommittee.fieldCode == field_code(STI_VL, 34));
|
||||
|
||||
AccountID const account{1};
|
||||
uint256 const origin{2};
|
||||
uint256 const intentDigest{3};
|
||||
uint256 const committeeDigest{4};
|
||||
uint256 const witnessHash{5};
|
||||
|
||||
SLE latch{keylet::exportLatch(account, origin)};
|
||||
latch.setAccountID(sfAccount, account);
|
||||
latch.setFieldU32(sfTicketSequence, 55'001);
|
||||
latch.setFieldH256(sfTransactionHash, origin);
|
||||
latch.setFieldH256(sfDigest, intentDigest);
|
||||
latch.setFieldU32(sfLedgerSequence, 4'123'200);
|
||||
latch.setFieldH256(sfExportCommitteeHash, committeeDigest);
|
||||
latch.setFieldU32(sfFlags, 1);
|
||||
latch.setFieldH256(sfExportSignatureHash, witnessHash);
|
||||
latch.setFieldU64(sfOwnerNode, 7);
|
||||
latch.setFieldU64(sfExportNode, 8);
|
||||
|
||||
auto const serialized = latch.getSerializer();
|
||||
|
||||
SerialIter sit{serialized.slice()};
|
||||
SLE const parsed{sit, latch.key()};
|
||||
BEAST_EXPECT(parsed.getType() == ltEXPORT_LATCH);
|
||||
BEAST_EXPECT(parsed.getAccountID(sfAccount) == account);
|
||||
BEAST_EXPECT(parsed.getFieldU32(sfTicketSequence) == 55'001);
|
||||
BEAST_EXPECT(parsed.getFieldH256(sfTransactionHash) == origin);
|
||||
BEAST_EXPECT(parsed.getFieldH256(sfDigest) == intentDigest);
|
||||
BEAST_EXPECT(parsed.getFieldU32(sfLedgerSequence) == 4'123'200);
|
||||
BEAST_EXPECT(
|
||||
parsed.getFieldH256(sfExportCommitteeHash) == committeeDigest);
|
||||
BEAST_EXPECT(parsed.getFieldU32(sfFlags) == 1);
|
||||
BEAST_EXPECT(parsed.getFieldH256(sfExportSignatureHash) == witnessHash);
|
||||
BEAST_EXPECT(parsed.getFieldU64(sfOwnerNode) == 7);
|
||||
BEAST_EXPECT(parsed.getFieldU64(sfExportNode) == 8);
|
||||
|
||||
SLE accountRoot{keylet::account(account)};
|
||||
accountRoot.setFieldU16(sfExportCount, 9);
|
||||
BEAST_EXPECT(accountRoot.getFieldU16(sfExportCount) == 9);
|
||||
|
||||
SLE pendingRoot{keylet::pendingExports()};
|
||||
pendingRoot.setFieldU16(sfExportCount, 10);
|
||||
BEAST_EXPECT(pendingRoot.getFieldU16(sfExportCount) == 10);
|
||||
|
||||
auto const master = randomKeyPair(KeyType::secp256k1).first;
|
||||
auto const roster = serializeExportCommittee({master});
|
||||
auto const digest = exportCommitteeHash(makeSlice(roster));
|
||||
auto const committeeKey = keylet::exportCommittee(account, digest);
|
||||
BEAST_EXPECT(committeeKey.type == ltEXPORT_COMMITTEE);
|
||||
|
||||
SLE committee{committeeKey};
|
||||
committee.setAccountID(sfAccount, account);
|
||||
committee.setFieldH256(sfExportCommitteeHash, digest);
|
||||
committee.setFieldVL(sfExportCommittee, roster);
|
||||
committee.setFieldU64(sfOwnerNode, 11);
|
||||
auto const committeeBytes = committee.getSerializer();
|
||||
SerialIter committeeIter{committeeBytes.slice()};
|
||||
SLE const parsedCommittee{committeeIter, committeeKey.key};
|
||||
BEAST_EXPECT(parsedCommittee.getType() == ltEXPORT_COMMITTEE);
|
||||
BEAST_EXPECT(parsedCommittee.getFieldVL(sfExportCommittee) == roster);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testOriginIdentity();
|
||||
testPendingDirectory();
|
||||
testStructuralLimits();
|
||||
testCommitteeProfile();
|
||||
testExportLatchFormat();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportKeylet, protocol, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,386 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/ExportOriginMemo.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple::test {
|
||||
namespace {
|
||||
|
||||
STTx
|
||||
makePayment(Blob userMemo = {})
|
||||
{
|
||||
STObject object{sfGeneric};
|
||||
object.setFieldU16(sfTransactionType, ttPAYMENT);
|
||||
object.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
object.setFieldU32(sfSequence, 0);
|
||||
object.setFieldU32(sfTicketSequence, 1);
|
||||
object.setFieldAmount(sfAmount, XRPAmount{1});
|
||||
object.setFieldAmount(sfFee, XRPAmount{10});
|
||||
object.setFieldVL(sfSigningPubKey, Blob{});
|
||||
object.setAccountID(sfAccount, AccountID{1});
|
||||
object.setAccountID(sfDestination, AccountID{2});
|
||||
|
||||
if (!userMemo.empty())
|
||||
{
|
||||
STArray memos{sfMemos};
|
||||
STObject memo{sfMemo};
|
||||
memo.setFieldVL(sfMemoType, Blob{'u', 's', 'e', 'r'});
|
||||
memo.setFieldVL(sfMemoData, userMemo);
|
||||
memos.emplace_back(std::move(memo));
|
||||
object.setFieldArray(sfMemos, memos);
|
||||
}
|
||||
return STTx{std::move(object)};
|
||||
}
|
||||
|
||||
STTx
|
||||
replaceReservedData(STTx const& tx, Blob const& data)
|
||||
{
|
||||
Serializer serializer;
|
||||
tx.add(serializer);
|
||||
SerialIter sit{serializer.slice()};
|
||||
STObject object{sfGeneric};
|
||||
object.set(sit);
|
||||
object.peekFieldArray(sfMemos).back().setFieldVL(sfMemoData, data);
|
||||
return STTx{std::move(object)};
|
||||
}
|
||||
|
||||
Blob
|
||||
reservedData(STTx const& tx)
|
||||
{
|
||||
return tx.getFieldArray(sfMemos).back().getFieldVL(sfMemoData);
|
||||
}
|
||||
|
||||
STObject
|
||||
cloneObject(STTx const& tx)
|
||||
{
|
||||
Serializer serializer;
|
||||
tx.add(serializer);
|
||||
SerialIter sit{serializer.slice()};
|
||||
STObject object{sfGeneric};
|
||||
object.set(sit);
|
||||
return object;
|
||||
}
|
||||
|
||||
STTx
|
||||
reparse(STTx const& tx)
|
||||
{
|
||||
return STTx{cloneObject(tx)};
|
||||
}
|
||||
|
||||
std::size_t
|
||||
memoBytes(STArray const& memos)
|
||||
{
|
||||
Serializer serializer;
|
||||
memos.add(serializer);
|
||||
return serializer.getDataLength();
|
||||
}
|
||||
|
||||
Blob
|
||||
serializeMemos(STArray const& memos)
|
||||
{
|
||||
Serializer serializer;
|
||||
memos.add(serializer);
|
||||
return serializer.getData();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
memoBytes(STTx const& tx)
|
||||
{
|
||||
return memoBytes(tx.getFieldArray(sfMemos));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ExportOriginMemo_test : public beast::unit_test::suite
|
||||
{
|
||||
ExportOriginMemo::Origin const origin_{21337, 0, uint256{2}};
|
||||
ExportOriginMemo::Anchor const anchor_{4123200, uint256{3}};
|
||||
|
||||
public:
|
||||
void
|
||||
testExactEncodingAndProjection()
|
||||
{
|
||||
testcase("exact encoding and projection");
|
||||
|
||||
auto const identity =
|
||||
ExportOriginMemo::identityForm(makePayment(), origin_);
|
||||
auto const release =
|
||||
ExportOriginMemo::releaseForm(makePayment(), origin_, anchor_);
|
||||
BEAST_EXPECT(identity.has_value());
|
||||
BEAST_EXPECT(release.has_value());
|
||||
if (!identity || !release)
|
||||
return;
|
||||
|
||||
auto const expectedIdentity = strUnHex(
|
||||
"0100000053590000000000000000000000000000000000000000000000000000"
|
||||
"00000000000000000002");
|
||||
auto const expectedRelease = strUnHex(
|
||||
"0100000053590000000000000000000000000000000000000000000000000000"
|
||||
"00000000000000000002003EEA40000000000000000000000000000000000000"
|
||||
"0000000000000000000000000003");
|
||||
BEAST_EXPECT(expectedIdentity);
|
||||
BEAST_EXPECT(expectedRelease);
|
||||
if (expectedIdentity && expectedRelease)
|
||||
{
|
||||
BEAST_EXPECT(reservedData(identity.value()) == *expectedIdentity);
|
||||
BEAST_EXPECTS(
|
||||
reservedData(release.value()) == *expectedRelease,
|
||||
strHex(reservedData(release.value())) +
|
||||
" != " + strHex(*expectedRelease));
|
||||
}
|
||||
|
||||
auto const parsedIdentity = ExportOriginMemo::parse(identity.value());
|
||||
auto const parsedRelease = ExportOriginMemo::parse(release.value());
|
||||
BEAST_EXPECT(parsedIdentity.has_value());
|
||||
BEAST_EXPECT(parsedRelease.has_value());
|
||||
if (parsedIdentity && parsedRelease)
|
||||
{
|
||||
BEAST_EXPECT(parsedIdentity.value().origin == origin_);
|
||||
BEAST_EXPECT(!parsedIdentity.value().anchor);
|
||||
BEAST_EXPECT(parsedRelease.value().origin == origin_);
|
||||
BEAST_EXPECT(parsedRelease.value().anchor == anchor_);
|
||||
}
|
||||
|
||||
auto const projected =
|
||||
ExportOriginMemo::projectIdentity(release.value());
|
||||
BEAST_EXPECT(projected.has_value());
|
||||
if (projected)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
projected.value().getSerializer().peekData() ==
|
||||
identity.value().getSerializer().peekData());
|
||||
BEAST_EXPECT(
|
||||
projected.value().getTransactionID() ==
|
||||
reparse(projected.value()).getTransactionID());
|
||||
|
||||
auto const projectedAgain =
|
||||
ExportOriginMemo::projectIdentity(projected.value());
|
||||
BEAST_EXPECT(projectedAgain.has_value());
|
||||
if (projectedAgain)
|
||||
BEAST_EXPECT(
|
||||
projectedAgain.value().getSerializer().peekData() ==
|
||||
projected.value().getSerializer().peekData());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPreservesUserMemos()
|
||||
{
|
||||
testcase("preserves user Memos");
|
||||
|
||||
Blob const userData{0xde, 0xad, 0xbe, 0xef};
|
||||
auto const base = makePayment(userData);
|
||||
auto const baseBytes = base.getSerializer().peekData();
|
||||
auto const release =
|
||||
ExportOriginMemo::releaseForm(base, origin_, anchor_);
|
||||
BEAST_EXPECT(release.has_value());
|
||||
BEAST_EXPECT(base.getSerializer().peekData() == baseBytes);
|
||||
if (!release)
|
||||
return;
|
||||
|
||||
auto const& memos = release.value().getFieldArray(sfMemos);
|
||||
BEAST_EXPECT(memos.size() == 2);
|
||||
BEAST_EXPECT(
|
||||
memos[0].getFieldVL(sfMemoType) == Blob({'u', 's', 'e', 'r'}));
|
||||
BEAST_EXPECT(memos[0].getFieldVL(sfMemoData) == userData);
|
||||
BEAST_EXPECT(
|
||||
memos[1].getFieldVL(sfMemoType) ==
|
||||
Blob(
|
||||
ExportOriginMemo::memoType.begin(),
|
||||
ExportOriginMemo::memoType.end()));
|
||||
|
||||
auto object = cloneObject(makePayment());
|
||||
STArray heterogeneous{sfMemos};
|
||||
STObject first{sfMemo};
|
||||
first.setFieldVL(sfMemoType, Blob{'a'});
|
||||
first.setFieldVL(sfMemoData, Blob{1});
|
||||
first.setFieldVL(sfMemoFormat, Blob{'t', 'e', 'x', 't'});
|
||||
heterogeneous.emplace_back(std::move(first));
|
||||
STObject second{sfMemo};
|
||||
second.setFieldVL(sfMemoData, Blob{2, 3});
|
||||
heterogeneous.emplace_back(std::move(second));
|
||||
object.setFieldArray(sfMemos, heterogeneous);
|
||||
auto const heterogeneousBase = STTx{std::move(object)};
|
||||
auto const heterogeneousBytes =
|
||||
serializeMemos(heterogeneousBase.getFieldArray(sfMemos));
|
||||
auto const heterogeneousRelease =
|
||||
ExportOriginMemo::releaseForm(heterogeneousBase, origin_, anchor_);
|
||||
BEAST_EXPECT(heterogeneousRelease.has_value());
|
||||
if (heterogeneousRelease)
|
||||
{
|
||||
auto preserved =
|
||||
heterogeneousRelease.value().getFieldArray(sfMemos);
|
||||
preserved.erase(std::prev(preserved.end()));
|
||||
BEAST_EXPECT(serializeMemos(preserved) == heterogeneousBytes);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsNonCanonicalReservedMemo()
|
||||
{
|
||||
testcase("rejects non-canonical reserved Memo");
|
||||
|
||||
auto const identity =
|
||||
ExportOriginMemo::identityForm(makePayment(), origin_);
|
||||
BEAST_EXPECT(identity.has_value());
|
||||
if (!identity)
|
||||
return;
|
||||
|
||||
auto const duplicate =
|
||||
ExportOriginMemo::identityForm(identity.value(), origin_);
|
||||
BEAST_EXPECT(!duplicate);
|
||||
if (!duplicate)
|
||||
BEAST_EXPECT(
|
||||
duplicate.error() ==
|
||||
ExportOriginMemo::Error::reservedMemoPresent);
|
||||
|
||||
{
|
||||
auto object = cloneObject(identity.value());
|
||||
auto memos = object.getFieldArray(sfMemos);
|
||||
memos.emplace_back(memos.back());
|
||||
object.setFieldArray(sfMemos, memos);
|
||||
auto const parsedDuplicate =
|
||||
ExportOriginMemo::parse(STTx{std::move(object)});
|
||||
BEAST_EXPECT(
|
||||
!parsedDuplicate &&
|
||||
parsedDuplicate.error() ==
|
||||
ExportOriginMemo::Error::reservedMemoPosition);
|
||||
}
|
||||
|
||||
{
|
||||
auto const withUser =
|
||||
ExportOriginMemo::identityForm(makePayment(Blob{1}), origin_);
|
||||
BEAST_EXPECT(withUser.has_value());
|
||||
if (withUser)
|
||||
{
|
||||
auto object = cloneObject(withUser.value());
|
||||
auto memos = object.getFieldArray(sfMemos);
|
||||
std::swap(memos[0], memos.back());
|
||||
object.setFieldArray(sfMemos, memos);
|
||||
auto const parsedMisplaced =
|
||||
ExportOriginMemo::parse(STTx{std::move(object)});
|
||||
BEAST_EXPECT(
|
||||
!parsedMisplaced &&
|
||||
parsedMisplaced.error() ==
|
||||
ExportOriginMemo::Error::reservedMemoPosition);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto object = cloneObject(identity.value());
|
||||
object.peekFieldArray(sfMemos).back().setFieldVL(
|
||||
sfMemoFormat, Blob{'b', 'i', 'n'});
|
||||
auto const parsedFormat =
|
||||
ExportOriginMemo::parse(STTx{std::move(object)});
|
||||
BEAST_EXPECT(
|
||||
!parsedFormat &&
|
||||
parsedFormat.error() == ExportOriginMemo::Error::malformedMemo);
|
||||
}
|
||||
|
||||
auto malformed = reservedData(identity.value());
|
||||
malformed[0] = 2;
|
||||
auto parsed = ExportOriginMemo::parse(
|
||||
replaceReservedData(identity.value(), malformed));
|
||||
BEAST_EXPECT(
|
||||
!parsed &&
|
||||
parsed.error() == ExportOriginMemo::Error::unsupportedVersion);
|
||||
|
||||
malformed[0] = ExportOriginMemo::version;
|
||||
malformed[1] = 1;
|
||||
parsed = ExportOriginMemo::parse(
|
||||
replaceReservedData(identity.value(), malformed));
|
||||
BEAST_EXPECT(
|
||||
!parsed &&
|
||||
parsed.error() == ExportOriginMemo::Error::unsupportedFlags);
|
||||
|
||||
malformed.pop_back();
|
||||
parsed = ExportOriginMemo::parse(
|
||||
replaceReservedData(identity.value(), malformed));
|
||||
BEAST_EXPECT(
|
||||
!parsed &&
|
||||
parsed.error() == ExportOriginMemo::Error::malformedMemo);
|
||||
}
|
||||
|
||||
void
|
||||
testProjectedMemoSize()
|
||||
{
|
||||
testcase("projected Memo size");
|
||||
|
||||
auto const base = makePayment(Blob(930, 0xa5));
|
||||
std::string reason;
|
||||
BEAST_EXPECT(passesLocalChecks(base, reason));
|
||||
|
||||
auto const identity = ExportOriginMemo::identityForm(base, origin_);
|
||||
auto const release =
|
||||
ExportOriginMemo::releaseForm(base, origin_, anchor_);
|
||||
BEAST_EXPECT(identity.has_value());
|
||||
BEAST_EXPECT(
|
||||
!release &&
|
||||
release.error() == ExportOriginMemo::Error::localChecks);
|
||||
|
||||
auto testExactBoundary = [&](auto project) {
|
||||
std::optional<std::size_t> lastAcceptedBytes;
|
||||
std::size_t firstRejected = 0;
|
||||
for (std::size_t size = 1; size <= 1024; ++size)
|
||||
{
|
||||
auto candidate = project(makePayment(Blob(size, 0xa5)));
|
||||
if (candidate)
|
||||
{
|
||||
lastAcceptedBytes = memoBytes(candidate.value());
|
||||
continue;
|
||||
}
|
||||
firstRejected = size;
|
||||
BEAST_EXPECT(
|
||||
candidate.error() == ExportOriginMemo::Error::localChecks);
|
||||
break;
|
||||
}
|
||||
|
||||
BEAST_EXPECT(lastAcceptedBytes.has_value());
|
||||
BEAST_EXPECT(firstRejected != 0);
|
||||
if (lastAcceptedBytes)
|
||||
BEAST_EXPECT(*lastAcceptedBytes == 1024);
|
||||
};
|
||||
|
||||
testExactBoundary([&](STTx const& tx) {
|
||||
return ExportOriginMemo::identityForm(tx, origin_);
|
||||
});
|
||||
testExactBoundary([&](STTx const& tx) {
|
||||
return ExportOriginMemo::releaseForm(tx, origin_, anchor_);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testExactEncodingAndProjection();
|
||||
testPreservesUserMemos();
|
||||
testRejectsNonCanonicalReservedMemo();
|
||||
testProjectedMemoSize();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportOriginMemo, protocol, ripple);
|
||||
|
||||
} // namespace ripple::test
|
||||
@@ -0,0 +1,123 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 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/basics/StringUtilities.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/ExportShare.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class ExportShare_test : public beast::unit_test::suite
|
||||
{
|
||||
static ExportShare
|
||||
makeShare()
|
||||
{
|
||||
auto const [key, secret] = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signature = sign(key, secret, Slice{"export-share", 12});
|
||||
return ExportShare{
|
||||
ExportShare::currentVersion,
|
||||
calcAccountID(key),
|
||||
uint256{1},
|
||||
4'200'000,
|
||||
uint256{2},
|
||||
17,
|
||||
key,
|
||||
signature};
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testRoundTrip()
|
||||
{
|
||||
testcase("canonical Export share round trip");
|
||||
|
||||
auto const share = makeShare();
|
||||
auto const encoded = share.serialize();
|
||||
BEAST_EXPECT(
|
||||
encoded.size() <= ExportLimits::maxSerializedExportShareBytes);
|
||||
|
||||
auto const parsed = ExportShare::parse(encoded.slice());
|
||||
BEAST_EXPECT(parsed.has_value());
|
||||
if (!parsed)
|
||||
return;
|
||||
BEAST_EXPECT(parsed->version == share.version);
|
||||
BEAST_EXPECT(parsed->owner == share.owner);
|
||||
BEAST_EXPECT(parsed->originTxn == share.originTxn);
|
||||
BEAST_EXPECT(parsed->originLedgerSeq == share.originLedgerSeq);
|
||||
BEAST_EXPECT(parsed->originLedgerHash == share.originLedgerHash);
|
||||
BEAST_EXPECT(parsed->committeePosition == share.committeePosition);
|
||||
BEAST_EXPECT(parsed->signingKey == share.signingKey);
|
||||
BEAST_EXPECT(parsed->signature == share.signature);
|
||||
BEAST_EXPECT(parsed->wireHash() == share.wireHash());
|
||||
BEAST_EXPECT(parsed->serialize().slice() == encoded.slice());
|
||||
}
|
||||
|
||||
void
|
||||
testMalformed()
|
||||
{
|
||||
testcase("malformed Export share framing");
|
||||
|
||||
auto share = makeShare();
|
||||
auto encoded = share.serialize().peekData();
|
||||
|
||||
BEAST_EXPECT(!ExportShare::parse(Slice{}));
|
||||
BEAST_EXPECT(!ExportShare::parse(Slice{encoded.data(), 20}));
|
||||
|
||||
encoded.push_back(0);
|
||||
BEAST_EXPECT(!ExportShare::parse(makeSlice(encoded)));
|
||||
|
||||
share.version = ExportShare::currentVersion + 1;
|
||||
BEAST_EXPECT(!share.validShape());
|
||||
bool threw = false;
|
||||
try
|
||||
{
|
||||
(void)share.serialize();
|
||||
}
|
||||
catch (std::invalid_argument const&)
|
||||
{
|
||||
threw = true;
|
||||
}
|
||||
BEAST_EXPECT(threw);
|
||||
|
||||
share = makeShare();
|
||||
share.committeePosition = ExportLimits::maxCommitteeMembers;
|
||||
BEAST_EXPECT(!share.validShape());
|
||||
|
||||
share = makeShare();
|
||||
share.signature = Buffer{73};
|
||||
BEAST_EXPECT(!share.validShape());
|
||||
|
||||
share = makeShare();
|
||||
share.signature = Buffer{4};
|
||||
BEAST_EXPECT(!share.validShape());
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testRoundTrip();
|
||||
testMalformed();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportShare, protocol, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user