diff --git a/modules/light-clients/07-celestia/client_state.go b/modules/light-clients/07-celestia/client_state.go index e4aea59aac1..18ee8cce5a3 100644 --- a/modules/light-clients/07-celestia/client_state.go +++ b/modules/light-clients/07-celestia/client_state.go @@ -48,7 +48,7 @@ func (cs *ClientState) VerifyMembership(ctx sdk.Context, clientStore storetypes. return errorsmod.Wrapf(commitmenttypes.ErrInvalidProof, "could not unmarshal share proof: %v", err) } - shareProof, err := shareProofFromProto(&shareProofProto) + shareProof, err := ShareProofFromProto(&shareProofProto) if err != nil { return err } diff --git a/modules/light-clients/07-celestia/client_state_test.go b/modules/light-clients/07-celestia/client_state_test.go new file mode 100644 index 00000000000..91db0528861 --- /dev/null +++ b/modules/light-clients/07-celestia/client_state_test.go @@ -0,0 +1,133 @@ +package celestia_test + +import ( + "encoding/hex" + "encoding/json" + + celestia "github.com/cosmos/ibc-go/modules/light-clients/07-celestia" + "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +// dataHash queried from commit at height=10000 +// https://public-celestia-rpc.numia.xyz/header?height=10000 +const dataHash string = "694F52677DDA82F3148D0A170ECC2A6A74A72563CC3F042BA7277AF3C1558127" + +// shareProofJSON for shares [0,1] queried at height=10000 +// https://public-celestia-rpc.numia.xyz/prove_shares?height=10000&startShare=0&endShare=1 +var shareProofJSON = `{ + "data": [ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAAENAAAACbsAgrAAQq9AQopL2liYy5hcHBsaWNhdGlvbnMudHJhbnNmZXIudjEuTXNnVHJhbnNmZXISjwEKCHRyYW5zZmVyEgljaGFubmVsLTIaEQoEdXRpYRIJMjU3MDAwMDAwIi9jZWxlc3RpYTF3ZWU2MjRscG53Y2NkcHB5dDJrZGZ5czZydnI5eHl2dThmbWVhZSorb3NtbzF3ZWU2MjRscG53Y2NkcHB5dDJrZGZ5czZydnI5eHl2dTdjZWUzeDIHCAEQ+p3mBRJlCk4KRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiEDQTeSPV//mL4658YJPa9fwwXHbo0zygD9Q0FWgyFN/QwSBAoCCH8SEwoNCgR1dGlhEgUyNzYyORCftwgaQBQg6ZFAoqJz3duveBY3NuDk7llRaGu/e6PQPTaoz4ZGNW5qZkFjgYhkglPXMEdKdktaNO/VEU+RZztQcIwE49DRAgqlAQqiAQojL2Nvc21vcy5zdGFraW5nLnYxYmV0YTEuTXNnRGVsZWdhdGUSewovY2VsZXN0aWExemVnN2xycXNjNjh5NW1nZHBnbnp5ZGUza3E3eDJ0cnE3NnZraDkSNmNlbGVzdGlhdmE=" + ], + "share_proofs": [ + { + "end": 1, + "nodes": [ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVCj8zV410D/2gZY+0iDX7AS/snewF0EQGkuuPWq24Te", + "/////////////////////////////////////////////////////////////////////////////7kDYk5RAaznnwDfIPmd6Iyw03FvBjhlOrAQEMY6dnNd" + ] + } + ], + "namespace_id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ==", + "row_proof": { + "row_roots": [ + "00000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000001D3C808F4BA0439B72D803F35FC92ED42B86A12C1121CD3A72EC07BE00FAF0DAD" + ], + "proofs": [ + { + "total": 8, + "index": 0, + "leaf_hash": "Oj+WbIzd1NCo4e4ptcPkyCR3vRW1eI28Fu+GfvCi1hk=", + "aunts": [ + "Dm+pi7IQIqqeDq5sA6aDRl29AddOikyIMsNKTs3TOvQ=", + "EN4RBE6ZgyzZAOYbzCPaNgUuxZv1F9a3Av0oqQ7VPEo=", + "ibs0Ape4CNv+qCosdw8W/m4ADHIt6HyqLRVMyNF5FqE=" + ] + } + ], + "start_row": 0, + "end_row": 0 + }, + "namespace_version": 0 + }` + +func (suite *CelestiaTestSuite) TestVerifyMembership() { + var ( + height exported.Height + path *ibctesting.Path + proof []byte + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success: with share proof queried from celestia-core", + func() { + // convert ShareProof json to protobuf encoded bytes + var shareProofInternal celestia.ShareProofInternal + err := json.Unmarshal([]byte(shareProofJSON), &shareProofInternal) + suite.Require().NoError(err) + + shareProofProto := shareProofInternal.ToProto() + + bz, err := suite.chainA.App.AppCodec().Marshal(&shareProofProto) + suite.Require().NoError(err) + + proof = bz // assign proof bytes to ShareProof proto + + // overwrite the client consensus state data root to the stub dataHash from queried Height + consensusState := path.EndpointA.GetConsensusState(height) + tmConsensusState, ok := consensusState.(*ibctm.ConsensusState) + suite.Require().True(ok) + + root, err := hex.DecodeString(dataHash) + suite.Require().NoError(err) + + // assign consensus state root as data availability root + tmConsensusState.Root = types.NewMerkleRoot(root) + + path.EndpointA.SetConsensusState(tmConsensusState, height) + }, + nil, + }, + // TODO: query blob.Proof from celestia-node API and plug in here + // { + // "failure: with proofs from celestia-node blob.GetProof api", + // func() { + // }, + // nil, + // }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupTest() + + path = ibctesting.NewPath(suite.chainA, suite.chainB) + + clientID := suite.CreateClient(path.EndpointA) + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + height = path.EndpointA.GetClientLatestHeight() + + tc.malleate() + + err := lightClientModule.VerifyMembership(suite.chainA.GetContext(), clientID, height, 0, 0, proof, nil, nil) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + } else { + suite.Require().ErrorIs(err, tc.expError, "failed verify membership") + } + }) + } +} diff --git a/modules/light-clients/07-celestia/light_client_module_test.go b/modules/light-clients/07-celestia/light_client_module_test.go index 73381671ed4..cf6be4eca3d 100644 --- a/modules/light-clients/07-celestia/light_client_module_test.go +++ b/modules/light-clients/07-celestia/light_client_module_test.go @@ -427,9 +427,9 @@ func (suite *CelestiaTestSuite) TestUpdateStateOnMisbehaviour() { } } -func (*CelestiaTestSuite) TestVerifyMembership() { - // TODO -} +// func (*CelestiaTestSuite) TestVerifyMembership() { +// // TODO +// } func (suite *CelestiaTestSuite) TestRecoverClient() { var subjectClientID, substituteClientID string diff --git a/modules/light-clients/07-celestia/row_proof.go b/modules/light-clients/07-celestia/row_proof.go index 50ae7f2e12d..6e97e4ce217 100644 --- a/modules/light-clients/07-celestia/row_proof.go +++ b/modules/light-clients/07-celestia/row_proof.go @@ -10,7 +10,7 @@ import ( // RowProof is a Merkle proof that a set of rows exist in a Merkle tree with a // given data root. -type rowProof struct { +type RowProofInternal struct { // RowRoots are the roots of the rows being proven. RowRoots []tmbytes.HexBytes `json:"row_roots"` // Proofs is a list of Merkle proofs where each proof proves that a row @@ -23,7 +23,7 @@ type rowProof struct { // Validate performs checks on the fields of this RowProof. Returns an error if // the proof fails validation. If the proof passes validation, this function // attempts to verify the proof. It returns nil if the proof is valid. -func (rp rowProof) Validate(root []byte) error { +func (rp RowProofInternal) Validate(root []byte) error { // HACKHACK performing subtraction with unsigned integers is unsafe. if int(rp.EndRow-rp.StartRow+1) != len(rp.RowRoots) { return fmt.Errorf("the number of rows %d must equal the number of row roots %d", int(rp.EndRow-rp.StartRow+1), len(rp.RowRoots)) @@ -40,7 +40,7 @@ func (rp rowProof) Validate(root []byte) error { // VerifyProof verifies that all the row roots in this RowProof exist in a // Merkle tree with the given root. Returns true if all proofs are valid. -func (rp rowProof) VerifyProof(root []byte) bool { +func (rp RowProofInternal) VerifyProof(root []byte) bool { for i, proof := range rp.Proofs { err := proof.Verify(root, rp.RowRoots[i]) if err != nil { @@ -50,9 +50,9 @@ func (rp rowProof) VerifyProof(root []byte) bool { return true } -func rowProofFromProto(p *RowProof) rowProof { +func RowProofFromProto(p *RowProof) RowProofInternal { if p == nil { - return rowProof{} + return RowProofInternal{} } rowRoots := make([]tmbytes.HexBytes, len(p.RowRoots)) rowProofs := make([]*merkle.Proof, len(p.Proofs)) @@ -66,7 +66,7 @@ func rowProofFromProto(p *RowProof) rowProof { } } - return rowProof{ + return RowProofInternal{ RowRoots: rowRoots, Proofs: rowProofs, StartRow: p.StartRow, diff --git a/modules/light-clients/07-celestia/share_proof.go b/modules/light-clients/07-celestia/share_proof.go index 9aa0483b142..b06100b0fed 100644 --- a/modules/light-clients/07-celestia/share_proof.go +++ b/modules/light-clients/07-celestia/share_proof.go @@ -13,7 +13,7 @@ import ( // ShareProof is an NMT proof that a set of shares exist in a set of rows and a // Merkle proof that those rows exist in a Merkle tree with a given data root. -type shareProof struct { +type ShareProofInternal struct { // Data are the raw shares that are being proven. Data [][]byte `json:"data"` // ShareProofs are NMT proofs that the shares in Data exist in a set of @@ -22,12 +22,12 @@ type shareProof struct { // NamespaceID is the namespace id of the shares being proven. This // namespace id is used when verifying the proof. If the namespace id doesn't // match the namespace of the shares, the proof will fail verification. - NamespaceID []byte `json:"namespace_id"` - RowProof rowProof `json:"row_proof"` - NamespaceVersion uint32 `json:"namespace_version"` + NamespaceID []byte `json:"namespace_id"` + RowProof RowProofInternal `json:"row_proof"` + NamespaceVersion uint32 `json:"namespace_version"` } -func (sp shareProof) ToProto() ShareProof { +func (sp ShareProofInternal) ToProto() ShareProof { // TODO consider extracting a ToProto function for RowProof rowRoots := make([][]byte, len(sp.RowProof.RowRoots)) rowProofs := make([]*crypto.Proof, len(sp.RowProof.Proofs)) @@ -51,15 +51,15 @@ func (sp shareProof) ToProto() ShareProof { return pbtp } -// shareProofFromProto creates a ShareProof from a proto message. +// ShareProofFromProto creates a ShareProof from a proto message. // Expects the proof to be pre-validated. -func shareProofFromProto(pb *ShareProof) (shareProof, error) { +func ShareProofFromProto(pb *ShareProof) (ShareProofInternal, error) { if pb == nil { - return shareProof{}, fmt.Errorf("nil share proof protobuf") + return ShareProofInternal{}, fmt.Errorf("nil share proof protobuf") } - return shareProof{ - RowProof: rowProofFromProto(pb.RowProof), + return ShareProofInternal{ + RowProof: RowProofFromProto(pb.RowProof), Data: pb.Data, ShareProofs: pb.ShareProofs, NamespaceID: pb.NamespaceId, @@ -71,7 +71,7 @@ func shareProofFromProto(pb *ShareProof) (shareProof, error) { // It returns nil if the proof is valid. Otherwise, it returns a sensible error. // The `root` is the block data root that the shares to be proven belong to. // Note: these proofs are tested on the app side. -func (sp shareProof) Validate(root []byte) error { +func (sp ShareProofInternal) Validate(root []byte) error { numberOfSharesInProofs := int32(0) for _, proof := range sp.ShareProofs { // the range is not inclusive from the left. @@ -105,7 +105,7 @@ func (sp shareProof) Validate(root []byte) error { return nil } -func (sp shareProof) VerifyProof() bool { +func (sp ShareProofInternal) VerifyProof() bool { cursor := int32(0) for i, proof := range sp.ShareProofs { nmtProof := nmt.NewInclusionProof(