github.com/sunriselayer/sunrise-da@v0.13.1-sr3/das/daser.go (about)

     1  package das
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync/atomic"
     8  	"time"
     9  
    10  	"github.com/ipfs/go-datastore"
    11  	logging "github.com/ipfs/go-log/v2"
    12  
    13  	"github.com/celestiaorg/go-fraud"
    14  	libhead "github.com/celestiaorg/go-header"
    15  
    16  	"github.com/sunriselayer/sunrise-da/header"
    17  	"github.com/sunriselayer/sunrise-da/share"
    18  	"github.com/sunriselayer/sunrise-da/share/eds/byzantine"
    19  	"github.com/sunriselayer/sunrise-da/share/p2p/shrexsub"
    20  )
    21  
    22  var log = logging.Logger("das")
    23  
    24  // DASer continuously validates availability of data committed to headers.
    25  type DASer struct {
    26  	params Parameters
    27  
    28  	da     share.Availability
    29  	bcast  fraud.Broadcaster[*header.ExtendedHeader]
    30  	hsub   libhead.Subscriber[*header.ExtendedHeader] // listens for new headers in the network
    31  	getter libhead.Getter[*header.ExtendedHeader]     // retrieves past headers
    32  
    33  	sampler    *samplingCoordinator
    34  	store      checkpointStore
    35  	subscriber subscriber
    36  
    37  	cancel         context.CancelFunc
    38  	subscriberDone chan struct{}
    39  	running        int32
    40  }
    41  
    42  type listenFn func(context.Context, *header.ExtendedHeader)
    43  type sampleFn func(context.Context, *header.ExtendedHeader) error
    44  
    45  // NewDASer creates a new DASer.
    46  func NewDASer(
    47  	da share.Availability,
    48  	hsub libhead.Subscriber[*header.ExtendedHeader],
    49  	getter libhead.Getter[*header.ExtendedHeader],
    50  	dstore datastore.Datastore,
    51  	bcast fraud.Broadcaster[*header.ExtendedHeader],
    52  	shrexBroadcast shrexsub.BroadcastFn,
    53  	options ...Option,
    54  ) (*DASer, error) {
    55  	d := &DASer{
    56  		params:         DefaultParameters(),
    57  		da:             da,
    58  		bcast:          bcast,
    59  		hsub:           hsub,
    60  		getter:         getter,
    61  		store:          newCheckpointStore(dstore),
    62  		subscriber:     newSubscriber(),
    63  		subscriberDone: make(chan struct{}),
    64  	}
    65  
    66  	for _, applyOpt := range options {
    67  		applyOpt(d)
    68  	}
    69  
    70  	err := d.params.Validate()
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	d.sampler = newSamplingCoordinator(d.params, getter, d.sample, shrexBroadcast)
    76  	return d, nil
    77  }
    78  
    79  // Start initiates subscription for new ExtendedHeaders and spawns a sampling routine.
    80  func (d *DASer) Start(ctx context.Context) error {
    81  	if !atomic.CompareAndSwapInt32(&d.running, 0, 1) {
    82  		return errors.New("da: DASer already started")
    83  	}
    84  
    85  	sub, err := d.hsub.Subscribe()
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	// load latest DASed checkpoint
    91  	cp, err := d.store.load(ctx)
    92  	if err != nil {
    93  		log.Warnw("checkpoint not found, initializing with height 1")
    94  
    95  		cp = checkpoint{
    96  			SampleFrom:  d.params.SampleFrom,
    97  			NetworkHead: d.params.SampleFrom,
    98  		}
    99  
   100  		// attempt to get head info. No need to handle error, later DASer
   101  		// will be able to find new head from subscriber after it is started
   102  		if h, err := d.getter.Head(ctx); err == nil {
   103  			cp.NetworkHead = h.Height()
   104  		}
   105  	}
   106  	log.Info("starting DASer from checkpoint: ", cp.String())
   107  
   108  	runCtx, cancel := context.WithCancel(context.Background())
   109  	d.cancel = cancel
   110  
   111  	go d.sampler.run(runCtx, cp)
   112  	go d.subscriber.run(runCtx, sub, d.sampler.listen)
   113  	go d.store.runBackgroundStore(runCtx, d.params.BackgroundStoreInterval, d.sampler.getCheckpoint)
   114  
   115  	return nil
   116  }
   117  
   118  // Stop stops sampling.
   119  func (d *DASer) Stop(ctx context.Context) error {
   120  	if !atomic.CompareAndSwapInt32(&d.running, 1, 0) {
   121  		return nil
   122  	}
   123  
   124  	// try to store checkpoint without waiting for coordinator and workers to stop
   125  	cp, err := d.sampler.getCheckpoint(ctx)
   126  	if err != nil {
   127  		log.Error("DASer coordinator checkpoint is unavailable")
   128  	}
   129  
   130  	if err = d.store.store(ctx, cp); err != nil {
   131  		log.Errorw("storing checkpoint to disk", "err", err)
   132  	}
   133  
   134  	d.cancel()
   135  	if err = d.sampler.wait(ctx); err != nil {
   136  		return fmt.Errorf("DASer force quit: %w", err)
   137  	}
   138  
   139  	// save updated checkpoint after sampler and all workers are shut down
   140  	if err = d.store.store(ctx, newCheckpoint(d.sampler.state.unsafeStats())); err != nil {
   141  		log.Errorw("storing checkpoint to disk", "err", err)
   142  	}
   143  
   144  	if err = d.store.wait(ctx); err != nil {
   145  		return fmt.Errorf("DASer force quit with err: %w", err)
   146  	}
   147  	return d.subscriber.wait(ctx)
   148  }
   149  
   150  func (d *DASer) sample(ctx context.Context, h *header.ExtendedHeader) error {
   151  	// short-circuit if pruning is enabled and the header is outside the
   152  	// availability window
   153  	if !d.isWithinSamplingWindow(h) {
   154  		log.Debugw("skipping header outside sampling window", "height", h.Height(),
   155  			"time", h.Time())
   156  		return nil
   157  	}
   158  
   159  	err := d.da.SharesAvailable(ctx, h)
   160  	if err != nil {
   161  		var byzantineErr *byzantine.ErrByzantine
   162  		if errors.As(err, &byzantineErr) {
   163  			log.Warn("Propagating proof...")
   164  			sendErr := d.bcast.Broadcast(ctx, byzantine.CreateBadEncodingProof(h.Hash(), h.Height(), byzantineErr))
   165  			if sendErr != nil {
   166  				log.Errorw("fraud proof propagating failed", "err", sendErr)
   167  			}
   168  		}
   169  		return err
   170  	}
   171  	return nil
   172  }
   173  
   174  func (d *DASer) isWithinSamplingWindow(eh *header.ExtendedHeader) bool {
   175  	// if sampling window is not set, then all headers are within the window
   176  	if d.params.SamplingWindow == 0 {
   177  		return true
   178  	}
   179  	return time.Since(eh.Time()) <= d.params.SamplingWindow
   180  }
   181  
   182  // SamplingStats returns the current statistics over the DA sampling process.
   183  func (d *DASer) SamplingStats(ctx context.Context) (SamplingStats, error) {
   184  	return d.sampler.stats(ctx)
   185  }
   186  
   187  // WaitCatchUp waits for DASer to indicate catchup is done
   188  func (d *DASer) WaitCatchUp(ctx context.Context) error {
   189  	return d.sampler.state.waitCatchUp(ctx)
   190  }