Skip to content

Commit

Permalink
fraud: Define Bad Encoding Fraud Poof struct. Provide functionality f…
Browse files Browse the repository at this point in the history
…or befp creation/validation. (celestiaorg#545)

* feat(fraud): implement BEFP data structure
* feat(fraud): add Proto message
* feat(fraud): add BEFP creation
* feat(fraud): add BEFP validation
  • Loading branch information
vgonkivs authored May 3, 2022
1 parent f5d1a79 commit 11e89a8
Show file tree
Hide file tree
Showing 15 changed files with 1,789 additions and 41 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ pb-gen:
@echo '--> Generating protobuf'
@for dir in $(PB_PKGS); \
do for file in `find $$dir -type f -name "*.proto"`; \
do protoc -I=. -I=${PB_CORE}/proto/ -I=${PB_GOGO} --gogofaster_out . $$file; \
do protoc -I=. -I=${PB_CORE}/proto/ -I=${PB_GOGO} --gogofaster_out=paths=source_relative:. $$file; \
echo '-->' $$file; \
done; \
done;
Expand Down
35 changes: 18 additions & 17 deletions docs/adr/adr-006-fraud-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ The result of `RepairExtendedDataSquare` will be an error [`ErrByzantineRow`](ht
- row/column numbers that do not match with the Merkle root
- shares that were successfully repaired and verified (all correct shares).

Based on `ErrByzantineRow`/`ErrByzantineCol` internal fields, we should generate [MerkleProof](https://github.com/celestiaorg/nmt/blob/e381b44f223e9ac570a8d59bbbdbb2d5a5f1ad5f/proof.go#L17) for respective verified shares from [nmt](https://github.com/celestiaorg/nmt) tree return as the `ErrBadEncoding` from `RetrieveData`.
Based on `ErrByzantineRow`/`ErrByzantineCol` internal fields, we should generate [MerkleProof](https://github.com/celestiaorg/nmt/blob/e381b44f223e9ac570a8d59bbbdbb2d5a5f1ad5f/proof.go#L17) for respective verified shares from [nmt](https://github.com/celestiaorg/nmt) tree return as the `ErrByzantine` from `RetrieveData`.

```go
type ErrBadEncoding struct {
type ErrByzantine struct {
// Shares contains all shares from row/col.
// For non-nil shares MerkleProof is computed
Shares []*Share
// Position represents the number of row/col where ErrByzantineRow/ErrByzantineColl occurred.
Position uint8
Shares []*NamespacedShareWithProof
// Index represents the number of row/col where ErrByzantineRow/ErrByzantineColl occurred.
Index uint8
isRow bool
}

Expand All @@ -54,18 +54,19 @@ In addition, `das.Daser`:
1. Creates a BEFP:

```go
// Currently, we support only one fraud proof. But this enum will be extended in the future with other
const (
BadEncoding ProofType = "BadEncoding"
BadEncoding ProofType = 0
)

type BadEncodingProof struct {
Height uint64
// Shares contains all shares from row/col
// Shares that did not pass verification in rmst2d will be nil
// For non-nil shares MerkleProofs are computed
Shares []*Share
// Position represents the number of row/col where ErrByzantineRow/ErrByzantineColl occurred
Position uint8
Shares []*NamespacedShareWithProof
// Index represents the number of row/col where ErrByzantineRow/ErrByzantineColl occurred
Index uint8
isRow bool
}
```
Expand All @@ -75,21 +76,21 @@ type BadEncodingProof struct {
```proto3
message MerkleProof {
int64 total = 1;
int64 index = 2;
bytes leaf_hash = 3;
repeated bytes aunts = 4;
int64 start = 1;
int64 end = 2;
repeated bytes nodes = 3;
bytes leaf_hash = 4;
}
message Share {
message ShareWithProof {
bytes Share = 1;
MerkleProof Proof = 2;
}
message BadEnconding {
required uint64 Height = 1;
repeated Share Shares = 2;
uint8 Position = 3;
uint8 Index = 3;
bool isRow = 4;
}
```
Expand All @@ -106,7 +107,7 @@ type Broadcaster interface {

```go
// ProofType is a enum type that represents a particular type of fraud proof.
type ProofType string
type ProofType int

// Proof is a generic interface that will be used for all types of fraud proofs in the network.
type Proof interface {
Expand Down Expand Up @@ -163,7 +164,7 @@ func(s *FraudSub) Broadcast(ctx context.Context, p Proof) error{}
### BEFP verification
Once a light node receives a `BadEncodingProof` fraud proof, it should:
* verify that Merkle proofs correspond to particular shares. If the Merkle proof does not correspond to a share, then the BEFP is not valid.
* using `BadEncodingProof.Shares`, light node should re-construct full row or column, compute its Merkle root as in [rsmt2d](https://github.com/celestiaorg/rsmt2d/blob/ac0f1e1a51bf7b5420965fb7c35fa32a56e02292/extendeddatacrossword.go#L410) and compare it with Merkle root that could be retrieved from the `DataAvailabilityHeader` inside the `ExtendedHeader`. If Merkle roots do not match, then the BEFP is not valid.
* using `BadEncodingProof.Shares`, light node should re-construct full row or column, compute its Merkle root as in [rsmt2d](https://github.com/celestiaorg/rsmt2d/blob/ac0f1e1a51bf7b5420965fb7c35fa32a56e02292/extendeddatacrossword.go#L410) and compare it with Merkle root that could be retrieved from the `DataAvailabilityHeader` inside the `ExtendedHeader`. If Merkle roots match, then the BEFP is not valid.

3. All celestia-nodes should stop some dependent services upon receiving a legitimate BEFP:
Both full and light nodes should stop `DAS`, `Syncer` and `SubmitTx` services.
Expand Down
154 changes: 154 additions & 0 deletions fraud/bad_encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package fraud

import (
"bytes"
"errors"
"fmt"

"github.com/tendermint/tendermint/pkg/consts"
"github.com/tendermint/tendermint/pkg/wrapper"

"github.com/celestiaorg/rsmt2d"

pb "github.com/celestiaorg/celestia-node/fraud/pb"
"github.com/celestiaorg/celestia-node/ipld"
ipld_pb "github.com/celestiaorg/celestia-node/ipld/pb"
"github.com/celestiaorg/celestia-node/service/header"
)

type BadEncodingProof struct {
BlockHeight uint64
// ShareWithProof contains all shares from row or col.
// Shares that did not pass verification in rmst2d will be nil.
// For non-nil shares MerkleProofs are computed.
Shares []*ipld.NamespacedShareWithProof
// Index represents the row/col index where ErrByzantineRow/ErrByzantineColl occurred
Index uint8
// isRow shows that verification failed on row
isRow bool
}

// CreateBadEncodingProof creates a new Bad Encoding Fraud Proof that should be propagated through network
// The fraud proof will contain shares that did not pass verification and their relevant Merkle proofs.
func CreateBadEncodingProof(
height uint64,
errByzantine *ipld.ErrByzantine,
) Proof {

return &BadEncodingProof{
BlockHeight: height,
Shares: errByzantine.Shares,
isRow: errByzantine.IsRow,
Index: errByzantine.Index,
}
}

// Type returns type of fraud proof
func (p *BadEncodingProof) Type() ProofType {
return BadEncoding
}

// Height returns block height
func (p *BadEncodingProof) Height() uint64 {
return p.BlockHeight
}

// MarshalBinary converts BadEncodingProof to binary
func (p *BadEncodingProof) MarshalBinary() ([]byte, error) {
shares := make([]*ipld_pb.Share, 0, len(p.Shares))
for _, share := range p.Shares {
shares = append(shares, share.ShareWithProofToProto())
}

badEncodingFraudProof := pb.BadEncoding{
Height: p.BlockHeight,
Shares: shares,
Index: uint32(p.Index),
IsRow: p.isRow,
}
return badEncodingFraudProof.Marshal()
}

// UnmarshalBinary converts binary to BadEncodingProof
func (p *BadEncodingProof) UnmarshalBinary(data []byte) error {
in := pb.BadEncoding{}
if err := in.Unmarshal(data); err != nil {
return err
}
befp := &BadEncodingProof{
BlockHeight: in.Height,
Shares: ipld.ProtoToShare(in.Shares),
}

*p = *befp

return nil
}

// 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(header *header.ExtendedHeader) error {
if header.Height != int64(p.BlockHeight) {
return errors.New("invalid fraud proof: incorrect block height")
}
merkleRowRoots := header.DAH.RowsRoots
merkleColRoots := header.DAH.ColumnRoots
if len(merkleRowRoots) != len(merkleColRoots) {
// NOTE: This should never happen as callers of this method should not feed it with a
// malformed extended header.
panic(fmt.Sprintf("invalid extended header: length of row and column roots do not match. (rowRoots=%d) (colRoots=%d)",
len(merkleRowRoots),
len(merkleColRoots)),
)
}
if int(p.Index) >= len(merkleRowRoots) {
return fmt.Errorf("invalid fraud proof: index out of bounds (%d >= %d)", int(p.Index), len(merkleRowRoots))
}
if len(merkleRowRoots) != len(p.Shares) {
return fmt.Errorf("invalid fraud proof: incorrect number of shares %d != %d", len(p.Shares), len(merkleRowRoots))
}

root := merkleRowRoots[p.Index]
if !p.isRow {
root = merkleColRoots[p.Index]
}

shares := make([][]byte, len(merkleRowRoots))

// verify that Merkle proofs correspond to particular shares
for index, share := range p.Shares {
if share == nil {
continue
}
shares[index] = share.Share
if ok := share.Validate(root); !ok {
return fmt.Errorf("invalid fraud proof: incorrect share received at Index %d", index)
}
}

codec := consts.DefaultCodec()
// rebuild a row or col
rebuiltShares, err := codec.Decode(shares)
if err != nil {
return err
}
rebuiltExtendedShares, err := codec.Encode(rebuiltShares[0 : len(shares)/2])
if err != nil {
return err
}
rebuiltShares = append(rebuiltShares, rebuiltExtendedShares...)

tree := wrapper.NewErasuredNamespacedMerkleTree(uint64(len(shares) / 2))
for i, share := range rebuiltShares {
tree.Push(share, rsmt2d.SquareIndex{Axis: uint(p.Index), Cell: uint(i)})
}

// comparing rebuilt Merkle Root of bad row/col with respective Merkle Root of row/col from block
if bytes.Equal(tree.Root(), root) {
return errors.New("invalid fraud proof: recomputed Merkle root matches the header's row/column root")
}

return nil
}
48 changes: 48 additions & 0 deletions fraud/bad_encoding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package fraud

import (
"context"
"errors"
"testing"

format "github.com/ipfs/go-ipld-format"
mdutils "github.com/ipfs/go-merkledag/test"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/pkg/consts"
"github.com/tendermint/tendermint/pkg/da"
"github.com/tendermint/tendermint/pkg/wrapper"

"github.com/celestiaorg/celestia-node/ipld"
"github.com/celestiaorg/celestia-node/service/header"
"github.com/celestiaorg/nmt"
"github.com/celestiaorg/rsmt2d"
)

func TestFraudProofValidation(t *testing.T) {
dag := mdutils.Mock()
eds := ipld.RandEDS(t, 2)
size := eds.Width()

shares := ipld.ExtractEDS(eds)
copy(shares[3][8:], shares[4][8:])
batchAdder := ipld.NewNmtNodeAdder(
context.Background(),
format.NewBatch(context.Background(), dag, format.MaxSizeBatchOption(int(size)*2)),
)
tree := wrapper.NewErasuredNamespacedMerkleTree(uint64(size/2), nmt.NodeVisitor(batchAdder.Visit))
attackerEDS, _ := rsmt2d.ImportExtendedDataSquare(shares, consts.DefaultCodec(), tree.Constructor)
err := batchAdder.Commit()
require.NoError(t, err)

da := da.NewDataAvailabilityHeader(attackerEDS)
r := ipld.NewRetriever(dag, consts.DefaultCodec())
_, err = r.Retrieve(context.Background(), &da)
var errByz *ipld.ErrByzantine
require.True(t, errors.As(err, &errByz))

dah := &header.ExtendedHeader{DAH: &da}

p := CreateBadEncodingProof(uint64(dah.Height), errByz)
err = p.Validate(dah)
require.NoError(t, err)
}
Loading

0 comments on commit 11e89a8

Please sign in to comment.