Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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)