07-tendermint: ignore misbehaviour if age is greater than consensus params (#6422)
* 07-tendermint: ignore misbehaviour if age is greater than consensus params * update tests and errors * add comment on consensus params * Update x/ibc/02-client/handler.go Co-authored-by: Anton Kaliaev <anton.kalyaev@gmail.com> * update evidence GetTime to use Max Co-authored-by: Anton Kaliaev <anton.kalyaev@gmail.com>
This commit is contained in:
parent
257354dbff
commit
fe9356ddb8
|
@ -86,7 +86,9 @@ func HandlerClientMisbehaviour(k Keeper) evidencetypes.Handler {
|
||||||
return func(ctx sdk.Context, evidence evidenceexported.Evidence) error {
|
return func(ctx sdk.Context, evidence evidenceexported.Evidence) error {
|
||||||
misbehaviour, ok := evidence.(exported.Misbehaviour)
|
misbehaviour, ok := evidence.(exported.Misbehaviour)
|
||||||
if !ok {
|
if !ok {
|
||||||
return types.ErrInvalidEvidence
|
return sdkerrors.Wrapf(types.ErrInvalidEvidence,
|
||||||
|
"expected evidence to implement client Misbehaviour interface, got %T", evidence,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return k.CheckMisbehaviourAndUpdateState(ctx, misbehaviour)
|
return k.CheckMisbehaviourAndUpdateState(ctx, misbehaviour)
|
||||||
|
|
|
@ -140,7 +140,7 @@ func (k Keeper) CheckMisbehaviourAndUpdateState(ctx sdk.Context, misbehaviour ex
|
||||||
switch e := misbehaviour.(type) {
|
switch e := misbehaviour.(type) {
|
||||||
case ibctmtypes.Evidence:
|
case ibctmtypes.Evidence:
|
||||||
clientState, err = tendermint.CheckMisbehaviourAndUpdateState(
|
clientState, err = tendermint.CheckMisbehaviourAndUpdateState(
|
||||||
clientState, consensusState, misbehaviour, consensusState.GetHeight(), ctx.BlockTime(),
|
clientState, consensusState, misbehaviour, consensusState.GetHeight(), ctx.BlockTime(), ctx.ConsensusParams(),
|
||||||
)
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -3,7 +3,7 @@ package tendermint
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
lite "github.com/tendermint/tendermint/lite2"
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported"
|
clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported"
|
||||||
|
@ -22,6 +22,7 @@ func CheckMisbehaviourAndUpdateState(
|
||||||
misbehaviour clientexported.Misbehaviour,
|
misbehaviour clientexported.Misbehaviour,
|
||||||
height uint64, // height at which the consensus state was loaded
|
height uint64, // height at which the consensus state was loaded
|
||||||
currentTimestamp time.Time,
|
currentTimestamp time.Time,
|
||||||
|
consensusParams *abci.ConsensusParams,
|
||||||
) (clientexported.ClientState, error) {
|
) (clientexported.ClientState, error) {
|
||||||
|
|
||||||
// cast the interface to specific types before checking for misbehaviour
|
// cast the interface to specific types before checking for misbehaviour
|
||||||
|
@ -47,9 +48,9 @@ func CheckMisbehaviourAndUpdateState(
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := checkMisbehaviour(
|
if err := checkMisbehaviour(
|
||||||
tmClientState, tmConsensusState, tmEvidence, height, currentTimestamp,
|
tmClientState, tmConsensusState, tmEvidence, height, currentTimestamp, consensusParams,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tmClientState.FrozenHeight = uint64(tmEvidence.GetHeight())
|
tmClientState.FrozenHeight = uint64(tmEvidence.GetHeight())
|
||||||
|
@ -59,8 +60,32 @@ func CheckMisbehaviourAndUpdateState(
|
||||||
// checkMisbehaviour checks if the evidence provided is a valid light client misbehaviour
|
// checkMisbehaviour checks if the evidence provided is a valid light client misbehaviour
|
||||||
func checkMisbehaviour(
|
func checkMisbehaviour(
|
||||||
clientState types.ClientState, consensusState types.ConsensusState, evidence types.Evidence,
|
clientState types.ClientState, consensusState types.ConsensusState, evidence types.Evidence,
|
||||||
height uint64, currentTimestamp time.Time,
|
height uint64, currentTimestamp time.Time, consensusParams *abci.ConsensusParams,
|
||||||
) error {
|
) error {
|
||||||
|
// calculate the age of the misbehaviour evidence
|
||||||
|
infractionHeight := evidence.GetHeight()
|
||||||
|
infractionTime := evidence.GetTime()
|
||||||
|
ageDuration := currentTimestamp.Sub(infractionTime)
|
||||||
|
ageBlocks := height - uint64(infractionHeight)
|
||||||
|
|
||||||
|
// Reject misbehaviour if the age is too old. Evidence is considered stale
|
||||||
|
// if the difference in time and number of blocks is greater than the allowed
|
||||||
|
// parameters defined.
|
||||||
|
//
|
||||||
|
// NOTE: The first condition is a safety check as the consensus params cannot
|
||||||
|
// be nil since the previous param values will be used in case they can't be
|
||||||
|
// retreived. If they are not set during initialization, Tendermint will always
|
||||||
|
// use the default values.
|
||||||
|
if consensusParams != nil &&
|
||||||
|
consensusParams.Evidence != nil &&
|
||||||
|
ageDuration > consensusParams.Evidence.MaxAgeDuration &&
|
||||||
|
ageBlocks > uint64(consensusParams.Evidence.MaxAgeNumBlocks) {
|
||||||
|
return sdkerrors.Wrapf(clienttypes.ErrInvalidEvidence,
|
||||||
|
"age duration (%s) and age blocks (%d) are greater than max consensus params for duration (%s) and block (%d)",
|
||||||
|
ageDuration, ageBlocks, consensusParams.Evidence.MaxAgeDuration, consensusParams.Evidence.MaxAgeNumBlocks,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// check if provided height matches the headers' height
|
// check if provided height matches the headers' height
|
||||||
if height > uint64(evidence.GetHeight()) {
|
if height > uint64(evidence.GetHeight()) {
|
||||||
return sdkerrors.Wrapf(
|
return sdkerrors.Wrapf(
|
||||||
|
@ -81,21 +106,18 @@ func checkMisbehaviour(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Evidence must be within trusting period
|
|
||||||
// Blocked on https://github.com/cosmos/ics/issues/379
|
|
||||||
|
|
||||||
// - ValidatorSet must have 2/3 similarity with trusted FromValidatorSet
|
// - ValidatorSet must have 2/3 similarity with trusted FromValidatorSet
|
||||||
// - ValidatorSets on both headers are valid given the last trusted ValidatorSet
|
// - ValidatorSets on both headers are valid given the last trusted ValidatorSet
|
||||||
if err := consensusState.ValidatorSet.VerifyCommitTrusting(
|
if err := consensusState.ValidatorSet.VerifyCommitTrusting(
|
||||||
evidence.ChainID, evidence.Header1.Commit.BlockID, evidence.Header1.Height,
|
evidence.ChainID, evidence.Header1.Commit.BlockID, evidence.Header1.Height,
|
||||||
evidence.Header1.Commit, lite.DefaultTrustLevel,
|
evidence.Header1.Commit, clientState.TrustLevel,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return sdkerrors.Wrapf(clienttypes.ErrInvalidEvidence, "validator set in header 1 has too much change from last known validator set: %v", err)
|
return sdkerrors.Wrapf(clienttypes.ErrInvalidEvidence, "validator set in header 1 has too much change from last known validator set: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := consensusState.ValidatorSet.VerifyCommitTrusting(
|
if err := consensusState.ValidatorSet.VerifyCommitTrusting(
|
||||||
evidence.ChainID, evidence.Header2.Commit.BlockID, evidence.Header2.Height,
|
evidence.ChainID, evidence.Header2.Commit.BlockID, evidence.Header2.Height,
|
||||||
evidence.Header2.Commit, lite.DefaultTrustLevel,
|
evidence.Header2.Commit, clientState.TrustLevel,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return sdkerrors.Wrapf(clienttypes.ErrInvalidEvidence, "validator set in header 2 has too much change from last known validator set: %v", err)
|
return sdkerrors.Wrapf(clienttypes.ErrInvalidEvidence, "validator set in header 2 has too much change from last known validator set: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,13 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||||
lite "github.com/tendermint/tendermint/lite2"
|
lite "github.com/tendermint/tendermint/lite2"
|
||||||
tmtypes "github.com/tendermint/tendermint/types"
|
tmtypes "github.com/tendermint/tendermint/types"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/simapp"
|
||||||
|
clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported"
|
||||||
tendermint "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint"
|
tendermint "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint"
|
||||||
ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types"
|
ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types"
|
||||||
commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types"
|
commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types"
|
||||||
|
@ -41,10 +44,12 @@ func (suite *TendermintTestSuite) TestCheckMisbehaviour() {
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
clientState ibctmtypes.ClientState
|
clientState clientexported.ClientState
|
||||||
consensusState ibctmtypes.ConsensusState
|
consensusState clientexported.ConsensusState
|
||||||
evidence ibctmtypes.Evidence
|
evidence clientexported.Misbehaviour
|
||||||
|
consensusParams *abci.ConsensusParams
|
||||||
height uint64
|
height uint64
|
||||||
|
timestamp time.Time
|
||||||
expPass bool
|
expPass bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -57,7 +62,9 @@ func (suite *TendermintTestSuite) TestCheckMisbehaviour() {
|
||||||
ChainID: chainID,
|
ChainID: chainID,
|
||||||
ClientID: chainID,
|
ClientID: chainID,
|
||||||
},
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
height,
|
height,
|
||||||
|
suite.now,
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -70,7 +77,9 @@ func (suite *TendermintTestSuite) TestCheckMisbehaviour() {
|
||||||
ChainID: chainID,
|
ChainID: chainID,
|
||||||
ClientID: chainID,
|
ClientID: chainID,
|
||||||
},
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
height - 1,
|
height - 1,
|
||||||
|
suite.now,
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -83,9 +92,111 @@ func (suite *TendermintTestSuite) TestCheckMisbehaviour() {
|
||||||
ChainID: chainID,
|
ChainID: chainID,
|
||||||
ClientID: chainID,
|
ClientID: chainID,
|
||||||
},
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
height - 1,
|
height - 1,
|
||||||
|
suite.now,
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"invalid tendermint client state",
|
||||||
|
nil,
|
||||||
|
ibctmtypes.ConsensusState{Timestamp: suite.now, Root: commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), ValidatorSet: bothValSet},
|
||||||
|
ibctmtypes.Evidence{
|
||||||
|
Header1: ibctmtypes.CreateTestHeader(chainID, height, suite.now, altValSet, altSigners),
|
||||||
|
Header2: ibctmtypes.CreateTestHeader(chainID, height, suite.now.Add(time.Minute), bothValSet, bothSigners),
|
||||||
|
ChainID: chainID,
|
||||||
|
ClientID: chainID,
|
||||||
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
|
height,
|
||||||
|
suite.now,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"already frozen client state",
|
||||||
|
ibctmtypes.ClientState{FrozenHeight: 1},
|
||||||
|
ibctmtypes.ConsensusState{Timestamp: suite.now, Root: commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), ValidatorSet: bothValSet},
|
||||||
|
ibctmtypes.Evidence{
|
||||||
|
Header1: ibctmtypes.CreateTestHeader(chainID, height, suite.now, bothValSet, bothSigners),
|
||||||
|
Header2: ibctmtypes.CreateTestHeader(chainID, height, suite.now.Add(time.Minute), bothValSet, bothSigners),
|
||||||
|
ChainID: chainID,
|
||||||
|
ClientID: chainID,
|
||||||
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
|
height,
|
||||||
|
suite.now,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid tendermint consensus state",
|
||||||
|
ibctmtypes.NewClientState(chainID, lite.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, suite.header, commitmenttypes.GetSDKSpecs()),
|
||||||
|
nil,
|
||||||
|
ibctmtypes.Evidence{
|
||||||
|
Header1: ibctmtypes.CreateTestHeader(chainID, height, suite.now, altValSet, altSigners),
|
||||||
|
Header2: ibctmtypes.CreateTestHeader(chainID, height, suite.now.Add(time.Minute), bothValSet, bothSigners),
|
||||||
|
ChainID: chainID,
|
||||||
|
ClientID: chainID,
|
||||||
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
|
height,
|
||||||
|
suite.now,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid tendermint misbehaviour evidence",
|
||||||
|
ibctmtypes.NewClientState(chainID, lite.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, suite.header, commitmenttypes.GetSDKSpecs()),
|
||||||
|
ibctmtypes.ConsensusState{Timestamp: suite.now, Root: commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), ValidatorSet: bothValSet},
|
||||||
|
nil,
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
|
height,
|
||||||
|
suite.now,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rejected misbehaviour due to expired age",
|
||||||
|
ibctmtypes.NewClientState(chainID, lite.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, suite.header, commitmenttypes.GetSDKSpecs()),
|
||||||
|
ibctmtypes.ConsensusState{Timestamp: suite.now, Root: commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), ValidatorSet: bothValSet},
|
||||||
|
ibctmtypes.Evidence{
|
||||||
|
Header1: ibctmtypes.CreateTestHeader(chainID, height, suite.now, bothValSet, bothSigners),
|
||||||
|
Header2: ibctmtypes.CreateTestHeader(chainID, height, suite.now.Add(time.Minute), bothValSet, bothSigners),
|
||||||
|
ChainID: chainID,
|
||||||
|
ClientID: chainID,
|
||||||
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
|
2*height + uint64(simapp.DefaultConsensusParams.Evidence.MaxAgeNumBlocks),
|
||||||
|
suite.now.Add(2 * time.Minute).Add(simapp.DefaultConsensusParams.Evidence.MaxAgeDuration),
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"provided height ≠ header height",
|
||||||
|
ibctmtypes.NewClientState(chainID, lite.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, suite.header, commitmenttypes.GetSDKSpecs()),
|
||||||
|
ibctmtypes.ConsensusState{Timestamp: suite.now, Root: commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), ValidatorSet: bothValSet},
|
||||||
|
ibctmtypes.Evidence{
|
||||||
|
Header1: ibctmtypes.CreateTestHeader(chainID, height, suite.now, bothValSet, bothSigners),
|
||||||
|
Header2: ibctmtypes.CreateTestHeader(chainID, height, suite.now.Add(time.Minute), bothValSet, bothSigners),
|
||||||
|
ChainID: chainID,
|
||||||
|
ClientID: chainID,
|
||||||
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
|
height + 10,
|
||||||
|
suite.now,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unbonding period expired",
|
||||||
|
ibctmtypes.NewClientState(chainID, lite.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, suite.header, commitmenttypes.GetSDKSpecs()),
|
||||||
|
ibctmtypes.ConsensusState{Timestamp: time.Time{}, Root: commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), ValidatorSet: bothValSet},
|
||||||
|
ibctmtypes.Evidence{
|
||||||
|
Header1: ibctmtypes.CreateTestHeader(chainID, height, suite.now, bothValSet, bothSigners),
|
||||||
|
Header2: ibctmtypes.CreateTestHeader(chainID, height, suite.now.Add(time.Minute), bothValSet, bothSigners),
|
||||||
|
ChainID: chainID,
|
||||||
|
ClientID: chainID,
|
||||||
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
|
height,
|
||||||
|
suite.now,
|
||||||
|
false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"first valset has too much change",
|
"first valset has too much change",
|
||||||
ibctmtypes.NewClientState(chainID, lite.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, suite.header, commitmenttypes.GetSDKSpecs()),
|
ibctmtypes.NewClientState(chainID, lite.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, suite.header, commitmenttypes.GetSDKSpecs()),
|
||||||
|
@ -96,7 +207,9 @@ func (suite *TendermintTestSuite) TestCheckMisbehaviour() {
|
||||||
ChainID: chainID,
|
ChainID: chainID,
|
||||||
ClientID: chainID,
|
ClientID: chainID,
|
||||||
},
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
height,
|
height,
|
||||||
|
suite.now,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -109,7 +222,9 @@ func (suite *TendermintTestSuite) TestCheckMisbehaviour() {
|
||||||
ChainID: chainID,
|
ChainID: chainID,
|
||||||
ClientID: chainID,
|
ClientID: chainID,
|
||||||
},
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
height,
|
height,
|
||||||
|
suite.now,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -122,7 +237,9 @@ func (suite *TendermintTestSuite) TestCheckMisbehaviour() {
|
||||||
ChainID: chainID,
|
ChainID: chainID,
|
||||||
ClientID: chainID,
|
ClientID: chainID,
|
||||||
},
|
},
|
||||||
|
simapp.DefaultConsensusParams,
|
||||||
height,
|
height,
|
||||||
|
suite.now,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -130,7 +247,7 @@ func (suite *TendermintTestSuite) TestCheckMisbehaviour() {
|
||||||
for i, tc := range testCases {
|
for i, tc := range testCases {
|
||||||
tc := tc
|
tc := tc
|
||||||
|
|
||||||
clientState, err := tendermint.CheckMisbehaviourAndUpdateState(tc.clientState, tc.consensusState, tc.evidence, tc.height, suite.now)
|
clientState, err := tendermint.CheckMisbehaviourAndUpdateState(tc.clientState, tc.consensusState, tc.evidence, tc.height, tc.timestamp, tc.consensusParams)
|
||||||
|
|
||||||
if tc.expPass {
|
if tc.expPass {
|
||||||
suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name)
|
suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
@ -73,6 +74,14 @@ func (ev Evidence) GetHeight() int64 {
|
||||||
return int64(math.Min(float64(ev.Header1.Height), float64(ev.Header2.Height)))
|
return int64(math.Min(float64(ev.Header1.Height), float64(ev.Header2.Height)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTime returns the timestamp at which misbehaviour occurred. It uses the
|
||||||
|
// maximum value from both headers to prevent producing an invalid header outside
|
||||||
|
// of the evidence age range.
|
||||||
|
func (ev Evidence) GetTime() time.Time {
|
||||||
|
minTime := int64(math.Max(float64(ev.Header1.Time.UnixNano()), float64(ev.Header2.Time.UnixNano())))
|
||||||
|
return time.Unix(0, minTime)
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateBasic implements Evidence interface
|
// ValidateBasic implements Evidence interface
|
||||||
func (ev Evidence) ValidateBasic() error {
|
func (ev Evidence) ValidateBasic() error {
|
||||||
if err := host.ClientIdentifierValidator(ev.ClientID); err != nil {
|
if err := host.ClientIdentifierValidator(ev.ClientID); err != nil {
|
||||||
|
|
Loading…
Reference in New Issue