From e43152102e898ea4b9bac6b95ca24e88dd70aee2 Mon Sep 17 00:00:00 2001 From: Ye Ding Date: Tue, 20 Dec 2022 18:41:47 +0800 Subject: [PATCH] Identify invalid signature within batch verification (#11582) (#11741) * Identify invalid signature within batch verification (#11582) * Fix issues found by linter * Make deepsource happy * Add signature description enums * Make descriptions a mandatory field * More readable error message of invalid signatures * Add 'enable-verbose-sig-verification' option * Fix format * Move descriptions to package signing * Add more validation and test cases * Fix build failure Co-authored-by: Nishant Das --- beacon-chain/blockchain/process_block.go | 20 +- beacon-chain/core/blocks/signature.go | 23 +- beacon-chain/core/blocks/withdrawals.go | 8 +- beacon-chain/core/signing/signing_root.go | 34 ++- beacon-chain/core/transition/BUILD.bazel | 1 + beacon-chain/core/transition/transition.go | 9 +- .../transition_no_verify_sig_test.go | 12 + beacon-chain/sync/batch_verifier_test.go | 15 +- beacon-chain/sync/validate_aggregate_proof.go | 14 +- .../sync/validate_sync_committee_message.go | 7 +- .../sync/validate_sync_contribution_proof.go | 21 +- config/features/config.go | 6 + config/features/flags.go | 5 + crypto/bls/bls.go | 5 + crypto/bls/blst/signature.go | 9 + crypto/bls/blst/signature_test.go | 25 ++ crypto/bls/signature_batch.go | 92 ++++-- crypto/bls/signature_batch_test.go | 284 ++++++++++++++---- testing/assert/assertions.go | 10 + testing/assertions/assertions.go | 17 ++ 20 files changed, 486 insertions(+), 131 deletions(-) diff --git a/beacon-chain/blockchain/process_block.go b/beacon-chain/blockchain/process_block.go index ca9442c39b..f6f5d5630e 100644 --- a/beacon-chain/blockchain/process_block.go +++ b/beacon-chain/blockchain/process_block.go @@ -337,11 +337,7 @@ func (s *Service) onBlockBatch(ctx context.Context, blks []interfaces.SignedBeac jCheckpoints := make([]*ethpb.Checkpoint, len(blks)) fCheckpoints := make([]*ethpb.Checkpoint, len(blks)) - sigSet := &bls.SignatureBatch{ - Signatures: [][]byte{}, - PublicKeys: []bls.PublicKey{}, - Messages: [][32]byte{}, - } + sigSet := bls.NewSet() type versionAndHeader struct { version int header interfaces.ExecutionData @@ -381,7 +377,13 @@ func (s *Service) onBlockBatch(ctx context.Context, blks []interfaces.SignedBeac } sigSet.Join(set) } - verify, err := sigSet.Verify() + + var verify bool + if features.Get().EnableVerboseSigVerification { + verify, err = sigSet.VerifyVerbosely() + } else { + verify, err = sigSet.Verify() + } if err != nil { return invalidBlock{error: err} } @@ -529,11 +531,7 @@ func (s *Service) insertBlockToForkchoiceStore(ctx context.Context, blk interfac } } - if err := s.cfg.ForkChoiceStore.InsertNode(ctx, st, root); err != nil { - return err - } - - return nil + return s.cfg.ForkChoiceStore.InsertNode(ctx, st, root) } // This feeds in the attestations included in the block to fork choice store. It's allows fork choice store diff --git a/beacon-chain/core/blocks/signature.go b/beacon-chain/core/blocks/signature.go index 9ba4afd716..d3d7ae0a61 100644 --- a/beacon-chain/core/blocks/signature.go +++ b/beacon-chain/core/blocks/signature.go @@ -19,7 +19,7 @@ import ( ) // retrieves the signature batch from the raw data, public key,signature and domain provided. -func signatureBatch(signedData, pub, signature, domain []byte) (*bls.SignatureBatch, error) { +func signatureBatch(signedData, pub, signature, domain []byte, desc string) (*bls.SignatureBatch, error) { publicKey, err := bls.PublicKeyFromBytes(pub) if err != nil { return nil, errors.Wrap(err, "could not convert bytes to public key") @@ -33,15 +33,16 @@ func signatureBatch(signedData, pub, signature, domain []byte) (*bls.SignatureBa return nil, errors.Wrap(err, "could not hash container") } return &bls.SignatureBatch{ - Signatures: [][]byte{signature}, - PublicKeys: []bls.PublicKey{publicKey}, - Messages: [][32]byte{root}, + Signatures: [][]byte{signature}, + PublicKeys: []bls.PublicKey{publicKey}, + Messages: [][32]byte{root}, + Descriptions: []string{desc}, }, nil } // verifies the signature from the raw data, public key and domain provided. func verifySignature(signedData, pub, signature, domain []byte) error { - set, err := signatureBatch(signedData, pub, signature, domain) + set, err := signatureBatch(signedData, pub, signature, domain, signing.UnknownSignature) if err != nil { return err } @@ -146,7 +147,7 @@ func RandaoSignatureBatch( if err != nil { return nil, err } - set, err := signatureBatch(buf, proposerPub, reveal, domain) + set, err := signatureBatch(buf, proposerPub, reveal, domain, signing.RandaoSignature) if err != nil { return nil, err } @@ -186,6 +187,7 @@ func createAttestationSignatureBatch( sigs := make([][]byte, len(atts)) pks := make([]bls.PublicKey, len(atts)) msgs := make([][32]byte, len(atts)) + descs := make([]string, len(atts)) for i, a := range atts { sigs[i] = a.Signature c, err := helpers.BeaconCommitteeFromState(ctx, beaconState, a.Data.Slot, a.Data.CommitteeIndex) @@ -216,11 +218,14 @@ func createAttestationSignatureBatch( return nil, errors.Wrap(err, "could not get signing root of object") } msgs[i] = root + + descs[i] = signing.AttestationSignature } return &bls.SignatureBatch{ - Signatures: sigs, - PublicKeys: pks, - Messages: msgs, + Signatures: sigs, + PublicKeys: pks, + Messages: msgs, + Descriptions: descs, }, nil } diff --git a/beacon-chain/core/blocks/withdrawals.go b/beacon-chain/core/blocks/withdrawals.go index 4e05059590..79ec767cab 100644 --- a/beacon-chain/core/blocks/withdrawals.go +++ b/beacon-chain/core/blocks/withdrawals.go @@ -170,9 +170,10 @@ func BLSChangesSignatureBatch( return bls.NewSet(), nil } batch := &bls.SignatureBatch{ - Signatures: make([][]byte, len(changes)), - PublicKeys: make([]bls.PublicKey, len(changes)), - Messages: make([][32]byte, len(changes)), + Signatures: make([][]byte, len(changes)), + PublicKeys: make([]bls.PublicKey, len(changes)), + Messages: make([][32]byte, len(changes)), + Descriptions: make([]string, len(changes)), } epoch := slots.ToEpoch(st.Slot()) domain, err := signing.Domain(st.Fork(), epoch, params.BeaconConfig().DomainBLSToExecutionChange, st.GenesisValidatorsRoot()) @@ -191,6 +192,7 @@ func BLSChangesSignatureBatch( return nil, errors.Wrap(err, "could not compute BLSToExecutionChange signing data") } batch.Messages[i] = htr + batch.Descriptions[i] = signing.BlsChangeSignature } return batch, nil } diff --git a/beacon-chain/core/signing/signing_root.go b/beacon-chain/core/signing/signing_root.go index d36a06ec85..16d86f3d91 100644 --- a/beacon-chain/core/signing/signing_root.go +++ b/beacon-chain/core/signing/signing_root.go @@ -21,6 +21,32 @@ const DomainByteLength = 4 // failed to verify. var ErrSigFailedToVerify = errors.New("signature did not verify") +// List of descriptions for different kinds of signatures +const ( + // UnknownSignature represents all signatures other than below types + UnknownSignature string = "unknown signature" + // BlockSignature represents the block signature from block proposer + BlockSignature = "block signature" + // RandaoSignature represents randao specific signature + RandaoSignature = "randao signature" + // SelectionProof represents selection proof + SelectionProof = "selection proof" + // AggregatorSignature represents aggregator's signature + AggregatorSignature = "aggregator signature" + // AttestationSignature represents aggregated attestation signature + AttestationSignature = "attestation signature" + // BlsChangeSignature represents signature to BLSToExecutionChange + BlsChangeSignature = "blschange signature" + // SyncCommitteeSignature represents sync committee signature + SyncCommitteeSignature = "sync committee signature" + // SyncSelectionProof represents sync committee selection proof + SyncSelectionProof = "sync selection proof" + // ContributionSignature represents sync committee contributor's signature + ContributionSignature = "sync committee contribution signature" + // SyncAggregateSignature represents sync committee aggregator's signature + SyncAggregateSignature = "sync committee aggregator signature" +) + // ComputeDomainAndSign computes the domain and signing root and sign it using the passed in private key. func ComputeDomainAndSign(st state.ReadOnlyBeaconState, epoch types.Epoch, obj fssz.HashRoot, domain [4]byte, key bls.SecretKey) ([]byte, error) { d, err := Domain(st.Fork(), epoch, domain, st.GenesisValidatorsRoot()) @@ -150,10 +176,12 @@ func BlockSignatureBatch(pub, signature, domain []byte, rootFunc func() ([32]byt if err != nil { return nil, errors.Wrap(err, "could not compute signing root") } + desc := BlockSignature return &bls.SignatureBatch{ - Signatures: [][]byte{signature}, - PublicKeys: []bls.PublicKey{publicKey}, - Messages: [][32]byte{root}, + Signatures: [][]byte{signature}, + PublicKeys: []bls.PublicKey{publicKey}, + Messages: [][32]byte{root}, + Descriptions: []string{desc}, }, nil } diff --git a/beacon-chain/core/transition/BUILD.bazel b/beacon-chain/core/transition/BUILD.bazel index 14c25e831f..458b731915 100644 --- a/beacon-chain/core/transition/BUILD.bazel +++ b/beacon-chain/core/transition/BUILD.bazel @@ -37,6 +37,7 @@ go_library( "//beacon-chain/state:go_default_library", "//beacon-chain/state/state-native:go_default_library", "//beacon-chain/state/stateutil:go_default_library", + "//config/features:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/blocks:go_default_library", diff --git a/beacon-chain/core/transition/transition.go b/beacon-chain/core/transition/transition.go index c523441e16..1f44e77c70 100644 --- a/beacon-chain/core/transition/transition.go +++ b/beacon-chain/core/transition/transition.go @@ -17,6 +17,7 @@ import ( "github.com/prysmaticlabs/prysm/v3/beacon-chain/core/execution" "github.com/prysmaticlabs/prysm/v3/beacon-chain/core/time" "github.com/prysmaticlabs/prysm/v3/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v3/config/features" "github.com/prysmaticlabs/prysm/v3/config/params" "github.com/prysmaticlabs/prysm/v3/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v3/consensus-types/interfaces" @@ -66,7 +67,13 @@ func ExecuteStateTransition( if err != nil { return nil, errors.Wrap(err, "could not execute state transition") } - valid, err := set.Verify() + + var valid bool + if features.Get().EnableVerboseSigVerification { + valid, err = set.VerifyVerbosely() + } else { + valid, err = set.Verify() + } if err != nil { return nil, errors.Wrap(err, "could not batch verify signature") } diff --git a/beacon-chain/core/transition/transition_no_verify_sig_test.go b/beacon-chain/core/transition/transition_no_verify_sig_test.go index 24a3d0e5c4..de53d1d056 100644 --- a/beacon-chain/core/transition/transition_no_verify_sig_test.go +++ b/beacon-chain/core/transition/transition_no_verify_sig_test.go @@ -151,6 +151,18 @@ func TestProcessBlockNoVerifyAnySigAltair_OK(t *testing.T) { require.Equal(t, true, verified, "Could not verify signature set") } +func TestProcessBlockNoVerify_SigSetContainsDescriptions(t *testing.T) { + beaconState, block, _, _, _ := createFullBlockWithOperations(t) + wsb, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + set, _, err := transition.ProcessBlockNoVerifyAnySig(context.Background(), beaconState, wsb) + require.NoError(t, err) + assert.Equal(t, len(set.Signatures), len(set.Descriptions), "Signatures and descriptions do not match up") + assert.Equal(t, "block signature", set.Descriptions[0]) + assert.Equal(t, "randao signature", set.Descriptions[1]) + assert.Equal(t, "attestation signature", set.Descriptions[2]) +} + func TestProcessOperationsNoVerifyAttsSigs_OK(t *testing.T) { beaconState, block := createFullAltairBlockWithOperations(t) wsb, err := blocks.NewSignedBeaconBlock(block) diff --git a/beacon-chain/sync/batch_verifier_test.go b/beacon-chain/sync/batch_verifier_test.go index 610c0cfe24..75047f6642 100644 --- a/beacon-chain/sync/batch_verifier_test.go +++ b/beacon-chain/sync/batch_verifier_test.go @@ -5,6 +5,7 @@ import ( "testing" pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/prysmaticlabs/prysm/v3/beacon-chain/core/signing" "github.com/prysmaticlabs/prysm/v3/crypto/bls" "github.com/prysmaticlabs/prysm/v3/testing/assert" "github.com/prysmaticlabs/prysm/v3/testing/util" @@ -16,14 +17,16 @@ func TestValidateWithBatchVerifier(t *testing.T) { sig := keys[0].Sign(make([]byte, 32)) badSig := keys[1].Sign(make([]byte, 32)) validSet := &bls.SignatureBatch{ - Messages: [][32]byte{{}}, - PublicKeys: []bls.PublicKey{keys[0].PublicKey()}, - Signatures: [][]byte{sig.Marshal()}, + Messages: [][32]byte{{}}, + PublicKeys: []bls.PublicKey{keys[0].PublicKey()}, + Signatures: [][]byte{sig.Marshal()}, + Descriptions: []string{signing.UnknownSignature}, } invalidSet := &bls.SignatureBatch{ - Messages: [][32]byte{{}}, - PublicKeys: []bls.PublicKey{keys[0].PublicKey()}, - Signatures: [][]byte{badSig.Marshal()}, + Messages: [][32]byte{{}}, + PublicKeys: []bls.PublicKey{keys[0].PublicKey()}, + Signatures: [][]byte{badSig.Marshal()}, + Descriptions: []string{signing.UnknownSignature}, } tests := []struct { name string diff --git a/beacon-chain/sync/validate_aggregate_proof.go b/beacon-chain/sync/validate_aggregate_proof.go index 4a74536e10..a24969f6d7 100644 --- a/beacon-chain/sync/validate_aggregate_proof.go +++ b/beacon-chain/sync/validate_aggregate_proof.go @@ -299,9 +299,10 @@ func validateSelectionIndex( return nil, err } return &bls.SignatureBatch{ - Signatures: [][]byte{proof}, - PublicKeys: []bls.PublicKey{publicKey}, - Messages: [][32]byte{root}, + Signatures: [][]byte{proof}, + PublicKeys: []bls.PublicKey{publicKey}, + Messages: [][32]byte{root}, + Descriptions: []string{signing.SelectionProof}, }, nil } @@ -326,8 +327,9 @@ func aggSigSet(s state.ReadOnlyBeaconState, a *ethpb.SignedAggregateAttestationA return nil, err } return &bls.SignatureBatch{ - Signatures: [][]byte{a.Signature}, - PublicKeys: []bls.PublicKey{publicKey}, - Messages: [][32]byte{root}, + Signatures: [][]byte{a.Signature}, + PublicKeys: []bls.PublicKey{publicKey}, + Messages: [][32]byte{root}, + Descriptions: []string{signing.AggregatorSignature}, }, nil } diff --git a/beacon-chain/sync/validate_sync_committee_message.go b/beacon-chain/sync/validate_sync_committee_message.go index 05b22e7eed..8fcf4dacfe 100644 --- a/beacon-chain/sync/validate_sync_committee_message.go +++ b/beacon-chain/sync/validate_sync_committee_message.go @@ -243,9 +243,10 @@ func (s *Service) rejectInvalidSyncCommitteeSignature(m *ethpb.SyncCommitteeMess // the signature to a G2 point if batch verification is // enabled. set := &bls.SignatureBatch{ - Messages: [][32]byte{sigRoot}, - PublicKeys: []bls.PublicKey{pKey}, - Signatures: [][]byte{m.Signature}, + Messages: [][32]byte{sigRoot}, + PublicKeys: []bls.PublicKey{pKey}, + Signatures: [][]byte{m.Signature}, + Descriptions: []string{signing.SyncCommitteeSignature}, } return s.validateWithBatchVerifier(ctx, "sync committee message", set) } diff --git a/beacon-chain/sync/validate_sync_contribution_proof.go b/beacon-chain/sync/validate_sync_contribution_proof.go index 3398b8d485..03610b1dce 100644 --- a/beacon-chain/sync/validate_sync_contribution_proof.go +++ b/beacon-chain/sync/validate_sync_contribution_proof.go @@ -236,9 +236,10 @@ func (s *Service) rejectInvalidContributionSignature(m *ethpb.SignedContribution return pubsub.ValidationReject, err } set := &bls.SignatureBatch{ - Messages: [][32]byte{root}, - PublicKeys: []bls.PublicKey{publicKey}, - Signatures: [][]byte{m.Signature}, + Messages: [][32]byte{root}, + PublicKeys: []bls.PublicKey{publicKey}, + Signatures: [][]byte{m.Signature}, + Descriptions: []string{signing.ContributionSignature}, } return s.validateWithBatchVerifier(ctx, "sync contribution signature", set) } @@ -292,9 +293,10 @@ func (s *Service) rejectInvalidSyncAggregateSignature(m *ethpb.SignedContributio return pubsub.ValidationIgnore, err } set := &bls.SignatureBatch{ - Messages: [][32]byte{sigRoot}, - PublicKeys: []bls.PublicKey{aggKey}, - Signatures: [][]byte{m.Message.Contribution.Signature}, + Messages: [][32]byte{sigRoot}, + PublicKeys: []bls.PublicKey{aggKey}, + Signatures: [][]byte{m.Message.Contribution.Signature}, + Descriptions: []string{signing.SyncAggregateSignature}, } return s.validateWithBatchVerifier(ctx, "sync contribution aggregate signature", set) } @@ -403,9 +405,10 @@ func (s *Service) verifySyncSelectionData(ctx context.Context, m *ethpb.Contribu return err } set := &bls.SignatureBatch{ - Messages: [][32]byte{root}, - PublicKeys: []bls.PublicKey{publicKey}, - Signatures: [][]byte{m.SelectionProof}, + Messages: [][32]byte{root}, + PublicKeys: []bls.PublicKey{publicKey}, + Signatures: [][]byte{m.SelectionProof}, + Descriptions: []string{signing.SyncSelectionProof}, } valid, err := s.validateWithBatchVerifier(ctx, "sync contribution selection signature", set) if err != nil { diff --git a/config/features/config.go b/config/features/config.go index ed571a227b..decc393a7a 100644 --- a/config/features/config.go +++ b/config/features/config.go @@ -72,6 +72,8 @@ type Flags struct { DisableStakinContractCheck bool // Disables check for deposit contract when proposing blocks + EnableVerboseSigVerification bool // EnableVerboseSigVerification specifies whether to verify individual signature if batch verification fails + // KeystoreImportDebounceInterval specifies the time duration the validator waits to reload new keys if they have // changed on disk. This feature is for advanced use cases only. KeystoreImportDebounceInterval time.Duration @@ -257,6 +259,10 @@ func ConfigureBeaconChain(ctx *cli.Context) error { logEnabled(enableFullSSZDataLogging) cfg.EnableFullSSZDataLogging = true } + if ctx.IsSet(enableVerboseSigVerification.Name) { + logEnabled(enableVerboseSigVerification) + cfg.EnableVerboseSigVerification = true + } Init(cfg) return nil } diff --git a/config/features/flags.go b/config/features/flags.go index 464d323851..791164c3f6 100644 --- a/config/features/flags.go +++ b/config/features/flags.go @@ -132,6 +132,10 @@ var ( Name: "enable-beacon-rest-api", Usage: "Experimental enable of the beacon REST API when querying a beacon node", } + enableVerboseSigVerification = &cli.BoolFlag{ + Name: "enable-verbose-sig-verification", + Usage: "Enables identifying invalid signatures if batch verification fails when processing block", + } ) // devModeFlags holds list of flags that are set when development mode is on. @@ -178,6 +182,7 @@ var BeaconChainFlags = append(deprecatedBeaconFlags, append(deprecatedFlags, []c enableStartupOptimistic, disableDefensivePull, enableFullSSZDataLogging, + enableVerboseSigVerification, }...)...) // E2EBeaconChainFlags contains a list of the beacon chain feature flags to be tested in E2E. diff --git a/crypto/bls/bls.go b/crypto/bls/bls.go index 463c93c2e1..ae79ecdbe2 100644 --- a/crypto/bls/bls.go +++ b/crypto/bls/bls.go @@ -54,6 +54,11 @@ func AggregateCompressedSignatures(multiSigs [][]byte) (common.Signature, error) return blst.AggregateCompressedSignatures(multiSigs) } +// VerifySignature verifies a single signature. For performance reason, always use VerifyMultipleSignatures if possible. +func VerifySignature(sig []byte, msg [32]byte, pubKey common.PublicKey) (bool, error) { + return blst.VerifySignature(sig, msg, pubKey) +} + // VerifyMultipleSignatures verifies multiple signatures for distinct messages securely. func VerifyMultipleSignatures(sigs [][]byte, msgs [][32]byte, pubKeys []common.PublicKey) (bool, error) { return blst.VerifyMultipleSignatures(sigs, msgs, pubKeys) diff --git a/crypto/bls/blst/signature.go b/crypto/bls/blst/signature.go index 819a2fbfde..b6b7c0c300 100644 --- a/crypto/bls/blst/signature.go +++ b/crypto/bls/blst/signature.go @@ -196,6 +196,15 @@ func AggregateSignatures(sigs []common.Signature) common.Signature { return &Signature{s: signature.ToAffine()} } +// VerifySignature verifies a single signature using public key and message. +func VerifySignature(sig []byte, msg [32]byte, pubKey common.PublicKey) (bool, error) { + rSig, err := SignatureFromBytes(sig) + if err != nil { + return false, err + } + return rSig.Verify(pubKey, msg[:]), nil +} + // VerifyMultipleSignatures verifies a non-singular set of signatures and its respective pubkeys and messages. // This method provides a safe way to verify multiple signatures at once. We pick a number randomly from 1 to max // uint64 and then multiply the signature by it. We continue doing this for all signatures and its respective pubkeys. diff --git a/crypto/bls/blst/signature_test.go b/crypto/bls/blst/signature_test.go index 6b63416fce..607a3b9451 100644 --- a/crypto/bls/blst/signature_test.go +++ b/crypto/bls/blst/signature_test.go @@ -36,6 +36,7 @@ func TestAggregateVerify(t *testing.T) { msgs = append(msgs, msg) } aggSig := AggregateSignatures(sigs) + // skipcq: GO-W1009 assert.Equal(t, true, aggSig.AggregateVerify(pubkeys, msgs), "Signature did not verify") } @@ -56,6 +57,7 @@ func TestAggregateVerify_CompressedSignatures(t *testing.T) { msgs = append(msgs, msg) } aggSig := AggregateSignatures(sigs) + // skipcq: GO-W1009 assert.Equal(t, true, aggSig.AggregateVerify(pubkeys, msgs), "Signature did not verify") aggSig2, err := AggregateCompressedSignatures(sigBytes) @@ -90,6 +92,29 @@ func TestVerifyCompressed(t *testing.T) { assert.Equal(t, true, VerifyCompressed(sig.Marshal(), pub.Marshal(), msg), "Compressed signatures and pubkeys did not verify") } +func TestVerifySingleSignature_InvalidSignature(t *testing.T) { + priv, err := RandKey() + require.NoError(t, err) + pub := priv.PublicKey() + msgA := [32]byte{'h', 'e', 'l', 'l', 'o'} + msgB := [32]byte{'o', 'l', 'l', 'e', 'h'} + sigA := priv.Sign(msgA[:]).Marshal() + valid, err := VerifySignature(sigA, msgB, pub) + assert.NoError(t, err) + assert.Equal(t, false, valid, "Signature did verify") +} + +func TestVerifySingleSignature_ValidSignature(t *testing.T) { + priv, err := RandKey() + require.NoError(t, err) + pub := priv.PublicKey() + msg := [32]byte{'h', 'e', 'l', 'l', 'o'} + sig := priv.Sign(msg[:]).Marshal() + valid, err := VerifySignature(sig, msg, pub) + assert.NoError(t, err) + assert.Equal(t, true, valid, "Signature did not verify") +} + func TestMultipleSignatureVerification(t *testing.T) { pubkeys := make([]common.PublicKey, 0, 100) sigs := make([][]byte, 0, 100) diff --git a/crypto/bls/signature_batch.go b/crypto/bls/signature_batch.go index be4d046fb9..526d0c9430 100644 --- a/crypto/bls/signature_batch.go +++ b/crypto/bls/signature_batch.go @@ -1,22 +1,32 @@ package bls -import "github.com/pkg/errors" +import ( + "encoding/hex" + "fmt" + + "github.com/pkg/errors" +) + +// AggregatedSignature represents aggregated signature produced by AggregateBatch() +const AggregatedSignature = "bls aggregated signature" // SignatureBatch refers to the defined set of // signatures and its respective public keys and // messages required to verify it. type SignatureBatch struct { - Signatures [][]byte - PublicKeys []PublicKey - Messages [][32]byte + Signatures [][]byte + PublicKeys []PublicKey + Messages [][32]byte + Descriptions []string } // NewSet constructs an empty signature batch object. func NewSet() *SignatureBatch { return &SignatureBatch{ - Signatures: [][]byte{}, - PublicKeys: []PublicKey{}, - Messages: [][32]byte{}, + Signatures: [][]byte{}, + PublicKeys: []PublicKey{}, + Messages: [][32]byte{}, + Descriptions: []string{}, } } @@ -25,6 +35,7 @@ func (s *SignatureBatch) Join(set *SignatureBatch) *SignatureBatch { s.Signatures = append(s.Signatures, set.Signatures...) s.PublicKeys = append(s.PublicKeys, set.PublicKeys...) s.Messages = append(s.Messages, set.Messages...) + s.Descriptions = append(s.Descriptions, set.Descriptions...) return s } @@ -33,12 +44,49 @@ func (s *SignatureBatch) Verify() (bool, error) { return VerifyMultipleSignatures(s.Signatures, s.Messages, s.PublicKeys) } +// VerifyVerbosely verifies signatures as a whole at first, if fails, fallback +// to verify each single signature to identify invalid ones. +func (s *SignatureBatch) VerifyVerbosely() (bool, error) { + valid, err := s.Verify() + if err != nil || valid { + return valid, err + } + + // if signature batch is invalid, we then verify signatures one by one. + + errmsg := "some signatures are invalid. details:" + for i := 0; i < len(s.Signatures); i++ { + sig := s.Signatures[i] + msg := s.Messages[i] + pubKey := s.PublicKeys[i] + + valid, err := VerifySignature(sig, msg, pubKey) + if !valid { + desc := s.Descriptions[i] + if err != nil { + errmsg += fmt.Sprintf("\nsignature '%s' is invalid."+ + " signature: 0x%s, public key: 0x%s, message: 0x%v, error: %v", + desc, hex.EncodeToString(sig), hex.EncodeToString(pubKey.Marshal()), + hex.EncodeToString(msg[:]), err) + } else { + errmsg += fmt.Sprintf("\nsignature '%s' is invalid."+ + " signature: 0x%s, public key: 0x%s, message: 0x%v", + desc, hex.EncodeToString(sig), hex.EncodeToString(pubKey.Marshal()), + hex.EncodeToString(msg[:])) + } + } + } + + return false, errors.Errorf(errmsg) +} + // Copy the attached signature batch and return it // to the caller. func (s *SignatureBatch) Copy() *SignatureBatch { signatures := make([][]byte, len(s.Signatures)) pubkeys := make([]PublicKey, len(s.PublicKeys)) messages := make([][32]byte, len(s.Messages)) + descriptions := make([]string, len(s.Descriptions)) for i := range s.Signatures { sig := make([]byte, len(s.Signatures[i])) copy(sig, s.Signatures[i]) @@ -50,10 +98,12 @@ func (s *SignatureBatch) Copy() *SignatureBatch { for i := range s.Messages { copy(messages[i][:], s.Messages[i][:]) } + copy(descriptions, s.Descriptions) return &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubkeys, - Messages: messages, + Signatures: signatures, + PublicKeys: pubkeys, + Messages: messages, + Descriptions: descriptions, } } @@ -82,6 +132,7 @@ func (s *SignatureBatch) RemoveDuplicates() (int, *SignatureBatch, error) { sigs := s.Signatures[:0] pubs := s.PublicKeys[:0] msgs := s.Messages[:0] + descs := s.Descriptions[:0] for i := 0; i < len(s.Signatures); i++ { if duplicateSet[i] { @@ -90,11 +141,13 @@ func (s *SignatureBatch) RemoveDuplicates() (int, *SignatureBatch, error) { sigs = append(sigs, s.Signatures[i]) pubs = append(pubs, s.PublicKeys[i]) msgs = append(msgs, s.Messages[i]) + descs = append(descs, s.Descriptions[i]) } s.Signatures = sigs s.PublicKeys = pubs s.Messages = msgs + s.Descriptions = descs return len(duplicateSet), s, nil } @@ -103,12 +156,12 @@ func (s *SignatureBatch) RemoveDuplicates() (int, *SignatureBatch, error) { // reduce the number of pairings required when we finally verify the // whole batch. func (s *SignatureBatch) AggregateBatch() (*SignatureBatch, error) { - if len(s.Signatures) == 0 || len(s.PublicKeys) == 0 || len(s.Messages) == 0 { - return s, nil + if len(s.Signatures) != len(s.PublicKeys) || len(s.Signatures) != len(s.Messages) || len(s.Signatures) != len(s.Descriptions) { + return s, errors.Errorf("mismatch number of signatures, publickeys, messages and descriptions in signature batch. "+ + "Signatures %d, Public Keys %d , Messages %d, Descriptions %d", len(s.Signatures), len(s.PublicKeys), len(s.Messages), len(s.Descriptions)) } - if len(s.Signatures) != len(s.PublicKeys) || len(s.Signatures) != len(s.Messages) { - return s, errors.Errorf("mismatch number of signatures, publickeys and messages in signature batch. "+ - "Signatures %d, Public Keys %d , Messages %d", s.Signatures, s.PublicKeys, s.Messages) + if len(s.Signatures) == 0 { + return s, nil } msgMap := make(map[[32]byte]*SignatureBatch) @@ -119,12 +172,14 @@ func (s *SignatureBatch) AggregateBatch() (*SignatureBatch, error) { currBatch.Signatures = append(currBatch.Signatures, s.Signatures[i]) currBatch.Messages = append(currBatch.Messages, s.Messages[i]) currBatch.PublicKeys = append(currBatch.PublicKeys, s.PublicKeys[i]) + currBatch.Descriptions = append(currBatch.Descriptions, s.Descriptions[i]) continue } currBatch = &SignatureBatch{ - Signatures: [][]byte{s.Signatures[i]}, - Messages: [][32]byte{s.Messages[i]}, - PublicKeys: []PublicKey{s.PublicKeys[i]}, + Signatures: [][]byte{s.Signatures[i]}, + Messages: [][32]byte{s.Messages[i]}, + PublicKeys: []PublicKey{s.PublicKeys[i]}, + Descriptions: []string{s.Descriptions[i]}, } msgMap[currMsg] = currBatch } @@ -140,6 +195,7 @@ func (s *SignatureBatch) AggregateBatch() (*SignatureBatch, error) { b.PublicKeys = []PublicKey{aggPub} b.Signatures = [][]byte{aggSig.Marshal()} b.Messages = [][32]byte{copiedRt} + b.Descriptions = []string{AggregatedSignature} } newObj := *b newSt = newSt.Join(&newObj) diff --git a/crypto/bls/signature_batch_test.go b/crypto/bls/signature_batch_test.go index 57e4c9319b..d51644286c 100644 --- a/crypto/bls/signature_batch_test.go +++ b/crypto/bls/signature_batch_test.go @@ -2,13 +2,18 @@ package bls import ( "bytes" + "fmt" "reflect" "sort" "testing" + "github.com/prysmaticlabs/prysm/v3/crypto/bls/common" "github.com/prysmaticlabs/prysm/v3/testing/assert" + "github.com/prysmaticlabs/prysm/v3/testing/require" ) +const TestSignature = "test signature" + func TestCopySignatureSet(t *testing.T) { t.Run("blst", func(t *testing.T) { key, err := RandKey() @@ -27,19 +32,22 @@ func TestCopySignatureSet(t *testing.T) { sig3 := key3.Sign(message3[:]) set := &SignatureBatch{ - Signatures: [][]byte{sig.Marshal()}, - PublicKeys: []PublicKey{key.PublicKey()}, - Messages: [][32]byte{message}, + Signatures: [][]byte{sig.Marshal()}, + PublicKeys: []PublicKey{key.PublicKey()}, + Messages: [][32]byte{message}, + Descriptions: createDescriptions(1), } set2 := &SignatureBatch{ - Signatures: [][]byte{sig2.Marshal()}, - PublicKeys: []PublicKey{key.PublicKey()}, - Messages: [][32]byte{message}, + Signatures: [][]byte{sig2.Marshal()}, + PublicKeys: []PublicKey{key.PublicKey()}, + Messages: [][32]byte{message}, + Descriptions: createDescriptions(1), } set3 := &SignatureBatch{ - Signatures: [][]byte{sig3.Marshal()}, - PublicKeys: []PublicKey{key.PublicKey()}, - Messages: [][32]byte{message}, + Signatures: [][]byte{sig3.Marshal()}, + PublicKeys: []PublicKey{key.PublicKey()}, + Messages: [][32]byte{message}, + Descriptions: createDescriptions(1), } aggSet := set.Join(set2).Join(set3) aggSet2 := aggSet.Copy() @@ -48,6 +56,38 @@ func TestCopySignatureSet(t *testing.T) { }) } +func TestVerifyVerbosely_AllSignaturesValid(t *testing.T) { + set := NewValidSignatureSet(t, "good", 3) + valid, err := set.VerifyVerbosely() + assert.NoError(t, err) + assert.Equal(t, true, valid, "SignatureSet is expected to be valid") +} + +func TestVerifyVerbosely_SomeSignaturesInvalid(t *testing.T) { + goodSet := NewValidSignatureSet(t, "good", 3) + badSet := NewInvalidSignatureSet(t, "bad", 3, false) + set := NewSet().Join(goodSet).Join(badSet) + valid, err := set.VerifyVerbosely() + assert.Equal(t, false, valid, "SignatureSet is expected to be invalid") + assert.StringContains(t, "signature 'signature of bad0' is invalid", err.Error()) + assert.StringContains(t, "signature 'signature of bad1' is invalid", err.Error()) + assert.StringContains(t, "signature 'signature of bad2' is invalid", err.Error()) + assert.StringNotContains(t, "signature 'signature of good0' is invalid", err.Error()) + assert.StringNotContains(t, "signature 'signature of good1' is invalid", err.Error()) + assert.StringNotContains(t, "signature 'signature of good2' is invalid", err.Error()) +} + +func TestVerifyVerbosely_VerificationThrowsError(t *testing.T) { + goodSet := NewValidSignatureSet(t, "good", 1) + badSet := NewInvalidSignatureSet(t, "bad", 1, true) + set := NewSet().Join(goodSet).Join(badSet) + valid, err := set.VerifyVerbosely() + assert.Equal(t, false, valid, "SignatureSet is expected to be invalid") + assert.StringContains(t, "signature 'signature of bad0' is invalid", err.Error()) + assert.StringContains(t, "error: could not unmarshal bytes into signature", err.Error()) + assert.StringNotContains(t, "signature 'signature of good0' is invalid", err.Error()) +} + func TestSignatureBatch_RemoveDuplicates(t *testing.T) { var keys []SecretKey for i := 0; i < 100; i++ { @@ -86,13 +126,15 @@ func TestSignatureBatch_RemoveDuplicates(t *testing.T) { allPubs := append(pubs, pubs...) allMsgs := append(messages, messages...) return &SignatureBatch{ - Signatures: allSigs, - PublicKeys: allPubs, - Messages: allMsgs, + Signatures: allSigs, + PublicKeys: allPubs, + Messages: allMsgs, + Descriptions: createDescriptions(len(allMsgs)), }, &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(allMsgs)), } }, want: 20, @@ -130,13 +172,15 @@ func TestSignatureBatch_RemoveDuplicates(t *testing.T) { allPubs := append(pubs, pubs...) allMsgs := append(messages, messages...) return &SignatureBatch{ - Signatures: allSigs, - PublicKeys: allPubs, - Messages: allMsgs, + Signatures: allSigs, + PublicKeys: allPubs, + Messages: allMsgs, + Descriptions: createDescriptions(len(allMsgs)), }, &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(allMsgs)), } }, want: 30, @@ -171,13 +215,15 @@ func TestSignatureBatch_RemoveDuplicates(t *testing.T) { pubs = append(pubs, k.PublicKey()) } return &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(messages)), }, &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(messages)), } }, want: 0, @@ -223,13 +269,15 @@ func TestSignatureBatch_RemoveDuplicates(t *testing.T) { // Zero out to expected result signatures[10] = make([]byte, 96) return &SignatureBatch{ - Signatures: allSigs, - PublicKeys: allPubs, - Messages: allMsgs, + Signatures: allSigs, + PublicKeys: allPubs, + Messages: allMsgs, + Descriptions: createDescriptions(len(allMsgs)), }, &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(allMsgs)), } }, want: 29, @@ -294,13 +342,15 @@ func TestSignatureBatch_RemoveDuplicates(t *testing.T) { messages[29] = [32]byte{'j', 'u', 'n', 'k'} return &SignatureBatch{ - Signatures: allSigs, - PublicKeys: allPubs, - Messages: allMsgs, + Signatures: allSigs, + PublicKeys: allPubs, + Messages: allMsgs, + Descriptions: createDescriptions(len(allMsgs)), }, &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(messages)), } }, want: 27, @@ -342,11 +392,37 @@ func TestSignatureBatch_AggregateBatch(t *testing.T) { { name: "empty batch", batchCreator: func(t *testing.T) (*SignatureBatch, *SignatureBatch) { - return &SignatureBatch{Signatures: nil, Messages: nil, PublicKeys: nil}, - &SignatureBatch{Signatures: nil, Messages: nil, PublicKeys: nil} + return &SignatureBatch{Signatures: nil, Messages: nil, PublicKeys: nil, Descriptions: nil}, + &SignatureBatch{Signatures: nil, Messages: nil, PublicKeys: nil, Descriptions: nil} }, wantErr: false, }, + { + name: "mismatch number of signatures and messages in batch", + batchCreator: func(t *testing.T) (*SignatureBatch, *SignatureBatch) { + key1 := keys[0] + key2 := keys[1] + msg := [32]byte{'r', 'a', 'n', 'd', 'o', 'm'} + sig1 := key1.Sign(msg[:]) + sig2 := key2.Sign(msg[:]) + signatures := [][]byte{sig1.Marshal(), sig2.Marshal()} + pubs := []common.PublicKey{key1.PublicKey(), key2.PublicKey()} + messages := [][32]byte{msg} + descs := createDescriptions(2) + return &SignatureBatch{ + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: descs, + }, &SignatureBatch{ + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: descs, + } + }, + wantErr: true, + }, { name: "valid signatures in batch", batchCreator: func(t *testing.T) (*SignatureBatch, *SignatureBatch) { @@ -366,13 +442,15 @@ func TestSignatureBatch_AggregateBatch(t *testing.T) { assert.NoError(t, err) aggPub := AggregateMultiplePubkeys(pubs) return &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(messages)), }, &SignatureBatch{ - Signatures: [][]byte{aggSig.Marshal()}, - PublicKeys: []PublicKey{aggPub}, - Messages: [][32]byte{msg}, + Signatures: [][]byte{aggSig.Marshal()}, + PublicKeys: []PublicKey{aggPub}, + Messages: [][32]byte{msg}, + Descriptions: createDescriptions(1, AggregatedSignature), } }, wantErr: false, @@ -394,9 +472,10 @@ func TestSignatureBatch_AggregateBatch(t *testing.T) { } signatures[10] = make([]byte, 96) return &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(messages)), }, nil }, wantErr: true, @@ -440,13 +519,15 @@ func TestSignatureBatch_AggregateBatch(t *testing.T) { aggPub2 := AggregateMultiplePubkeys(pubs[10:20]) aggPub3 := AggregateMultiplePubkeys(pubs[20:30]) return &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(messages)), }, &SignatureBatch{ - Signatures: [][]byte{aggSig1.Marshal(), aggSig2.Marshal(), aggSig3.Marshal()}, - PublicKeys: []PublicKey{aggPub1, aggPub2, aggPub3}, - Messages: [][32]byte{msg, msg1, msg2}, + Signatures: [][]byte{aggSig1.Marshal(), aggSig2.Marshal(), aggSig3.Marshal()}, + PublicKeys: []PublicKey{aggPub1, aggPub2, aggPub3}, + Messages: [][32]byte{msg, msg1, msg2}, + Descriptions: createDescriptions(3, AggregatedSignature), } }, wantErr: false, @@ -521,13 +602,15 @@ func TestSignatureBatch_AggregateBatch(t *testing.T) { aggPub3 := AggregateMultiplePubkeys(newPubs) return &SignatureBatch{ - Signatures: signatures, - PublicKeys: pubs, - Messages: messages, + Signatures: signatures, + PublicKeys: pubs, + Messages: messages, + Descriptions: createDescriptions(len(messages)), }, &SignatureBatch{ - Signatures: [][]byte{aggSig1.Marshal(), signatures[5], aggSig2.Marshal(), signatures[15], aggSig3.Marshal(), signatures[25]}, - PublicKeys: []PublicKey{aggPub1, pubs[5], aggPub2, pubs[15], aggPub3, pubs[25]}, - Messages: [][32]byte{msg, messages[5], msg1, messages[15], msg2, messages[25]}, + Signatures: [][]byte{aggSig1.Marshal(), signatures[5], aggSig2.Marshal(), signatures[15], aggSig3.Marshal(), signatures[25]}, + PublicKeys: []PublicKey{aggPub1, pubs[5], aggPub2, pubs[15], aggPub3, pubs[25]}, + Messages: [][32]byte{msg, messages[5], msg1, messages[15], msg2, messages[25]}, + Descriptions: []string{AggregatedSignature, TestSignature, AggregatedSignature, TestSignature, AggregatedSignature, TestSignature}, } }, wantErr: false, @@ -556,10 +639,87 @@ func TestSignatureBatch_AggregateBatch(t *testing.T) { if !reflect.DeepEqual(got.Messages, output.Messages) { t.Errorf("AggregateBatch() Messages got = %v, want %v", got.Messages, output.Messages) } + if !reflect.DeepEqual(got.Descriptions, output.Descriptions) { + t.Errorf("AggregateBatch() Descriptions got = %v, want %v", got.Descriptions, output.Descriptions) + } }) } } +func NewValidSignatureSet(t *testing.T, msgBody string, num int) *SignatureBatch { + set := &SignatureBatch{ + Signatures: make([][]byte, num), + PublicKeys: make([]common.PublicKey, num), + Messages: make([][32]byte, num), + Descriptions: make([]string, num), + } + + for i := 0; i < num; i++ { + priv, err := RandKey() + require.NoError(t, err) + pubkey := priv.PublicKey() + msg := messageBytes(fmt.Sprintf("%s%d", msgBody, i)) + sig := priv.Sign(msg[:]).Marshal() + desc := fmt.Sprintf("signature of %s%d", msgBody, i) + + set.Signatures[i] = sig + set.PublicKeys[i] = pubkey + set.Messages[i] = msg + set.Descriptions[i] = desc + } + + return set +} + +func NewInvalidSignatureSet(t *testing.T, msgBody string, num int, throwErr bool) *SignatureBatch { + set := &SignatureBatch{ + Signatures: make([][]byte, num), + PublicKeys: make([]common.PublicKey, num), + Messages: make([][32]byte, num), + Descriptions: make([]string, num), + } + + for i := 0; i < num; i++ { + priv, err := RandKey() + require.NoError(t, err) + pubkey := priv.PublicKey() + msg := messageBytes(fmt.Sprintf("%s%d", msgBody, i)) + var sig []byte + if throwErr { + sig = make([]byte, 96) + } else { + badMsg := messageBytes("badmsg") + sig = priv.Sign(badMsg[:]).Marshal() + } + desc := fmt.Sprintf("signature of %s%d", msgBody, i) + + set.Signatures[i] = sig + set.PublicKeys[i] = pubkey + set.Messages[i] = msg + set.Descriptions[i] = desc + } + + return set +} + +func messageBytes(message string) [32]byte { + bytes := [32]byte{} + copy(bytes[:], []byte(message)) + return bytes +} + +func createDescriptions(length int, text ...string) []string { + desc := make([]string, length) + for i := range desc { + if len(text) > 0 { + desc[i] = text[0] + } else { + desc[i] = TestSignature + } + } + return desc +} + func sortSet(s *SignatureBatch) *SignatureBatch { sort.Sort(sorter{set: s}) return s diff --git a/testing/assert/assertions.go b/testing/assert/assertions.go index 71857823ec..9a5ffd5531 100644 --- a/testing/assert/assertions.go +++ b/testing/assert/assertions.go @@ -41,6 +41,16 @@ func DeepNotSSZEqual(tb assertions.AssertionTestingTB, expected, actual interfac assertions.DeepNotSSZEqual(tb.Errorf, expected, actual, msg...) } +// StringContains asserts a string contains specified substring. +func StringContains(tb assertions.AssertionTestingTB, expected, actual string, msg ...interface{}) { + assertions.StringContains(tb.Errorf, expected, actual, true, msg...) +} + +// StringContains asserts a string does not contain specified substring. +func StringNotContains(tb assertions.AssertionTestingTB, expected, actual string, msg ...interface{}) { + assertions.StringContains(tb.Errorf, expected, actual, false, msg...) +} + // NoError asserts that error is nil. func NoError(tb assertions.AssertionTestingTB, err error, msg ...interface{}) { assertions.NoError(tb.Errorf, err, msg...) diff --git a/testing/assertions/assertions.go b/testing/assertions/assertions.go index 1154465bfd..e5ce3c1cde 100644 --- a/testing/assertions/assertions.go +++ b/testing/assertions/assertions.go @@ -78,6 +78,23 @@ func DeepNotSSZEqual(loggerFn assertionLoggerFn, expected, actual interface{}, m } } +// StringContains checks whether a string contains specified substring. If flag is false, inverse is checked. +func StringContains(loggerFn assertionLoggerFn, expected, actual string, flag bool, msg ...interface{}) { + if flag { + if !strings.Contains(actual, expected) { + errMsg := parseMsg("Expected substring is not found", msg...) + _, file, line, _ := runtime.Caller(2) + loggerFn("%s:%d %s, got: %v, want: %s", filepath.Base(file), line, errMsg, actual, expected) + } + } else { + if strings.Contains(actual, expected) { + errMsg := parseMsg("Unexpected substring is found", msg...) + _, file, line, _ := runtime.Caller(2) + loggerFn("%s:%d %s, got: %v, not want: %s", filepath.Base(file), line, errMsg, actual, expected) + } + } +} + // NoError asserts that error is nil. func NoError(loggerFn assertionLoggerFn, err error, msg ...interface{}) { if err != nil {