diff --git a/modules/light-clients/06-solomachine/client_state.go b/modules/light-clients/06-solomachine/client_state.go index 93e61b7c00a..6c6c56f72d9 100644 --- a/modules/light-clients/06-solomachine/client_state.go +++ b/modules/light-clients/06-solomachine/client_state.go @@ -118,19 +118,9 @@ func (cs *ClientState) VerifyMembership( path []byte, value []byte, ) error { - // TODO: Attempt to refactor code to smaller function - if revision := height.GetRevisionNumber(); revision != 0 { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidHeight, "revision must be 0 for solomachine, got revision-number: %d", revision) - } - - // sequence is encoded in the revision height of height struct - sequence := height.GetRevisionHeight() - latestSequence := cs.GetLatestHeight().GetRevisionHeight() - if latestSequence != sequence { - return sdkerrors.Wrapf( - sdkerrors.ErrInvalidHeight, - "client state sequence != proof sequence (%d != %d)", latestSequence, sequence, - ) + publicKey, sigData, timestamp, sequence, err := produceVerificationArgs(cdc, cs, height, proof) + if err != nil { + return err } var merklePath commitmenttypes.MerklePath @@ -138,35 +128,6 @@ func (cs *ClientState) VerifyMembership( return sdkerrors.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal path into ICS 23 commitment merkle path") } - var timestampedSigData TimestampedSignatureData - if err := cdc.Unmarshal(proof, ×tampedSigData); err != nil { - return sdkerrors.Wrapf(err, "failed to unmarshal proof into type %T", timestampedSigData) - } - - timestamp := timestampedSigData.Timestamp - - if len(timestampedSigData.SignatureData) == 0 { - return sdkerrors.Wrap(ErrInvalidProof, "signature data cannot be empty") - } - - sigData, err := UnmarshalSignatureData(cdc, timestampedSigData.SignatureData) - if err != nil { - return err - } - - if cs.ConsensusState == nil { - return sdkerrors.Wrap(clienttypes.ErrInvalidConsensus, "consensus state cannot be empty") - } - - if cs.ConsensusState.GetTimestamp() > timestamp { - return sdkerrors.Wrapf(ErrInvalidProof, "the consensus state timestamp is greater than the signature timestamp (%d >= %d)", cs.ConsensusState.GetTimestamp(), timestamp) - } - - publicKey, err := cs.ConsensusState.GetPubKey() - if err != nil { - return err - } - signBytes := &SignBytesV2{ Sequence: sequence, Timestamp: timestamp, @@ -203,11 +164,40 @@ func (cs *ClientState) VerifyNonMembership( proof []byte, path []byte, ) error { - // TODO: Implement 06-solomachine VerifyNonMembership + publicKey, sigData, timestamp, sequence, err := produceVerificationArgs(cdc, cs, height, proof) + if err != nil { + return err + } + + var merklePath commitmenttypes.MerklePath + if err := cdc.Unmarshal(path, &merklePath); err != nil { + return sdkerrors.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal path into ICS 23 commitment merkle path") + } + + signBytes := &SignBytesV2{ + Sequence: sequence, + Timestamp: timestamp, + Diversifier: cs.ConsensusState.Diversifier, + Path: []byte(merklePath.String()), + Data: nil, + } + + signBz, err := cdc.Marshal(signBytes) + if err != nil { + return err + } + + if err := VerifySignature(publicKey, signBz, sigData); err != nil { + return err + } + + cs.Sequence++ + cs.ConsensusState.Timestamp = timestamp + setClientState(clientStore, cdc, cs) + return nil } -// TODO: Remove function as no longer used // produceVerificationArgs perfoms the basic checks on the arguments that are // shared between the verification functions and returns the public key of the // consensus state, the unmarshalled proof representing the signature and timestamp @@ -216,34 +206,22 @@ func produceVerificationArgs( cdc codec.BinaryCodec, cs *ClientState, height exported.Height, - prefix exported.Prefix, proof []byte, ) (cryptotypes.PubKey, signing.SignatureData, uint64, uint64, error) { if revision := height.GetRevisionNumber(); revision != 0 { return nil, nil, 0, 0, sdkerrors.Wrapf(sdkerrors.ErrInvalidHeight, "revision must be 0 for solomachine, got revision-number: %d", revision) } - // sequence is encoded in the revision height of height struct - sequence := height.GetRevisionHeight() - if prefix == nil { - return nil, nil, 0, 0, sdkerrors.Wrap(commitmenttypes.ErrInvalidPrefix, "prefix cannot be empty") - } - - _, ok := prefix.(*commitmenttypes.MerklePrefix) - if !ok { - return nil, nil, 0, 0, sdkerrors.Wrapf(commitmenttypes.ErrInvalidPrefix, "invalid prefix type %T, expected MerklePrefix", prefix) - } if proof == nil { return nil, nil, 0, 0, sdkerrors.Wrap(ErrInvalidProof, "proof cannot be empty") } - timestampedSigData := &TimestampedSignatureData{} - if err := cdc.Unmarshal(proof, timestampedSigData); err != nil { + var timestampedSigData TimestampedSignatureData + if err := cdc.Unmarshal(proof, ×tampedSigData); err != nil { return nil, nil, 0, 0, sdkerrors.Wrapf(err, "failed to unmarshal proof into type %T", timestampedSigData) } timestamp := timestampedSigData.Timestamp - if len(timestampedSigData.SignatureData) == 0 { return nil, nil, 0, 0, sdkerrors.Wrap(ErrInvalidProof, "signature data cannot be empty") } @@ -257,6 +235,8 @@ func produceVerificationArgs( return nil, nil, 0, 0, sdkerrors.Wrap(clienttypes.ErrInvalidConsensus, "consensus state cannot be empty") } + // sequence is encoded in the revision height of height struct + sequence := height.GetRevisionHeight() latestSequence := cs.GetLatestHeight().GetRevisionHeight() if latestSequence != sequence { return nil, nil, 0, 0, sdkerrors.Wrapf( diff --git a/modules/light-clients/06-solomachine/client_state_test.go b/modules/light-clients/06-solomachine/client_state_test.go index ce05993abe7..984bb1b1547 100644 --- a/modules/light-clients/06-solomachine/client_state_test.go +++ b/modules/light-clients/06-solomachine/client_state_test.go @@ -536,6 +536,13 @@ func (suite *SoloMachineTestSuite) TestVerifyMembership() { }, false, }, + { + "proof is nil", + func() { + proof = nil + }, + false, + }, { "proof verification failed", func() { @@ -605,6 +612,234 @@ func (suite *SoloMachineTestSuite) TestVerifyMembership() { } } +func (suite *SoloMachineTestSuite) TestVerifyNonMembership() { + // test singlesig and multisig public keys + for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { + + var ( + clientState *solomachine.ClientState + err error + height clienttypes.Height + path []byte + proof []byte + signBytes solomachine.SignBytesV2 + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", + func() {}, + true, + }, + { + "success: packet receipt absence verification", + func() { + merklePath := suite.solomachine.GetPacketReceiptPath(ibctesting.MockPort, ibctesting.FirstChannelID) + signBytes = solomachine.SignBytesV2{ + Sequence: sm.GetHeight().GetRevisionHeight(), + Timestamp: sm.Time, + Diversifier: sm.Diversifier, + Path: []byte(merklePath.String()), + 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, + } + + path, err = suite.chainA.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, err = suite.chainA.Codec.Marshal(signatureDoc) + suite.Require().NoError(err) + }, + true, + }, + { + "consensus state in client state is nil", + func() { + clientState = solomachine.NewClientState(1, nil, false) + }, + false, + }, + { + "client state latest height is less than sequence", + func() { + consensusState := &solomachine.ConsensusState{ + Timestamp: sm.Time, + PublicKey: sm.ConsensusState().PublicKey, + } + + clientState = solomachine.NewClientState(sm.Sequence-1, consensusState, false) + }, + false, + }, + { + "height revision number is not zero", + func() { + height = clienttypes.NewHeight(1, sm.GetHeight().GetRevisionHeight()) + }, + false, + }, + { + "malformed merkle path fails to unmarshal", + func() { + path = []byte("invalid path") + }, + false, + }, + { + "malformed proof fails to unmarshal", + func() { + merklePath := suite.solomachine.GetClientStatePath(counterpartyClientIdentifier) + path, err = suite.chainA.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof = []byte("invalid proof") + }, + false, + }, + { + "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, false) + }, + false, + }, + { + "signature data is nil", + func() { + signatureDoc := &solomachine.TimestampedSignatureData{ + SignatureData: nil, + Timestamp: sm.Time, + } + + proof, err = suite.chainA.Codec.Marshal(signatureDoc) + suite.Require().NoError(err) + }, + false, + }, + { + "consensus state public key is nil", + func() { + clientState.ConsensusState.PublicKey = nil + }, + false, + }, + { + "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) + }, + false, + }, + { + "proof is nil", + func() { + proof = nil + }, + false, + }, + { + "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) + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + clientState = sm.ClientState() + height = clienttypes.NewHeight(sm.GetHeight().GetRevisionNumber(), sm.GetHeight().GetRevisionHeight()) + + merklePath := commitmenttypes.NewMerklePath("ibc", "solomachine") + signBytes = solomachine.SignBytesV2{ + Sequence: sm.GetHeight().GetRevisionHeight(), + Timestamp: sm.Time, + Diversifier: sm.Diversifier, + Path: []byte(merklePath.String()), + 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, + } + + path, err = suite.chainA.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, err = suite.chainA.Codec.Marshal(signatureDoc) + suite.Require().NoError(err) + + tc.malleate() + + var expSeq uint64 + if clientState.ConsensusState != nil { + expSeq = clientState.Sequence + 1 + } + + err = clientState.VerifyNonMembership( + suite.chainA.GetContext(), suite.store, suite.chainA.Codec, + height, 0, 0, // solomachine does not check delay periods + proof, path, + ) + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expSeq, clientState.Sequence) + suite.Require().Equal(expSeq, suite.GetSequenceFromStore(), "sequence not updated in the store (%d) on valid test case %s", suite.GetSequenceFromStore(), tc.name) + } else { + suite.Require().Error(err) + } + }) + } + } +} + func (suite *SoloMachineTestSuite) TestGetTimestampAtHeight() { tmPath := ibctesting.NewPath(suite.chainA, suite.chainB) suite.coordinator.SetupClients(tmPath)