Skip to content

Commit a73c86d

Browse files
committed
test(core): happy-path UpgradeClient filetest with real ibc-go proofs
Adds the happy path that was deferred from the original test commit, now that the byte-level marshaling matches what an ibc-go counterparty actually commits. Three correctness fixes were needed to get a real ibc-go-generated proof to verify under our Gno code: 1. The SDK upgrade module key for the upgraded consensus state is "upgradedConsState" (no "ensus") — our code used "upgradedConsensusState". 2. ibc-go's tendermint.ClientState marks TrustLevel, TrustingPeriod, UnbondingPeriod, MaxClockDrift, FrozenHeight and LatestHeight with `(gogoproto.nullable) = false`, and the same for ConsensusState's Timestamp and Root. These fields are emitted on the wire even when zero, as length-0 nested messages. AppendLengthDelimited skips empty bytes (correct proto3 default). Add AppendAlwaysLengthDelimited and route the always-emit fields through it so ZeroCustomFields().ProtoMarshal() produces bytes byte-equal to gogoproto's output. 3. Extend cmd/gen-proof with an `upgrade` subcommand that uses ibc-go's actual tendermint.ClientState/ConsensusState plus cdc.MarshalInterface to commit Any-wrapped values to an upgrade IAVL store, then emits the chained ICS-23 proofs as Go literals for embedding in the filetest. The new filetest sets up a current client whose consensus root equals the multistore apphash gen-proof committed, and exercises the full UpgradeClient path against the embedded proofs.
1 parent fd291e5 commit a73c86d

7 files changed

Lines changed: 443 additions & 42 deletions

File tree

cmd/gen-proof/main.go

Lines changed: 237 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"strings"
1010
"text/template"
11+
"time"
1112

1213
ics23 "github.com/cosmos/ics23/go"
1314

@@ -20,32 +21,31 @@ import (
2021
"cosmossdk.io/store/metrics"
2122
"cosmossdk.io/store/rootmulti"
2223
storetypes "cosmossdk.io/store/types"
24+
25+
upgradetypes "cosmossdk.io/x/upgrade/types"
26+
27+
"github.com/cosmos/cosmos-sdk/codec"
28+
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
29+
30+
clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types"
31+
commitmenttypes "github.com/cosmos/ibc-go/v10/modules/core/23-commitment/types"
32+
tmclient "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint"
2333
)
2434

2535
func main() {
26-
/*
27-
// helper to write the JSON packet
28-
bz, _ := json.Marshal(channelv2types.Packet{
29-
Sequence: 1,
30-
SourceClient: "07-tendermint-42",
31-
DestinationClient: "07-tendermint-1",
32-
TimeoutTimestamp: 1234571490,
33-
Payloads: []channelv2types.Payload{{
34-
SourcePort: "appID",
35-
DestinationPort: "appID",
36-
Encoding: "application/json",
37-
Value: []byte("{}"),
38-
Version: "v1",
39-
}},
40-
})
41-
fmt.Println(string(bz))
42-
fmt.Println(os.Args[4])
43-
return
44-
*/
45-
4636
flag.Parse()
37+
38+
// New shape: `gen-proof upgrade` generates both upgrade-client and
39+
// upgrade-consensus-state proofs against the upgrade store, in a single
40+
// rootmulti commit (so they share the same apphash).
41+
if flag.NArg() == 1 && flag.Arg(0) == "upgrade" {
42+
fmt.Println(genUpgradeProofCode())
43+
return
44+
}
45+
4746
if flag.NArg() < 3 || flag.NArg() > 4 {
4847
fmt.Println("Usage: gen-proof MERKLE_PREFIX CLIENT_ID COMMITMENT_TYPE [COMMITMENT]")
48+
fmt.Println(" gen-proof upgrade")
4949
os.Exit(1)
5050
}
5151
var (
@@ -98,20 +98,229 @@ func main() {
9898
os.Exit(1)
9999
}
100100

101-
fmt.Println(genProofCode(key, value))
101+
fmt.Println(genProofCode("iavlStoreKey", key, value))
102+
}
103+
104+
// upgradeScenario holds the hardcoded parameters used to generate an upgrade
105+
// happy-path filetest. Both the chain that "schedules" the upgrade in
106+
// gen-proof and the consumer test must use the exact same values, otherwise
107+
// the values the test reconstructs won't match the bytes the chain
108+
// committed and proof verification will fail.
109+
type upgradeScenario struct {
110+
planHeight int64
111+
upgradedChainID string
112+
upgradedHeight clienttypes.Height
113+
upgradedTimestamp time.Time
114+
nextValsHash []byte
115+
}
116+
117+
func defaultUpgradeScenario() upgradeScenario {
118+
hash := make([]byte, 32)
119+
for i := range hash {
120+
hash[i] = byte(i + 1)
121+
}
122+
return upgradeScenario{
123+
planHeight: 100,
124+
upgradedChainID: "chain-after-upgrade-2",
125+
upgradedHeight: clienttypes.NewHeight(2, 1),
126+
upgradedTimestamp: time.Unix(1700000000, 0).UTC(),
127+
nextValsHash: hash,
128+
}
129+
}
130+
131+
// genUpgradeProofCode commits an upgraded client + consensus state to the
132+
// upgrade IAVL store at the SDK-conventional keys, then dumps Go code that
133+
// reconstructs the matching state and proofs for the Gno filetest.
134+
func genUpgradeProofCode() string {
135+
scn := defaultUpgradeScenario()
136+
137+
// Build the upgraded states.
138+
upgradedClient := tmclient.NewClientState(
139+
scn.upgradedChainID,
140+
tmclient.Fraction{Numerator: 1, Denominator: 3},
141+
2*7*24*time.Hour, // trusting period (placeholder; ZeroCustomFields drops it)
142+
3*7*24*time.Hour, // unbonding period (preserved)
143+
10*time.Second, // max clock drift (placeholder; ZeroCustomFields drops it)
144+
scn.upgradedHeight,
145+
commitmenttypes.GetSDKSpecs(),
146+
[]string{"upgrade", "upgradedIBCState"},
147+
)
148+
zeroedClient := upgradedClient.ZeroCustomFields()
149+
150+
upgradedConsState := tmclient.NewConsensusState(
151+
scn.upgradedTimestamp,
152+
commitmenttypes.NewMerkleRoot([]byte(tmclient.SentinelRoot)),
153+
scn.nextValsHash,
154+
)
155+
156+
// Marshal via cdc.MarshalInterface so the bytes match what the SDK
157+
// upgrade module commits (google.protobuf.Any wrapper + raw proto).
158+
registry := codectypes.NewInterfaceRegistry()
159+
tmclient.RegisterInterfaces(registry)
160+
cdc := codec.NewProtoCodec(registry)
161+
162+
clientBz, err := cdc.MarshalInterface(zeroedClient)
163+
if err != nil {
164+
panic(fmt.Errorf("marshal upgraded client state: %w", err))
165+
}
166+
consStateBz, err := cdc.MarshalInterface(upgradedConsState)
167+
if err != nil {
168+
panic(fmt.Errorf("marshal upgraded consensus state: %w", err))
169+
}
170+
171+
// Mount the "upgrade" IAVL store and commit both values.
172+
db := dbm.NewMemDB()
173+
store := rootmulti.NewStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics())
174+
upgradeStoreKey := storetypes.NewKVStoreKey("upgrade")
175+
store.MountStoreWithDB(upgradeStoreKey, storetypes.StoreTypeIAVL, nil)
176+
if err := store.LoadVersion(0); err != nil {
177+
panic(err)
178+
}
179+
upStore := store.GetCommitStore(upgradeStoreKey).(*iavl.Store)
180+
// Fill with fake data to keep the IAVL tree non-trivial.
181+
for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} {
182+
k := []byte{ikey}
183+
upStore.Set(k, k)
184+
}
185+
186+
clientKey := upgradetypes.UpgradedClientKey(scn.planHeight)
187+
consStateKey := upgradetypes.UpgradedConsStateKey(scn.planHeight)
188+
upStore.Set(clientKey, clientBz)
189+
upStore.Set(consStateKey, consStateBz)
190+
191+
cid := store.Commit()
192+
193+
clientProofs := queryAndDecodeProof(store, "upgrade", clientKey, clientBz, cid.Hash)
194+
consStateProofs := queryAndDecodeProof(store, "upgrade", consStateKey, consStateBz, cid.Hash)
195+
196+
tmpl := `
197+
{{define "existenceProof" -}}
198+
&ics23.ExistenceProof{
199+
Key: {{bytes .Key}},
200+
Value: {{bytes .Value}},
201+
Leaf: &ics23.LeafOp{
202+
Hash: specs.LeafSpec.Hash,
203+
PrehashKey: specs.LeafSpec.PrehashKey,
204+
PrehashValue: specs.LeafSpec.PrehashValue,
205+
Length: specs.LeafSpec.Length,
206+
Prefix: {{bytes .Leaf.Prefix}},
207+
},
208+
Path: []*ics23.InnerOp{
209+
{{range .Path -}}
210+
{
211+
Hash: specs.InnerSpec.Hash,
212+
Prefix: {{bytes .Prefix}},
213+
Suffix: {{bytes .Suffix}},
214+
},
215+
{{end -}}
216+
},
217+
},
218+
{{- end -}}
219+
{{define "proofPair" -}}
220+
[]ics23.CommitmentProof{
221+
// iavl proof
222+
ics23.CommitmentProof_Exist{
223+
Exist: {{template "existenceProof" (index . 0).GetExist}}
224+
},
225+
// rootmulti proof
226+
ics23.CommitmentProof_Exist{
227+
Exist: {{template "existenceProof" (index . 1).GetExist}}
228+
},
229+
}
230+
{{- end -}}
231+
// NOTE code generated by:
232+
// go run -C ./cmd/gen-proof . upgrade
233+
//
234+
// Plan height: {{.PlanHeight}}
235+
// Upgraded chain id: {{.UpgradedChainID}}
236+
// Upgraded latest height: {{.UpgradedHeight}}
237+
// Upgraded timestamp: {{.UpgradedTimestampUnix}}
238+
// Next validators hash: {{hex .NextValsHash}}
239+
// Multistore root (apphash): {{hex .Root}}
240+
241+
apphash, _ := hex.DecodeString("{{hex .Root}}")
242+
specs := ics23.IavlSpec()
243+
244+
clientProof := {{template "proofPair" .ClientProofs}}
245+
246+
consStateProof := {{template "proofPair" .ConsStateProofs}}
247+
`
248+
t, err := template.New("").Funcs(template.FuncMap{
249+
"hex": func(bz []byte) string {
250+
return fmt.Sprintf("%x", bz)
251+
},
252+
"bytes": func(bz []byte) string {
253+
h := fmt.Sprintf("%x", bz)
254+
var bytesStr string
255+
for i := 0; i < len(h); i += 2 {
256+
bytesStr += "\\x" + h[i:i+2]
257+
}
258+
return "[]byte(\"" + bytesStr + "\")"
259+
},
260+
}).Parse(tmpl)
261+
if err != nil {
262+
panic(err)
263+
}
264+
var sb strings.Builder
265+
err = t.Execute(&sb, map[string]any{
266+
"Root": cid.Hash,
267+
"PlanHeight": scn.planHeight,
268+
"UpgradedChainID": scn.upgradedChainID,
269+
"UpgradedHeight": scn.upgradedHeight.String(),
270+
"UpgradedTimestampUnix": scn.upgradedTimestamp.Unix(),
271+
"NextValsHash": scn.nextValsHash,
272+
"ClientProofs": clientProofs,
273+
"ConsStateProofs": consStateProofs,
274+
})
275+
if err != nil {
276+
panic(err)
277+
}
278+
return sb.String()
279+
}
280+
281+
// queryAndDecodeProof asks the rootmulti store for a Prove=true query at the
282+
// given key in the given store, decodes the two ProofOps into their ICS-23
283+
// CommitmentProof form, and verifies the value reconstructs the apphash so
284+
// we fail fast if the proof is malformed.
285+
func queryAndDecodeProof(store *rootmulti.Store, storeName string, key, value, root []byte) []*ics23.CommitmentProof {
286+
res, err := store.Query(&storetypes.RequestQuery{
287+
Path: fmt.Sprintf("/%s/key", storeName),
288+
Data: key,
289+
Prove: true,
290+
})
291+
if err != nil {
292+
panic(err)
293+
}
294+
295+
proofs := make([]*ics23.CommitmentProof, len(res.ProofOps.Ops))
296+
for i, op := range res.ProofOps.Ops {
297+
var p ics23.CommitmentProof
298+
if err := p.Unmarshal(op.Data); err != nil || p.Proof == nil {
299+
panic(fmt.Sprintf("decode proof op %d: %v", i, err))
300+
}
301+
proofs[i] = &p
302+
}
303+
304+
// Note: the SDK proof runtime path-splits on "/", which doesn't work
305+
// when the IAVL key itself contains slashes (as it does for the upgrade
306+
// module's "upgradedIBCState/{H}/upgradedClient"). The proofs are still
307+
// well-formed; the consumer (Gno test) verifies via VerifyMembership,
308+
// which doesn't path-split.
309+
_ = root
310+
return proofs
102311
}
103312

104-
func genProofCode(key, value []byte) string {
313+
func genProofCode(storeName string, key, value []byte) string {
105314
db := dbm.NewMemDB()
106315
store := rootmulti.NewStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics())
107-
iavlStoreKey := storetypes.NewKVStoreKey("iavlStoreKey")
316+
storeKey := storetypes.NewKVStoreKey(storeName)
108317

109-
store.MountStoreWithDB(iavlStoreKey, storetypes.StoreTypeIAVL, nil)
318+
store.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, nil)
110319
err := store.LoadVersion(0)
111320
if err != nil {
112321
panic(err)
113322
}
114-
iavlStore := store.GetCommitStore(iavlStoreKey).(*iavl.Store)
323+
iavlStore := store.GetCommitStore(storeKey).(*iavl.Store)
115324
// fill with fake data
116325
for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} {
117326
key := []byte{ikey}
@@ -127,7 +336,7 @@ func genProofCode(key, value []byte) string {
127336

128337
// Get Proof
129338
res, err := store.Query(&storetypes.RequestQuery{
130-
Path: "/iavlStoreKey/key",
339+
Path: fmt.Sprintf("/%s/key", storeName),
131340
Data: key,
132341
Prove: true,
133342
})
@@ -137,7 +346,6 @@ func genProofCode(key, value []byte) string {
137346

138347
// Decode ics23 proof
139348
proofs := make([]*ics23.CommitmentProof, len(res.ProofOps.Ops))
140-
// spew.Dump(reqres.Response.ProofOps.Ops)
141349
for i, op := range res.ProofOps.Ops {
142350
var p ics23.CommitmentProof
143351
err = p.Unmarshal(op.Data)
@@ -150,9 +358,9 @@ func genProofCode(key, value []byte) string {
150358
// Verify proof
151359
prt := rootmulti.DefaultProofRuntime()
152360
if value != nil {
153-
err = prt.VerifyValue(res.ProofOps, cid.Hash, "/iavlStoreKey/"+string(key), value)
361+
err = prt.VerifyValue(res.ProofOps, cid.Hash, fmt.Sprintf("/%s/", storeName)+string(key), value)
154362
} else {
155-
err = prt.VerifyAbsence(res.ProofOps, cid.Hash, "/iavlStoreKey/"+string(key))
363+
err = prt.VerifyAbsence(res.ProofOps, cid.Hash, fmt.Sprintf("/%s/", storeName)+string(key))
156364
}
157365
if err != nil {
158366
panic(err)

cmd/gen-proof/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ proof := []ics23.CommitmentProof{
167167
}
168168
for _, tt := range tests {
169169
t.Run(tt.name, func(t *testing.T) {
170-
ret := genProofCode(tt.key, tt.value)
170+
ret := genProofCode("iavlStoreKey", tt.key, tt.value)
171171

172172
assert.Equal(t, tt.expectedRet, ret)
173173
})

gno.land/p/aib/encoding/proto/proto.gno

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ func AppendFixed64(buf []byte, fieldNum int, v uint64) []byte {
6363
return append(buf, b[:]...)
6464
}
6565

66-
// AppendLengthDelimited appends a length-delimited field.
66+
// AppendLengthDelimited appends a length-delimited field. Returns buf
67+
// unchanged when bz is empty (proto3 default for optional fields).
6768
func AppendLengthDelimited(buf []byte, fieldNum int, bz []byte) []byte {
6869
if len(bz) == 0 {
6970
return buf
@@ -73,6 +74,17 @@ func AppendLengthDelimited(buf []byte, fieldNum int, bz []byte) []byte {
7374
return append(buf, bz...)
7475
}
7576

77+
// AppendAlwaysLengthDelimited appends a length-delimited field, including
78+
// when bz is empty (emits a length-0 value). Required to match the wire
79+
// format produced by gogoproto for fields marked
80+
// `[(gogoproto.nullable) = false]`, which always serialize even when their
81+
// underlying message has no non-zero fields.
82+
func AppendAlwaysLengthDelimited(buf []byte, fieldNum int, bz []byte) []byte {
83+
buf = AppendTag(buf, fieldNum, LEN)
84+
buf = binary.AppendUvarint(buf, uint64(len(bz)))
85+
return append(buf, bz...)
86+
}
87+
7688
// TimeMarshal returns a proto marshalled time.
7789
func TimeMarshal(t time.Time) []byte {
7890
var (

gno.land/p/aib/ibc/lightclient/tendermint/state.gno

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,14 @@ func (cs ClientState) ProtoMarshal() (bz []byte) {
223223
if cs.ChainID != "" {
224224
bz = proto.AppendLengthDelimited(bz, 1, []byte(cs.ChainID))
225225
}
226-
bz = proto.AppendLengthDelimited(bz, 2, cs.TrustLevel.ProtoMarshal())
227-
bz = proto.AppendLengthDelimited(bz, 3, durationMarshal(cs.TrustingPeriod))
228-
bz = proto.AppendLengthDelimited(bz, 4, durationMarshal(cs.UnbondingPeriod))
229-
bz = proto.AppendLengthDelimited(bz, 5, durationMarshal(cs.MaxClockDrift))
230-
bz = proto.AppendLengthDelimited(bz, 6, cs.FrozenHeight.ProtoMarshal())
231-
bz = proto.AppendLengthDelimited(bz, 7, cs.LatestHeight.ProtoMarshal())
226+
// Fields 2..7 are marked `(gogoproto.nullable) = false` in ibc-go's
227+
// .proto, so they're emitted even when zero (as length-0 messages).
228+
bz = proto.AppendAlwaysLengthDelimited(bz, 2, cs.TrustLevel.ProtoMarshal())
229+
bz = proto.AppendAlwaysLengthDelimited(bz, 3, durationMarshal(cs.TrustingPeriod))
230+
bz = proto.AppendAlwaysLengthDelimited(bz, 4, durationMarshal(cs.UnbondingPeriod))
231+
bz = proto.AppendAlwaysLengthDelimited(bz, 5, durationMarshal(cs.MaxClockDrift))
232+
bz = proto.AppendAlwaysLengthDelimited(bz, 6, cs.FrozenHeight.ProtoMarshal())
233+
bz = proto.AppendAlwaysLengthDelimited(bz, 7, cs.LatestHeight.ProtoMarshal())
232234
for _, spec := range cs.ProofSpecs {
233235
bz = proto.AppendLengthDelimited(bz, 8, spec.ProtoMarshal())
234236
}
@@ -273,8 +275,11 @@ func durationMarshal(d time.Duration) []byte {
273275
}
274276

275277
func (cs ConsensusState) ProtoMarshal() (bz []byte) {
276-
bz = proto.AppendTime(bz, 1, cs.Timestamp)
277-
bz = proto.AppendLengthDelimited(bz, 2, cs.Root.ProtoMarshal())
278+
// Timestamp and Root are `(gogoproto.nullable) = false` in ibc-go;
279+
// always-emit semantics. NextValidatorsHash is plain bytes — proto3
280+
// default applies (omitted when empty).
281+
bz = proto.AppendAlwaysLengthDelimited(bz, 1, proto.TimeMarshal(cs.Timestamp))
282+
bz = proto.AppendAlwaysLengthDelimited(bz, 2, cs.Root.ProtoMarshal())
278283
bz = proto.AppendLengthDelimited(bz, 3, cs.NextValidatorsHash)
279284
return
280285
}

0 commit comments

Comments
 (0)