z10b_async_ack_filetest.gno
16.14 Kb · 404 lines
1// PKGPATH: gno.land/r/aib/main
2package main
3
4
5import (
6 "encoding/hex"
7 "testing"
8 "time"
9
10 "gno.land/p/aib/ibc/lightclient/tendermint"
11 tmtesting "gno.land/p/aib/ibc/lightclient/tendermint/testing"
12 "gno.land/p/aib/ibc/types"
13 "gno.land/p/aib/ics23"
14 appstesting "gno.land/r/aib/ibc/apps/testing"
15 "gno.land/r/aib/ibc/core"
16)
17
18// forwardingApp wraps the base mock and writes the deferred parent ack from
19// inside OnTimeoutPacket. Defined here (rather than in apps/testing) so that
20// apps/testing does not need to import core, which would create an import
21// cycle with this directory's filetests during gnodev deploy. Pending state
22// is held in package-level vars because the receiver is read-only when
23// invoked across realms via the IBCApp interface.
24type forwardingApp struct {
25 *appstesting.App
26}
27
28var (
29 pendingClientID string
30 pendingSequence uint64
31 asyncDeferredAck = types.Acknowledgement{
32 AppAcknowledgements: [][]byte{{0x02}},
33 }
34)
35
36func (f *forwardingApp) OnRecvPacket(
37 cur realm,
38 sourceClient string,
39 destinationClient string,
40 sequence uint64,
41 payload types.Payload,
42) types.RecvPacketResult {
43 res := f.App.OnRecvPacket(cross(cur), sourceClient, destinationClient, sequence, payload)
44 if res.Status == types.PacketStatus_Async {
45 pendingClientID = destinationClient
46 pendingSequence = sequence
47 }
48 return res
49}
50
51func (f *forwardingApp) OnTimeoutPacket(
52 cur realm,
53 sourceClient string,
54 destinationClient string,
55 sequence uint64,
56 payload types.Payload,
57) error {
58 err := f.App.OnTimeoutPacket(cross(cur), sourceClient, destinationClient, sequence, payload)
59 if pendingClientID != "" {
60 clientID, seq := pendingClientID, pendingSequence
61 pendingClientID, pendingSequence = "", 0
62 core.WriteAcknowledgement(cross(cur), clientID, seq, asyncDeferredAck)
63 }
64 return err
65}
66
67// Async ack via OnTimeoutPacket. Mirrors the PFM/ZKGM forwarded-ack timeout
68// path on the middle chain B of A -> B -> C:
69//
70// - parent packet arrives from chain A, app returns Async (deferred)
71// - child packet is sent to chain C
72// - chain C never receives the child; timeout elapses
73// - chain B times out the child; that triggers the mock's
74// OnTimeoutPacket, which calls core.WriteAcknowledgement to finally
75// ack the parent (with the deferred async ack)
76//
77// On us (chain B) two clients are needed, one per remote chain:
78//
79// - clientToA: local id 07-tendermint-1, chain A's id for us is 07-tendermint-42
80// - clientToC: local id 07-tendermint-2, chain C's id for us is 07-tendermint-99
81//
82// clientToC mirrors z6a_timeout_filetest's validator set / chain id so the
83// gen-block-signatures output for atomone-1 height=12 can be reused to
84// advance the client past the child's timeout.
85func main(cur realm) {
86 var (
87 trustedHeight = types.NewHeight(2, 2)
88 trustedValset = tmtesting.GenValset()
89 chainAClientIDForUs = "07-tendermint-42"
90 chainCClientIDForUs = "07-tendermint-99"
91 )
92
93 // clientToA: tracks chain A. apphash commits to the parent packet
94 // commitment at prefix2/07-tendermint-42/packet/seq=1 on chain A.
95 apphashA, _ := hex.DecodeString("8da82bb625ee946f4b7aa03a82010cb115f9ce3879c7caad8d92e71eb821996e")
96 clientToA := core.CreateClient(cross(cur),
97 tmtesting.NewClientState("chainA-2", trustedHeight),
98 tmtesting.GenConsensusState(time.Now(), apphashA, trustedValset.Hash()),
99 )
100 core.RegisterCounterparty(cross(cur), clientToA, [][]byte{[]byte("iavlStoreKey"), []byte("prefix2")}, chainAClientIDForUs)
101
102 // clientToC: tracks chain C with a deterministic validator set so the
103 // timeout flow can advance the client via a generated block signature.
104 // priv=8a6cAbQSpDbebmcTEhCMPhhr/SkL/2pizo60yzHRkN9Uyk7RHOZm7g4xW+yeJh147/Z4/6HXF6gBwcFNkLsZ/A==
105 val1 := tendermint.NewValidator("9DIBYr64rywKO3Kk6+743xDHcEU=",
106 "VMpO0RzmZu4OMVvsniYdeO/2eP+h1xeoAcHBTZC7Gfw=", 1)
107 // priv=nWg6ETc62tyxd94lh8fFaQnZKaAW6vlS0L/4lfseJuI14ZXUKp7AZROkflLFVF+SBg4wJVfzgzIKyWq3D066+g==
108 val2 := tendermint.NewValidator("y+naL3ubs9q1bXrY9+uRxY9c+J8=",
109 "NeGV1CqewGUTpH5SxVRfkgYOMCVX84MyCslqtw9Ouvo=", 1)
110 chainCValset := tendermint.NewValset(val1, val2)
111 chainCInitialHeight := types.NewHeight(1, 2)
112 chainCInitialApphash := tmtesting.Hash("apphash-1")
113 chainCInitialConsensus := tmtesting.GenConsensusState(time.Now(), chainCInitialApphash, chainCValset.Hash())
114 clientToC := core.CreateClient(cross(cur),
115 tmtesting.NewClientState("atomone-1", chainCInitialHeight),
116 chainCInitialConsensus,
117 )
118 core.RegisterCounterparty(cross(cur), clientToC, [][]byte{[]byte("iavlStoreKey"), []byte("prefix3")}, chainCClientIDForUs)
119
120 // Register the forwarding mock from this realm, so the pkgPath stored
121 // in core is gno.land/r/aib/main — the same realm that later calls
122 // core.WriteAcknowledgement from inside OnTimeoutPacket.
123 var (
124 base = appstesting.NewApp(cross(cur))
125 app = &forwardingApp{App: base}
126 appPortID = "appID"
127 )
128 app.SetOnRecvPacketReturn(types.RecvPacketResult{Status: types.PacketStatus_Async})
129 core.RegisterApp(cross(cur), appPortID, app)
130
131 // Send the child packet to chain C. Its eventual timeout will trigger
132 // the mock's OnTimeoutPacket and the deferred parent ack write.
133 sendPacket := types.MsgSendPacket{
134 SourceClient: clientToC,
135 TimeoutTimestamp: uint64(time.Now().Add(10 * time.Minute).Unix()),
136 Payloads: []types.Payload{{
137 SourcePort: appPortID,
138 DestinationPort: appPortID,
139 Encoding: "application/json",
140 Value: []byte("{}"),
141 Version: "v1",
142 }},
143 }
144 childSeq := core.SendPacket(cross(cur), sendPacket)
145
146 // Receive the parent packet from chain A. The mock returns Async, so
147 // core stores a pending entry and does not yet write the ack.
148 specs := ics23.IavlSpec()
149 // NOTE proof generated by:
150 // go run -C ./cmd/gen-proof . 'prefix2' '07-tendermint-42' 'packet' '{"sequence":1,"source_client":"07-tendermint-42","destination_client":"07-tendermint-1","timeout_timestamp":1234571490,"payloads":[{"source_port":"appID","destination_port":"appID","encoding":"application/json","value":"e30=","version":"v1"}]}'
151 parentRecvProof := []ics23.CommitmentProof{
152 // iavl proof
153 ics23.CommitmentProof_Exist{
154 Exist: &ics23.ExistenceProof{
155 Key: []byte("\x70\x72\x65\x66\x69\x78\x32\x30\x37\x2d\x74\x65\x6e\x64\x65\x72\x6d\x69\x6e\x74\x2d\x34\x32\x01\x00\x00\x00\x00\x00\x00\x00\x01"),
156 Value: []byte("\x23\x99\xf6\x84\x16\xb7\xd0\x09\x3b\xe4\x9e\x5f\x8e\xe1\xbe\x1c\x8e\x07\xa0\x93\xc5\x67\x09\x03\x46\xce\x36\xc9\x7d\x11\x71\x71"),
157 Leaf: &ics23.LeafOp{
158 Hash: specs.LeafSpec.Hash,
159 PrehashKey: specs.LeafSpec.PrehashKey,
160 PrehashValue: specs.LeafSpec.PrehashValue,
161 Length: specs.LeafSpec.Length,
162 Prefix: []byte("\x00\x02\x02"),
163 },
164 Path: []*ics23.InnerOp{
165 {
166 Hash: specs.InnerSpec.Hash,
167 Prefix: []byte("\x02\x04\x02\x20\x35\xf8\xea\x80\x53\x90\xe0\x84\x85\x4f\x39\x9b\x42\xcc\xde\xae\xa3\x3a\x1d\xed\xc1\x15\x63\x8a\xc4\x8d\x06\x00\x63\x7d\xba\x1f\x20"),
168 Suffix: []byte(""),
169 },
170 {
171 Hash: specs.InnerSpec.Hash,
172 Prefix: []byte("\x04\x08\x02\x20"),
173 Suffix: []byte("\x20\x79\x8e\x2c\xaa\x96\xfd\xfb\xa3\x76\xdd\xeb\x47\x99\x99\x54\xd2\xf4\x7e\x65\x16\x22\x64\xb0\x53\x6a\xb5\xdf\xf7\xfc\x0a\x2e\x07"),
174 },
175 {
176 Hash: specs.InnerSpec.Hash,
177 Prefix: []byte("\x06\x0c\x02\x20\x9a\xf3\x7d\xd5\x95\xa0\x19\x08\x03\xb5\xe0\x5a\xae\xf4\x2a\xe3\xfa\xd4\x99\xe4\xfb\xe3\x7f\x7c\xd3\x1c\xad\xff\x22\xa9\xee\x74\x20"),
178 Suffix: []byte(""),
179 },
180 },
181 },
182 },
183 // rootmulti proof
184 ics23.CommitmentProof_Exist{
185 Exist: &ics23.ExistenceProof{
186 Key: []byte("\x69\x61\x76\x6c\x53\x74\x6f\x72\x65\x4b\x65\x79"),
187 Value: []byte("\xeb\xfd\x02\x8a\x5d\xe1\xcf\x93\xde\x5d\x0b\xa8\xcf\x6e\x0d\x5e\x29\xf8\x80\x1a\x08\x0b\x07\x20\x3c\xf1\xca\xf2\xf4\xdd\xd3\x10"),
188 Leaf: &ics23.LeafOp{
189 Hash: specs.LeafSpec.Hash,
190 PrehashKey: specs.LeafSpec.PrehashKey,
191 PrehashValue: specs.LeafSpec.PrehashValue,
192 Length: specs.LeafSpec.Length,
193 Prefix: []byte("\x00"),
194 },
195 Path: []*ics23.InnerOp{},
196 },
197 },
198 }
199 recvPacket := types.MsgRecvPacket{
200 Packet: types.Packet{
201 Sequence: 1,
202 SourceClient: chainAClientIDForUs,
203 DestinationClient: clientToA,
204 TimeoutTimestamp: uint64(time.Now().Add(time.Hour).Unix()),
205 Payloads: []types.Payload{{
206 SourcePort: appPortID,
207 DestinationPort: appPortID,
208 Encoding: "application/json",
209 Value: []byte("{}"),
210 Version: "v1",
211 }},
212 },
213 ProofCommitment: parentRecvProof,
214 ProofHeight: trustedHeight,
215 }
216 recvRes := core.RecvPacket(cross(cur), recvPacket)
217
218 println("recv res:", recvRes)
219 println("\n----------- parent ack BEFORE child times out (deferred, should not exist)")
220 println(core.Render("clients/" + clientToA + "/packet_acknowledgements/1"))
221
222 // Advance block time past the child's timeout, then update chain C's
223 // client to a header that proves the receipt is absent. Reuses the
224 // height/timestamps/signatures from z6a_timeout_filetest because the
225 // inputs to gen-block-signatures (chainid, apphash, height, time-shift,
226 // privkeys) match and ed25519 signing is deterministic.
227 ctx := testing.GetContext()
228 ctx.Time = time.Now().Add(12 * time.Minute)
229 testing.SetContext(ctx)
230 // NOTE code generated by:
231 // go run -C ./cmd/gen-block-signatures . -apphash-hex=82ea91f259352674360c621c4b0a1975cc076e3792b6d22a8bfbe8e7f7d0d2d7 -chainid=atomone-1 -header-time-shift=12 -height=12 -privkeys=8a6cAbQSpDbebmcTEhCMPhhr/SkL/2pizo60yzHRkN9Uyk7RHOZm7g4xW+yeJh147/Z4/6HXF6gBwcFNkLsZ/A==,nWg6ETc62tyxd94lh8fFaQnZKaAW6vlS0L/4lfseJuI14ZXUKp7AZROkflLFVF+SBg4wJVfzgzIKyWq3D066+g==
232 // NOTE this apphash was provided by the gen-proof command below.
233 apphashCAfter, _ := hex.DecodeString("82ea91f259352674360c621c4b0a1975cc076e3792b6d22a8bfbe8e7f7d0d2d7")
234 commitTimestamp := tmtesting.ToTime("2025-09-25T07:55:57.306746166Z")
235 newTimestamp := chainCInitialConsensus.Timestamp.Add(12 * time.Minute)
236 signatures := []tendermint.CommitSig{
237 {
238 BlockIDFlag: tendermint.BlockIDFlagCommit,
239 ValidatorAddress: chainCValset.Validators[0].Address,
240 Timestamp: commitTimestamp,
241 Signature: []byte("\x90\x76\xf4\x27\xaf\x23\xd0\xcb\x97\xb1\x6d\xfa\x86\xae\x6d\x46\xe9\xe4\x65\x76\xf2\x6a\x1d\x5f\x83\xa6\x9e\x4a\x28\x55\x6e\x74\x84\x02\xff\x15\xc0\x41\xed\x15\xb7\xef\x3a\x3f\xe1\xe6\xa0\x8f\x34\x83\x9c\x2e\x27\x26\x69\x00\x73\x45\x87\xd2\x3c\xe6\x12\x07"),
242 },
243 {
244 BlockIDFlag: tendermint.BlockIDFlagCommit,
245 ValidatorAddress: chainCValset.Validators[1].Address,
246 Timestamp: commitTimestamp,
247 Signature: []byte("\x45\x61\x3b\x34\x58\xa1\xf5\x75\x16\x6f\xb3\xad\xda\x12\xa8\xef\xb6\x64\x3f\x6b\x80\x73\x74\xe3\x15\x6f\xe1\x3b\x3f\x50\xed\x0d\xbc\xa7\x54\x11\x72\xe3\x71\x22\x95\xec\x77\x93\x45\x03\x67\xa5\x00\x4c\x84\x3b\x1c\xa4\xf7\x0e\x95\xd1\xe2\x04\xb6\x91\x63\x09"),
248 },
249 }
250 msgHeader := tmtesting.NewMsgHeader(
251 "atomone-1", newTimestamp, apphashCAfter, uint64(12), chainCInitialHeight,
252 chainCValset, chainCValset, chainCValset, signatures,
253 )
254 core.UpdateClient(cross(cur), clientToC, msgHeader)
255
256 // Time out the child packet. core invokes the mock's OnTimeoutPacket,
257 // which itself calls core.WriteAcknowledgement for the pending parent.
258 // NOTE proof generated by:
259 // go run -C ./cmd/gen-proof . 'prefix3' '07-tendermint-99' 'receipt'
260 childTimeoutProof := []ics23.CommitmentProof{
261 // iavl proof
262 ics23.CommitmentProof_Nonexist{
263 Nonexist: &ics23.NonExistenceProof{
264 Key: []byte("\x70\x72\x65\x66\x69\x78\x33\x30\x37\x2d\x74\x65\x6e\x64\x65\x72\x6d\x69\x6e\x74\x2d\x39\x39\x02\x00\x00\x00\x00\x00\x00\x00\x01"),
265 Left: &ics23.ExistenceProof{
266 Key: []byte("\x50"),
267 Value: []byte("\x50"),
268 Leaf: &ics23.LeafOp{
269 Hash: specs.LeafSpec.Hash,
270 PrehashKey: specs.LeafSpec.PrehashKey,
271 PrehashValue: specs.LeafSpec.PrehashValue,
272 Length: specs.LeafSpec.Length,
273 Prefix: []byte("\x00\x02\x02"),
274 },
275 Path: []*ics23.InnerOp{
276 {
277 Hash: specs.InnerSpec.Hash,
278 Prefix: []byte("\x04\x06\x02\x20"),
279 Suffix: []byte("\x20\x79\x8e\x2c\xaa\x96\xfd\xfb\xa3\x76\xdd\xeb\x47\x99\x99\x54\xd2\xf4\x7e\x65\x16\x22\x64\xb0\x53\x6a\xb5\xdf\xf7\xfc\x0a\x2e\x07"),
280 },
281 {
282 Hash: specs.InnerSpec.Hash,
283 Prefix: []byte("\x06\x0a\x02\x20\x9a\xf3\x7d\xd5\x95\xa0\x19\x08\x03\xb5\xe0\x5a\xae\xf4\x2a\xe3\xfa\xd4\x99\xe4\xfb\xe3\x7f\x7c\xd3\x1c\xad\xff\x22\xa9\xee\x74\x20"),
284 Suffix: []byte(""),
285 },
286 },
287 },
288 Right: &ics23.ExistenceProof{
289 Key: []byte("\x72"),
290 Value: []byte("\x72"),
291 Leaf: &ics23.LeafOp{
292 Hash: specs.LeafSpec.Hash,
293 PrehashKey: specs.LeafSpec.PrehashKey,
294 PrehashValue: specs.LeafSpec.PrehashValue,
295 Length: specs.LeafSpec.Length,
296 Prefix: []byte("\x00\x02\x02"),
297 },
298 Path: []*ics23.InnerOp{
299 {
300 Hash: specs.InnerSpec.Hash,
301 Prefix: []byte("\x02\x04\x02\x20"),
302 Suffix: []byte("\x20\xa3\x03\x93\x0c\xa8\x83\x16\x18\xac\x7e\x4d\xdd\x10\x54\x6c\xfc\x36\x6f\xb7\x30\xd6\x63\x0c\x03\x0a\x97\x22\x6b\xbe\xfc\x69\x35"),
303 },
304 {
305 Hash: specs.InnerSpec.Hash,
306 Prefix: []byte("\x04\x06\x02\x20\x35\xf8\xea\x80\x53\x90\xe0\x84\x85\x4f\x39\x9b\x42\xcc\xde\xae\xa3\x3a\x1d\xed\xc1\x15\x63\x8a\xc4\x8d\x06\x00\x63\x7d\xba\x1f\x20"),
307 Suffix: []byte(""),
308 },
309 {
310 Hash: specs.InnerSpec.Hash,
311 Prefix: []byte("\x06\x0a\x02\x20\x9a\xf3\x7d\xd5\x95\xa0\x19\x08\x03\xb5\xe0\x5a\xae\xf4\x2a\xe3\xfa\xd4\x99\xe4\xfb\xe3\x7f\x7c\xd3\x1c\xad\xff\x22\xa9\xee\x74\x20"),
312 Suffix: []byte(""),
313 },
314 },
315 },
316 },
317 },
318 // rootmulti proof
319 ics23.CommitmentProof_Exist{
320 Exist: &ics23.ExistenceProof{
321 Key: []byte("\x69\x61\x76\x6c\x53\x74\x6f\x72\x65\x4b\x65\x79"),
322 Value: []byte("\x38\x22\xf3\x2f\x21\x66\x53\x63\x29\x4f\x96\xed\xda\x15\xa7\x81\x1a\x67\x6c\x2b\xa9\xdd\xcc\xec\x46\x63\x64\xf1\x00\x69\x82\x7d"),
323 Leaf: &ics23.LeafOp{
324 Hash: specs.LeafSpec.Hash,
325 PrehashKey: specs.LeafSpec.PrehashKey,
326 PrehashValue: specs.LeafSpec.PrehashValue,
327 Length: specs.LeafSpec.Length,
328 Prefix: []byte("\x00"),
329 },
330 Path: []*ics23.InnerOp{},
331 },
332 },
333 }
334 timeoutPacket := types.MsgTimeout{
335 Packet: types.Packet{
336 Sequence: childSeq,
337 SourceClient: clientToC,
338 DestinationClient: chainCClientIDForUs,
339 TimeoutTimestamp: sendPacket.TimeoutTimestamp,
340 Payloads: sendPacket.Payloads,
341 },
342 ProofUnreceived: childTimeoutProof,
343 ProofHeight: msgHeader.GetHeight(),
344 }
345 timeoutRes := core.Timeout(cross(cur), timeoutPacket)
346
347 println("\ntimeout res:", timeoutRes)
348 println("\n----------- parent ack AFTER child timeout (written by mock.OnTimeout)")
349 println(core.Render("clients/" + clientToA + "/packet_acknowledgements/1"))
350 println("\n----------- child commitment AFTER timeout (cleared)")
351 println(core.Render("clients/" + clientToC + "/packet_commitments/1"))
352 println("\n----------- app report")
353 println(app.Report())
354}
355
356// Output:
357// recv res: (2 gno.land/p/aib/ibc/types.ResponseResultType)
358//
359// ----------- parent ack BEFORE child times out (deferred, should not exist)
360// {"error":"sequence 1 not found"}
361//
362// timeout res: (2 gno.land/p/aib/ibc/types.ResponseResultType)
363//
364// ----------- parent ack AFTER child timeout (written by mock.OnTimeout)
365// {"sequence":"1","data":"5HN8pSo727oJNsvAPYWESzo9UNpM5u0nIHMoEOg7WuA="}
366//
367// ----------- child commitment AFTER timeout (cleared)
368// {"error":"sequence 1 not found"}
369//
370// ----------- app report
371// OnSendPacket (1)
372// - sourceClient: 07-tendermint-2
373// - destinationClient: 07-tendermint-99
374// - sequence: 1
375// - payload:
376// - sourcePort: appID
377// - destinationPort: appID
378// - version: v1
379// - encoding: application/json
380// - value: {}
381//
382// OnRecvPacket (1)
383// - sourceClient: 07-tendermint-42
384// - destinationClient: 07-tendermint-1
385// - sequence: 1
386// - payload:
387// - sourcePort: appID
388// - destinationPort: appID
389// - version: v1
390// - encoding: application/json
391// - value: {}
392//
393// OnTimeoutPacket (1)
394// - sourceClient: 07-tendermint-2
395// - destinationClient: 07-tendermint-99
396// - sequence: 1
397// - payload:
398// - sourcePort: appID
399// - destinationPort: appID
400// - version: v1
401// - encoding: application/json
402// - value: {}
403//
404// OnAcknowledgementPacket (0)