Skip to content

Commit

Permalink
Move timestamp verification into validity window package (#1895)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronbuchwald authored Jan 31, 2025
1 parent 3b4e97f commit 397710b
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 62 deletions.
24 changes: 10 additions & 14 deletions chain/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import (

"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/consts"
"github.com/ava-labs/hypersdk/internal/validitywindow"
)

const BaseSize = consts.Uint64Len*2 + ids.IDLen
const (
BaseSize = consts.Uint64Len*2 + ids.IDLen
// TODO: make this divisor configurable
validityWindowTimestampDivisor = consts.MillisecondsPerSecond
)

type Base struct {
// Timestamp is the expiry of the transaction (inclusive). Once this time passes and the
Expand All @@ -30,19 +35,10 @@ type Base struct {
}

func (b *Base) Execute(r Rules, timestamp int64) error {
switch {
case b.Timestamp%consts.MillisecondsPerSecond != 0:
// TODO: make this modulus configurable
return fmt.Errorf("%w: timestamp=%d", ErrMisalignedTime, b.Timestamp)
case b.Timestamp < timestamp: // tx: 100 block: 110
return fmt.Errorf("%w: tx timestamp (%d) < block timestamp (%d)", ErrTimestampTooLate, b.Timestamp, timestamp)
case b.Timestamp > timestamp+r.GetValidityWindow(): // tx: 100 block 10
return fmt.Errorf("%w: tx timestamp (%d) > block timestamp (%d) + validity window (%d)", ErrTimestampTooEarly, b.Timestamp, timestamp, r.GetValidityWindow())
case b.ChainID != r.GetChainID():
return ErrInvalidChainID
default:
return nil
if b.ChainID != r.GetChainID() {
return fmt.Errorf("%w: chainID=%s, expected=%s", ErrInvalidChainID, b.ChainID, r.GetChainID())
}
return validitywindow.VerifyTimestamp(b.Timestamp, timestamp, validityWindowTimestampDivisor, r.GetValidityWindow())
}

func (*Base) Size() int {
Expand All @@ -62,7 +58,7 @@ func UnmarshalBase(p *codec.Packer) (*Base, error) {
base.Timestamp = p.UnpackInt64(true)
if base.Timestamp%consts.MillisecondsPerSecond != 0 {
// TODO: make this modulus configurable
return nil, fmt.Errorf("%w: timestamp=%d", ErrMisalignedTime, base.Timestamp)
return nil, fmt.Errorf("%w: timestamp=%d", validitywindow.ErrMisalignedTime, base.Timestamp)
}
p.UnpackID(true, &base.ChainID)
base.MaxFee = p.UnpackUint64(true)
Expand Down
1 change: 0 additions & 1 deletion chain/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ var (
ErrActionNotActivated = errors.New("action not activated")
ErrAuthNotActivated = errors.New("auth not activated")
ErrAuthFailed = errors.New("auth failed")
ErrMisalignedTime = errors.New("misaligned time")
ErrInvalidActor = errors.New("invalid actor")
ErrInvalidSponsor = errors.New("invalid sponsor")
ErrTooManyActions = errors.New("too many actions")
Expand Down
2 changes: 1 addition & 1 deletion chain/pre_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func TestPreExecutor(t *testing.T) {
Auth: &mockAuth{},
},
validityWindow: &validitywindowtest.MockTimeValidityWindow[*chain.Transaction]{},
err: chain.ErrTimestampTooLate,
err: validitywindow.ErrTimestampExpired,
},
}

Expand Down
11 changes: 6 additions & 5 deletions chain/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/ava-labs/hypersdk/crypto/ed25519"
"github.com/ava-labs/hypersdk/genesis"
"github.com/ava-labs/hypersdk/internal/fees"
"github.com/ava-labs/hypersdk/internal/validitywindow"
"github.com/ava-labs/hypersdk/state"
"github.com/ava-labs/hypersdk/utils"

Expand Down Expand Up @@ -370,21 +371,21 @@ func TestPreExecute(t *testing.T) {
},
},
},
err: chain.ErrMisalignedTime,
err: validitywindow.ErrMisalignedTime,
},
{
name: "base transaction timestamp too early (61ms > 60ms)",
name: "base transaction timestamp too far in future (61ms > 60ms)",
tx: &chain.Transaction{
TransactionData: chain.TransactionData{
Base: &chain.Base{
Timestamp: testRules.GetValidityWindow() + consts.MillisecondsPerSecond,
},
},
},
err: chain.ErrTimestampTooEarly,
err: validitywindow.ErrFutureTimestamp,
},
{
name: "base transaction timestamp too late (1ms < 2ms)",
name: "base transaction timestamp expired (1ms < 2ms)",
tx: &chain.Transaction{
TransactionData: chain.TransactionData{
Base: &chain.Base{
Expand All @@ -393,7 +394,7 @@ func TestPreExecute(t *testing.T) {
},
},
timestamp: 2 * consts.MillisecondsPerSecond,
err: chain.ErrTimestampTooLate,
err: validitywindow.ErrTimestampExpired,
},
{
name: "base transaction invalid chain id",
Expand Down
16 changes: 16 additions & 0 deletions internal/validitywindow/validitywindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
var (
_ Interface[emap.Item] = (*TimeValidityWindow[emap.Item])(nil)
ErrDuplicateContainer = errors.New("duplicate container")
ErrMisalignedTime = errors.New("misaligned time")
ErrTimestampExpired = errors.New("declared timestamp expired")
ErrFutureTimestamp = errors.New("declared timestamp too far in the future")
)

type GetTimeValidityWindowFunc func(timestamp int64) int64
Expand Down Expand Up @@ -179,3 +182,16 @@ func (v *TimeValidityWindow[T]) isRepeat(
func (v *TimeValidityWindow[T]) calculateOldestAllowed(timestamp int64) int64 {
return max(0, timestamp-v.getTimeValidityWindow(timestamp))
}

func VerifyTimestamp(containerTimestamp int64, executionTimestamp int64, divisor int64, validityWindow int64) error {
switch {
case containerTimestamp%divisor != 0:
return fmt.Errorf("%w: timestamp (%d) %% divisor (%d) != 0", ErrMisalignedTime, containerTimestamp, divisor)
case containerTimestamp < executionTimestamp: // expiry: 100 block: 110
return fmt.Errorf("%w: timestamp (%d) < block timestamp (%d)", ErrTimestampExpired, containerTimestamp, executionTimestamp)
case containerTimestamp > executionTimestamp+validityWindow: // expiry: 100 block 10
return fmt.Errorf("%w: timestamp (%d) > block timestamp (%d) + validity window (%d)", ErrFutureTimestamp, containerTimestamp, executionTimestamp, validityWindow)
default:
return nil
}
}
97 changes: 97 additions & 0 deletions internal/validitywindow/validitywindow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,103 @@ func TestValidityWindowIsRepeat(t *testing.T) {
}
}

func TestVerifyTimestamp(t *testing.T) {
tests := []struct {
name string
containerTimestamp int64
executionTimestamp int64
divisor int64
validityWindow int64
expectedErr error
}{
{
name: "container ts = execution ts",
containerTimestamp: 10,
executionTimestamp: 10,
divisor: 1,
validityWindow: 10,
},
{
name: "container expired",
containerTimestamp: 9,
executionTimestamp: 10,
divisor: 1,
validityWindow: 10,
expectedErr: ErrTimestampExpired,
},
{
name: "container ts inside validity window",
containerTimestamp: 11,
executionTimestamp: 10,
divisor: 1,
validityWindow: 10,
},
{
name: "container ts past validity window",
containerTimestamp: 21,
executionTimestamp: 10,
divisor: 1,
validityWindow: 10,
expectedErr: ErrFutureTimestamp,
},
{
name: "container ts is not multiple of divisor",
containerTimestamp: 11,
executionTimestamp: 10,
divisor: 2,
validityWindow: 10,
expectedErr: ErrMisalignedTime,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := require.New(t)
err := VerifyTimestamp(test.containerTimestamp, test.executionTimestamp, test.divisor, test.validityWindow)
r.ErrorIs(err, test.expectedErr)
})
}
}

// TestValidityWindowBoundaryLifespan tests that a container included at the validity window boundary transitions
// seamlessly from failing veriifcation due to a duplicate within the validity window to failing because it expired.
func TestValidityWindowBoundaryLifespan(t *testing.T) {
r := require.New(t)

chainIndex := &testChainIndex{}
validityWindowDuration := int64(10)
validityWindow := NewTimeValidityWindow(&logging.NoLog{}, trace.Noop, chainIndex, func(int64) int64 {
return validityWindowDuration
})

// Create accepted genesis block
genesisBlk := newExecutionBlock(0, 0, []int64{1})
chainIndex.set(genesisBlk.GetID(), genesisBlk)
validityWindow.Accept(genesisBlk)

blk1 := newExecutionBlock(1, 0, []int64{validityWindowDuration})
blk2 := newExecutionBlock(2, validityWindowDuration, []int64{validityWindowDuration})

// Verify a timestamp at the validity window boundary
r.NoError(VerifyTimestamp(validityWindowDuration, 0, 1, validityWindowDuration))

// Including the first block should pass
r.NoError(validityWindow.VerifyExpiryReplayProtection(context.Background(), blk1))
chainIndex.set(blk1.GetID(), blk1)

// Verify a timestamp at the validity window boundary fails for both a processing
// and accepted parent.
// Processing:
r.ErrorIs(validityWindow.VerifyExpiryReplayProtection(context.Background(), blk2), ErrDuplicateContainer)

// Accepted:
validityWindow.Accept(blk1)
r.ErrorIs(validityWindow.VerifyExpiryReplayProtection(context.Background(), blk2), ErrDuplicateContainer)

// Verify that after passing the validity window, the timestamp is no longer valid
r.ErrorIs(VerifyTimestamp(validityWindowDuration, validityWindowDuration+1, 1, validityWindowDuration), ErrTimestampExpired)
}

type testChainIndex struct {
blocks map[ids.ID]ExecutionBlock[container]
}
Expand Down
13 changes: 7 additions & 6 deletions vm/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/ava-labs/hypersdk/extension/externalsubscriber"
"github.com/ava-labs/hypersdk/genesis"
"github.com/ava-labs/hypersdk/internal/mempool"
"github.com/ava-labs/hypersdk/internal/validitywindow"
"github.com/ava-labs/hypersdk/keys"
"github.com/ava-labs/hypersdk/state"
"github.com/ava-labs/hypersdk/state/balance"
Expand Down Expand Up @@ -209,7 +210,7 @@ func TestSubmitTx(t *testing.T) {
targetErr: nil,
},
{
name: chain.ErrMisalignedTime.Error(),
name: validitywindow.ErrMisalignedTime.Error(),
makeTx: func(r *require.Assertions, network *vmtest.TestNetwork) *chain.Transaction {
unsignedTx := chain.NewTxData(
&chain.Base{
Expand All @@ -223,10 +224,10 @@ func TestSubmitTx(t *testing.T) {
r.NoError(err)
return tx
},
targetErr: chain.ErrMisalignedTime,
targetErr: validitywindow.ErrMisalignedTime,
},
{
name: chain.ErrTimestampTooLate.Error(),
name: validitywindow.ErrTimestampExpired.Error(),
makeTx: func(r *require.Assertions, network *vmtest.TestNetwork) *chain.Transaction {
unsignedTx := chain.NewTxData(
&chain.Base{
Expand All @@ -240,10 +241,10 @@ func TestSubmitTx(t *testing.T) {
r.NoError(err)
return tx
},
targetErr: chain.ErrTimestampTooLate,
targetErr: validitywindow.ErrTimestampExpired,
},
{
name: chain.ErrTimestampTooEarly.Error(),
name: validitywindow.ErrFutureTimestamp.Error(),
makeTx: func(r *require.Assertions, network *vmtest.TestNetwork) *chain.Transaction {
unsignedTx := chain.NewTxData(
&chain.Base{
Expand All @@ -257,7 +258,7 @@ func TestSubmitTx(t *testing.T) {
r.NoError(err)
return tx
},
targetErr: chain.ErrTimestampTooEarly,
targetErr: validitywindow.ErrFutureTimestamp,
},
{
name: "invalid auth",
Expand Down
14 changes: 11 additions & 3 deletions x/dsmr/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ type Validator struct {
PublicKey *bls.PublicKey
}

type Rules interface {
GetValidityWindow() int64
}

type RuleFactory interface {
GetRules(t int64) Rules
}

func New[T Tx](
log logging.Logger,
nodeID ids.NodeID,
Expand All @@ -75,7 +83,7 @@ func New[T Tx](
quorumNum uint64,
quorumDen uint64,
timeValidityWindow TimeValidityWindow[*emapChunkCertificate],
validityWindowDuration time.Duration,
ruleFactory RuleFactory,
) (*Node[T], error) {
return &Node[T]{
ID: nodeID,
Expand All @@ -96,7 +104,7 @@ func New[T Tx](
storage: chunkStorage,
log: log,
validityWindow: timeValidityWindow,
validityWindowDuration: validityWindowDuration,
ruleFactory: ruleFactory,
}, nil
}

Expand All @@ -121,6 +129,7 @@ type Node[T Tx] struct {
LastAccepted Block
networkID uint32
chainID ids.ID
ruleFactory RuleFactory
getChunkClient *TypedClient[*dsmr.GetChunkRequest, Chunk[T], []byte]
chunkCertificateGossipClient *TypedClient[[]byte, []byte, *dsmr.ChunkCertificateGossip]
validators []Validator
Expand All @@ -133,7 +142,6 @@ type Node[T Tx] struct {
ChunkCertificateGossipHandler p2p.Handler
storage *ChunkStorage[T]
log logging.Logger
validityWindowDuration time.Duration
validityWindow TimeValidityWindow[*emapChunkCertificate]
}

Expand Down
Loading

0 comments on commit 397710b

Please sign in to comment.