package solomachine_test import ( "errors" "fmt" codectypes "git.cw.tr/mukan-network/mukan-sdk/codec/types" sdk "git.cw.tr/mukan-network/mukan-sdk/types" clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types" channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types" commitmenttypesv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/23-commitment/types/v2" host "git.cw.tr/mukan-network/mukan-ibc/modules/core/24-host" ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors" "git.cw.tr/mukan-network/mukan-ibc/modules/core/exported" solomachine "git.cw.tr/mukan-network/mukan-ibc/modules/light-clients/06-solomachine" ibctm "git.cw.tr/mukan-network/mukan-ibc/modules/light-clients/07-tendermint" ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing" ibcmock "git.cw.tr/mukan-network/mukan-ibc/testing/mock" ) const ( unusedSmClientID = "06-solomachine-999" wasmClientID = "08-wasm-0" ) func (suite *SoloMachineTestSuite) TestStatus() { var ( clientState *solomachine.ClientState clientID string ) testCases := []struct { name string malleate func() expStatus exported.Status }{ { "client is active", func() {}, exported.Active, }, { "client is frozen", func() { clientState = solomachine.NewClientState(0, &solomachine.ConsensusState{}) clientState.IsFrozen = true suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) }, exported.Frozen, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, exported.Unknown, }, } for _, tc := range testCases { suite.Run(tc.name, func() { clientID = suite.solomachine.ClientID lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, suite.solomachine.ClientState()) tc.malleate() status := lightClientModule.Status(suite.chainA.GetContext(), clientID) suite.Require().Equal(tc.expStatus, status) }) } } func (suite *SoloMachineTestSuite) TestGetTimestampAtHeight() { var ( clientID string height exported.Height ) testCases := []struct { name string malleate func() expValue uint64 expErr error }{ { "success: get timestamp at height exists", func() {}, suite.solomachine.ClientState().ConsensusState.Timestamp, nil, }, { "success: modified height", func() { height = clienttypes.ZeroHeight() }, // Timestamp should be the same. suite.solomachine.ClientState().ConsensusState.Timestamp, nil, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, 0, clienttypes.ErrClientNotFound, }, } for _, tc := range testCases { suite.Run(tc.name, func() { clientID = suite.solomachine.ClientID clientState := suite.solomachine.ClientState() height = clienttypes.NewHeight(0, suite.solomachine.ClientState().Sequence) lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) tc.malleate() ts, err := lightClientModule.TimestampAtHeight(suite.chainA.GetContext(), clientID, height) suite.Require().Equal(tc.expValue, ts) suite.Require().ErrorIs(err, tc.expErr) }) } } func (suite *SoloMachineTestSuite) TestInitialize() { // test singlesig and multisig public keys for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { malleatedConsensus := sm.ClientState().ConsensusState malleatedConsensus.Timestamp += 10 testCases := []struct { name string consState exported.ConsensusState clientState exported.ClientState expErr error }{ { "success: valid consensus state", sm.ConsensusState(), sm.ClientState(), nil, }, { "failure: nil consensus state", nil, sm.ClientState(), clienttypes.ErrInvalidConsensus, }, { "failure: invalid consensus state: Tendermint consensus state", &ibctm.ConsensusState{}, sm.ClientState(), errors.New("proto: wrong wireType = 0 for field TypeUrl"), }, { "failure: invalid consensus state: consensus state does not match consensus state in client", malleatedConsensus, sm.ClientState(), clienttypes.ErrInvalidConsensus, }, { "failure: invalid client state: sequence is zero", sm.ConsensusState(), solomachine.NewClientState(0, sm.ConsensusState()), clienttypes.ErrInvalidClient, }, { "failure: invalid client state: Tendermint client state", sm.ConsensusState(), &ibctm.ClientState{}, errors.New("proto: wrong wireType = 2 for field IsFrozen"), }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() clientID := sm.ClientID clientStateBz := suite.chainA.Codec.MustMarshal(tc.clientState) consStateBz := suite.chainA.Codec.MustMarshal(tc.consState) lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) err = lightClientModule.Initialize(suite.chainA.GetContext(), clientID, clientStateBz, consStateBz) store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID) if tc.expErr == nil { suite.Require().NoError(err) suite.Require().True(store.Has(host.ClientStateKey())) } else { suite.Require().ErrorContains(err, tc.expErr.Error()) suite.Require().False(store.Has(host.ClientStateKey())) } }) } } } func (suite *SoloMachineTestSuite) TestVerifyMembership() { var ( clientState *solomachine.ClientState path exported.Path proof []byte testingPath *ibctesting.Path signBytes solomachine.SignBytes err error clientID string ) // test singlesig and multisig public keys for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { testCases := []struct { name string malleate func() expErr error }{ { "success", func() {}, nil, }, { "success: client state verification", func() { clientState = sm.ClientState() clientStateBz, err := suite.chainA.Codec.MarshalInterface(clientState) suite.Require().NoError(err) path = sm.GetClientStatePath(counterpartyClientIdentifier) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.GetHeight().GetRevisionHeight(), Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: clientStateBz, } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "success: consensus state verification", func() { clientState = sm.ClientState() consensusState := clientState.ConsensusState consensusStateBz, err := suite.chainA.Codec.MarshalInterface(consensusState) suite.Require().NoError(err) path = sm.GetConsensusStatePath(counterpartyClientIdentifier, clienttypes.NewHeight(0, 1)) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.Sequence, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: consensusStateBz, } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "success: connection state verification", func() { testingPath.SetupConnections() connectionEnd, found := suite.chainA.GetSimApp().IBCKeeper.ConnectionKeeper.GetConnection(suite.chainA.GetContext(), ibctesting.FirstConnectionID) suite.Require().True(found) connectionEndBz, err := suite.chainA.Codec.Marshal(&connectionEnd) suite.Require().NoError(err) path = sm.GetConnectionStatePath(ibctesting.FirstConnectionID) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.Sequence, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: connectionEndBz, } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "success: channel state verification", func() { testingPath.SetupConnections() suite.coordinator.CreateMockChannels(testingPath) channelEnd, found := suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.GetChannel(suite.chainA.GetContext(), ibctesting.MockPort, testingPath.EndpointA.ChannelID) suite.Require().True(found) channelEndBz, err := suite.chainA.Codec.Marshal(&channelEnd) suite.Require().NoError(err) path = sm.GetChannelStatePath(ibctesting.MockPort, ibctesting.FirstChannelID) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.Sequence, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: channelEndBz, } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "success: next sequence recv verification", func() { testingPath.SetupConnections() suite.coordinator.CreateMockChannels(testingPath) nextSeqRecv, found := suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.GetNextSequenceRecv(suite.chainA.GetContext(), ibctesting.MockPort, testingPath.EndpointA.ChannelID) suite.Require().True(found) path = sm.GetNextSequenceRecvPath(ibctesting.MockPort, ibctesting.FirstChannelID) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.Sequence, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: sdk.Uint64ToBigEndian(nextSeqRecv), } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "success: packet commitment verification", func() { packet := channeltypes.NewPacket( ibctesting.MockPacketData, 1, ibctesting.MockPort, ibctesting.FirstChannelID, ibctesting.MockPort, ibctesting.FirstChannelID, clienttypes.NewHeight(0, 10), 0, ) commitmentBz := channeltypes.CommitPacket(packet) path = sm.GetPacketCommitmentPath(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.Sequence, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: commitmentBz, } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "success: packet acknowledgement verification", func() { path = sm.GetPacketAcknowledgementPath(ibctesting.MockPort, ibctesting.FirstChannelID, 1) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.Sequence, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: ibctesting.MockAcknowledgement, } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "success: packet receipt verification", func() { path = sm.GetPacketReceiptPath(ibctesting.MockPort, ibctesting.FirstChannelID, 1) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.Sequence, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: []byte{byte(1)}, // packet receipt is stored as a single byte } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, clienttypes.ErrClientNotFound, }, { "failure: invalid path type - empty", func() { path = ibcmock.KeyPath{} }, ibcerrors.ErrInvalidType, }, { "failure: malformed proof fails to unmarshal", func() { path = sm.GetClientStatePath(counterpartyClientIdentifier) proof = []byte("invalid proof") }, errors.New("failed to unmarshal proof into type"), }, { "failure: consensus state timestamp is greater than signature", func() { consensusState := &solomachine.ConsensusState{ Timestamp: sm.Time + 1, PublicKey: sm.ConsensusState().PublicKey, } clientState = solomachine.NewClientState(sm.Sequence, consensusState) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) }, fmt.Errorf("the consensus state timestamp is greater than the signature timestamp (11 >= 10): %s", solomachine.ErrInvalidProof), }, { "failure: signature data is nil", func() { signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: nil, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, fmt.Errorf("signature data cannot be empty: %s", solomachine.ErrInvalidProof), }, { "failure: consensus state public key is nil", func() { clientState.ConsensusState.PublicKey = nil suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) }, fmt.Errorf("consensus state PublicKey cannot be nil: %s", clienttypes.ErrInvalidConsensus), }, { "failure: malformed signature data fails to unmarshal", func() { signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: []byte("invalid signature data"), Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, errors.New("failed to unmarshal proof into type"), }, { "failure: proof is nil", func() { proof = nil }, fmt.Errorf("proof cannot be empty: %s", solomachine.ErrInvalidProof), }, { "failure: proof verification failed", func() { signBytes.Data = []byte("invalid membership data value") }, solomachine.ErrSignatureVerificationFailed, }, { "failure: empty path", func() { path = commitmenttypesv2.MerklePath{} }, fmt.Errorf("path must be of length 2: []: %s", host.ErrInvalidPath), }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() testingPath = ibctesting.NewPath(suite.chainA, suite.chainB) clientID = sm.ClientID clientState = sm.ClientState() path = commitmenttypesv2.NewMerklePath([]byte("ibc"), []byte("solomachine")) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.GetHeight().GetRevisionHeight(), Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: []byte("solomachine"), } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) // Set the client state in the store for light client call to find. suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) tc.malleate() var expSeq uint64 if clientState.ConsensusState != nil { expSeq = clientState.Sequence + 1 } // Verify the membership proof err = lightClientModule.VerifyMembership( suite.chainA.GetContext(), clientID, clienttypes.ZeroHeight(), 0, 0, proof, path, signBytes.Data, ) if tc.expErr == nil { // Grab fresh client state after updates. cs, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetClientState(suite.chainA.GetContext(), clientID) suite.Require().True(found) clientState, ok = cs.(*solomachine.ClientState) suite.Require().True(ok) suite.Require().NoError(err) // clientState.Sequence is the most recent view of state. suite.Require().Equal(expSeq, clientState.Sequence) } else { suite.Require().Error(err) suite.Require().ErrorContains(err, tc.expErr.Error()) } }) } } } func (suite *SoloMachineTestSuite) TestVerifyNonMembership() { var ( clientState *solomachine.ClientState path exported.Path proof []byte signBytes solomachine.SignBytes err error clientID string ) // test singlesig and multisig public keys for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { testCases := []struct { name string malleate func() expErr error }{ { "success", func() {}, nil, }, { "success: packet receipt absence verification", func() { path = sm.GetPacketReceiptPath(ibctesting.MockPort, ibctesting.FirstChannelID, 1) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.GetHeight().GetRevisionHeight(), Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: nil, } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, nil, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, clienttypes.ErrClientNotFound, }, { "failure: invalid path type", func() { path = ibcmock.KeyPath{} }, ibcerrors.ErrInvalidType, }, { "failure: malformed proof fails to unmarshal", func() { path = sm.GetClientStatePath(counterpartyClientIdentifier) proof = []byte("invalid proof") }, errors.New("failed to unmarshal proof into type"), }, { "failure: consensus state timestamp is greater than signature", func() { consensusState := &solomachine.ConsensusState{ Timestamp: sm.Time + 1, PublicKey: sm.ConsensusState().PublicKey, } clientState = solomachine.NewClientState(sm.Sequence, consensusState) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) }, fmt.Errorf("the consensus state timestamp is greater than the signature timestamp (11 >= 10): %s", solomachine.ErrInvalidProof), }, { "failure: signature data is nil", func() { signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: nil, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, fmt.Errorf("signature data cannot be empty: %s", solomachine.ErrInvalidProof), }, { "failure: consensus state public key is nil", func() { clientState.ConsensusState.PublicKey = nil suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) }, fmt.Errorf("consensus state PublicKey cannot be nil: %s", clienttypes.ErrInvalidConsensus), }, { "failure: malformed signature data fails to unmarshal", func() { signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: []byte("invalid signature data"), Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, errors.New("failed to unmarshal proof into type"), }, { "failure: proof is nil", func() { proof = nil }, fmt.Errorf("proof cannot be empty: %s", solomachine.ErrInvalidProof), }, { "failure: proof verification failed", func() { signBytes.Data = []byte("invalid non-membership data value") signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) }, solomachine.ErrSignatureVerificationFailed, }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() clientState = sm.ClientState() clientID = sm.ClientID path = commitmenttypesv2.NewMerklePath([]byte("ibc"), []byte("solomachine")) merklePath, ok := path.(commitmenttypesv2.MerklePath) suite.Require().True(ok) key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store suite.Require().NoError(err) signBytes = solomachine.SignBytes{ Sequence: sm.GetHeight().GetRevisionHeight(), Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: key, Data: nil, } signBz, err := suite.chainA.Codec.Marshal(&signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) signatureDoc := &solomachine.TimestampedSignatureData{ SignatureData: sig, Timestamp: sm.Time, } proof, err = suite.chainA.Codec.Marshal(signatureDoc) suite.Require().NoError(err) lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) // Set the client state in the store for light client call to find. suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) tc.malleate() var expSeq uint64 if clientState.ConsensusState != nil { expSeq = clientState.Sequence + 1 } // Verify the membership proof err = lightClientModule.VerifyNonMembership( suite.chainA.GetContext(), clientID, clienttypes.ZeroHeight(), 0, 0, proof, path, ) if tc.expErr == nil { // Grab fresh client state after updates. cs, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetClientState(suite.chainA.GetContext(), clientID) suite.Require().True(found) clientState, ok = cs.(*solomachine.ClientState) suite.Require().True(ok) suite.Require().NoError(err) suite.Require().Equal(expSeq, clientState.Sequence) } else { suite.Require().Error(err) suite.Require().ErrorContains(err, tc.expErr.Error()) } }) } } } func (suite *SoloMachineTestSuite) TestRecoverClient() { var ( subjectClientID, substituteClientID string subjectClientState, substituteClientState *solomachine.ClientState ) testCases := []struct { name string malleate func() expErr error }{ { "success", func() { }, nil, }, { "failure: cannot parse malformed substitute client ID", func() { substituteClientID = ibctesting.InvalidID }, host.ErrInvalidID, }, { "failure: substitute client ID does not contain 06-solomachine prefix", func() { substituteClientID = wasmClientID }, clienttypes.ErrInvalidClientType, }, { "failure: cannot find subject client state", func() { subjectClientID = unusedSmClientID }, clienttypes.ErrClientNotFound, }, { "failure: cannot find substitute client state", func() { substituteClientID = unusedSmClientID }, clienttypes.ErrClientNotFound, }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() // reset ctx := suite.chainA.GetContext() subjectClientID = suite.chainA.App.GetIBCKeeper().ClientKeeper.GenerateClientIdentifier(ctx, exported.Solomachine) subject := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, substituteClientID, "testing", 1) subjectClientState = subject.ClientState() substituteClientID = suite.chainA.App.GetIBCKeeper().ClientKeeper.GenerateClientIdentifier(ctx, exported.Solomachine) substitute := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, substituteClientID, "testing", 1) substitute.Sequence++ // increase sequence so that latest height of substitute is > than subject's latest height substituteClientState = substitute.ClientState() clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, substituteClientID) clientStore.Get(host.ClientStateKey()) bz := clienttypes.MustMarshalClientState(suite.chainA.Codec, substituteClientState) clientStore.Set(host.ClientStateKey(), bz) subjectClientState.IsFrozen = true suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(ctx, subjectClientID, subjectClientState) lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), subjectClientID) suite.Require().NoError(err) tc.malleate() err = lightClientModule.RecoverClient(ctx, subjectClientID, substituteClientID) if tc.expErr == nil { suite.Require().NoError(err) // assert that status of subject client is now Active clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, subjectClientID) bz = clientStore.Get(host.ClientStateKey()) smClientState, ok := clienttypes.MustUnmarshalClientState(suite.chainA.Codec, bz).(*solomachine.ClientState) suite.Require().True(ok) suite.Require().Equal(substituteClientState.ConsensusState, smClientState.ConsensusState) suite.Require().Equal(substituteClientState.Sequence, smClientState.Sequence) suite.Require().Equal(exported.Active, lightClientModule.Status(ctx, subjectClientID)) } else { suite.Require().Error(err) suite.Require().ErrorIs(err, tc.expErr) } }) } } func (suite *SoloMachineTestSuite) TestUpdateState() { var ( clientState *solomachine.ClientState clientMsg exported.ClientMessage clientID string ) // test singlesig and multisig public keys for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { testCases := []struct { name string malleate func() expPanic error }{ { "successful update", func() {}, nil, }, { "invalid type misbehaviour no-ops", func() { clientState = sm.ClientState() clientMsg = sm.CreateMisbehaviour() suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) }, nil, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound), }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() clientID = sm.ClientID clientState = sm.ClientState() clientMsg = sm.CreateHeader(sm.Diversifier) lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) tc.malleate() // setup test store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID) var consensusHeights []exported.Height updateStateFunc := func() { consensusHeights = lightClientModule.UpdateState(suite.chainA.GetContext(), clientID, clientMsg) } if tc.expPanic == nil { updateStateFunc() clientStateBz := store.Get(host.ClientStateKey()) suite.Require().NotEmpty(clientStateBz) newClientState := clienttypes.MustUnmarshalClientState(suite.chainA.Codec, clientStateBz) if len(consensusHeights) == 0 { suite.Require().Equal(clientState, newClientState) return } suite.Require().Len(consensusHeights, 1) suite.Require().Equal(uint64(0), consensusHeights[0].GetRevisionNumber()) suite.Require().Equal(newClientState.(*solomachine.ClientState).Sequence, consensusHeights[0].GetRevisionHeight()) suite.Require().False(newClientState.(*solomachine.ClientState).IsFrozen) suite.Require().Equal(clientMsg.(*solomachine.Header).NewPublicKey, newClientState.(*solomachine.ClientState).ConsensusState.PublicKey) suite.Require().Equal(clientMsg.(*solomachine.Header).NewDiversifier, newClientState.(*solomachine.ClientState).ConsensusState.Diversifier) suite.Require().Equal(clientMsg.(*solomachine.Header).Timestamp, newClientState.(*solomachine.ClientState).ConsensusState.Timestamp) } else { suite.Require().PanicsWithError(tc.expPanic.Error(), updateStateFunc) } }) } } } func (suite *SoloMachineTestSuite) TestCheckForMisbehaviour() { var ( clientMsg exported.ClientMessage clientID string ) // test singlesig and multisig public keys for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { testCases := []struct { name string malleate func() foundMisbehaviour bool expPanic error }{ { "success", func() { clientMsg = sm.CreateMisbehaviour() }, true, nil, }, { "failure: normal header returns false", func() { clientMsg = sm.CreateHeader(sm.Diversifier) }, false, nil, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, false, fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound), }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() clientID = sm.ClientID lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, sm.ClientState()) tc.malleate() var foundMisbehaviour bool foundMisbehaviourFunc := func() { foundMisbehaviour = lightClientModule.CheckForMisbehaviour(suite.chainA.GetContext(), clientID, clientMsg) } if tc.expPanic == nil { foundMisbehaviourFunc() suite.Require().Equal(tc.foundMisbehaviour, foundMisbehaviour) } else { suite.Require().PanicsWithError(tc.expPanic.Error(), foundMisbehaviourFunc) suite.Require().False(foundMisbehaviour) } }) } } } func (suite *SoloMachineTestSuite) TestUpdateStateOnMisbehaviour() { var clientID string // test singlesig and multisig public keys for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { testCases := []struct { name string malleate func() expPanic error }{ { "success", func() {}, nil, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound), }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() clientID = sm.ClientID lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, sm.ClientState()) tc.malleate() updateOnMisbehaviourFunc := func() { lightClientModule.UpdateStateOnMisbehaviour(suite.chainA.GetContext(), clientID, nil) } if tc.expPanic == nil { updateOnMisbehaviourFunc() store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID) clientStateBz := store.Get(host.ClientStateKey()) suite.Require().NotEmpty(clientStateBz) newClientState := clienttypes.MustUnmarshalClientState(suite.chainA.Codec, clientStateBz) suite.Require().True(newClientState.(*solomachine.ClientState).IsFrozen) } else { suite.Require().PanicsWithError(tc.expPanic.Error(), updateOnMisbehaviourFunc) } }) } } } func (suite *SoloMachineTestSuite) TestVerifyClientMessageHeader() { var ( clientID string clientMsg exported.ClientMessage ) // test singlesig and multisig public keys for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { testCases := []struct { name string malleate func() expErr error }{ { "success: successful header", func() { clientMsg = sm.CreateHeader(sm.Diversifier) }, nil, }, { "success: successful header with new diversifier", func() { clientMsg = sm.CreateHeader(sm.Diversifier + "0") }, nil, }, { "success: successful misbehaviour", func() { clientMsg = sm.CreateMisbehaviour() }, nil, }, { "failure: invalid client message type", func() { clientMsg = &ibctm.Header{} }, clienttypes.ErrInvalidClientType, }, { "failure: invalid header Signature", func() { h := sm.CreateHeader(sm.Diversifier) h.Signature = suite.GetInvalidProof() clientMsg = h }, errors.New("proto: wrong wireType = 0 for field Multi"), }, { "failure: invalid timestamp in header", func() { h := sm.CreateHeader(sm.Diversifier) h.Timestamp-- clientMsg = h }, clienttypes.ErrInvalidHeader, }, { "failure: signature uses wrong sequence", func() { sm.Sequence++ clientMsg = sm.CreateHeader(sm.Diversifier) }, solomachine.ErrSignatureVerificationFailed, }, { "signature uses new pubkey to sign", func() { // store in temp before assigning to interface type cs := sm.ClientState() h := sm.CreateHeader(sm.Diversifier) publicKey, err := codectypes.NewAnyWithValue(sm.PublicKey) suite.NoError(err) data := &solomachine.HeaderData{ NewPubKey: publicKey, NewDiversifier: h.NewDiversifier, } dataBz, err := suite.chainA.Codec.Marshal(data) suite.Require().NoError(err) // generate invalid signature signBytes := &solomachine.SignBytes{ Sequence: cs.Sequence, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: []byte("invalid signature data"), Data: dataBz, } signBz, err := suite.chainA.Codec.Marshal(signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(signBz) suite.Require().NoError(err) h.Signature = sig clientMsg = h }, solomachine.ErrSignatureVerificationFailed, }, { "failure: signature signs over old pubkey", func() { // store in temp before assigning to interface type cs := sm.ClientState() oldPubKey := sm.PublicKey h := sm.CreateHeader(sm.Diversifier) // generate invalid signature data := append(sdk.Uint64ToBigEndian(cs.Sequence), oldPubKey.Bytes()...) sig := sm.GenerateSignature(data) h.Signature = sig clientMsg = h }, solomachine.ErrSignatureVerificationFailed, }, { "failure: consensus state public key is nil - header", func() { h := sm.CreateHeader(sm.Diversifier) h.NewPublicKey = nil clientMsg = h }, solomachine.ErrSignatureVerificationFailed, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound), }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() clientID = sm.ClientID lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, sm.ClientState()) tc.malleate() err = lightClientModule.VerifyClientMessage(suite.chainA.GetContext(), clientID, clientMsg) if tc.expErr == nil { suite.Require().NoError(err) } else { suite.Require().ErrorContains(err, tc.expErr.Error()) } }) } } } func (suite *SoloMachineTestSuite) TestVerifyClientMessageMisbehaviour() { var ( clientMsg exported.ClientMessage clientState *solomachine.ClientState clientID string ) // test singlesig and multisig public keys for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { testCases := []struct { name string malleate func() expErr error }{ { "success: successful misbehaviour", func() { clientMsg = sm.CreateMisbehaviour() }, nil, }, { "success: old misbehaviour is successful (timestamp is less than current consensus state)", func() { clientState = sm.ClientState() sm.Time -= 5 clientMsg = sm.CreateMisbehaviour() }, nil, }, { "failure: invalid client message type", func() { clientMsg = &ibctm.Header{} }, clienttypes.ErrInvalidClientType, }, { "failure: consensus state pubkey is nil", func() { clientState.ConsensusState.PublicKey = nil clientMsg = sm.CreateMisbehaviour() suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) }, clienttypes.ErrInvalidConsensus, }, { "failure: invalid SignatureOne SignatureData", func() { m := sm.CreateMisbehaviour() m.SignatureOne.Signature = suite.GetInvalidProof() clientMsg = m }, errors.New("proto: wrong wireType = 0 for field Multi"), }, { "failure: invalid SignatureTwo SignatureData", func() { m := sm.CreateMisbehaviour() m.SignatureTwo.Signature = suite.GetInvalidProof() clientMsg = m }, errors.New("proto: wrong wireType = 0 for field Multi"), }, { "failure: invalid SignatureOne timestamp", func() { m := sm.CreateMisbehaviour() m.SignatureOne.Timestamp = 1000000000000 clientMsg = m }, solomachine.ErrSignatureVerificationFailed, }, { "failure: invalid SignatureTwo timestamp", func() { m := sm.CreateMisbehaviour() m.SignatureTwo.Timestamp = 1000000000000 clientMsg = m }, solomachine.ErrSignatureVerificationFailed, }, { "failure: invalid first signature data", func() { // store in temp before assigning to interface type m := sm.CreateMisbehaviour() msg := []byte("DATA ONE") signBytes := &solomachine.SignBytes{ Sequence: sm.Sequence + 1, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: []byte("invalid signature data"), Data: msg, } data, err := suite.chainA.Codec.Marshal(signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(data) m.SignatureOne.Signature = sig m.SignatureOne.Data = msg clientMsg = m }, solomachine.ErrSignatureVerificationFailed, }, { "failure: invalid second signature data", func() { // store in temp before assigning to interface type m := sm.CreateMisbehaviour() msg := []byte("DATA TWO") signBytes := &solomachine.SignBytes{ Sequence: sm.Sequence + 1, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: []byte("invalid signature data"), Data: msg, } data, err := suite.chainA.Codec.Marshal(signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(data) m.SignatureTwo.Signature = sig m.SignatureTwo.Data = msg clientMsg = m }, solomachine.ErrSignatureVerificationFailed, }, { "failure: wrong pubkey generates first signature", func() { badMisbehaviour := sm.CreateMisbehaviour() // update public key to a new one sm.CreateHeader(sm.Diversifier) m := sm.CreateMisbehaviour() // set SignatureOne to use the wrong signature m.SignatureOne = badMisbehaviour.SignatureOne clientMsg = m }, solomachine.ErrSignatureVerificationFailed, }, { "failure: wrong pubkey generates second signature", func() { badMisbehaviour := sm.CreateMisbehaviour() // update public key to a new one sm.CreateHeader(sm.Diversifier) m := sm.CreateMisbehaviour() // set SignatureTwo to use the wrong signature m.SignatureTwo = badMisbehaviour.SignatureTwo clientMsg = m }, solomachine.ErrSignatureVerificationFailed, }, { "failure: signatures sign over different sequence", func() { // store in temp before assigning to interface type m := sm.CreateMisbehaviour() // Signature One msg := []byte("DATA ONE") // sequence used is plus 1 signBytes := &solomachine.SignBytes{ Sequence: sm.Sequence + 1, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: []byte("invalid signature data"), Data: msg, } data, err := suite.chainA.Codec.Marshal(signBytes) suite.Require().NoError(err) sig := sm.GenerateSignature(data) m.SignatureOne.Signature = sig m.SignatureOne.Data = msg // Signature Two msg = []byte("DATA TWO") // sequence used is minus 1 signBytes = &solomachine.SignBytes{ Sequence: sm.Sequence - 1, Timestamp: sm.Time, Diversifier: sm.Diversifier, Path: []byte("invalid signature data"), Data: msg, } data, err = suite.chainA.Codec.Marshal(signBytes) suite.Require().NoError(err) sig = sm.GenerateSignature(data) m.SignatureTwo.Signature = sig m.SignatureTwo.Data = msg clientMsg = m }, solomachine.ErrSignatureVerificationFailed, }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound), }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() clientID = sm.ClientID lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, sm.ClientState()) tc.malleate() err = lightClientModule.VerifyClientMessage(suite.chainA.GetContext(), clientID, clientMsg) if tc.expErr == nil { suite.Require().NoError(err) } else { suite.Require().ErrorContains(err, tc.expErr.Error()) } }) } } } func (suite *SoloMachineTestSuite) TestVerifyUpgradeAndUpdateState() { clientID := suite.solomachine.ClientID lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) err = lightClientModule.VerifyUpgradeAndUpdateState(suite.chainA.GetContext(), clientID, nil, nil, nil, nil) suite.Require().Error(err) } func (suite *SoloMachineTestSuite) TestLatestHeight() { var clientID string testCases := []struct { name string malleate func() expHeight clienttypes.Height }{ { "success", func() {}, // Default as returned by solomachine.ClientState() clienttypes.NewHeight(0, 1), }, { "failure: cannot find client state", func() { clientID = unusedSmClientID }, clienttypes.ZeroHeight(), }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() clientID = suite.solomachine.ClientID clientState := suite.solomachine.ClientState() lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID) suite.Require().NoError(err) suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState) tc.malleate() height := lightClientModule.LatestHeight(suite.chainA.GetContext(), clientID) suite.Require().Equal(tc.expHeight, height) }) } }