diff --git a/share/eds/byzantine/bad_encoding.go b/share/eds/byzantine/bad_encoding.go index e3a862e38a..fbb6b592ea 100644 --- a/share/eds/byzantine/bad_encoding.go +++ b/share/eds/byzantine/bad_encoding.go @@ -102,15 +102,31 @@ func (p *BadEncodingProof) UnmarshalBinary(data []byte) error { return nil } +var ( + errHeightMismatch = errors.New("height reported in proof does not match with the header's height") + errIncorrectIndex = errors.New("row/col index is more then the roots amount") + errIncorrectAmountOfShares = errors.New("incorrect amount of shares") + errIncorrectShare = errors.New("incorrect share received") + errNMTTreeRootsMatch = errors.New("recomputed root matches the DAH root") +) + +var ( + invalidProofPrefix = fmt.Sprintf("invalid %s proof", BadEncoding) +) + // Validate ensures that fraud proof is correct. // Validate checks that provided Merkle Proofs correspond to the shares, // rebuilds bad row or col from received shares, computes Merkle Root // and compares it with block's Merkle Root. func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { if hdr.Height() != p.BlockHeight { - return fmt.Errorf("incorrect block height during BEFP validation: expected %d, got %d", - p.BlockHeight, hdr.Height(), + log.Debugf("%s: %s. expected block's height: %d, got: %d", + invalidProofPrefix, + errHeightMismatch, + hdr.Height(), + p.BlockHeight, ) + return errHeightMismatch } if len(hdr.DAH.RowRoots) != len(hdr.DAH.ColumnRoots) { @@ -132,9 +148,10 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { } if int(p.Index) >= len(merkleRoots) { - return fmt.Errorf("invalid %s proof: index out of bounds (%d >= %d)", - BadEncoding, int(p.Index), len(merkleRoots), + log.Debugf("%s:%s (%d >= %d)", + invalidProofPrefix, errIncorrectIndex, int(p.Index), len(merkleRoots), ) + return errIncorrectIndex } if len(p.Shares) != len(merkleRoots) { @@ -142,9 +159,10 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { // column, it should exactly match the number of row roots. In this // context, the number of row roots is the width of the extended data // square. - return fmt.Errorf("invalid %s proof: incorrect number of shares %d != %d", - BadEncoding, len(p.Shares), len(merkleRoots), + log.Infof("%s: %s (%d >= %d)", + invalidProofPrefix, errIncorrectAmountOfShares, int(p.Index), len(merkleRoots), ) + return errIncorrectAmountOfShares } odsWidth := uint64(len(merkleRoots) / 2) @@ -160,7 +178,9 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { } if amount < odsWidth { - return errors.New("fraud: invalid proof: not enough shares provided to reconstruct row/col") + log.Debugf("%s: %s. not enough shares provided to reconstruct row/col", + invalidProofPrefix, errIncorrectAmountOfShares) + return errIncorrectAmountOfShares } // verify that Merkle proofs correspond to particular shares. @@ -171,7 +191,8 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { } // validate inclusion of the share into one of the DAHeader roots if ok := shr.Validate(ipld.MustCidFromNamespacedSha256(merkleRoots[index])); !ok { - return fmt.Errorf("invalid %s proof: incorrect share received at index %d", BadEncoding, index) + log.Debugf("%s: %s at index %d", invalidProofPrefix, errIncorrectShare, index) + return errIncorrectShare } // NMTree commits the additional namespace while rsmt2d does not know about, so we trim it // this is ugliness from NMTWrapper that we have to embrace ¯\_(ツ)_/¯ @@ -184,7 +205,7 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { // the row/col can't be reconstructed, or the building of NMTree fails. rebuiltShares, err := codec.Decode(shares) if err != nil { - log.Infow("failed to decode shares at height", + log.Debugw("failed to decode shares at height", "height", hdr.Height(), "err", err, ) return nil @@ -192,7 +213,7 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { rebuiltExtendedShares, err := codec.Encode(rebuiltShares[0:odsWidth]) if err != nil { - log.Infow("failed to encode shares at height", + log.Debugw("failed to encode shares at height", "height", hdr.Height(), "err", err, ) return nil @@ -203,7 +224,7 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { for _, share := range rebuiltShares { err = tree.Push(share) if err != nil { - log.Infow("failed to build a tree from the reconstructed shares at height", + log.Debugw("failed to build a tree from the reconstructed shares at height", "height", hdr.Height(), "err", err, ) return nil @@ -212,7 +233,7 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { expectedRoot, err := tree.Root() if err != nil { - log.Infow("failed to build a tree root at height", + log.Debugw("failed to build a tree root at height", "height", hdr.Height(), "err", err, ) return nil @@ -226,7 +247,8 @@ func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { // comparing rebuilt Merkle Root of bad row/col with respective Merkle Root of row/col from block. if bytes.Equal(expectedRoot, root) { - return fmt.Errorf("invalid %s proof: recomputed Merkle root matches the DAH's row/column root", BadEncoding) + log.Debugf("invalid %s proof:%s", BadEncoding, errNMTTreeRootsMatch) + return errNMTTreeRootsMatch } return nil } diff --git a/share/eds/byzantine/bad_encoding_test.go b/share/eds/byzantine/bad_encoding_test.go index 3e245c1ab3..e7032107ca 100644 --- a/share/eds/byzantine/bad_encoding_test.go +++ b/share/eds/byzantine/bad_encoding_test.go @@ -5,11 +5,12 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" core "github.com/tendermint/tendermint/types" "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/celestia-app/test/util/malicious" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/header" @@ -19,8 +20,8 @@ import ( "github.com/celestiaorg/celestia-node/share/sharetest" ) -func TestBadEncodingFraudProof(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) +func TestBEFP_Validate(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer t.Cleanup(cancel) bServ := ipld.NewMemBlockservice() @@ -37,10 +38,115 @@ func TestBadEncodingFraudProof(t *testing.T) { errByz := NewErrByzantine(ctx, bServ, &dah, errRsmt2d) befp := CreateBadEncodingProof([]byte("hash"), 0, errByz) - err = befp.Validate(&header.ExtendedHeader{ - DAH: &dah, - }) - assert.NoError(t, err) + + var test = []struct { + name string + prepareFn func() error + expectedResult func(error) + }{ + { + name: "valid BEFP", + prepareFn: func() error { + return befp.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.NoError(t, err) + }, + }, + { + name: "invalid BEFP for valid header", + prepareFn: func() error { + validSquare := edstest.RandEDS(t, 2) + validDah, err := da.NewDataAvailabilityHeader(validSquare) + require.NoError(t, err) + err = ipld.ImportEDS(ctx, validSquare, bServ) + require.NoError(t, err) + validShares := validSquare.Flattened() + errInvalidByz := NewErrByzantine(ctx, bServ, &validDah, + &rsmt2d.ErrByzantineData{ + Axis: rsmt2d.Row, + Index: 0, + Shares: validShares[0:4], + }, + ) + invalidBefp := CreateBadEncodingProof([]byte("hash"), 0, errInvalidByz) + return invalidBefp.Validate(&header.ExtendedHeader{DAH: &validDah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errNMTTreeRootsMatch) + }, + }, + { + name: "incorrect share with Proof", + prepareFn: func() error { + befp, ok := befp.(*BadEncodingProof) + require.True(t, ok) + befp.Shares[0].Share = befp.Shares[1].Share + return befp.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errIncorrectShare) + }, + }, + { + name: "invalid amount of shares", + prepareFn: func() error { + befp, ok := befp.(*BadEncodingProof) + require.True(t, ok) + befp.Shares = befp.Shares[0 : len(befp.Shares)/2] + return befp.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errIncorrectAmountOfShares) + }, + }, + { + name: "not enough shares to recompute the root", + prepareFn: func() error { + befp, ok := befp.(*BadEncodingProof) + require.True(t, ok) + befp.Shares[0] = nil + return befp.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errIncorrectAmountOfShares) + }, + }, + { + name: "index out of bounds", + prepareFn: func() error { + befp, ok := befp.(*BadEncodingProof) + require.True(t, ok) + befpCopy := *befp + befpCopy.Index = 100 + return befpCopy.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errIncorrectIndex) + }, + }, + { + name: "heights mismatch", + prepareFn: func() error { + return befp.Validate(&header.ExtendedHeader{ + RawHeader: core.Header{ + Height: 42, + }, + DAH: &dah, + }) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errHeightMismatch) + }, + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + err = tt.prepareFn() + tt.expectedResult(err) + }) + } } // TestIncorrectBadEncodingFraudProof asserts that BEFP is not generated for the correct data @@ -90,3 +196,36 @@ func TestIncorrectBadEncodingFraudProof(t *testing.T) { err = proof.Validate(h) require.Error(t, err) } + +func TestBEFP_ValidateOutOfOrderShares(t *testing.T) { + // skipping it for now because `malicious` package has a small issue: Constructor does not apply + // passed options, so it's not possible to store shares and thus get proofs for them. + // should be ok once app team will fix it. + t.Skip() + eds := edstest.RandEDS(t, 16) + shares := eds.Flattened() + shares[0], shares[1] = shares[1], shares[0] // corrupting eds + bServ := ipld.NewMemBlockservice() + batchAddr := ipld.NewNmtNodeAdder(context.Background(), bServ, ipld.MaxSizeBatchOption(16*2)) + eds, err := rsmt2d.ImportExtendedDataSquare(shares, + share.DefaultRSMT2DCodec(), + malicious.NewConstructor(16, nmt.NodeVisitor(batchAddr.Visit)), + ) + require.NoError(t, err, "failure to recompute the extended data square") + + err = batchAddr.Commit() + require.NoError(t, err) + + dah, err := da.NewDataAvailabilityHeader(eds) + require.NoError(t, err) + + var errRsmt2d *rsmt2d.ErrByzantineData + err = eds.Repair(dah.RowRoots, dah.ColumnRoots) + require.ErrorAs(t, err, &errRsmt2d) + + errByz := NewErrByzantine(context.Background(), bServ, &dah, errRsmt2d) + + befp := CreateBadEncodingProof([]byte("hash"), 0, errByz) + err = befp.Validate(&header.ExtendedHeader{DAH: &dah}) + require.Error(t, err) +}