From ac5d7a7e2f9b8234cdc28c468f5241a39967eb18 Mon Sep 17 00:00:00 2001 From: rene <41963722+renaynay@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:24:07 +0100 Subject: [PATCH] refactor(share/availability/light | share/availability/full): Availability implementations are aware of sampling window, removed from DASer (#3957) --- core/eds.go | 3 +- core/exchange.go | 6 ++- core/exchange_test.go | 49 +++++++++++++++++ core/listener.go | 4 +- core/option.go | 8 +++ das/daser.go | 14 ----- das/daser_test.go | 41 --------------- das/options.go | 13 ----- das/worker.go | 5 +- nodebuilder/das/constructors.go | 4 -- nodebuilder/pruner/module.go | 13 ++--- share/availability/full/availability.go | 28 ++++++++-- share/availability/full/availability_test.go | 52 +++++++++++++++++++ share/availability/full/options.go | 25 +++++++++ share/availability/light/availability.go | 9 ++++ share/availability/light/availability_test.go | 1 + share/availability/light/options.go | 2 +- share/availability/window.go | 3 ++ 18 files changed, 192 insertions(+), 88 deletions(-) create mode 100644 share/availability/full/options.go diff --git a/core/eds.go b/core/eds.go index 5e95f30103..2e8ce7ea19 100644 --- a/core/eds.go +++ b/core/eds.go @@ -62,8 +62,9 @@ func storeEDS( eds *rsmt2d.ExtendedDataSquare, store *store.Store, window time.Duration, + archival bool, ) error { - if !availability.IsWithinWindow(eh.Time(), window) { + if !archival && !availability.IsWithinWindow(eh.Time(), window) { log.Debugw("skipping storage of historic block", "height", eh.Height()) return nil } diff --git a/core/exchange.go b/core/exchange.go index a813955968..372906ff85 100644 --- a/core/exchange.go +++ b/core/exchange.go @@ -23,6 +23,7 @@ type Exchange struct { construct header.ConstructFn availabilityWindow time.Duration + archival bool metrics *exchangeMetrics } @@ -54,6 +55,7 @@ func NewExchange( store: store, construct: construct, availabilityWindow: p.availabilityWindow, + archival: p.archival, metrics: metrics, }, nil } @@ -147,7 +149,7 @@ func (ce *Exchange) Get(ctx context.Context, hash libhead.Hash) (*header.Extende &block.Height, hash, eh.Hash()) } - err = storeEDS(ctx, eh, eds, ce.store, ce.availabilityWindow) + err = storeEDS(ctx, eh, eds, ce.store, ce.availabilityWindow, ce.archival) if err != nil { return nil, err } @@ -187,7 +189,7 @@ func (ce *Exchange) getExtendedHeaderByHeight(ctx context.Context, height *int64 panic(fmt.Errorf("constructing extended header for height %d: %w", b.Header.Height, err)) } - err = storeEDS(ctx, eh, eds, ce.store, ce.availabilityWindow) + err = storeEDS(ctx, eh, eds, ce.store, ce.availabilityWindow, ce.archival) if err != nil { return nil, err } diff --git a/core/exchange_test.go b/core/exchange_test.go index f7e69be8a4..a2187ed7c8 100644 --- a/core/exchange_test.go +++ b/core/exchange_test.go @@ -111,6 +111,55 @@ func TestExchange_DoNotStoreHistoric(t *testing.T) { } } +// TestExchange_StoreHistoricIfArchival makes sure blocks are stored past +// sampling window if archival is enabled +func TestExchange_StoreHistoricIfArchival(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + cfg := DefaultTestConfig() + fetcher, cctx := createCoreFetcher(t, cfg) + + generateNonEmptyBlocks(t, ctx, fetcher, cfg, cctx) + + store, err := store.NewStore(store.DefaultParameters(), t.TempDir()) + require.NoError(t, err) + + ce, err := NewExchange( + fetcher, + store, + header.MakeExtendedHeader, + WithAvailabilityWindow(time.Nanosecond), // all blocks will be "historic" + WithArchivalMode(), // make sure to store them anyway + ) + require.NoError(t, err) + + // initialize store with genesis block + genHeight := int64(1) + genBlock, err := fetcher.GetBlock(ctx, &genHeight) + require.NoError(t, err) + genHeader, err := ce.Get(ctx, genBlock.Header.Hash().Bytes()) + require.NoError(t, err) + + headers, err := ce.GetRangeByHeight(ctx, genHeader, 30) + require.NoError(t, err) + + // ensure all "historic" EDSs were stored + for _, h := range headers { + has, err := store.HasByHeight(ctx, h.Height()) + require.NoError(t, err) + assert.True(t, has) + + // empty EDSs are expected to exist in the store, so we skip them + if h.DAH.Equals(share.EmptyEDSRoots()) { + continue + } + has, err = store.HasByHash(ctx, h.DAH.Hash()) + require.NoError(t, err) + assert.True(t, has) + } +} + func createCoreFetcher(t *testing.T, cfg *testnode.Config) (*BlockFetcher, testnode.Context) { cctx := StartTestNodeWithConfig(t, cfg) // wait for height 2 in order to be able to start submitting txs (this prevents diff --git a/core/listener.go b/core/listener.go index 1d7dc1b061..d403421175 100644 --- a/core/listener.go +++ b/core/listener.go @@ -38,6 +38,7 @@ type Listener struct { construct header.ConstructFn store *store.Store availabilityWindow time.Duration + archival bool headerBroadcaster libhead.Broadcaster[*header.ExtendedHeader] hashBroadcaster shrexsub.BroadcastFn @@ -83,6 +84,7 @@ func NewListener( construct: construct, store: store, availabilityWindow: p.availabilityWindow, + archival: p.archival, listenerTimeout: 5 * blocktime, metrics: metrics, chainID: p.chainID, @@ -237,7 +239,7 @@ func (cl *Listener) handleNewSignedBlock(ctx context.Context, b types.EventDataS panic(fmt.Errorf("making extended header: %w", err)) } - err = storeEDS(ctx, eh, eds, cl.store, cl.availabilityWindow) + err = storeEDS(ctx, eh, eds, cl.store, cl.availabilityWindow, cl.archival) if err != nil { return fmt.Errorf("storing EDS: %w", err) } diff --git a/core/option.go b/core/option.go index 3e9b5a8e20..874246bd84 100644 --- a/core/option.go +++ b/core/option.go @@ -12,11 +12,13 @@ type params struct { metrics bool chainID string availabilityWindow time.Duration + archival bool } func defaultParams() params { return params{ availabilityWindow: time.Duration(0), + archival: false, } } @@ -39,3 +41,9 @@ func WithAvailabilityWindow(window time.Duration) Option { p.availabilityWindow = window } } + +func WithArchivalMode() Option { + return func(p *params) { + p.archival = true + } +} diff --git a/das/daser.go b/das/daser.go index fa41bd56ad..d255d4e293 100644 --- a/das/daser.go +++ b/das/daser.go @@ -14,18 +14,12 @@ import ( "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/availability" "github.com/celestiaorg/celestia-node/share/eds/byzantine" "github.com/celestiaorg/celestia-node/share/shwap/p2p/shrex/shrexsub" ) var log = logging.Logger("das") -// errOutsideSamplingWindow is an error used to inform -// the caller of Sample that the given header is outside -// the sampling window. -var errOutsideSamplingWindow = fmt.Errorf("skipping header outside of sampling window") - // DASer continuously validates availability of data committed to headers. type DASer struct { params Parameters @@ -160,14 +154,6 @@ func (d *DASer) Stop(ctx context.Context) error { } func (d *DASer) sample(ctx context.Context, h *header.ExtendedHeader) error { - // short-circuit if pruning is enabled and the header is outside the - // availability window - if !availability.IsWithinWindow(h.Time(), d.params.samplingWindow) { - log.Debugw("skipping header outside sampling window", "height", h.Height(), - "time", h.Time()) - return errOutsideSamplingWindow - } - err := d.da.SharesAvailable(ctx, h) if err != nil { var byzantineErr *byzantine.ErrByzantine diff --git a/das/daser_test.go b/das/daser_test.go index 2cdc43497a..ac1d3b6190 100644 --- a/das/daser_test.go +++ b/das/daser_test.go @@ -3,7 +3,6 @@ package das import ( "context" "fmt" - "strconv" "testing" "time" @@ -20,7 +19,6 @@ import ( "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/availability" "github.com/celestiaorg/celestia-node/share/availability/mocks" "github.com/celestiaorg/celestia-node/share/eds/edstest" ) @@ -243,45 +241,6 @@ func TestDASerSampleTimeout(t *testing.T) { } } -// TestDASer_SamplingWindow tests the sampling window determination -// for headers. -func TestDASer_SamplingWindow(t *testing.T) { - ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - sub := new(headertest.Subscriber) - fserv := &fraudtest.DummyService[*header.ExtendedHeader]{} - getter := getterStub{} - avail := mocks.NewMockAvailability(gomock.NewController(t)) - - // create and start DASer - daser, err := NewDASer(avail, sub, getter, ds, fserv, newBroadcastMock(1), - WithSamplingWindow(time.Second)) - require.NoError(t, err) - - tests := []struct { - timestamp time.Time - withinWindow bool - }{ - {timestamp: time.Now().Add(-(time.Second * 5)), withinWindow: false}, - {timestamp: time.Now().Add(-(time.Millisecond * 800)), withinWindow: true}, - {timestamp: time.Now().Add(-(time.Hour)), withinWindow: false}, - {timestamp: time.Now().Add(-(time.Hour * 24 * 30)), withinWindow: false}, - {timestamp: time.Now(), withinWindow: true}, - } - - for i, tt := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - eh := headertest.RandExtendedHeader(t) - eh.RawHeader.Time = tt.timestamp - - assert.Equal( - t, - tt.withinWindow, - availability.IsWithinWindow(eh.Time(), daser.params.samplingWindow), - ) - }) - } -} - // createDASerSubcomponents takes numGetter (number of headers // to store in mockGetter) and numSub (number of headers to store // in the mock header.Subscriber), returning a newly instantiated diff --git a/das/options.go b/das/options.go index 6a665d5577..cd7444ac16 100644 --- a/das/options.go +++ b/das/options.go @@ -41,11 +41,6 @@ type Parameters struct { // divided between parallel workers. SampleTimeout should be adjusted proportionally to // ConcurrencyLimit. SampleTimeout time.Duration - - // samplingWindow determines the time window that headers should fall into - // in order to be sampled. If set to 0, the sampling window will include - // all headers. - samplingWindow time.Duration } // DefaultParameters returns the default configuration values for the daser parameters @@ -161,11 +156,3 @@ func WithSampleTimeout(sampleTimeout time.Duration) Option { d.params.SampleTimeout = sampleTimeout } } - -// WithSamplingWindow is a functional option to configure the DASer's -// `samplingWindow` parameter. -func WithSamplingWindow(samplingWindow time.Duration) Option { - return func(d *DASer) { - d.params.samplingWindow = samplingWindow - } -} diff --git a/das/worker.go b/das/worker.go index 0acd4b41a6..b9dff58445 100644 --- a/das/worker.go +++ b/das/worker.go @@ -10,6 +10,7 @@ import ( libhead "github.com/celestiaorg/go-header" "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/share/availability" "github.com/celestiaorg/celestia-node/share/shwap/p2p/shrex/shrexsub" ) @@ -83,7 +84,7 @@ func (w *worker) run(ctx context.Context, timeout time.Duration, resultCh chan<- // sampling worker will resume upon restart return } - if errors.Is(err, errOutsideSamplingWindow) { + if errors.Is(err, availability.ErrOutsideSamplingWindow) { skipped++ err = nil } @@ -119,7 +120,7 @@ func (w *worker) sample(ctx context.Context, timeout time.Duration, height uint6 defer cancel() err = w.sampleFn(ctx, h) - if errors.Is(err, errOutsideSamplingWindow) { + if errors.Is(err, availability.ErrOutsideSamplingWindow) { // if header is outside sampling window, do not log // or record it. return err diff --git a/nodebuilder/das/constructors.go b/nodebuilder/das/constructors.go index b9b7bdf100..db35c1a0c2 100644 --- a/nodebuilder/das/constructors.go +++ b/nodebuilder/das/constructors.go @@ -12,7 +12,6 @@ import ( "github.com/celestiaorg/celestia-node/das" "github.com/celestiaorg/celestia-node/header" modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud" - modshare "github.com/celestiaorg/celestia-node/nodebuilder/share" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds/byzantine" "github.com/celestiaorg/celestia-node/share/shwap/p2p/shrex/shrexsub" @@ -45,11 +44,8 @@ func newDASer( batching datastore.Batching, fraudServ fraud.Service[*header.ExtendedHeader], bFn shrexsub.BroadcastFn, - availWindow modshare.Window, options ...das.Option, ) (*das.DASer, *modfraud.ServiceBreaker[*das.DASer, *header.ExtendedHeader], error) { - options = append(options, das.WithSamplingWindow(availWindow.Duration())) - ds, err := das.NewDASer(da, hsub, store, batching, fraudServ, bFn, options...) if err != nil { return nil, nil, err diff --git a/nodebuilder/pruner/module.go b/nodebuilder/pruner/module.go index 537d9127f2..22b86ee6ec 100644 --- a/nodebuilder/pruner/module.go +++ b/nodebuilder/pruner/module.go @@ -15,6 +15,7 @@ import ( "github.com/celestiaorg/celestia-node/pruner" "github.com/celestiaorg/celestia-node/pruner/full" "github.com/celestiaorg/celestia-node/share/availability" + fullavail "github.com/celestiaorg/celestia-node/share/availability/full" "github.com/celestiaorg/celestia-node/share/availability/light" "github.com/celestiaorg/celestia-node/share/shwap/p2p/discovery" ) @@ -59,6 +60,7 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { baseComponents, prunerService, fxutil.ProvideAs(full.NewPruner, new(pruner.Pruner)), + fx.Supply([]fullavail.Option{}), ) } return fx.Module("prune", @@ -66,6 +68,7 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { fx.Invoke(func(ctx context.Context, ds datastore.Batching) error { return pruner.DetectPreviousRun(ctx, ds) }), + fx.Supply([]fullavail.Option{fullavail.WithArchivalMode()}), ) case node.Bridge: if cfg.EnableService { @@ -73,9 +76,8 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { baseComponents, prunerService, fxutil.ProvideAs(full.NewPruner, new(pruner.Pruner)), - fx.Provide(func(window modshare.Window) []core.Option { - return []core.Option{core.WithAvailabilityWindow(window.Duration())} - }), + fx.Supply([]fullavail.Option{}), + fx.Supply([]core.Option{}), ) } return fx.Module("prune", @@ -83,9 +85,8 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { fx.Invoke(func(ctx context.Context, ds datastore.Batching) error { return pruner.DetectPreviousRun(ctx, ds) }), - fx.Provide(func() []core.Option { - return []core.Option{} - }), + fx.Supply([]fullavail.Option{fullavail.WithArchivalMode()}), + fx.Supply([]core.Option{core.WithArchivalMode()}), ) default: panic("unknown node type") diff --git a/share/availability/full/availability.go b/share/availability/full/availability.go index b95a648d5d..c7f7999366 100644 --- a/share/availability/full/availability.go +++ b/share/availability/full/availability.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" logging "github.com/ipfs/go-log/v2" @@ -23,16 +24,27 @@ var log = logging.Logger("share/full") type ShareAvailability struct { store *store.Store getter shwap.Getter + + storageWindow time.Duration + archival bool } // NewShareAvailability creates a new full ShareAvailability. func NewShareAvailability( store *store.Store, getter shwap.Getter, + opts ...Option, ) *ShareAvailability { + p := defaultParams() + for _, opt := range opts { + opt(p) + } + return &ShareAvailability{ - store: store, - getter: getter, + store: store, + getter: getter, + storageWindow: availability.StorageWindow, + archival: p.archival, } } @@ -40,6 +52,16 @@ func NewShareAvailability( // enough Shares from the network. func (fa *ShareAvailability) SharesAvailable(ctx context.Context, header *header.ExtendedHeader) error { dah := header.DAH + + if !fa.archival { + // do not sync blocks outside of sampling window if not archival + if !availability.IsWithinWindow(header.Time(), fa.storageWindow) { + log.Debugw("skipping availability check for block outside sampling"+ + " window", "height", header.Height(), "data hash", dah.String()) + return availability.ErrOutsideSamplingWindow + } + } + // if the data square is empty, we can safely link the header height in the store to an empty EDS. if share.DataHash(dah.Hash()).IsEmptyEDS() { err := fa.store.PutODSQ4(ctx, dah, header.Height(), share.EmptyEDS()) @@ -68,7 +90,7 @@ func (fa *ShareAvailability) SharesAvailable(ctx context.Context, header *header } // archival nodes should not store Q4 outside the availability window. - if availability.IsWithinWindow(header.Time(), availability.StorageWindow) { + if availability.IsWithinWindow(header.Time(), fa.storageWindow) { err = fa.store.PutODSQ4(ctx, dah, header.Height(), eds) } else { err = fa.store.PutODS(ctx, dah, header.Height(), eds) diff --git a/share/availability/full/availability_test.go b/share/availability/full/availability_test.go index 95f8bda533..24435ee0d8 100644 --- a/share/availability/full/availability_test.go +++ b/share/availability/full/availability_test.go @@ -6,10 +6,12 @@ import ( "time" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/availability" "github.com/celestiaorg/celestia-node/share/eds/edstest" "github.com/celestiaorg/celestia-node/share/shwap" "github.com/celestiaorg/celestia-node/share/shwap/getters/mock" @@ -97,3 +99,53 @@ func TestSharesAvailable_ErrNotAvailable(t *testing.T) { require.ErrorIs(t, err, share.ErrNotAvailable) } } + +// TestSharesAvailable_OutsideSamplingWindow_NonArchival tests to make sure +// blocks are skipped that are outside sampling window. +func TestSharesAvailable_OutsideSamplingWindow_NonArchival(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + getter := mock.NewMockGetter(gomock.NewController(t)) + getter.EXPECT().GetEDS(gomock.Any(), gomock.Any()).Times(0) + store, err := store.NewStore(store.DefaultParameters(), t.TempDir()) + require.NoError(t, err) + + suite := headertest.NewTestSuite(t, 3, time.Nanosecond) + headers := suite.GenExtendedHeaders(10) + + avail := NewShareAvailability(store, getter) + avail.storageWindow = time.Nanosecond // make all headers outside sampling window + + for _, h := range headers { + err := avail.SharesAvailable(ctx, h) + assert.ErrorIs(t, err, availability.ErrOutsideSamplingWindow) + } +} + +// TestSharesAvailable_OutsideSamplingWindow_Archival tests to make sure +// blocks are still synced that are outside sampling window. +func TestSharesAvailable_OutsideSamplingWindow_Archival(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + getter := mock.NewMockGetter(gomock.NewController(t)) + store, err := store.NewStore(store.DefaultParameters(), t.TempDir()) + require.NoError(t, err) + + eds := edstest.RandEDS(t, 4) + roots, err := share.NewAxisRoots(eds) + eh := headertest.RandExtendedHeaderWithRoot(t, roots) + require.NoError(t, err) + + getter.EXPECT().GetEDS(gomock.Any(), gomock.Any()).Times(1).Return(eds, nil) + + avail := NewShareAvailability(store, getter, WithArchivalMode()) + avail.storageWindow = time.Nanosecond // make all headers outside sampling window + + err = avail.SharesAvailable(ctx, eh) + require.NoError(t, err) + has, err := store.HasByHash(ctx, roots.Hash()) + require.NoError(t, err) + assert.True(t, has) +} diff --git a/share/availability/full/options.go b/share/availability/full/options.go new file mode 100644 index 0000000000..689a225a9a --- /dev/null +++ b/share/availability/full/options.go @@ -0,0 +1,25 @@ +package full + +type params struct { + archival bool +} + +// Option is a function that configures light availability Parameters +type Option func(*params) + +// DefaultParameters returns the default Parameters' configuration values +// for the light availability implementation +func defaultParams() *params { + return ¶ms{ + archival: false, + } +} + +// WithArchivalMode is a functional option to tell the full availability +// implementation that the node wants to sync *all* blocks, not just those +// within the sampling window. +func WithArchivalMode() Option { + return func(p *params) { + p.archival = true + } +} diff --git a/share/availability/light/availability.go b/share/availability/light/availability.go index d26aee281d..f3afbb26fd 100644 --- a/share/availability/light/availability.go +++ b/share/availability/light/availability.go @@ -17,6 +17,7 @@ import ( "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/availability" "github.com/celestiaorg/celestia-node/share/ipld" "github.com/celestiaorg/celestia-node/share/shwap" "github.com/celestiaorg/celestia-node/share/shwap/p2p/bitswap" @@ -37,6 +38,8 @@ type ShareAvailability struct { bs blockstore.Blockstore params Parameters + storageWindow time.Duration + activeHeights *utils.Sessions dsLk sync.RWMutex ds *autobatch.Datastore @@ -61,6 +64,7 @@ func NewShareAvailability( getter: getter, bs: bs, params: params, + storageWindow: availability.StorageWindow, activeHeights: utils.NewSessions(), ds: autoDS, } @@ -76,6 +80,11 @@ func (la *ShareAvailability) SharesAvailable(ctx context.Context, header *header return nil } + // short-circuit if outside sampling window + if !availability.IsWithinWindow(header.Time(), la.storageWindow) { + return availability.ErrOutsideSamplingWindow + } + // Prevent multiple sampling and pruning sessions for the same header height release, err := la.activeHeights.StartSession(ctx, header.Height()) if err != nil { diff --git a/share/availability/light/availability_test.go b/share/availability/light/availability_test.go index b1a9299bea..191c286938 100644 --- a/share/availability/light/availability_test.go +++ b/share/availability/light/availability_test.go @@ -517,6 +517,7 @@ func randEdsAndHeader(t *testing.T, size int) (*rsmt2d.ExtendedDataSquare, *head h := &header.ExtendedHeader{ RawHeader: header.RawHeader{ Height: int64(height), + Time: time.Now(), }, DAH: roots, } diff --git a/share/availability/light/options.go b/share/availability/light/options.go index 466b9fb030..3ed8038f51 100644 --- a/share/availability/light/options.go +++ b/share/availability/light/options.go @@ -4,7 +4,7 @@ import ( "fmt" ) -// SampleAmount specifies the minimum required amount of samples a light node must perform +// DefaultSampleAmount specifies the minimum required amount of samples a light node must perform // before declaring that a block is available var ( DefaultSampleAmount uint = 16 diff --git a/share/availability/window.go b/share/availability/window.go index 6227c54d64..cdf60c106b 100644 --- a/share/availability/window.go +++ b/share/availability/window.go @@ -1,6 +1,7 @@ package availability import ( + "errors" "os" "time" ) @@ -10,6 +11,8 @@ const ( StorageWindow = RequestWindow + time.Hour ) +var ErrOutsideSamplingWindow = errors.New("timestamp outside sampling window") + // IsWithinWindow checks whether the given timestamp is within the // given AvailabilityWindow. If the window is disabled (0), it returns true for // every timestamp.