From b78861cd54fda3c734202b632030fb98d3daac79 Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Thu, 17 Aug 2023 11:23:27 +0300 Subject: [PATCH 01/14] tests(swamp): extend swamp with Validators method --- nodebuilder/tests/swamp/swamp.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nodebuilder/tests/swamp/swamp.go b/nodebuilder/tests/swamp/swamp.go index 58584912be..4f07d96b51 100644 --- a/nodebuilder/tests/swamp/swamp.go +++ b/nodebuilder/tests/swamp/swamp.go @@ -16,6 +16,8 @@ import ( mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/types" "go.uber.org/fx" "golang.org/x/exp/maps" @@ -335,3 +337,15 @@ func (s *Swamp) SetBootstrapper(t *testing.T, bootstrappers ...*nodebuilder.Node s.Bootstrappers = append(s.Bootstrappers, addrs[0]) } } + +// Validators retrieves keys from the app node in order to build the validators. +func (s *Swamp) Validators(t *testing.T) (*types.ValidatorSet, types.PrivValidator) { + privPath := s.cfg.TmConfig.PrivValidatorKeyFile() + statePath := s.cfg.TmConfig.PrivValidatorStateFile() + priv := privval.LoadFilePV(privPath, statePath) + key, err := priv.GetPubKey() + require.NoError(t, err) + validator := types.NewValidator(key, 100) + set := types.NewValidatorSet([]*types.Validator{validator}) + return set, priv +} From c800446f171e16cad440fe74227660f0f0dc37e9 Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Thu, 17 Aug 2023 11:26:52 +0300 Subject: [PATCH 02/14] nodebuilder(feat): add WithBlockService option --- nodebuilder/p2p/opts.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nodebuilder/p2p/opts.go b/nodebuilder/p2p/opts.go index 9501dfe8e1..8e5d714a64 100644 --- a/nodebuilder/p2p/opts.go +++ b/nodebuilder/p2p/opts.go @@ -3,6 +3,7 @@ package p2p import ( "encoding/hex" + "github.com/ipfs/go-blockservice" "github.com/libp2p/go-libp2p/core/crypto" hst "github.com/libp2p/go-libp2p/core/host" "go.uber.org/fx" @@ -34,3 +35,8 @@ func WithP2PKeyStr(key string) fx.Option { func WithHost(hst hst.Host) fx.Option { return fxutil.ReplaceAs(hst, new(HostBase)) } + +// WithBlockService allows to replace the default BlockService. +func WithBlockService(bServ blockservice.BlockService) fx.Option { + return fxutil.ReplaceAs(bServ, new(blockservice.BlockService)) +} From 1d38ffa6172d025e43dd00c23ff53a61d2d1731b Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Thu, 17 Aug 2023 11:50:53 +0300 Subject: [PATCH 03/14] test(headertest): add FraudMaker --- header/headertest/testing.go | 84 +++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/header/headertest/testing.go b/header/headertest/testing.go index 65ae8c950f..f784f72376 100644 --- a/header/headertest/testing.go +++ b/header/headertest/testing.go @@ -296,32 +296,6 @@ func RandBlockID(*testing.T) types.BlockID { return bid } -// FraudMaker creates a custom ConstructFn that breaks the block at the given height. -func FraudMaker(t *testing.T, faultHeight int64, bServ blockservice.BlockService) header.ConstructFn { - log.Warn("Corrupting block...", "height", faultHeight) - return func( - h *types.Header, - comm *types.Commit, - vals *types.ValidatorSet, - eds *rsmt2d.ExtendedDataSquare, - ) (*header.ExtendedHeader, error) { - if h.Height == faultHeight { - eh := &header.ExtendedHeader{ - RawHeader: *h, - Commit: comm, - ValidatorSet: vals, - } - - eh, dataSq := CreateFraudExtHeader(t, eh, bServ) - if eds != nil { - *eds = *dataSq - } - return eh, nil - } - return header.MakeExtendedHeader(h, comm, vals, eds) - } -} - func ExtendedHeaderFromEDS(t *testing.T, height uint64, eds *rsmt2d.ExtendedDataSquare) *header.ExtendedHeader { valSet, vals := RandValidatorSet(10, 10) gen := RandRawHeader(t) @@ -348,19 +322,51 @@ func ExtendedHeaderFromEDS(t *testing.T, height uint64, eds *rsmt2d.ExtendedData return eh } -func CreateFraudExtHeader( - t *testing.T, - eh *header.ExtendedHeader, - serv blockservice.BlockService, -) (*header.ExtendedHeader, *rsmt2d.ExtendedDataSquare) { - square := edstest.RandByzantineEDS(t, len(eh.DAH.RowRoots)) - err := ipld.ImportEDS(context.Background(), square, serv) - require.NoError(t, err) - dah, err := da.NewDataAvailabilityHeader(square) - require.NoError(t, err) - eh.DAH = &dah - eh.RawHeader.DataHash = dah.Hash() - return eh, square +// FraudMaker allows to produce an invalid header at the specified height in order to produce the BEFP. +type FraudMaker struct { + t *testing.T + + vals []types.PrivValidator + valSet *types.ValidatorSet + + // height of the invalid header + height int64 +} + +func NewFraudMaker(t *testing.T, height int64, vals []types.PrivValidator, valSet *types.ValidatorSet) *FraudMaker { + return &FraudMaker{ + t: t, + vals: vals, + valSet: valSet, + height: height, + } +} + +func (f *FraudMaker) MakeExtendedHeader(odsSize int, bServ blockservice.BlockService) header.ConstructFn { + return func(ctx context.Context, + h *types.Header, + comm *types.Commit, + vals *types.ValidatorSet, + eds *rsmt2d.ExtendedDataSquare, + ) (*header.ExtendedHeader, error) { + if h.Height == f.height { + square := edstest.RandByzantineEDS(f.t, odsSize) + err := ipld.ImportEDS(ctx, square, bServ) + require.NoError(f.t, err) + dah, err := da.NewDataAvailabilityHeader(square) + require.NoError(f.t, err) + + h.DataHash = dah.Hash() + blockID := comm.BlockID + blockID.Hash = h.Hash() + + voteSet := types.NewVoteSet(h.ChainID, h.Height, 0, tmproto.PrecommitType, f.valSet) + comm, err = MakeCommit(blockID, h.Height, 0, voteSet, f.vals, time.Now()) + require.NoError(f.t, err) + eds = square + } + return header.MakeExtendedHeader(ctx, h, comm, vals, eds) + } } type Subscriber struct { From 40549870edc1466a26b52a0d81c21cd5f43eacaa Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Thu, 17 Aug 2023 11:52:19 +0300 Subject: [PATCH 04/14] fix(share/getters): handle byzantine error correctly --- share/getters/cascade.go | 6 ++++++ share/getters/ipld.go | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/share/getters/cascade.go b/share/getters/cascade.go index 63d7713d3d..bd183d9273 100644 --- a/share/getters/cascade.go +++ b/share/getters/cascade.go @@ -11,6 +11,7 @@ import ( "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/byzantine" ) var _ share.Getter = (*CascadeGetter)(nil) @@ -130,6 +131,11 @@ func cascadeGetters[V any]( continue } + var byzantineErr *byzantine.ErrByzantine + if errors.As(getErr, &byzantineErr) { + return zero, byzantineErr + } + err = errors.Join(err, getErr) span.RecordError(getErr, trace.WithAttributes(attribute.Int("getter_idx", i))) if ctx.Err() != nil { diff --git a/share/getters/ipld.go b/share/getters/ipld.go index a892e0fc82..8e11a389bd 100644 --- a/share/getters/ipld.go +++ b/share/getters/ipld.go @@ -16,6 +16,7 @@ import ( "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/byzantine" "github.com/celestiaorg/celestia-node/share/ipld" ) @@ -82,6 +83,10 @@ func (ig *IPLDGetter) GetEDS(ctx context.Context, root *share.Root) (eds *rsmt2d // convert error to satisfy getter interface contract err = share.ErrNotFound } + var errByz *byzantine.ErrByzantine + if errors.As(err, &errByz) { + return nil, err + } if err != nil { return nil, fmt.Errorf("getter/ipld: failed to retrieve eds: %w", err) } From 20d8dd523bd85f80c326a43ff6ec59e04919d7c3 Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Fri, 18 Aug 2023 15:55:23 +0300 Subject: [PATCH 05/14] fix(lifecycle): cancel context after service stops --- nodebuilder/fraud/lifecycle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodebuilder/fraud/lifecycle.go b/nodebuilder/fraud/lifecycle.go index 1a6702aafa..50f4e1035b 100644 --- a/nodebuilder/fraud/lifecycle.go +++ b/nodebuilder/fraud/lifecycle.go @@ -73,7 +73,7 @@ func (breaker *ServiceBreaker[S, H]) Stop(ctx context.Context) error { } breaker.sub.Cancel() - breaker.cancel() + defer breaker.cancel() return breaker.Service.Stop(ctx) } From a6c0409e6621debbc0d7c47e82081ec01afa678b Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Fri, 18 Aug 2023 15:59:33 +0300 Subject: [PATCH 06/14] fix(headertest): rework FraudMaker --- header/headertest/testing.go | 23 ++++++++++++++++------- share/eds/edstest/testing.go | 8 ++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/header/headertest/testing.go b/header/headertest/testing.go index f784f72376..23d9dfdb69 100644 --- a/header/headertest/testing.go +++ b/header/headertest/testing.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/ipfs/go-blockservice" logging "github.com/ipfs/go-log/v2" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto/tmhash" @@ -23,9 +22,11 @@ import ( "github.com/celestiaorg/celestia-app/pkg/da" libhead "github.com/celestiaorg/go-header" "github.com/celestiaorg/go-header/headertest" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/eds/edstest" "github.com/celestiaorg/celestia-node/share/ipld" ) @@ -342,7 +343,7 @@ func NewFraudMaker(t *testing.T, height int64, vals []types.PrivValidator, valSe } } -func (f *FraudMaker) MakeExtendedHeader(odsSize int, bServ blockservice.BlockService) header.ConstructFn { +func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header.ConstructFn { return func(ctx context.Context, h *types.Header, comm *types.Commit, @@ -350,9 +351,9 @@ func (f *FraudMaker) MakeExtendedHeader(odsSize int, bServ blockservice.BlockSer eds *rsmt2d.ExtendedDataSquare, ) (*header.ExtendedHeader, error) { if h.Height == f.height { - square := edstest.RandByzantineEDS(f.t, odsSize) - err := ipld.ImportEDS(ctx, square, bServ) - require.NoError(f.t, err) + adder := ipld.NewProofsAdder(odsSize) + square := edstest.RandByzantineEDS(f.t, odsSize, nmt.NodeVisitor(adder.VisitFn())) + dah, err := da.NewDataAvailabilityHeader(square) require.NoError(f.t, err) @@ -361,9 +362,17 @@ func (f *FraudMaker) MakeExtendedHeader(odsSize int, bServ blockservice.BlockSer blockID.Hash = h.Hash() voteSet := types.NewVoteSet(h.ChainID, h.Height, 0, tmproto.PrecommitType, f.valSet) - comm, err = MakeCommit(blockID, h.Height, 0, voteSet, f.vals, time.Now()) + commit, err := MakeCommit(blockID, h.Height, 0, voteSet, f.vals, time.Now()) require.NoError(f.t, err) - eds = square + + // overwrite all data + *eds = *square + *comm = *commit + bb := *h + *h = bb + + ctx = ipld.CtxWithProofsAdder(ctx, adder) + require.NoError(f.t, edsStore.Put(ctx, h.DataHash.Bytes(), square)) } return header.MakeExtendedHeader(ctx, h, comm, vals, eds) } diff --git a/share/eds/edstest/testing.go b/share/eds/edstest/testing.go index ddca285f0c..f75e8b619b 100644 --- a/share/eds/edstest/testing.go +++ b/share/eds/edstest/testing.go @@ -7,17 +7,21 @@ import ( "github.com/celestiaorg/celestia-app/pkg/da" "github.com/celestiaorg/celestia-app/pkg/wrapper" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/sharetest" ) -func RandByzantineEDS(t *testing.T, size int) *rsmt2d.ExtendedDataSquare { +func RandByzantineEDS(t *testing.T, size int, options ...nmt.Option) *rsmt2d.ExtendedDataSquare { eds := RandEDS(t, size) shares := eds.Flattened() copy(share.GetData(shares[0]), share.GetData(shares[1])) // corrupting eds - eds, err := rsmt2d.ImportExtendedDataSquare(shares, share.DefaultRSMT2DCodec(), wrapper.NewConstructor(uint64(size))) + eds, err := rsmt2d.ImportExtendedDataSquare(shares, + share.DefaultRSMT2DCodec(), + wrapper.NewConstructor(uint64(size), + options...)) require.NoError(t, err, "failure to recompute the extended data square") return eds } From 4cb778802758552fee5c88e4d7f02b19fb776a53 Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Fri, 18 Aug 2023 18:13:56 +0300 Subject: [PATCH 07/14] feat(fraud/test): adapt new logic of Fraud Maker --- nodebuilder/tests/fraud_test.go | 142 ++++++++------------------------ 1 file changed, 36 insertions(+), 106 deletions(-) diff --git a/nodebuilder/tests/fraud_test.go b/nodebuilder/tests/fraud_test.go index f652724d55..1d79cf8cf3 100644 --- a/nodebuilder/tests/fraud_test.go +++ b/nodebuilder/tests/fraud_test.go @@ -5,16 +5,20 @@ import ( "testing" "time" - mdutils "github.com/ipfs/go-merkledag/test" + "github.com/ipfs/go-datastore" + ds_sync "github.com/ipfs/go-datastore/sync" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/types" + "go.uber.org/fx" "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/nodebuilder" "github.com/celestiaorg/celestia-node/nodebuilder/core" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" + "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/eds/byzantine" ) @@ -29,156 +33,82 @@ Steps: 4. Start a FN. 5. Subscribe to a fraud proof and wait when it will be received. 6. Check FN is not synced to 15. -Note: 15 is not available because DASer will be stopped before reaching this height due to receiving a fraud proof. +Note: 15 is not available because DASer/Syncer will be stopped before reaching this height due to receiving a fraud proof. Another note: this test disables share exchange to speed up test results. */ -func TestFraudProofBroadcasting(t *testing.T) { - t.Skip("requires BEFP generation on app side to work") +func TestFraudProofHandling(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) t.Cleanup(cancel) const ( blocks = 15 - blockSize = 2 - blockTime = time.Millisecond * 300 + blockSize = 4 + blockTime = time.Second ) sw := swamp.NewSwamp(t, swamp.WithBlockTime(blockTime)) fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, blockSize, blocks) + set, val := sw.Validators(t) + fMaker := headertest.NewFraudMaker(t, 10, []types.PrivValidator{val}, set) + + tmpDir := t.TempDir() + ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) + edsStore, err := eds.NewStore(tmpDir, ds) + require.NoError(t, err) + require.NoError(t, edsStore.Start(ctx)) + t.Cleanup(func() { + _ = edsStore.Stop(ctx) + }) cfg := nodebuilder.DefaultConfig(node.Bridge) - cfg.Share.UseShareExchange = false bridge := sw.NewNodeWithConfig( node.Bridge, cfg, - core.WithHeaderConstructFn(headertest.FraudMaker(t, 10, mdutils.Bserv())), + core.WithHeaderConstructFn(fMaker.MakeExtendedHeader(16, edsStore)), + fx.Replace(edsStore), ) - - err := bridge.Start(ctx) + err = bridge.Start(ctx) require.NoError(t, err) cfg = nodebuilder.DefaultConfig(node.Full) - cfg.Share.UseShareExchange = false addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) require.NoError(t, err) cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - + cfg.Share.UseShareExchange = false store := nodebuilder.MockStore(t, cfg) full := sw.NewNodeWithStore(node.Full, store) err = full.Start(ctx) require.NoError(t, err) - // subscribe to fraud proof before node starts helps - // to prevent flakiness when fraud proof is propagating before subscribing on it - subscr, err := full.FraudServ.Subscribe(ctx, byzantine.BadEncoding) + subCtx, subCancel := context.WithCancel(ctx) + subscr, err := full.FraudServ.Subscribe(subCtx, byzantine.BadEncoding) require.NoError(t, err) - select { case p := <-subscr: require.Equal(t, 10, int(p.Height())) + subCancel() case <-ctx.Done(): + subCancel() t.Fatal("fraud proof was not received in time") } + // This is an obscure way to check if the Syncer was stopped. // If we cannot get a height header within a timeframe it means the syncer was stopped // FIXME: Eventually, this should be a check on service registry managing and keeping // lifecycles of each Module. - syncCtx, syncCancel := context.WithTimeout(context.Background(), btime) - _, err = full.HeaderServ.WaitForHeight(syncCtx, 100) + syncCtx, syncCancel := context.WithTimeout(context.Background(), blockTime*5) + _, err = full.HeaderServ.WaitForHeight(syncCtx, 15) require.ErrorIs(t, err, context.DeadlineExceeded) syncCancel() - sw.StopNode(ctx, full) - - full = sw.NewNodeWithStore(node.Full, store) - - require.Error(t, full.Start(ctx)) - proofs, err := full.FraudServ.Get(ctx, byzantine.BadEncoding) + fN := sw.NewNodeWithStore(node.Full, store) + require.Error(t, fN.Start(ctx)) + proofs, err := fN.FraudServ.Get(ctx, byzantine.BadEncoding) require.NoError(t, err) require.NotNil(t, proofs) - require.NoError(t, <-fillDn) -} -/* -Test-Case: Light node receives a fraud proof using Fraud Sync -Pre-Requisites: -- CoreClient is started by swamp. -Steps: -1. Create a Bridge Node(BN) with broken extended header at height 10. -2. Start a BN. -3. Create a Full Node(FN) with a connection to BN as a trusted peer. -4. Start a FN. -5. Subscribe to a fraud proof and wait when it will be received. -6. Start LN once a fraud proof is received and verified by FN. -7. Wait until LN will be connected to FN and fetch a fraud proof. -Note: this test disables share exchange to speed up test results. -*/ -func TestFraudProofSyncing(t *testing.T) { - t.Skip("requires BEFP generation on app side to work") - - const ( - blocks = 15 - bsize = 2 - btime = time.Millisecond * 300 - ) - sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) - ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) - t.Cleanup(cancel) - - fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, blocks) - cfg := nodebuilder.DefaultConfig(node.Bridge) - cfg.Share.UseShareExchange = false - store := nodebuilder.MockStore(t, cfg) - bridge := sw.NewNodeWithStore( - node.Bridge, - store, - core.WithHeaderConstructFn(headertest.FraudMaker(t, 10, mdutils.Bserv())), - ) - - err := bridge.Start(ctx) - require.NoError(t, err) - addr := host.InfoFromHost(bridge.Host) - addrs, err := peer.AddrInfoToP2pAddrs(addr) - require.NoError(t, err) - - fullCfg := nodebuilder.DefaultConfig(node.Full) - fullCfg.Share.UseShareExchange = false - fullCfg.Header.TrustedPeers = append(fullCfg.Header.TrustedPeers, addrs[0].String()) - full := sw.NewNodeWithStore(node.Full, nodebuilder.MockStore(t, fullCfg)) - - lightCfg := nodebuilder.DefaultConfig(node.Light) - lightCfg.Header.TrustedPeers = append(lightCfg.Header.TrustedPeers, addrs[0].String()) - ln := sw.NewNodeWithStore(node.Light, nodebuilder.MockStore(t, lightCfg)) - require.NoError(t, full.Start(ctx)) - - subsFN, err := full.FraudServ.Subscribe(ctx, byzantine.BadEncoding) - require.NoError(t, err) - - select { - case <-subsFN: - case <-ctx.Done(): - t.Fatal("full node didn't get FP in time") - } - - // start LN to enforce syncing logic, not the PubSub's broadcasting - err = ln.Start(ctx) - require.NoError(t, err) - - // internal subscription for the fraud proof is done in order to ensure that light node - // receives the BEFP. - subsLN, err := ln.FraudServ.Subscribe(ctx, byzantine.BadEncoding) - require.NoError(t, err) - - // ensure that the full and light node are connected to speed up test - // alternatively, they would discover each other - err = ln.Host.Connect(ctx, *host.InfoFromHost(full.Host)) - require.NoError(t, err) - - select { - case <-subsLN: - case <-ctx.Done(): - t.Fatal("light node didn't get FP in time") - } + sw.StopNode(ctx, bridge) + sw.StopNode(ctx, full) require.NoError(t, <-fillDn) } From 71b7aa0ba198dd7269618bf00b2a90acf50d0948 Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Mon, 21 Aug 2023 11:29:49 +0300 Subject: [PATCH 08/14] fix: fix unit tests --- das/daser_test.go | 2 +- header/headertest/testing.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/das/daser_test.go b/das/daser_test.go index 68f6e01ef2..f8ca46c6d4 100644 --- a/das/daser_test.go +++ b/das/daser_test.go @@ -180,7 +180,7 @@ func TestDASer_stopsAfter_BEFP(t *testing.T) { "private", ) require.NoError(t, fserv.Start(ctx)) - mockGet.headers[1], _ = headertest.CreateFraudExtHeader(t, mockGet.headers[1], bServ) + mockGet.headers[1], _ = headerfraud.CreateFraudExtHeader(t, mockGet.headers[1], bServ) newCtx := context.Background() // create and start DASer diff --git a/header/headertest/testing.go b/header/headertest/testing.go index 23d9dfdb69..3f08e4e7de 100644 --- a/header/headertest/testing.go +++ b/header/headertest/testing.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/ipfs/go-blockservice" logging "github.com/ipfs/go-log/v2" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto/tmhash" @@ -378,6 +379,21 @@ func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header } } +func CreateFraudExtHeader( + t *testing.T, + eh *header.ExtendedHeader, + serv blockservice.BlockService, +) *header.ExtendedHeader { + square := edstest.RandByzantineEDS(t, len(eh.DAH.RowRoots)) + err := ipld.ImportEDS(context.Background(), square, serv) + require.NoError(t, err) + dah, err := da.NewDataAvailabilityHeader(square) + require.NoError(t, err) + eh.DAH = &dah + eh.RawHeader.DataHash = dah.Hash() + return eh +} + type Subscriber struct { headertest.Subscriber[*header.ExtendedHeader] } From a4909ad07189b04f995990d2d4167d0d5e1b662a Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Mon, 21 Aug 2023 14:44:25 +0300 Subject: [PATCH 09/14] rework fraud maker --- header/headertest/testing.go | 39 +++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/header/headertest/testing.go b/header/headertest/testing.go index 3f08e4e7de..5dd3aff945 100644 --- a/header/headertest/testing.go +++ b/header/headertest/testing.go @@ -333,6 +333,8 @@ type FraudMaker struct { // height of the invalid header height int64 + + prevHash bytes.HexBytes } func NewFraudMaker(t *testing.T, height int64, vals []types.PrivValidator, valSet *types.ValidatorSet) *FraudMaker { @@ -351,34 +353,39 @@ func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header vals *types.ValidatorSet, eds *rsmt2d.ExtendedDataSquare, ) (*header.ExtendedHeader, error) { + if h.Height < f.height { + return header.MakeExtendedHeader(ctx, h, comm, vals, eds) + } + + hdr := *h if h.Height == f.height { adder := ipld.NewProofsAdder(odsSize) square := edstest.RandByzantineEDS(f.t, odsSize, nmt.NodeVisitor(adder.VisitFn())) - dah, err := da.NewDataAvailabilityHeader(square) require.NoError(f.t, err) - - h.DataHash = dah.Hash() - blockID := comm.BlockID - blockID.Hash = h.Hash() - - voteSet := types.NewVoteSet(h.ChainID, h.Height, 0, tmproto.PrecommitType, f.valSet) - commit, err := MakeCommit(blockID, h.Height, 0, voteSet, f.vals, time.Now()) - require.NoError(f.t, err) - - // overwrite all data - *eds = *square - *comm = *commit - bb := *h - *h = bb + hdr.DataHash = dah.Hash() ctx = ipld.CtxWithProofsAdder(ctx, adder) require.NoError(f.t, edsStore.Put(ctx, h.DataHash.Bytes(), square)) + + *eds = *square + } + if h.Height > f.height { + hdr.LastBlockID.Hash = f.prevHash } + + blockID := comm.BlockID + blockID.Hash = hdr.Hash() + voteSet := types.NewVoteSet(hdr.ChainID, hdr.Height, 0, tmproto.PrecommitType, f.valSet) + commit, err := MakeCommit(blockID, hdr.Height, 0, voteSet, f.vals, time.Now()) + require.NoError(f.t, err) + + *h = hdr + *comm = *commit + f.prevHash = h.Hash() return header.MakeExtendedHeader(ctx, h, comm, vals, eds) } } - func CreateFraudExtHeader( t *testing.T, eh *header.ExtendedHeader, From 46c5afc2700548680d2a35d20ae0d9c472c4a06b Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Mon, 21 Aug 2023 14:54:15 +0300 Subject: [PATCH 10/14] extend test --- nodebuilder/tests/fraud_test.go | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/nodebuilder/tests/fraud_test.go b/nodebuilder/tests/fraud_test.go index 1d79cf8cf3..300319ea3b 100644 --- a/nodebuilder/tests/fraud_test.go +++ b/nodebuilder/tests/fraud_test.go @@ -35,6 +35,9 @@ Steps: 6. Check FN is not synced to 15. Note: 15 is not available because DASer/Syncer will be stopped before reaching this height due to receiving a fraud proof. Another note: this test disables share exchange to speed up test results. +7. Spawn a Light Node(LN) in order to sync a BEFP. +8. Ensure that the BEFP was received. +9. Try to start a Full Node(FN) that contains a BEFP in its store. */ func TestFraudProofHandling(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) @@ -61,15 +64,18 @@ func TestFraudProofHandling(t *testing.T) { }) cfg := nodebuilder.DefaultConfig(node.Bridge) + // 1. bridge := sw.NewNodeWithConfig( node.Bridge, cfg, core.WithHeaderConstructFn(fMaker.MakeExtendedHeader(16, edsStore)), fx.Replace(edsStore), ) + // 2. err = bridge.Start(ctx) require.NoError(t, err) + // 3. cfg = nodebuilder.DefaultConfig(node.Full) addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) require.NoError(t, err) @@ -78,9 +84,11 @@ func TestFraudProofHandling(t *testing.T) { store := nodebuilder.MockStore(t, cfg) full := sw.NewNodeWithStore(node.Full, store) + // 4. err = full.Start(ctx) require.NoError(t, err) + // 5. subCtx, subCancel := context.WithCancel(ctx) subscr, err := full.FraudServ.Subscribe(subCtx, byzantine.BadEncoding) require.NoError(t, err) @@ -90,18 +98,40 @@ func TestFraudProofHandling(t *testing.T) { subCancel() case <-ctx.Done(): subCancel() - t.Fatal("fraud proof was not received in time") + t.Fatal("full node did not receive a fraud proof in time") } // This is an obscure way to check if the Syncer was stopped. // If we cannot get a height header within a timeframe it means the syncer was stopped // FIXME: Eventually, this should be a check on service registry managing and keeping // lifecycles of each Module. + // 6. syncCtx, syncCancel := context.WithTimeout(context.Background(), blockTime*5) _, err = full.HeaderServ.WaitForHeight(syncCtx, 15) require.ErrorIs(t, err, context.DeadlineExceeded) syncCancel() + // 7. + cfg = nodebuilder.DefaultConfig(node.Light) + cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) + lnStore := nodebuilder.MockStore(t, cfg) + light := sw.NewNodeWithStore(node.Light, lnStore) + require.NoError(t, light.Start(ctx)) + + // 8. + subCtx, subCancel = context.WithCancel(ctx) + subscr, err = light.FraudServ.Subscribe(subCtx, byzantine.BadEncoding) + require.NoError(t, err) + select { + case p := <-subscr: + require.Equal(t, 10, int(p.Height())) + subCancel() + case <-ctx.Done(): + subCancel() + t.Fatal("light node did not receive a fraud proof in time") + } + + // 9. fN := sw.NewNodeWithStore(node.Full, store) require.Error(t, fN.Start(ctx)) proofs, err := fN.FraudServ.Get(ctx, byzantine.BadEncoding) @@ -110,5 +140,6 @@ func TestFraudProofHandling(t *testing.T) { sw.StopNode(ctx, bridge) sw.StopNode(ctx, full) + sw.StopNode(ctx, light) require.NoError(t, <-fillDn) } From 6d60d7d16de95087b3c48591c87b66504dccf5d1 Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Mon, 21 Aug 2023 15:13:43 +0300 Subject: [PATCH 11/14] fix circular dependency --- das/daser_test.go | 1 + header/headertest/fraud/testing.go | 101 +++++++++++++++++++++++++++++ header/headertest/testing.go | 86 ------------------------ nodebuilder/tests/fraud_test.go | 7 +- 4 files changed, 106 insertions(+), 89 deletions(-) create mode 100644 header/headertest/fraud/testing.go diff --git a/das/daser_test.go b/das/daser_test.go index f8ca46c6d4..8875bf8981 100644 --- a/das/daser_test.go +++ b/das/daser_test.go @@ -23,6 +23,7 @@ import ( "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/header/headertest" + headerfraud "github.com/celestiaorg/celestia-node/header/headertest/fraud" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/availability/full" "github.com/celestiaorg/celestia-node/share/availability/light" diff --git a/header/headertest/fraud/testing.go b/header/headertest/fraud/testing.go new file mode 100644 index 0000000000..9ca63a1828 --- /dev/null +++ b/header/headertest/fraud/testing.go @@ -0,0 +1,101 @@ +package headerfraud + +import ( + "context" + "testing" + "time" + + "github.com/ipfs/go-blockservice" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/bytes" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + "github.com/tendermint/tendermint/types" + + "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/nmt" + "github.com/celestiaorg/rsmt2d" + + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/header/headertest" + "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/ipld" +) + +// FraudMaker allows to produce an invalid header at the specified height in order to produce the +// BEFP. +type FraudMaker struct { + t *testing.T + + vals []types.PrivValidator + valSet *types.ValidatorSet + + // height of the invalid header + height int64 + + prevHash bytes.HexBytes +} + +func NewFraudMaker(t *testing.T, height int64, vals []types.PrivValidator, valSet *types.ValidatorSet) *FraudMaker { + return &FraudMaker{ + t: t, + vals: vals, + valSet: valSet, + height: height, + } +} + +func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header.ConstructFn { + return func(ctx context.Context, + h *types.Header, + comm *types.Commit, + vals *types.ValidatorSet, + eds *rsmt2d.ExtendedDataSquare, + ) (*header.ExtendedHeader, error) { + if h.Height < f.height { + return header.MakeExtendedHeader(ctx, h, comm, vals, eds) + } + + hdr := *h + if h.Height == f.height { + adder := ipld.NewProofsAdder(odsSize) + square := edstest.RandByzantineEDS(f.t, odsSize, nmt.NodeVisitor(adder.VisitFn())) + dah, err := da.NewDataAvailabilityHeader(square) + require.NoError(f.t, err) + hdr.DataHash = dah.Hash() + + ctx = ipld.CtxWithProofsAdder(ctx, adder) + require.NoError(f.t, edsStore.Put(ctx, h.DataHash.Bytes(), square)) + + *eds = *square + } + if h.Height > f.height { + hdr.LastBlockID.Hash = f.prevHash + } + + blockID := comm.BlockID + blockID.Hash = hdr.Hash() + voteSet := types.NewVoteSet(hdr.ChainID, hdr.Height, 0, tmproto.PrecommitType, f.valSet) + commit, err := headertest.MakeCommit(blockID, hdr.Height, 0, voteSet, f.vals, time.Now()) + require.NoError(f.t, err) + + *h = hdr + *comm = *commit + f.prevHash = h.Hash() + return header.MakeExtendedHeader(ctx, h, comm, vals, eds) + } +} +func CreateFraudExtHeader( + t *testing.T, + eh *header.ExtendedHeader, + serv blockservice.BlockService, +) *header.ExtendedHeader { + square := edstest.RandByzantineEDS(t, len(eh.DAH.RowRoots)) + err := ipld.ImportEDS(context.Background(), square, serv) + require.NoError(t, err) + dah, err := da.NewDataAvailabilityHeader(square) + require.NoError(t, err) + eh.DAH = &dah + eh.RawHeader.DataHash = dah.Hash() + return eh +} diff --git a/header/headertest/testing.go b/header/headertest/testing.go index 5dd3aff945..f288556bd9 100644 --- a/header/headertest/testing.go +++ b/header/headertest/testing.go @@ -1,7 +1,6 @@ package headertest import ( - "context" "crypto/rand" "fmt" mrand "math/rand" @@ -9,8 +8,6 @@ import ( "testing" "time" - "github.com/ipfs/go-blockservice" - logging "github.com/ipfs/go-log/v2" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/libs/bytes" @@ -23,17 +20,11 @@ import ( "github.com/celestiaorg/celestia-app/pkg/da" libhead "github.com/celestiaorg/go-header" "github.com/celestiaorg/go-header/headertest" - "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/header" - "github.com/celestiaorg/celestia-node/share/eds" - "github.com/celestiaorg/celestia-node/share/eds/edstest" - "github.com/celestiaorg/celestia-node/share/ipld" ) -var log = logging.Logger("headertest") - // TestSuite provides everything you need to test chain of Headers. // If not, please don't hesitate to extend it for your case. type TestSuite struct { @@ -324,83 +315,6 @@ func ExtendedHeaderFromEDS(t *testing.T, height uint64, eds *rsmt2d.ExtendedData return eh } -// FraudMaker allows to produce an invalid header at the specified height in order to produce the BEFP. -type FraudMaker struct { - t *testing.T - - vals []types.PrivValidator - valSet *types.ValidatorSet - - // height of the invalid header - height int64 - - prevHash bytes.HexBytes -} - -func NewFraudMaker(t *testing.T, height int64, vals []types.PrivValidator, valSet *types.ValidatorSet) *FraudMaker { - return &FraudMaker{ - t: t, - vals: vals, - valSet: valSet, - height: height, - } -} - -func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header.ConstructFn { - return func(ctx context.Context, - h *types.Header, - comm *types.Commit, - vals *types.ValidatorSet, - eds *rsmt2d.ExtendedDataSquare, - ) (*header.ExtendedHeader, error) { - if h.Height < f.height { - return header.MakeExtendedHeader(ctx, h, comm, vals, eds) - } - - hdr := *h - if h.Height == f.height { - adder := ipld.NewProofsAdder(odsSize) - square := edstest.RandByzantineEDS(f.t, odsSize, nmt.NodeVisitor(adder.VisitFn())) - dah, err := da.NewDataAvailabilityHeader(square) - require.NoError(f.t, err) - hdr.DataHash = dah.Hash() - - ctx = ipld.CtxWithProofsAdder(ctx, adder) - require.NoError(f.t, edsStore.Put(ctx, h.DataHash.Bytes(), square)) - - *eds = *square - } - if h.Height > f.height { - hdr.LastBlockID.Hash = f.prevHash - } - - blockID := comm.BlockID - blockID.Hash = hdr.Hash() - voteSet := types.NewVoteSet(hdr.ChainID, hdr.Height, 0, tmproto.PrecommitType, f.valSet) - commit, err := MakeCommit(blockID, hdr.Height, 0, voteSet, f.vals, time.Now()) - require.NoError(f.t, err) - - *h = hdr - *comm = *commit - f.prevHash = h.Hash() - return header.MakeExtendedHeader(ctx, h, comm, vals, eds) - } -} -func CreateFraudExtHeader( - t *testing.T, - eh *header.ExtendedHeader, - serv blockservice.BlockService, -) *header.ExtendedHeader { - square := edstest.RandByzantineEDS(t, len(eh.DAH.RowRoots)) - err := ipld.ImportEDS(context.Background(), square, serv) - require.NoError(t, err) - dah, err := da.NewDataAvailabilityHeader(square) - require.NoError(t, err) - eh.DAH = &dah - eh.RawHeader.DataHash = dah.Hash() - return eh -} - type Subscriber struct { headertest.Subscriber[*header.ExtendedHeader] } diff --git a/nodebuilder/tests/fraud_test.go b/nodebuilder/tests/fraud_test.go index 300319ea3b..95c702c0c0 100644 --- a/nodebuilder/tests/fraud_test.go +++ b/nodebuilder/tests/fraud_test.go @@ -13,7 +13,7 @@ import ( "github.com/tendermint/tendermint/types" "go.uber.org/fx" - "github.com/celestiaorg/celestia-node/header/headertest" + headerfraud "github.com/celestiaorg/celestia-node/header/headertest/fraud" "github.com/celestiaorg/celestia-node/nodebuilder" "github.com/celestiaorg/celestia-node/nodebuilder/core" "github.com/celestiaorg/celestia-node/nodebuilder/node" @@ -33,7 +33,8 @@ Steps: 4. Start a FN. 5. Subscribe to a fraud proof and wait when it will be received. 6. Check FN is not synced to 15. -Note: 15 is not available because DASer/Syncer will be stopped before reaching this height due to receiving a fraud proof. +Note: 15 is not available because DASer/Syncer will be stopped +before reaching this height due to receiving a fraud proof. Another note: this test disables share exchange to speed up test results. 7. Spawn a Light Node(LN) in order to sync a BEFP. 8. Ensure that the BEFP was received. @@ -52,7 +53,7 @@ func TestFraudProofHandling(t *testing.T) { sw := swamp.NewSwamp(t, swamp.WithBlockTime(blockTime)) fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, blockSize, blocks) set, val := sw.Validators(t) - fMaker := headertest.NewFraudMaker(t, 10, []types.PrivValidator{val}, set) + fMaker := headerfraud.NewFraudMaker(t, 10, []types.PrivValidator{val}, set) tmpDir := t.TempDir() ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) From f45679f8d1d2360ade439407aba3d9df38529f17 Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Wed, 30 Aug 2023 17:21:03 +0300 Subject: [PATCH 12/14] fix after rebasing --- das/daser_test.go | 2 +- header/headertest/fraud/testing.go | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/das/daser_test.go b/das/daser_test.go index 8875bf8981..e4e74dc7ff 100644 --- a/das/daser_test.go +++ b/das/daser_test.go @@ -181,7 +181,7 @@ func TestDASer_stopsAfter_BEFP(t *testing.T) { "private", ) require.NoError(t, fserv.Start(ctx)) - mockGet.headers[1], _ = headerfraud.CreateFraudExtHeader(t, mockGet.headers[1], bServ) + mockGet.headers[1] = headerfraud.CreateFraudExtHeader(t, mockGet.headers[1], bServ) newCtx := context.Background() // create and start DASer diff --git a/header/headertest/fraud/testing.go b/header/headertest/fraud/testing.go index 9ca63a1828..6a5cda733d 100644 --- a/header/headertest/fraud/testing.go +++ b/header/headertest/fraud/testing.go @@ -46,14 +46,13 @@ func NewFraudMaker(t *testing.T, height int64, vals []types.PrivValidator, valSe } func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header.ConstructFn { - return func(ctx context.Context, - h *types.Header, + return func(h *types.Header, comm *types.Commit, vals *types.ValidatorSet, eds *rsmt2d.ExtendedDataSquare, ) (*header.ExtendedHeader, error) { if h.Height < f.height { - return header.MakeExtendedHeader(ctx, h, comm, vals, eds) + return header.MakeExtendedHeader(h, comm, vals, eds) } hdr := *h @@ -64,7 +63,7 @@ func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header require.NoError(f.t, err) hdr.DataHash = dah.Hash() - ctx = ipld.CtxWithProofsAdder(ctx, adder) + ctx := ipld.CtxWithProofsAdder(context.Background(), adder) require.NoError(f.t, edsStore.Put(ctx, h.DataHash.Bytes(), square)) *eds = *square @@ -82,7 +81,7 @@ func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header *h = hdr *comm = *commit f.prevHash = h.Hash() - return header.MakeExtendedHeader(ctx, h, comm, vals, eds) + return header.MakeExtendedHeader(h, comm, vals, eds) } } func CreateFraudExtHeader( From 0eb06a3d4f67899157d041e1b424199354d888d9 Mon Sep 17 00:00:00 2001 From: Viacheslav Gonkivskyi Date: Wed, 30 Aug 2023 18:21:57 +0300 Subject: [PATCH 13/14] add comment --- share/getters/cascade.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/share/getters/cascade.go b/share/getters/cascade.go index bd183d9273..abd071c92b 100644 --- a/share/getters/cascade.go +++ b/share/getters/cascade.go @@ -131,13 +131,15 @@ func cascadeGetters[V any]( continue } + span.RecordError(getErr, trace.WithAttributes(attribute.Int("getter_idx", i))) var byzantineErr *byzantine.ErrByzantine if errors.As(getErr, &byzantineErr) { + // short circuit if byzantine error was detected(to be able to handle it correctly + // and create the BEFP) return zero, byzantineErr } err = errors.Join(err, getErr) - span.RecordError(getErr, trace.WithAttributes(attribute.Int("getter_idx", i))) if ctx.Err() != nil { return zero, err } From 6a3d2d0aee0630d423e4fff9c595a9b65b90d3ad Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Wed, 30 Aug 2023 18:42:01 +0300 Subject: [PATCH 14/14] apply suggestion Co-authored-by: Ryan --- share/getters/cascade.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/getters/cascade.go b/share/getters/cascade.go index abd071c92b..eb3e969c1c 100644 --- a/share/getters/cascade.go +++ b/share/getters/cascade.go @@ -134,7 +134,7 @@ func cascadeGetters[V any]( span.RecordError(getErr, trace.WithAttributes(attribute.Int("getter_idx", i))) var byzantineErr *byzantine.ErrByzantine if errors.As(getErr, &byzantineErr) { - // short circuit if byzantine error was detected(to be able to handle it correctly + // short circuit if byzantine error was detected (to be able to handle it correctly // and create the BEFP) return zero, byzantineErr }