diff --git a/das/daser_test.go b/das/daser_test.go index 68f6e01ef2..e4e74dc7ff 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" @@ -180,7 +181,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/fraud/testing.go b/header/headertest/fraud/testing.go new file mode 100644 index 0000000000..6a5cda733d --- /dev/null +++ b/header/headertest/fraud/testing.go @@ -0,0 +1,100 @@ +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(h *types.Header, + comm *types.Commit, + vals *types.ValidatorSet, + eds *rsmt2d.ExtendedDataSquare, + ) (*header.ExtendedHeader, error) { + if h.Height < f.height { + return header.MakeExtendedHeader(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(context.Background(), 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(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 65ae8c950f..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" @@ -26,12 +23,8 @@ import ( "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/header" - "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 { @@ -296,32 +289,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,21 +315,6 @@ 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 -} - type Subscriber struct { headertest.Subscriber[*header.ExtendedHeader] } 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) } 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)) +} diff --git a/nodebuilder/tests/fraud_test.go b/nodebuilder/tests/fraud_test.go index f652724d55..95c702c0c0 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" + 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" "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,114 @@ 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. +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 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 := headerfraud.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 + // 1. 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) + // 2. + err = bridge.Start(ctx) require.NoError(t, err) + // 3. 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) + // 4. 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) + // 5. + 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(): - t.Fatal("fraud proof was not received in time") + subCancel() + 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. - syncCtx, syncCancel := context.WithTimeout(context.Background(), btime) - _, err = full.HeaderServ.WaitForHeight(syncCtx, 100) + // 6. + 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) - 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)) + // 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)) - subsFN, err := full.FraudServ.Subscribe(ctx, byzantine.BadEncoding) + // 8. + subCtx, subCancel = context.WithCancel(ctx) + subscr, err = light.FraudServ.Subscribe(subCtx, byzantine.BadEncoding) require.NoError(t, err) - select { - case <-subsFN: + case p := <-subscr: + require.Equal(t, 10, int(p.Height())) + subCancel() case <-ctx.Done(): - t.Fatal("full node didn't get FP in time") + subCancel() + t.Fatal("light node did not receive a fraud proof 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)) + // 9. + 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) - select { - case <-subsLN: - case <-ctx.Done(): - t.Fatal("light node didn't get FP in time") - } + sw.StopNode(ctx, bridge) + sw.StopNode(ctx, full) + sw.StopNode(ctx, light) require.NoError(t, <-fillDn) } 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 +} 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 } diff --git a/share/getters/cascade.go b/share/getters/cascade.go index 63d7713d3d..eb3e969c1c 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,8 +131,15 @@ func cascadeGetters[V any]( continue } - err = errors.Join(err, getErr) 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) if ctx.Err() != nil { return zero, err } 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) }