z4g_on_recv_packet_after_client_recovery_filetest.gno
12.26 Kb · 273 lines
1// PKGPATH: gno.land/r/aib/main
2package main
3
4import (
5 "chain"
6 "chain/banker"
7 "testing"
8 "time"
9
10 "gno.land/p/aib/ibc/host"
11 "gno.land/p/aib/ibc/lightclient/tendermint"
12 tmtesting "gno.land/p/aib/ibc/lightclient/tendermint/testing"
13 "gno.land/p/aib/ibc/types"
14 "gno.land/p/aib/ics23"
15 "gno.land/p/nt/testutils/v0"
16 "gno.land/r/aib/ibc/apps/transfer"
17 "gno.land/r/aib/ibc/core"
18)
19
20// OnRecvPacket: escrow stays reachable after the escrowing client is repaired.
21//
22// Escrow is accounted per (client, denom), so escrow is only ever releasable
23// through the client ID it was locked under. That makes the escrow reachable
24// only for as long as that client ID survives, which this test pins down: a
25// client that breaks must be repaired in place by core.RecoverClient, never
26// replaced by a fresh client ID, or every balance escrowed under the old ID
27// becomes unreleasable.
28//
29// Flow: lock 100ugnot under clientA -> break clientA -> recover it from a
30// healthy substitute -> receive a returning-token packet on clientA and see
31// the coins actually paid out.
32//
33// The three properties that must hold across recovery, none of which
34// RecoverClient touches (it replaces only the light-client state):
35//
36// 1. The client ID is preserved, so escrow[clientA] is still the bucket the
37// returning packet debits.
38// 2. The registered counterparty survives, so the packet's SourceClient still
39// matches and no re-registration is needed.
40// 3. The client is Active again, so core.RecvPacket's status gate passes.
41//
42// clientA is broken by misbehaviour (Frozen) rather than by expiry, because a
43// filetest clock cannot be advanced past the trusting period. Both statuses
44// take the identical recovery path -- core.RecoverClient accepts Frozen or
45// Expired and rejects everything else -- so the property under test is the same.
46func main(cur realm) {
47 const admin = "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
48 core.SetAdmin(cross(cur), admin)
49
50 var (
51 // clientA locks the escrow, breaks, and is recovered in place.
52 clientAID = "07-tendermint-1"
53 // The healthy substitute recovery copies its state from.
54 substituteID = "07-tendermint-2"
55 // clientA's counterparty, registered once and expected to survive.
56 cpartyA = "07-tendermint-42"
57
58 prefix = [][]byte{[]byte("iavlStoreKey"), []byte("prefix2")}
59 // The substitute's height; clientA adopts it during recovery, so it is
60 // also the height the returning packet's proof is verified at.
61 proofHeight = types.NewHeight(1, 100)
62
63 // Preamble reused verbatim from core's z9aaa recovery filetest: the
64 // misbehaviour signatures below are over these exact chainID, heights,
65 // valset and timestamps, so none of it can be changed independently.
66 chainID = "atomone-1"
67 val1 = tendermint.NewValidator("9DIBYr64rywKO3Kk6+743xDHcEU=", "VMpO0RzmZu4OMVvsniYdeO/2eP+h1xeoAcHBTZC7Gfw=", 1)
68 val2 = tendermint.NewValidator("y+naL3ubs9q1bXrY9+uRxY9c+J8=", "NeGV1CqewGUTpH5SxVRfkgYOMCVX84MyCslqtw9Ouvo=", 1)
69 trustedValset = tendermint.NewValset(val1, val2)
70 clientAState = tmtesting.NewClientState(chainID, types.NewHeight(1, 2))
71 consA = tmtesting.GenConsensusState(time.Now(), tmtesting.Hash("apphash-2"), trustedValset.Hash())
72 )
73
74 if got := core.CreateClient(cross(cur), clientAState, consA); got != clientAID {
75 panic("clientA id race: " + got)
76 }
77 core.RegisterCounterparty(cross(cur), clientAID, prefix, cpartyA)
78
79 // --- Lock 100ugnot under clientA through a real Transfer. ---
80 transferAppAddr := chain.PackageAddress("gno.land/r/aib/ibc/apps/transfer")
81 testing.IssueCoins(transferAppAddr, chain.NewCoins(chain.NewCoin("ugnot", 100)))
82 sender := testutils.TestAddress("sender")
83 testing.SetRealm(testing.NewUserRealm(sender))
84 testing.SetOriginSend(chain.NewCoins(chain.NewCoin("ugnot", 100)))
85 transfer.Transfer(cross(cur), clientAID, "atone1user", "ugnot", 100,
86 uint64(time.Now().Add(time.Hour).Unix()), "")
87 testing.SetOriginSend(nil)
88 // Restore the admin EOA: RecoverClient is gated on unsafe.OriginCaller().
89 testing.SetRealm(testing.NewUserRealm(address(admin)))
90
91 println("----------- escrow locked under clientA, clientA Active")
92 println(transfer.Render("total_escrow/ugnot"))
93 println(core.Render("clients/" + clientAID + "/status"))
94
95 // --- Break clientA: two conflicting headers at the same trusted height. ---
96 var header1, header2 *tendermint.MsgHeader
97 {
98 var (
99 apphash = tmtesting.Hash("apphash-4")
100 val1 = tendermint.NewValidator("9DIBYr64rywKO3Kk6+743xDHcEU=", "VMpO0RzmZu4OMVvsniYdeO/2eP+h1xeoAcHBTZC7Gfw=", 10)
101 val2 = tendermint.NewValidator("y+naL3ubs9q1bXrY9+uRxY9c+J8=", "NeGV1CqewGUTpH5SxVRfkgYOMCVX84MyCslqtw9Ouvo=", 10)
102 valset = tendermint.NewValset(val1, val2)
103 commitTimestamp = tmtesting.ToTime("2025-09-25T07:55:57.306746166Z")
104 newHeight = uint64(4)
105 newTimestamp = consA.Timestamp.Add(time.Minute * time.Duration(0))
106 nextValset = tendermint.NewValset(val1, val2)
107 trustedHeight = clientAState.LatestHeight
108
109 signatures = []tendermint.CommitSig{
110 {
111 BlockIDFlag: tendermint.BlockIDFlagCommit,
112 ValidatorAddress: valset.Validators[0].Address,
113 Timestamp: commitTimestamp,
114 Signature: []byte("\x2e\xba\x21\xb0\x2f\xd2\x85\xb9\xef\x82\x68\xdc\xef\xd1\xd1\x12\x70\x88\x94\x10\x7e\x4d\x49\xac\x46\x3d\x86\xe2\xf2\xae\x38\xb4\xa5\xab\x0c\xc1\x8f\x8a\x59\xda\x36\x17\x01\xe4\x16\x49\xbf\x03\x86\xf0\x31\x3f\x30\x37\x9e\x47\x28\x72\x3b\x0c\x89\xb9\x94\x05"),
115 },
116 {
117 BlockIDFlag: tendermint.BlockIDFlagCommit,
118 ValidatorAddress: valset.Validators[1].Address,
119 Timestamp: commitTimestamp,
120 Signature: []byte("\x67\xcc\xc6\xc4\x20\xa0\xcb\x36\xaf\x33\x5c\xf8\xe4\xad\x47\x49\x38\x9d\x9e\xa3\x7c\xe6\x88\x81\x62\x51\x7f\xcc\xa5\x92\x0d\x98\xe9\xbe\x71\x2d\x22\xcd\x41\x30\x28\x83\x03\xcb\xf5\xd7\x28\xde\x7b\x92\x85\x9e\xa6\xe5\xde\x50\x89\x2d\x2b\xc5\x9d\x9a\x33\x00"),
121 },
122 }
123 )
124 header1 = tmtesting.NewMsgHeader(chainID, newTimestamp, apphash, newHeight,
125 trustedHeight, valset, nextValset, trustedValset, signatures)
126 }
127 {
128 var (
129 apphash = tmtesting.Hash("apphash-3")
130 val1 = tendermint.NewValidator("9DIBYr64rywKO3Kk6+743xDHcEU=", "VMpO0RzmZu4OMVvsniYdeO/2eP+h1xeoAcHBTZC7Gfw=", 10)
131 val2 = tendermint.NewValidator("y+naL3ubs9q1bXrY9+uRxY9c+J8=", "NeGV1CqewGUTpH5SxVRfkgYOMCVX84MyCslqtw9Ouvo=", 10)
132 valset = tendermint.NewValset(val1, val2)
133 commitTimestamp = tmtesting.ToTime("2025-09-25T07:55:57.306746166Z")
134 newHeight = uint64(3)
135 newTimestamp = consA.Timestamp.Add(time.Minute * time.Duration(0))
136 nextValset = tendermint.NewValset(val1, val2)
137 trustedHeight = clientAState.LatestHeight
138
139 signatures = []tendermint.CommitSig{
140 {
141 BlockIDFlag: tendermint.BlockIDFlagCommit,
142 ValidatorAddress: valset.Validators[0].Address,
143 Timestamp: commitTimestamp,
144 Signature: []byte("\x51\xa4\x8f\x78\x42\x4e\x6e\x0d\xc3\x2b\xe6\xcb\x09\x5c\xe5\x7d\x35\x84\xcf\xb1\x0f\x53\x72\x0c\x41\xde\xd3\x6d\xbc\x81\x28\x64\x41\xf7\xe9\x1e\xa9\x93\xd0\xa4\x84\x16\xca\xa8\xa3\x8e\x56\x45\xec\xae\x1d\x24\xb4\xa8\xf4\x29\x85\x04\x06\xe4\x18\x6b\x7d\x08"),
145 },
146 {
147 BlockIDFlag: tendermint.BlockIDFlagCommit,
148 ValidatorAddress: valset.Validators[1].Address,
149 Timestamp: commitTimestamp,
150 Signature: []byte("\xd9\xd6\x82\x83\x78\x75\xfa\x2e\xd7\x1b\xa5\x3d\xfe\xb3\x80\xb6\xc7\x59\x20\x45\x10\xdf\x0c\x19\xd1\xcc\xfc\x37\xd5\x59\x79\x95\x93\x60\x08\xfb\x5f\xa0\x87\x95\x51\x1e\x29\xed\x94\xd1\x31\x5e\xda\x90\x86\x11\xbe\xb2\x88\x68\xe1\xba\xa7\x31\xc7\xa2\xda\x07"),
151 },
152 }
153 )
154 header2 = tmtesting.NewMsgHeader(chainID, newTimestamp, apphash, newHeight,
155 trustedHeight, valset, nextValset, trustedValset, signatures)
156 }
157 core.UpdateClient(cross(cur), clientAID, &tendermint.Misbehaviour{Header1: header1, Header2: header2})
158
159 println("----------- clientA broken; escrow is now unreleasable")
160 println(core.Render("clients/" + clientAID + "/status"))
161
162 // --- The returning-token packet that should release the escrow. ---
163 // Its denom traces through clientA's counterparty, so OnRecvPacket takes
164 // the unescrow branch rather than minting a voucher.
165 receiver := testutils.TestAddress("receiver")
166 denom := "transfer/" + cpartyA + "/ugnot"
167 payload := transfer.NewFungibleTokenPacketData(denom, "100", "atone1user", receiver.String(), "")
168 packet := types.Packet{
169 Sequence: 1,
170 SourceClient: cpartyA,
171 DestinationClient: clientAID,
172 TimeoutTimestamp: uint64(time.Now().Add(time.Hour).Unix()),
173 Payloads: []types.Payload{{
174 SourcePort: transfer.PortID,
175 DestinationPort: transfer.PortID,
176 Encoding: transfer.EncodingProtobuf,
177 Value: payload.ProtoMarshal(),
178 Version: transfer.V1,
179 }},
180 }
181
182 // Build the packet-commitment proof, then derive the root it commits to.
183 // The substitute is created carrying that root, so after recovery clientA
184 // verifies this proof against the state it inherited.
185 mpath := types.BuildMerklePath(prefix, host.PacketCommitmentKey(cpartyA, 1))
186 iavl, tmspec := ics23.IavlSpec(), ics23.TendermintSpec()
187 e0 := &ics23.ExistenceProof{
188 Key: mpath.KeyPath[1],
189 Value: types.CommitPacket(packet),
190 Leaf: &ics23.LeafOp{
191 Hash: iavl.LeafSpec.Hash, PrehashKey: iavl.LeafSpec.PrehashKey,
192 PrehashValue: iavl.LeafSpec.PrehashValue, Length: iavl.LeafSpec.Length,
193 Prefix: []byte("\x00\x02\x02"),
194 },
195 }
196 sr0, err := e0.Calculate()
197 if err != nil {
198 panic(err)
199 }
200 e1 := &ics23.ExistenceProof{
201 Key: mpath.KeyPath[0],
202 Value: sr0,
203 Leaf: &ics23.LeafOp{
204 Hash: tmspec.LeafSpec.Hash, PrehashKey: tmspec.LeafSpec.PrehashKey,
205 PrehashValue: tmspec.LeafSpec.PrehashValue, Length: tmspec.LeafSpec.Length,
206 Prefix: []byte("\x00"),
207 },
208 }
209 root, err := e1.Calculate()
210 if err != nil {
211 panic(err)
212 }
213 proof := []ics23.CommitmentProof{
214 ics23.CommitmentProof_Exist{Exist: e0},
215 ics23.CommitmentProof_Exist{Exist: e1},
216 }
217
218 // --- The healthy substitute. Same params as clientA (recovery rejects a
219 // substitute differing on TrustLevel/UnbondingPeriod/MaxClockDrift/
220 // ProofSpecs/UpgradePath), at a higher height, carrying the root above. ---
221 substituteConsState := tendermint.ConsensusState{
222 Timestamp: time.Now(),
223 Root: tendermint.NewMerkleRoot(root),
224 NextValidatorsHash: trustedValset.Hash(),
225 }
226 if got := core.CreateClient(cross(cur), tmtesting.NewClientState(chainID, proofHeight), substituteConsState); got != substituteID {
227 panic("substitute id race: " + got)
228 }
229
230 // --- Repair clientA in place. ---
231 core.RecoverClient(cross(cur), clientAID, substituteID)
232
233 println("----------- clientA recovered in place, same ID")
234 println(core.Render("clients/" + clientAID + "/status"))
235
236 // --- The returning packet now releases the escrow it was locked under. ---
237 // Note this call proves all three properties at once: it is addressed to
238 // clientAID (ID preserved), its SourceClient matches the counterparty
239 // registered before the break (registration survived), and it clears
240 // core.RecvPacket's Active gate (client revived).
241 res := core.RecvPacket(cross(cur), types.MsgRecvPacket{
242 Packet: packet,
243 ProofCommitment: proof,
244 ProofHeight: proofHeight,
245 })
246
247 println("\nrecv result (2 == SUCCESS):", int(res))
248 println("----------- escrow drawn down to 0")
249 println(transfer.Render("total_escrow/ugnot"))
250 // The decisive assertion: the coins physically moved. A regression that
251 // left the accounting right but paid nothing out would pass on the render
252 // alone.
253 println("----------- receiver balance (100ugnot released)")
254 println(banker.NewReadonlyBanker().GetCoins(receiver).String())
255 println("----------- transfer realm balance (escrow emptied)")
256 println(banker.NewReadonlyBanker().GetCoins(transferAppAddr).String())
257}
258
259// Output:
260// ----------- escrow locked under clientA, clientA Active
261// {"denom":"ugnot","amount":100,"clients":[{"client":"07-tendermint-1","amount":100}]}
262// {"status":"Active"}
263// ----------- clientA broken; escrow is now unreleasable
264// {"status":"Frozen"}
265// ----------- clientA recovered in place, same ID
266// {"status":"Active"}
267//
268// recv result (2 == SUCCESS): 2
269// ----------- escrow drawn down to 0
270// {"denom":"ugnot","amount":0,"clients":[{"client":"07-tendermint-1","amount":0}]}
271// ----------- receiver balance (100ugnot released)
272// 100ugnot
273// ----------- transfer realm balance (escrow emptied)