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 }