diff --git a/msm/encoding.canoto.go b/msm/encoding.canoto.go index 30f82706..d2651feb 100644 --- a/msm/encoding.canoto.go +++ b/msm/encoding.canoto.go @@ -28,12 +28,14 @@ const ( canotoNumber_StateMachineMetadata__SimplexBlacklist = 3 canotoNumber_StateMachineMetadata__PChainHeight = 4 canotoNumber_StateMachineMetadata__Timestamp = 5 + canotoNumber_StateMachineMetadata__ICMEpochInfo = 6 canotoTag_StateMachineMetadata__SimplexEpochInfo = "\x0a" // canoto.Tag(canotoNumber_StateMachineMetadata__SimplexEpochInfo, canoto.Len) canotoTag_StateMachineMetadata__SimplexProtocolMetadata = "\x12" // canoto.Tag(canotoNumber_StateMachineMetadata__SimplexProtocolMetadata, canoto.Len) canotoTag_StateMachineMetadata__SimplexBlacklist = "\x1a" // canoto.Tag(canotoNumber_StateMachineMetadata__SimplexBlacklist, canoto.Len) canotoTag_StateMachineMetadata__PChainHeight = "\x20" // canoto.Tag(canotoNumber_StateMachineMetadata__PChainHeight, canoto.Varint) canotoTag_StateMachineMetadata__Timestamp = "\x28" // canoto.Tag(canotoNumber_StateMachineMetadata__Timestamp, canoto.Varint) + canotoTag_StateMachineMetadata__ICMEpochInfo = "\x32" // canoto.Tag(canotoNumber_StateMachineMetadata__ICMEpochInfo, canoto.Len) ) type canotoData_StateMachineMetadata struct { @@ -81,6 +83,16 @@ func (*StateMachineMetadata) CanotoSpec(types ...reflect.Type) *canoto.Spec { OneOf: "", TypeUint: canoto.SizeOf(zero.Timestamp), }, + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.ICMEpochInfo), + /*FieldNumber: */ canotoNumber_StateMachineMetadata__ICMEpochInfo, + /*Name: */ "ICMEpochInfo", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), }, } s.CalculateCanotoCache() @@ -187,6 +199,30 @@ func (c *StateMachineMetadata) UnmarshalCanotoFrom(r canoto.Reader) error { if canoto.IsZero(c.Timestamp) { return canoto.ErrZeroValue } + case canotoNumber_StateMachineMetadata__ICMEpochInfo: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + if err := (&c.ICMEpochInfo).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes default: return canoto.ErrUnknownField } @@ -207,6 +243,9 @@ func (c *StateMachineMetadata) ValidCanoto() bool { if !(&c.SimplexEpochInfo).ValidCanoto() { return false } + if !(&c.ICMEpochInfo).ValidCanoto() { + return false + } return true } @@ -232,6 +271,10 @@ func (c *StateMachineMetadata) CalculateCanotoCache() { if !canoto.IsZero(c.Timestamp) { size += uint64(len(canotoTag_StateMachineMetadata__Timestamp)) + canoto.SizeUint(c.Timestamp) } + (&c.ICMEpochInfo).CalculateCanotoCache() + if fieldSize := (&c.ICMEpochInfo).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canotoTag_StateMachineMetadata__ICMEpochInfo)) + canoto.SizeUint(fieldSize) + fieldSize + } atomic.StoreUint64(&c.canotoData.size, size) } @@ -291,6 +334,208 @@ func (c *StateMachineMetadata) MarshalCanotoInto(w canoto.Writer) canoto.Writer canoto.Append(&w, canotoTag_StateMachineMetadata__Timestamp) canoto.AppendUint(&w, c.Timestamp) } + if fieldSize := (&c.ICMEpochInfo).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canotoTag_StateMachineMetadata__ICMEpochInfo) + canoto.AppendUint(&w, fieldSize) + w = (&c.ICMEpochInfo).MarshalCanotoInto(w) + } + return w +} + +const ( + canotoNumber_ICMEpochInfo__EpochStartTime = 1 + canotoNumber_ICMEpochInfo__EpochNumber = 2 + canotoNumber_ICMEpochInfo__PChainEpochHeight = 3 + + canotoTag_ICMEpochInfo__EpochStartTime = "\x08" // canoto.Tag(canotoNumber_ICMEpochInfo__EpochStartTime, canoto.Varint) + canotoTag_ICMEpochInfo__EpochNumber = "\x10" // canoto.Tag(canotoNumber_ICMEpochInfo__EpochNumber, canoto.Varint) + canotoTag_ICMEpochInfo__PChainEpochHeight = "\x18" // canoto.Tag(canotoNumber_ICMEpochInfo__PChainEpochHeight, canoto.Varint) +) + +type canotoData_ICMEpochInfo struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*ICMEpochInfo) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero ICMEpochInfo + s := &canoto.Spec{ + Name: "ICMEpochInfo", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_ICMEpochInfo__EpochStartTime, + Name: "EpochStartTime", + OneOf: "", + TypeUint: canoto.SizeOf(zero.EpochStartTime), + }, + { + FieldNumber: canotoNumber_ICMEpochInfo__EpochNumber, + Name: "EpochNumber", + OneOf: "", + TypeUint: canoto.SizeOf(zero.EpochNumber), + }, + { + FieldNumber: canotoNumber_ICMEpochInfo__PChainEpochHeight, + Name: "PChainEpochHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainEpochHeight), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *ICMEpochInfo) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *ICMEpochInfo) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = ICMEpochInfo{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_ICMEpochInfo__EpochStartTime: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.EpochStartTime); err != nil { + return err + } + if canoto.IsZero(c.EpochStartTime) { + return canoto.ErrZeroValue + } + case canotoNumber_ICMEpochInfo__EpochNumber: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.EpochNumber); err != nil { + return err + } + if canoto.IsZero(c.EpochNumber) { + return canoto.ErrZeroValue + } + case canotoNumber_ICMEpochInfo__PChainEpochHeight: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.PChainEpochHeight); err != nil { + return err + } + if canoto.IsZero(c.PChainEpochHeight) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *ICMEpochInfo) ValidCanoto() bool { + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *ICMEpochInfo) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.EpochStartTime) { + size += uint64(len(canotoTag_ICMEpochInfo__EpochStartTime)) + canoto.SizeUint(c.EpochStartTime) + } + if !canoto.IsZero(c.EpochNumber) { + size += uint64(len(canotoTag_ICMEpochInfo__EpochNumber)) + canoto.SizeUint(c.EpochNumber) + } + if !canoto.IsZero(c.PChainEpochHeight) { + size += uint64(len(canotoTag_ICMEpochInfo__PChainEpochHeight)) + canoto.SizeUint(c.PChainEpochHeight) + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *ICMEpochInfo) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *ICMEpochInfo) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *ICMEpochInfo) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.EpochStartTime) { + canoto.Append(&w, canotoTag_ICMEpochInfo__EpochStartTime) + canoto.AppendUint(&w, c.EpochStartTime) + } + if !canoto.IsZero(c.EpochNumber) { + canoto.Append(&w, canotoTag_ICMEpochInfo__EpochNumber) + canoto.AppendUint(&w, c.EpochNumber) + } + if !canoto.IsZero(c.PChainEpochHeight) { + canoto.Append(&w, canotoTag_ICMEpochInfo__PChainEpochHeight) + canoto.AppendUint(&w, c.PChainEpochHeight) + } return w } diff --git a/msm/encoding.go b/msm/encoding.go index f50f43cc..f832e89d 100644 --- a/msm/encoding.go +++ b/msm/encoding.go @@ -30,10 +30,36 @@ type StateMachineMetadata struct { PChainHeight uint64 `canoto:"uint,4"` // Timestamp is the time when the block is being built, in milliseconds since Unix epoch. Timestamp uint64 `canoto:"uint,5"` + // ICMEpochInfo is the metadata that the StateMachine uses for ICM epoching. + ICMEpochInfo ICMEpochInfo `canoto:"value,6"` canotoData canotoData_StateMachineMetadata } +// ICMEpochInfo is the ICM epoch information that is maintained by the StateMachine and used for the ICM protocol. +// The StateMachine maintains this information identically to how the proposerVM maintains it, and it does so by +// building the ICMEpochInput and then passing it into the StateMachine's ComputeICMEpoch function. +type ICMEpochInfo struct { + // EpochStartTime is the Unix timestamp when this ICM epoch started. + EpochStartTime uint64 `canoto:"uint,1"` + // EpochNumber is the sequential identifier of this ICM epoch. + EpochNumber uint64 `canoto:"uint,2"` + // PChainEpochHeight is the P-chain height associated with this ICM epoch. + PChainEpochHeight uint64 `canoto:"uint,3"` + + canotoData canotoData_ICMEpochInfo +} + +func (ei *ICMEpochInfo) Equal(other *ICMEpochInfo) bool { + if ei == nil { + return other == nil + } + if other == nil { + return ei == nil + } + return ei.EpochStartTime == other.EpochStartTime && ei.EpochNumber == other.EpochNumber && ei.PChainEpochHeight == other.PChainEpochHeight +} + // SimplexEpochInfo is metadata used by the StateMachine. type SimplexEpochInfo struct { // PChainReferenceHeight is the P-Chain height that the StateMachine uses as a reference for the current epoch. diff --git a/msm/fuzz_test.go b/msm/fuzz_test.go new file mode 100644 index 00000000..90031f8a --- /dev/null +++ b/msm/fuzz_test.go @@ -0,0 +1,299 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/ava-labs/simplex" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +// noopLogger is a simplex.Logger that discards everything, so buildEpochChain can run +// without a *testing.T and the chain can be built once, outside the fuzz target. +type noopLogger struct{} + +func (noopLogger) Fatal(string, ...zap.Field) {} +func (noopLogger) Error(string, ...zap.Field) {} +func (noopLogger) Warn(string, ...zap.Field) {} +func (noopLogger) Info(string, ...zap.Field) {} +func (noopLogger) Trace(string, ...zap.Field) {} +func (noopLogger) Debug(string, ...zap.Field) {} +func (noopLogger) Verbo(string, ...zap.Field) {} + +// authoritativeField names a consensus field that the verifier reconstructs itself +// (from the parent block and the state machine), rather than copying it from the +// proposer. Tampering with any of these must be rejected, either by an explicit check +// or by the expected-vs-proposed block digest comparison. +type authoritativeField struct { + name string + // set overwrites the field with v. + set func(m *StateMachineMetadata, v uint64) +} + +var authoritativeFields = []authoritativeField{ + {"SimplexEpochInfo.PChainReferenceHeight", func(m *StateMachineMetadata, v uint64) { + m.SimplexEpochInfo.PChainReferenceHeight = v + }}, + {"SimplexEpochInfo.EpochNumber", func(m *StateMachineMetadata, v uint64) { + m.SimplexEpochInfo.EpochNumber = v + }}, + {"SimplexEpochInfo.PrevVMBlockSeq", func(m *StateMachineMetadata, v uint64) { + m.SimplexEpochInfo.PrevVMBlockSeq = v + }}, + {"SimplexEpochInfo.SealingBlockSeq", func(m *StateMachineMetadata, v uint64) { + m.SimplexEpochInfo.SealingBlockSeq = v + }}, + {"ICMEpochInfo.EpochNumber", func(m *StateMachineMetadata, v uint64) { + m.ICMEpochInfo.EpochNumber = v + }}, + {"ICMEpochInfo.EpochStartTime", func(m *StateMachineMetadata, v uint64) { + m.ICMEpochInfo.EpochStartTime = v + }}, + {"ICMEpochInfo.PChainEpochHeight", func(m *StateMachineMetadata, v uint64) { + m.ICMEpochInfo.PChainEpochHeight = v + }}, +} + +// numBuiltBlocks is the number of blocks buildEpochChain produces (used to seed the +// fuzzer with one entry per block); buildEpochChain asserts it stays in sync. +const numBuiltBlocks = 8 + +// FuzzVerifyBlock has one MSM build a full chain of blocks: the first Simplex block on +// top of genesis, blocks transitioning the epoch through every state (normal op, +// collecting approvals, sealing/epoch-sealed), the transition from the first epoch into +// a second epoch, and a block within that second epoch. Those blocks are the fuzzer's +// inputs (selected by index). For each input, a freshly instantiated verifier MSM first +// verifies the unfuzzed block (which must succeed), then verifies a copy whose +// consensus-authoritative metadata has been mutated (which must fail). +// +// The mutation is applied at the field level (rather than by flipping serialized bytes) +// so the fuzzed block is always well-formed: byte-level mutations of the Canoto encoding +// overwhelmingly corrupt the structure and merely exercise the decoder. Each fuzzed field +// is reconstructed by the verifier from the parent block (or checked against an exact +// value), so any real change is guaranteed to be rejected. +func FuzzVerifyBlock(f *testing.F) { + for blockIdx := 0; blockIdx < numBuiltBlocks; blockIdx++ { + for fieldIdx := range authoritativeFields { + f.Add(blockIdx, fieldIdx, uint64(0)) + f.Add(blockIdx, fieldIdx, uint64(0xffffffffffffffff)) + } + } + + // Build the chain (and its verifier MSM) once and reuse it across iterations: the + // blocks are fixed inputs, so there's no need to rebuild them every time. + blocks, sm := buildEpochChain(f, noopLogger{}) + + f.Fuzz(func(t *testing.T, blockIdx, fieldIdx int, value uint64) { + + // Converting to uint wraps negative fuzz inputs into range without the overflow + // edge case that int negation (e.g. -math.MinInt) would hit. + bi := int(uint(blockIdx) % uint(len(blocks))) + fi := int(uint(fieldIdx) % uint(len(authoritativeFields))) + field := authoritativeFields[fi] + + // Clone the selected block so mutations can't leak into the shared chain that is + // reused across iterations. + block := cloneBlock(t, blocks[bi]) + + // Model the verifier as knowing the P-chain exactly up to the height the block + // references — the minimal knowledge required to verify it, mirroring production + // where GetPChainHeight returns the verifier's latest observed height. + sm.GetPChainHeight = func() uint64 { return block.Metadata.PChainHeight } + + // The unfuzzed block built by the chain MSM must verify. + require.NoError(t, sm.VerifyBlock(context.Background(), block), + "unfuzzed block at index %d must verify", bi) + + // Mutating an authoritative field must make the block fail verification. + fuzzedMD := block.Metadata + field.set(&fuzzedMD, value) + if bytes.Equal(fuzzedMD.MarshalCanoto(), block.Metadata.MarshalCanoto()) { + t.Skip() // no-op mutation + } + fuzzed := &StateMachineBlock{InnerBlock: block.InnerBlock, Metadata: fuzzedMD} + require.Error(t, sm.VerifyBlock(context.Background(), fuzzed), + "block at index %d with mutated %s must be rejected", bi, field.name) + }) +} + +// cloneBlock returns a deep copy of b: the metadata is round-tripped through its Canoto +// encoding so the copy shares no pointers with b, and the inner block is reused as-is +// since verification only reads it. +func cloneBlock(t *testing.T, b *StateMachineBlock) *StateMachineBlock { + var md StateMachineMetadata + require.NoError(t, md.UnmarshalCanoto(b.Metadata.MarshalCanoto())) + return &StateMachineBlock{InnerBlock: b.InnerBlock, Metadata: md} +} + +// buildEpochChain drives a single MSM from genesis through a full epoch lifecycle and +// into a second epoch, and returns the blocks it built (seq 1..numBuiltBlocks) together +// with a separate verifier MSM preloaded with the whole chain. +// +// The blocks exercise every build/verify state: +// +// block1: stateFirstSimplexBlock (zero block, epoch 1) +// block2: stateBuildBlockNormalOp (epoch 1) +// block3: stateBuildBlockNormalOp, observes a validator-set change (epoch 1) +// block4: stateBuildCollectingApprovals (epoch 1) +// block5: stateBuildCollectingApprovals (epoch 1) +// block6: sealing block, built while collecting the quorum approval (epoch 1) +// block7: stateBuildBlockEpochSealed -> first block of epoch 2 +// block8: stateBuildBlockNormalOp (epoch 2) +func buildEpochChain(tb testing.TB, logger simplex.Logger) ([]*StateMachineBlock, *StateMachine) { + node1 := [20]byte{1} + node2 := [20]byte{2} + node3 := [20]byte{3} + + validatorSet1 := NodeBLSMappings{ + {NodeID: node1, BLSKey: []byte{1}, Weight: 1}, + {NodeID: node2, BLSKey: []byte{2}, Weight: 1}, + {NodeID: node3, BLSKey: []byte{3}, Weight: 1}, + } + validatorSet2 := NodeBLSMappings{ + {NodeID: node1, BLSKey: []byte{1}, Weight: 1}, + {NodeID: node2, BLSKey: []byte{4}, Weight: 1}, + {NodeID: node3, BLSKey: []byte{5}, Weight: 1}, + } + + pChainHeight1 := uint64(100) + pChainHeight2 := uint64(200) + + // Align to a whole second so the second-granular ICM epoch boundary is deterministic + // (see the comment in TestMSMFullEpochLifecycle). + startTime := time.Now().Truncate(time.Second) + + getValidatorSet := func(height uint64) (NodeBLSMappings, error) { + if height >= pChainHeight2 { + return validatorSet2, nil + } + return validatorSet1, nil + } + + // Use a genesis block anchored at startTime: the zero block carries over the last + // non-Simplex block's timestamp, so it must not be ahead of the blocks built after it. + genesis := StateMachineBlock{InnerBlock: &InnerBlock{BlockHeight: 0, TS: startTime, Bytes: []byte{0}}} + + currentPChainHeight := pChainHeight1 + currentTime := startTime + + sm, tc := newStateMachineWithLogger(tb, logger) + sm.GetValidatorSet = getValidatorSet + sm.GetPChainHeight = func() uint64 { return currentPChainHeight } + sm.GetTime = func() time.Time { return currentTime } + sm.GenesisValidatorSet = validatorSet1 + sm.LastNonSimplexBlockPChainHeight = pChainHeight1 + sm.LastNonSimplexInnerBlock = genesis.InnerBlock + tc.blockStore[0] = &outerBlock{block: genesis} + + var approvalsResult ValidatorSetApprovals + sm.ApprovalsRetriever = &dynamicApprovalsRetriever{approvals: &approvalsResult} + + ctx := context.Background() + + addBlock := func(seq uint64, b *StateMachineBlock, fin *simplex.Finalization) { + tc.blockStore[seq] = &outerBlock{block: *b, finalization: fin} + } + nextInner := func(h uint64) *InnerBlock { + return &InnerBlock{ + TS: startTime.Add(time.Duration(h) * time.Millisecond), + BlockHeight: h, + Bytes: []byte{byte(h)}, + } + } + build := func(seq, round, epoch uint64, prev *StateMachineBlock) *StateMachineBlock { + var prevDigest [32]byte + if prev != nil { + prevDigest = prev.Digest() + } else { + prevDigest = genesis.Digest() + } + md := simplex.ProtocolMetadata{Seq: seq, Round: round, Epoch: epoch, Prev: prevDigest} + block, err := sm.BuildBlock(ctx, md, nil) + require.NoError(tb, err) + return block + } + + // ----- Epoch 1 ----- + + // block1: the zero block, finalized so it can later serve as the epoch's reference for + // the validator-set change and the sealing-block computation. + tc.blockBuilder.block = nextInner(1) + block1 := build(1, 0, 1, nil) + addBlock(1, block1, &simplex.Finalization{}) + sm.LatestPersistedHeight = 1 + + // block2: a normal in-epoch block. + currentTime = startTime.Add(2 * time.Millisecond) + tc.blockBuilder.block = nextInner(2) + block2 := build(2, 1, 1, block1) + addBlock(2, block2, nil) + + // block3: a normal block that observes a validator-set change, so its successor must + // collect approvals for the next epoch. + currentPChainHeight = pChainHeight2 + currentTime = startTime.Add(time.Second + 3*time.Millisecond) + tc.blockBuilder.block = nextInner(3) + block3 := build(3, 2, 1, block2) + addBlock(3, block3, nil) + + // block4 & block5: collecting-approvals blocks (1/3 then 2/3, not enough to seal). + approvalsResult = ValidatorSetApprovals{{NodeID: node1, PChainHeight: pChainHeight2, Signature: []byte("sig1")}} + currentTime = startTime.Add(time.Second + 4*time.Millisecond) + tc.blockBuilder.block = nextInner(4) + block4 := build(4, 3, 1, block3) + addBlock(4, block4, nil) + + approvalsResult = ValidatorSetApprovals{{NodeID: node2, PChainHeight: pChainHeight2, Signature: []byte("sig2")}} + currentTime = startTime.Add(time.Second + 5*time.Millisecond) + tc.blockBuilder.block = nextInner(5) + block5 := build(5, 4, 1, block4) + addBlock(5, block5, nil) + + // block6: the sealing block (3/3 approvals). Its successor is in stateBuildBlockEpochSealed. + approvalsResult = ValidatorSetApprovals{{NodeID: node3, PChainHeight: pChainHeight2, Signature: []byte("sig3")}} + currentTime = startTime.Add(time.Second + 6*time.Millisecond) + tc.blockBuilder.block = nextInner(6) + block6 := build(6, 5, 1, block5) + require.Equal(tb, stateBuildBlockEpochSealed, block6.Metadata.SimplexEpochInfo.NextState()) + // Finalize the sealing block so the epoch transition can proceed. + addBlock(6, block6, &simplex.Finalization{}) + + // ----- Epoch 2 (its epoch number is the sealing block's sequence, 6) ----- + + // block7: the first block of the new epoch, built in stateBuildBlockEpochSealed. + sealingSeq := uint64(6) + currentTime = startTime.Add(time.Second + 7*time.Millisecond) + tc.blockBuilder.block = nextInner(7) + block7 := build(7, 6, sealingSeq, block6) + addBlock(7, block7, nil) + + // block8: a normal in-epoch block in the second epoch. + currentTime = startTime.Add(time.Second + 8*time.Millisecond) + tc.blockBuilder.block = nextInner(8) + block8 := build(8, 7, sealingSeq, block7) + addBlock(8, block8, nil) + + blocks := []*StateMachineBlock{block1, block2, block3, block4, block5, block6, block7, block8} + require.Len(tb, blocks, numBuiltBlocks) + + // Build a separate verifier MSM with its own copy of the fully populated store. + verifier, vtc := newStateMachineWithLogger(tb, logger) + vtc.blockStore = tc.blockStore.clone() + verifier.GetBlock = vtc.blockStore.getBlock + verifier.GetValidatorSet = getValidatorSet + // GetPChainHeight is set by the caller per verified block (see FuzzVerifyBlock). + // A time safely after every block, so the not-too-far-in-future check always passes. + verifyTime := startTime.Add(2 * time.Second) + verifier.GetTime = func() time.Time { return verifyTime } + verifier.GenesisValidatorSet = validatorSet1 + verifier.LastNonSimplexBlockPChainHeight = pChainHeight1 + verifier.LastNonSimplexInnerBlock = genesis.InnerBlock + + return blocks, verifier +} diff --git a/msm/misc.go b/msm/misc.go index 267df4e3..c0f13239 100644 --- a/msm/misc.go +++ b/msm/misc.go @@ -48,11 +48,9 @@ type VMBlock interface { // // If nil is returned, it is guaranteed that either Accept or Reject will be // called on this block, unless the VM is shut down. - Verify(context.Context) error + Verify(ctx context.Context, pChainHeight uint64) error } -type UpgradeConfig = any - type bitmask big.Int func (bm *bitmask) Bytes() []byte { diff --git a/msm/msm.go b/msm/msm.go index 3ee6e79a..872acb49 100644 --- a/msm/msm.go +++ b/msm/msm.go @@ -10,12 +10,19 @@ import ( "encoding/binary" "errors" "fmt" + "math" "time" "github.com/ava-labs/simplex" "go.uber.org/zap" ) +const ( + // Max allowed time difference between a block's timestamp and the current time. + // A block cannot be more than a certain time in the future compared to the current time. + maxSkew = 10 * time.Second +) + // state encodes the different stages of the epoch transition process, which determines how we build and verify blocks. // // SimplexEpochInfo.NextState() inspects the parent block's metadata to perform the following state transitions: @@ -62,7 +69,6 @@ var ( errZeroBlockParentNoInnerBlock = errors.New("zero block's parent has no inner block") errNilBlock = errors.New("block is nil") errInvalidPChainHeight = errors.New("invalid P-chain height") - errInvalidSimplexEpochInfo = errors.New("invalid SimplexEpochInfo") errZeroBlockHasInnerBlock = errors.New("zero block must not have an inner block") errZeroBlockInnerDigestMismatch = errors.New("zero block inner block digest does not match last non-Simplex inner block digest") errZeroBlockTimestampMismatch = errors.New("zero block timestamp does not match last non-Simplex inner block timestamp") @@ -78,6 +84,9 @@ var ( errPChainHeightSmallerThanParent = errors.New("invalid P-chain height: smaller than parent block's") errSignerSetShrunk = errors.New("some signers from parent block are missing from next epoch approvals of proposed block") errNextEpochApprovalsShrunk = errors.New("previous block has next epoch approvals but proposed block doesn't have next epoch approvals") + errTimestampTooBig = errors.New("invalid timestamp: exceeds maximum int64 value") + errTimestampDecreasing = errors.New("invalid timestamp: proposed timestamp is before parent block's timestamp") + errTimestampTooFarInFuture = errors.New("invalid timestamp: proposed timestamp is too far in the future compared to current time") signatureContext = "MSM approval" ) @@ -105,6 +114,21 @@ func (smb *StateMachineBlock) Digest() [32]byte { return sha256.Sum256(combined) } +// ICMEpochInput defines the input for computing the ICM Epoch information for the next block. +type ICMEpochInput struct { + // ParentPChainHeight is the P-chain height recorded in the parent block. + ParentPChainHeight uint64 + // ParentTimestamp is the timestamp of the parent block. + ParentTimestamp time.Time + // ChildTimestamp is the timestamp of the block being built. + ChildTimestamp time.Time + // ParentEpoch is the ICM epoch information from the parent block. + ParentEpoch ICMEpochInfo +} + +// ICMEpochTransition computes the next ICM epoch given the current upgrade configuration and epoch input. +type ICMEpochTransition func(ICMEpochInput) ICMEpochInfo + // ApprovalsRetriever retrieves the approvals from validators of the next epoch for the epoch change. type ApprovalsRetriever interface { Approvals() ValidatorSetApprovals @@ -166,8 +190,6 @@ type Config struct { GetTime func() time.Time // GetPChainHeight returns the latest known P-chain height. GetPChainHeight func() uint64 - // GetUpgrades returns the current upgrade configuration. - GetUpgrades func() UpgradeConfig // BlockBuilder builds new VM blocks. BlockBuilder BlockBuilder // Logger is used for logging state machine operations. @@ -197,6 +219,8 @@ type Config struct { MyNodeID simplex.NodeID // Signer Signer simplex.Signer + // ComputeICMEpoch computes the ICM epoch information in order to know which P-chain height to encode. + ComputeICMEpoch ICMEpochTransition } type state uint8 @@ -213,6 +237,9 @@ func NewStateMachine(config *Config) (*StateMachine, error) { config.Logger.Error("Last non-Simplex inner block is nil, cannot build zero block with correct metadata") return nil, errLastNonSimplexInnerBlockNil } + if config.TimeSkewLimit == 0 { + config.TimeSkewLimit = maxSkew + } sm := StateMachine{Config: config} return &sm, nil } @@ -314,6 +341,10 @@ func (sm *StateMachine) verifyNonZeroBlock(ctx context.Context, block, prevBlock prevBlockMD := prevBlock.Metadata currentState := prevBlockMD.SimplexEpochInfo.NextState() + if err := verifyTimestamp(block, prevBlock, sm.GetTime(), sm.TimeSkewLimit); err != nil { + return fmt.Errorf("failed to verify timestamp: %w", err) + } + currentPChainHeight := sm.GetPChainHeight() prevPChainHeight := prevBlockMD.PChainHeight proposedPChainHeight := block.Metadata.PChainHeight @@ -339,6 +370,23 @@ func (sm *StateMachine) verifyNonZeroBlock(ctx context.Context, block, prevBlock } } +func verifyTimestamp(block *StateMachineBlock, prevBlock *StateMachineBlock, now time.Time, timeSkewLimit time.Duration) error { + if block.Metadata.Timestamp > math.MaxInt64 { + return fmt.Errorf("%w: timestamp %d exceeds maximum int64 value", errTimestampTooBig, block.Metadata.Timestamp) + } + + if block.Metadata.Timestamp < prevBlock.Metadata.Timestamp { + return fmt.Errorf("%w: proposed %d < parent %d", errTimestampDecreasing, block.Metadata.Timestamp, prevBlock.Metadata.Timestamp) + } + + proposedTime := time.UnixMilli(int64(block.Metadata.Timestamp)) + + if now.Add(timeSkewLimit).Before(proposedTime) { + return fmt.Errorf("%w: proposed timestamp %v, max skew: %v", errTimestampTooFarInFuture, proposedTime, maxSkew) + } + return nil +} + func (sm *StateMachine) verifyEpochNumber(block *StateMachineBlock) error { md, err := simplex.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) if err != nil { @@ -402,36 +450,54 @@ func (sm *StateMachine) buildBlockOrTransitionEpoch(ctx context.Context, parentB newSimplexEpochInfo.NextPChainReferenceHeight = decisionToBuildBlock.pChainHeight } + now := sm.GetTime() + icmEpochInfo := computeICMEpochInfo(parentBlock, sm.ComputeICMEpoch, now) + var innerBlock VMBlock if decisionToBuildBlock.buildInnerBlock { - // TODO: This P-chain height should be taken from the ICM epoch - innerBlock, err = sm.BlockBuilder.BuildBlock(ctx, decisionToBuildBlock.pChainHeight) + innerBlock, err = sm.BlockBuilder.BuildBlock(ctx, icmEpochInfo.PChainEpochHeight) if err != nil { return nil, err } } - return wrapBlock(parentBlock, innerBlock, newSimplexEpochInfo, decisionToBuildBlock.pChainHeight, simplexMetadata, simplexBlacklist), nil + return wrapBlock(innerBlock, newSimplexEpochInfo, decisionToBuildBlock.pChainHeight, simplexMetadata, simplexBlacklist, now, icmEpochInfo), nil +} + +func computeICMEpochInfo(parentBlock StateMachineBlock, computeICMEpoch ICMEpochTransition, childTimestamp time.Time) ICMEpochInfo { + parentTimestamp := time.UnixMilli(int64(parentBlock.Metadata.Timestamp)) + + icmEpochInfo := computeICMEpoch(ICMEpochInput{ + ParentPChainHeight: parentBlock.Metadata.PChainHeight, + ParentEpoch: ICMEpochInfo{ + PChainEpochHeight: parentBlock.Metadata.ICMEpochInfo.PChainEpochHeight, + EpochNumber: parentBlock.Metadata.ICMEpochInfo.EpochNumber, + EpochStartTime: parentBlock.Metadata.ICMEpochInfo.EpochStartTime, + }, + ChildTimestamp: childTimestamp, + ParentTimestamp: parentTimestamp, + }) + return icmEpochInfo } func verifyAgainstExpected( ctx context.Context, - parentBlock StateMachineBlock, innerBlock VMBlock, expectedSimplexEpochInfo SimplexEpochInfo, expectedPChainHeight uint64, nextBlock *StateMachineBlock, + timestamp time.Time, + expectedIcmEpochInfo ICMEpochInfo, ) error { if innerBlock != nil { - if err := innerBlock.Verify(ctx); err != nil { + if err := innerBlock.Verify(ctx, expectedIcmEpochInfo.PChainEpochHeight); err != nil { return err } } expectedBlock := wrapBlock( - parentBlock, innerBlock, expectedSimplexEpochInfo, expectedPChainHeight, - nextBlock.Metadata.SimplexProtocolMetadata, nextBlock.Metadata.SimplexBlacklist, - ) + innerBlock, expectedSimplexEpochInfo, expectedPChainHeight, + nextBlock.Metadata.SimplexProtocolMetadata, nextBlock.Metadata.SimplexBlacklist, timestamp, expectedIcmEpochInfo) if expectedBlock.Digest() != nextBlock.Digest() { return fmt.Errorf("expected block digest %s does not match proposed block digest %s: %w", expectedBlock.Digest(), @@ -450,12 +516,16 @@ func (sm *StateMachine) verifyNormalBlock(ctx context.Context, parentBlock State proposedPChainHeight := nextBlock.Metadata.PChainHeight + timestamp := time.UnixMilli(int64(nextBlock.Metadata.Timestamp)) + + icmEpochInfo := computeICMEpochInfo(parentBlock, sm.ComputeICMEpoch, timestamp) + if err := sm.verifyNextPChainRefHeightNormal(parentBlock.Metadata, nextBlock.Metadata.SimplexEpochInfo); err != nil { return fmt.Errorf("failed to verify next P-chain reference height for normal block: %w", err) } newSimplexEpochInfo.NextPChainReferenceHeight = nextBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight - return verifyAgainstExpected(ctx, parentBlock, nextBlock.InnerBlock, newSimplexEpochInfo, proposedPChainHeight, nextBlock) + return verifyAgainstExpected(ctx, nextBlock.InnerBlock, newSimplexEpochInfo, proposedPChainHeight, nextBlock, timestamp, icmEpochInfo) } func verifyPChainHeight(proposedPChainHeight uint64, currentPChainHeight uint64, prevPChainHeight uint64) error { @@ -664,6 +734,8 @@ func (sm *StateMachine) buildBlockZero(parentBlock StateMachineBlock, simplexMet return nil, errZeroBlockParentNoInnerBlock } + // For the zero block, we set the timestamp to be the same as the last non-Simplex inner block's timestamp. + // We do it because we need to carry over a minimum timestamp from the non-Simplex blocks. timestamp := sm.LastNonSimplexInnerBlock.Timestamp().UnixMilli() simplexEpochInfo := constructSimplexZeroBlockSimplexEpochInfo(pChainHeight, validatorSet, prevVMBlockSeq) @@ -674,20 +746,27 @@ func (sm *StateMachine) buildBlockZero(parentBlock StateMachineBlock, simplexMet md.Prev = sm.LastNonSimplexInnerBlock.Digest() md.Seq = sm.LastNonSimplexInnerBlock.Height() - return &StateMachineBlock{ - Metadata: StateMachineMetadata{ - Timestamp: uint64(timestamp), - SimplexProtocolMetadata: simplexMetadata, - SimplexBlacklist: simplexBlacklist, - SimplexEpochInfo: simplexEpochInfo, - PChainHeight: pChainHeight, - }, - }, nil + // The zero block carries over the parent's ICM epoch unchanged, just as it carries over the + // timestamp. If the parent is a genesis block that predates ICM, the carried-over epoch is empty, + // and the first ICM epoch begins on the block built on top of the zero block. + parentICMEpochInfo := parentBlock.Metadata.ICMEpochInfo + icmEpochInfo := ICMEpochInfo{ + PChainEpochHeight: parentICMEpochInfo.PChainEpochHeight, + EpochNumber: parentICMEpochInfo.EpochNumber, + EpochStartTime: parentICMEpochInfo.EpochStartTime, + } + + return wrapBlock(nil, + simplexEpochInfo, + pChainHeight, + simplexMetadata, + simplexBlacklist, + time.UnixMilli(timestamp), + icmEpochInfo, + ), nil } func (sm *StateMachine) verifyBlockZero(block *StateMachineBlock, prevBlock StateMachineBlock) error { - simplexEpochInfo := block.Metadata.SimplexEpochInfo - if prevBlock.InnerBlock == nil { return fmt.Errorf("%w: parent digest %s", errZeroBlockParentNoInnerBlock, prevBlock.Digest()) } @@ -711,11 +790,27 @@ func (sm *StateMachine) verifyBlockZero(block *StateMachineBlock, prevBlock Stat } } + now := sm.GetTime() + if err := verifyTimestamp(block, &prevBlock, now, sm.TimeSkewLimit); err != nil { + return fmt.Errorf("failed to verify timestamp for zero block: %w", err) + } + + // The zero block carries over the parent's ICM epoch unchanged (see buildBlockZero). + expectedICMEpochInfo := prevBlock.Metadata.ICMEpochInfo + // If we have compared all fields so far, the rest of the fields we compare by constructing an explicit expected SimplexEpochInfo expectedSimplexEpochInfo := constructSimplexZeroBlockSimplexEpochInfo(pChainHeight, expectedValidatorSet, prevVMBlockSeq) + expectedBlock := wrapBlock(nil, + expectedSimplexEpochInfo, + pChainHeight, + block.Metadata.SimplexProtocolMetadata, + block.Metadata.SimplexBlacklist, + time.UnixMilli(int64(block.Metadata.Timestamp)), + expectedICMEpochInfo, + ) - if !expectedSimplexEpochInfo.Equal(&simplexEpochInfo) { - return fmt.Errorf("%w: expected %v, got %v", errInvalidSimplexEpochInfo, expectedSimplexEpochInfo, simplexEpochInfo) + if expectedBlock.Digest() != block.Digest() { + return fmt.Errorf("%w: expected %s but got %s", errBlockDigestMismatch, expectedBlock.Digest(), block.Digest()) } // The InnerBlock must match the last non-Simplex inner block. @@ -727,6 +822,7 @@ func (sm *StateMachine) verifyBlockZero(block *StateMachineBlock, prevBlock Stat } // The timestamp must equal the last non-Simplex inner block's timestamp. + // We do it because we need to carry over a minimum timestamp from the non-Simplex blocks. expectedTimestamp := uint64(sm.LastNonSimplexInnerBlock.Timestamp().UnixMilli()) if block.Metadata.Timestamp != expectedTimestamp { return fmt.Errorf("%w: expected %d but got %d", errZeroBlockTimestampMismatch, expectedTimestamp, block.Metadata.Timestamp) @@ -761,18 +857,21 @@ func (sm *StateMachine) buildBlockCollectingApprovals(ctx context.Context, paren pChainHeight := parentBlock.Metadata.PChainHeight + now := sm.GetTime() + icmEpochInfo := computeICMEpochInfo(parentBlock, sm.ComputeICMEpoch, now) + // We might not have enough approvals to seal the current epoch, // in which case we just carry over the approvals we have so far to the next block, // so that eventually we'll have enough approvals to seal the epoch. if !newApprovals.canSeal { sm.Logger.Debug("Not enough approvals to seal epoch, building block without sealing the epoch") - return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, pChainHeight) + return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, pChainHeight, icmEpochInfo) } sm.Logger.Debug("Have enough approvals to seal epoch, building sealing block") // Else, we have enough approvals to seal the epoch, so we create the sealing block. - return sm.createSealingBlock(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, pChainHeight) + return sm.createSealingBlock(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, pChainHeight, icmEpochInfo) } func (sm *StateMachine) verifyCollectingApprovalsBlock(ctx context.Context, parentBlock StateMachineBlock, nextBlock *StateMachineBlock, prevBlockSeq uint64) error { @@ -813,8 +912,6 @@ func (sm *StateMachine) verifyCollectingApprovalsBlock(ctx context.Context, pare approvals := bitmaskFromBytes(newApprovals.NodeIDs) canSeal := sigAggr.IsQuorum(validators.SelectSubset(approvals)) - // TODO: P-chain height should be taken from the ICM epoch. For now we pass the block proposer's P-chain height. - if canSeal { newSimplexEpochInfo, err = sm.computeSimplexEpochInfoForSealingBlock(newSimplexEpochInfo) if err != nil { @@ -822,7 +919,11 @@ func (sm *StateMachine) verifyCollectingApprovalsBlock(ctx context.Context, pare } } - return verifyAgainstExpected(ctx, parentBlock, nextBlock.InnerBlock, newSimplexEpochInfo, nextMD.PChainHeight, nextBlock) + timestamp := time.UnixMilli(int64(nextMD.Timestamp)) + + icmEpochInfo := computeICMEpochInfo(parentBlock, sm.ComputeICMEpoch, timestamp) + + return verifyAgainstExpected(ctx, nextBlock.InnerBlock, newSimplexEpochInfo, nextMD.PChainHeight, nextBlock, timestamp, icmEpochInfo) } func (sm *StateMachine) verifyNextEpochApprovalsSignature(prev SimplexEpochInfo, next SimplexEpochInfo, validators NodeBLSMappings) error { @@ -933,14 +1034,13 @@ func (sm *StateMachine) computeNewApprovals(parentBlock StateMachineBlock) (*app // buildBlockImpatiently builds a block by waiting for the VM to build a block until MaxBlockBuildingWaitTime. // If the VM fails to build a block within that time, we build a block without an inner block, // so that we can continue making progress and not get stuck waiting for the VM. -func (sm *StateMachine) buildBlockImpatiently(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata []byte, simplexBlacklist []byte, simplexEpochInfo SimplexEpochInfo, pChainHeight uint64) (*StateMachineBlock, error) { +func (sm *StateMachine) buildBlockImpatiently(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata []byte, simplexBlacklist []byte, simplexEpochInfo SimplexEpochInfo, pChainHeight uint64, icmEpochInfo ICMEpochInfo) (*StateMachineBlock, error) { impatientContext, cancel := context.WithTimeout(ctx, sm.MaxBlockBuildingWaitTime) defer cancel() - start := time.Now() + start := sm.GetTime() - // TODO: This P-chain height should be taken from the ICM epoch - childBlock, err := sm.BlockBuilder.BuildBlock(impatientContext, pChainHeight) + innerBlock, err := sm.BlockBuilder.BuildBlock(impatientContext, icmEpochInfo.PChainEpochHeight) if err != nil && impatientContext.Err() == nil { // If we got an error building the block, and we didn't time out, log the error but continue building the block without the inner block, // so that we can continue making progress and not get stuck on a single block. @@ -950,15 +1050,24 @@ func (sm *StateMachine) buildBlockImpatiently(ctx context.Context, parentBlock S sm.Logger.Debug("Timed out waiting for block to be built, building block without inner block instead", zap.Duration("elapsed", time.Since(start)), zap.Duration("maxBlockBuildingWaitTime", sm.MaxBlockBuildingWaitTime)) } - return wrapBlock(parentBlock, childBlock, simplexEpochInfo, pChainHeight, simplexMetadata, simplexBlacklist), nil + + now := sm.GetTime() + icmEpochInfo = computeICMEpochInfo(parentBlock, sm.ComputeICMEpoch, now) + return wrapBlock(innerBlock, simplexEpochInfo, pChainHeight, simplexMetadata, simplexBlacklist, now, icmEpochInfo), nil } -func (sm *StateMachine) createSealingBlock(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata []byte, simplexBlacklist []byte, simplexEpochInfo SimplexEpochInfo, pChainHeight uint64) (*StateMachineBlock, error) { +func (sm *StateMachine) createSealingBlock(ctx context.Context, + parentBlock StateMachineBlock, + simplexMetadata []byte, + simplexBlacklist []byte, + simplexEpochInfo SimplexEpochInfo, + pChainHeight uint64, + icmEpochInfo ICMEpochInfo) (*StateMachineBlock, error) { simplexEpochInfo, err := sm.computeSimplexEpochInfoForSealingBlock(simplexEpochInfo) if err != nil { return nil, fmt.Errorf("failed to compute simplex epoch info for sealing block: %w", err) } - return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, simplexBlacklist, simplexEpochInfo, pChainHeight) + return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, simplexBlacklist, simplexEpochInfo, pChainHeight, icmEpochInfo) } func (sm *StateMachine) computeSimplexEpochInfoForSealingBlock(simplexEpochInfo SimplexEpochInfo) (SimplexEpochInfo, error) { @@ -986,25 +1095,24 @@ func (sm *StateMachine) computeSimplexEpochInfoForSealingBlock(simplexEpochInfo } // wrapBlock creates a new StateMachineBlock by wrapping the VM block (if applicable) and adding the appropriate metadata. -func wrapBlock(parentBlock StateMachineBlock, childBlock VMBlock, newSimplexEpochInfo SimplexEpochInfo, pChainHeight uint64, simplexMetadata, simplexBlacklist []byte) *StateMachineBlock { - timestamp := parentBlock.Metadata.Timestamp - - hasChildBlock := childBlock != nil - - var newTimestamp time.Time - if hasChildBlock { - newTimestamp = childBlock.Timestamp() - timestamp = uint64(newTimestamp.UnixMilli()) - } +func wrapBlock( + childBlock VMBlock, + newSimplexEpochInfo SimplexEpochInfo, + pChainHeight uint64, + simplexMetadata, + simplexBlacklist []byte, + timestamp time.Time, + icmEpochInfo ICMEpochInfo) *StateMachineBlock { return &StateMachineBlock{ InnerBlock: childBlock, Metadata: StateMachineMetadata{ - Timestamp: timestamp, + Timestamp: uint64(timestamp.UnixMilli()), SimplexProtocolMetadata: simplexMetadata, SimplexBlacklist: simplexBlacklist, SimplexEpochInfo: newSimplexEpochInfo, PChainHeight: pChainHeight, + ICMEpochInfo: icmEpochInfo, }, } } @@ -1054,16 +1162,16 @@ func (sm *StateMachine) buildBlockEpochSealed(ctx context.Context, parentBlock S } if !readyToTransitionEpoch { + now := sm.GetTime() + icmEpochInfo := computeICMEpochInfo(parentBlock, sm.ComputeICMEpoch, now) newSimplexEpochInfo := computeSimplexEpochInfoForTelock(parentBlock, sealingBlockSeq, prevBlockSeq) pChainHeight := parentBlock.Metadata.PChainHeight - return wrapBlock(parentBlock, nil, newSimplexEpochInfo, pChainHeight, simplexMetadata, simplexBlacklist), nil + return wrapBlock(nil, newSimplexEpochInfo, pChainHeight, simplexMetadata, simplexBlacklist, now, icmEpochInfo), nil } // Else, we build a block for the new epoch. newSimplexEpochInfo := computeSimplexEpochInfoForNewEpoch(parentBlock, sealingBlockSeq, prevBlockSeq) - // TODO: This P-chain height should be taken from the ICM epoch - return sm.buildBlockOrTransitionEpoch(ctx, sealingBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo) } @@ -1096,10 +1204,15 @@ func (sm *StateMachine) verifyBlockEpochSealed(ctx context.Context, parentBlock return err } + timestamp := time.UnixMilli(int64(nextBlock.Metadata.Timestamp)) + + now := sm.GetTime() + icmEpochInfo := computeICMEpochInfo(parentBlock, sm.ComputeICMEpoch, now) + newSimplexEpochInfo := computeSimplexEpochInfoForTelock(parentBlock, sealingBlockSeq, prevBlockSeq) if !isSealingBlockFinalized { - return verifyAgainstExpected(ctx, parentBlock, nil, newSimplexEpochInfo, nextBlock.Metadata.PChainHeight, nextBlock) + return verifyAgainstExpected(ctx, nil, newSimplexEpochInfo, nextBlock.Metadata.PChainHeight, nextBlock, timestamp, icmEpochInfo) } // Else, it's a new epoch. @@ -1120,8 +1233,7 @@ func (sm *StateMachine) verifyBlockEpochSealed(ctx context.Context, parentBlock } newSimplexEpochInfo.NextPChainReferenceHeight = nextBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight - // TODO: This P-chain height should be taken from the ICM epoch - return verifyAgainstExpected(ctx, parentBlock, nextBlock.InnerBlock, newSimplexEpochInfo, proposedPChainHeight, nextBlock) + return verifyAgainstExpected(ctx, nextBlock.InnerBlock, newSimplexEpochInfo, proposedPChainHeight, nextBlock, timestamp, icmEpochInfo) } // constructSimplexZeroBlockSimplexEpochInfo constructs the SimplexEpochInfo for the zero block, which is the first ever block built by Simplex. diff --git a/msm/msm_test.go b/msm/msm_test.go index 935af8b7..90ef0146 100644 --- a/msm/msm_test.go +++ b/msm/msm_test.go @@ -7,6 +7,7 @@ import ( "context" "crypto/rand" "fmt" + "math" "testing" "time" @@ -69,7 +70,7 @@ func TestMSMBuildAndVerifyBlocksAfterGenesis(t *testing.T) { mutateBlock: func(block *StateMachineBlock) { block.Metadata.SimplexEpochInfo.EpochNumber = 2 }, - err: errInvalidSimplexEpochInfo, + err: errBlockDigestMismatch, }, { name: "P-chain height too big", @@ -93,7 +94,7 @@ func TestMSMBuildAndVerifyBlocksAfterGenesis(t *testing.T) { mutateBlock: func(block *StateMachineBlock) { block.Metadata.SimplexEpochInfo.BlockValidationDescriptor = nil }, - err: errInvalidSimplexEpochInfo, + err: errBlockDigestMismatch, }, { name: "membership mismatch", @@ -103,7 +104,7 @@ func TestMSMBuildAndVerifyBlocksAfterGenesis(t *testing.T) { {BLSKey: []byte{1}, Weight: 1}, } }, - err: errInvalidSimplexEpochInfo, + err: errBlockDigestMismatch, }, { name: "SimplexEpochInfo mismatch", @@ -111,7 +112,7 @@ func TestMSMBuildAndVerifyBlocksAfterGenesis(t *testing.T) { mutateBlock: func(block *StateMachineBlock) { block.Metadata.SimplexEpochInfo.PrevVMBlockSeq = 999 }, - err: errInvalidSimplexEpochInfo, + err: errBlockDigestMismatch, }, } { t.Run(testCase.name, func(t *testing.T) { @@ -147,9 +148,15 @@ func TestMSMFirstSimplexBlockAfterPreSimplexBlocks(t *testing.T) { BlockHeight: 42, Bytes: []byte{4, 5, 6}, }, - // Zero-valued metadata means this is a pre-Simplex block or a genesis block. - // But since the height is 42, it can't be a genesis block, so it must be a pre-Simplex block. - Metadata: StateMachineMetadata{}, + // Since the height is 42, this can't be a genesis block, so it must be a + // pre-Simplex block. It already participates in an ICM epoch, which the zero + // block built on top of it inherits. + Metadata: StateMachineMetadata{ + ICMEpochInfo: ICMEpochInfo{ + PChainEpochHeight: 100, + EpochNumber: 1, + }, + }, } md := simplex.ProtocolMetadata{ @@ -182,8 +189,6 @@ func TestMSMFirstSimplexBlockAfterPreSimplexBlocks(t *testing.T) { require.NoError(t, err) require.NotNil(t, block) - require.NoError(t, sm2.VerifyBlock(context.Background(), block)) - require.Equal(t, &StateMachineBlock{ Metadata: StateMachineMetadata{ Timestamp: uint64(preSimplexParent.InnerBlock.Timestamp().UnixMilli()), @@ -199,8 +204,14 @@ func TestMSMFirstSimplexBlockAfterPreSimplexBlocks(t *testing.T) { }, }, }, + ICMEpochInfo: ICMEpochInfo{ + PChainEpochHeight: 100, + EpochNumber: 1, + }, }, }, block) + + require.NoError(t, sm2.VerifyBlock(context.Background(), block)) } func TestMSMBuildBlockRejectsZeroSeq(t *testing.T) { @@ -225,10 +236,12 @@ func TestMSMNormalOp(t *testing.T) { err error expectedPChainHeight uint64 expectedNextPChainRefHeight uint64 + expectedICMEpochInfo ICMEpochInfo }{ { name: "correct information", expectedPChainHeight: 100, + expectedICMEpochInfo: ICMEpochInfo{PChainEpochHeight: 100, EpochNumber: 1}, }, { name: "trying to build a genesis block", @@ -316,6 +329,7 @@ func TestMSMNormalOp(t *testing.T) { }, expectedPChainHeight: newPChainHeight, expectedNextPChainRefHeight: newPChainHeight, + expectedICMEpochInfo: ICMEpochInfo{PChainEpochHeight: 100, EpochNumber: 1}, }, } { t.Run(testCase.name, func(t *testing.T) { @@ -341,6 +355,10 @@ func TestMSMNormalOp(t *testing.T) { blockTime := lastBlock.InnerBlock.Timestamp().Add(time.Second) + fixedTime := func() time.Time { return blockTime } + sm1.GetTime = fixedTime + sm2.GetTime = fixedTime + content := make([]byte, 10) _, err = rand.Read(content) require.NoError(t, err) @@ -388,6 +406,7 @@ func TestMSMNormalOp(t *testing.T) { PrevVMBlockSeq: lastBlock.InnerBlock.Height(), NextPChainReferenceHeight: testCase.expectedNextPChainRefHeight, }, + ICMEpochInfo: testCase.expectedICMEpochInfo, }, } require.Equal(t, expected.Digest(), block1.Digest()) @@ -415,7 +434,12 @@ func TestMSMFullEpochLifecycle(t *testing.T) { pChainHeight1 := uint64(100) pChainHeight2 := uint64(200) - startTime := time.Now() + // Align to a whole second: the ICM epoch boundary is second-granular (ComputeICMEpoch + // truncates timestamps with .Unix() and uses a 1-second window), while the blocks below + // are placed at sub-second offsets from startTime. If startTime had a sub-second component + // close to 1s, the "+1s + few ms" offsets would spill into the next second and trigger an + // extra ICM epoch transition, making the test flaky. + startTime := time.Now().Truncate(time.Second) nextBlock := func(height uint64) *InnerBlock { return &InnerBlock{ @@ -445,16 +469,26 @@ func TestMSMFullEpochLifecycle(t *testing.T) { name string firstBlockBeforeSimplex StateMachineBlock epochNum uint64 + // firstBlockICMEpochInfo is the ICM epoch of the pre-Simplex parent, which the zero block + // carries over. A genesis parent predates ICM, so its ICM epoch is empty and the first epoch + // (icmEpoch1) begins on the block built on top of the zero block. + firstBlockICMEpochInfo ICMEpochInfo }{ { name: "building on top of genesis", firstBlockBeforeSimplex: genesis, epochNum: 1, + firstBlockICMEpochInfo: ICMEpochInfo{}, }, { name: "upgrading to Simplex from pre-Simplex blocks", firstBlockBeforeSimplex: notGenesis, epochNum: notGenesis.InnerBlock.Height() + 1, + firstBlockICMEpochInfo: ICMEpochInfo{ + PChainEpochHeight: pChainHeight1, + EpochNumber: 1, + EpochStartTime: uint64(startTime.Unix()), + }, }, } { t.Run(testCase.name, func(t *testing.T) { @@ -471,10 +505,44 @@ func TestMSMFullEpochLifecycle(t *testing.T) { return currentPChainHeight } + // Since we explicitly compare the built block with an expected value, + // we need the timestamps to be deterministic. So instead of using time.Now(), we use a fixed + // startTime and add offsets to it for each block. + currentTime := startTime + fixedTime := func() time.Time { return currentTime } + + // We exercise an ICM epoch transition by jumping block3's timestamp + // past the 1-second ICM-epoch window. + // ComputeICMEpoch transitions when the parent block's timestamp has + // crossed the current ICM epoch's start + 1s, so block4 (and every + // block after it) lands in ICM epoch 2. + // + // block3 is also the block where the validator set change is first + // observed, so its Metadata.PChainHeight = pChainHeight2. Since the + // transition takes input.ParentPChainHeight as the new epoch's + // PChainEpochHeight, icmEpoch2.PChainEpochHeight = pChainHeight2. + // block2, block3: ICM epoch 1, started at startTime. + // block4 onward: ICM epoch 2, started at block3's timestamp, + // PChainEpochHeight = pChainHeight2. + icmEpoch1 := ICMEpochInfo{ + PChainEpochHeight: pChainHeight1, + EpochNumber: 1, + EpochStartTime: uint64(startTime.Unix()), + } + icmEpoch2 := ICMEpochInfo{ + PChainEpochHeight: pChainHeight2, + EpochNumber: 2, + EpochStartTime: uint64(startTime.Unix()) + 1, + } + + // The zero block carries over the parent's ICM epoch. + testCase.firstBlockBeforeSimplex.Metadata.ICMEpochInfo = testCase.firstBlockICMEpochInfo + // Create fresh state machine instances for each iteration. sm, tc := newStateMachine(t) sm.GetValidatorSet = getValidatorSet sm.GetPChainHeight = getPChainHeight + sm.GetTime = fixedTime tc.blockStore[0] = &outerBlock{block: genesis} tc.blockStore[42] = &outerBlock{block: notGenesis} @@ -485,6 +553,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { smVerify, tcVerify := newStateMachine(t) smVerify.GetValidatorSet = getValidatorSet smVerify.GetPChainHeight = getPChainHeight + smVerify.GetTime = fixedTime smVerify.LastNonSimplexInnerBlock = testCase.firstBlockBeforeSimplex.InnerBlock smVerify.GenesisValidatorSet = validatorSet1 @@ -527,6 +596,9 @@ func TestMSMFullEpochLifecycle(t *testing.T) { }, }, }, + // The zero block carries over the parent's ICM epoch (icmEpoch1 for a + // pre-Simplex parent, empty for a genesis parent). + ICMEpochInfo: testCase.firstBlockICMEpochInfo, }, }, block1) addBlock(md.Seq, *block1, &simplex.Finalization{}) @@ -538,6 +610,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { smVerify.LatestPersistedHeight = baseSeq + 1 // ----- Step 2: Build a normal block (no validator set change) ----- + currentTime = startTime.Add(2 * time.Millisecond) tc.blockBuilder.block = nextBlock(2) md = simplex.ProtocolMetadata{Seq: baseSeq + 2, Round: 1, Epoch: testCase.epochNum, Prev: block1.Digest()} block2, err := sm.BuildBlock(context.Background(), md, nil) @@ -545,7 +618,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(2), Metadata: StateMachineMetadata{ - Timestamp: uint64(startTime.Add(2 * time.Millisecond).UnixMilli()), + Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight1, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: SimplexEpochInfo{ @@ -553,6 +626,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { EpochNumber: testCase.epochNum, PrevVMBlockSeq: baseSeq, }, + ICMEpochInfo: icmEpoch1, }, }, block2) addBlock(md.Seq, *block2, nil) @@ -563,6 +637,10 @@ func TestMSMFullEpochLifecycle(t *testing.T) { // Advance P-chain height so that GetValidatorSet returns a different set. currentPChainHeight = pChainHeight2 + // Jump block3's timestamp past the 1-second ICM-epoch window so + // block4 (whose parent is block3) sees parentTimestamp >= + // epochStart + 1s and transitions ICM to epoch 2. + currentTime = startTime.Add(time.Second + 3*time.Millisecond) tc.blockBuilder.block = nextBlock(3) md = simplex.ProtocolMetadata{Seq: baseSeq + 3, Round: 2, Epoch: testCase.epochNum, Prev: block2.Digest()} block3, err := sm.BuildBlock(context.Background(), md, nil) @@ -570,7 +648,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(3), Metadata: StateMachineMetadata{ - Timestamp: uint64(startTime.Add(3 * time.Millisecond).UnixMilli()), + Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: SimplexEpochInfo{ @@ -579,6 +657,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { PrevVMBlockSeq: baseSeq + 2, NextPChainReferenceHeight: pChainHeight2, }, + ICMEpochInfo: icmEpoch1, }, }, block3) addBlock(md.Seq, *block3, nil) @@ -604,6 +683,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { sig, err := aggr.AppendSignatures(nil, []byte("sig1")) require.NoError(t, err) + currentTime = startTime.Add(time.Second + 4*time.Millisecond) tc.blockBuilder.block = nextBlock(4) md = simplex.ProtocolMetadata{Seq: baseSeq + 4, Round: 3, Epoch: testCase.epochNum, Prev: block3.Digest()} block4, err := sm.BuildBlock(context.Background(), md, nil) @@ -611,7 +691,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(4), Metadata: StateMachineMetadata{ - Timestamp: uint64(startTime.Add(4 * time.Millisecond).UnixMilli()), + Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: SimplexEpochInfo{ @@ -624,6 +704,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { Signature: sig, }, }, + ICMEpochInfo: icmEpoch2, }, }, block4) addBlock(md.Seq, *block4, nil) @@ -644,6 +725,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.NoError(t, err) bitmask = []byte{3} + currentTime = startTime.Add(time.Second + 5*time.Millisecond) tc.blockBuilder.block = nextBlock(5) md = simplex.ProtocolMetadata{Seq: baseSeq + 5, Round: 4, Epoch: testCase.epochNum, Prev: block4.Digest()} block5, err := sm.BuildBlock(context.Background(), md, nil) @@ -651,7 +733,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(5), Metadata: StateMachineMetadata{ - Timestamp: uint64(startTime.Add(5 * time.Millisecond).UnixMilli()), + Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: SimplexEpochInfo{ @@ -664,6 +746,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { Signature: sig, }, }, + ICMEpochInfo: icmEpoch2, }, }, block5) addBlock(md.Seq, *block5, nil) @@ -684,6 +767,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.NoError(t, err) bitmask = []byte{7} + currentTime = startTime.Add(time.Second + 6*time.Millisecond) tc.blockBuilder.block = nextBlock(6) md = simplex.ProtocolMetadata{Seq: baseSeq + 6, Round: 5, Epoch: testCase.epochNum, Prev: block5.Digest()} block6, err := sm.BuildBlock(context.Background(), md, nil) @@ -691,7 +775,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(6), Metadata: StateMachineMetadata{ - Timestamp: uint64(startTime.Add(6 * time.Millisecond).UnixMilli()), + Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: SimplexEpochInfo{ @@ -711,6 +795,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { Signature: sig6, }, }, + ICMEpochInfo: icmEpoch2, }, }, block6) addBlock(md.Seq, *block6, nil) @@ -755,13 +840,15 @@ func TestMSMFullEpochLifecycle(t *testing.T) { // However, despite the fact that the block builder is willing to build a new block, // a Telock shouldn't contain an inner block. if tc.blockStore[sealingSeq].finalization == nil { + // Telock shares the sealing block's timestamp slot. + currentTime = startTime.Add(time.Second + 6*time.Millisecond) telock, err := sm.BuildBlock(context.Background(), md, nil) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ InnerBlock: nil, Metadata: StateMachineMetadata{ - Timestamp: uint64(startTime.Add(6 * time.Millisecond).UnixMilli()), + Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: SimplexEpochInfo{ @@ -771,6 +858,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { PrevVMBlockSeq: baseSeq + 6, SealingBlockSeq: sealingSeq, }, + ICMEpochInfo: icmEpoch2, }, }, telock) @@ -785,12 +873,13 @@ func TestMSMFullEpochLifecycle(t *testing.T) { // and the protocol metadata's Epoch field. md.Epoch = sealingSeq + currentTime = startTime.Add(time.Second + 7*time.Millisecond) block7, err := sm.BuildBlock(context.Background(), md, nil) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(7), Metadata: StateMachineMetadata{ - Timestamp: uint64(startTime.Add(7 * time.Millisecond).UnixMilli()), + Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: SimplexEpochInfo{ @@ -798,6 +887,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { EpochNumber: sealingSeq, PrevVMBlockSeq: baseSeq + 6, }, + ICMEpochInfo: icmEpoch2, }, }, block7) addBlock(md.Seq, *block7, nil) @@ -1062,6 +1152,66 @@ func TestVerifyPChainHeight(t *testing.T) { } } +func TestVerifyTimestamp(t *testing.T) { + now := time.Now() + nowMilli := uint64(now.UnixMilli()) + skewMilli := uint64(maxSkew / time.Millisecond) + + tests := []struct { + name string + proposed uint64 + prev uint64 + err error + }{ + { + name: "proposed equals parent", + proposed: nowMilli, + prev: nowMilli, + }, + { + name: "proposed after parent, well within skew", + proposed: nowMilli + 100, + prev: nowMilli - 100, + }, + { + name: "proposed exactly at now + maxSkew", + proposed: nowMilli + skewMilli, + prev: nowMilli, + }, + { + name: "proposed below parent", + proposed: nowMilli - 1, + prev: nowMilli, + err: errTimestampDecreasing, + }, + { + name: "proposed one millisecond past now + maxSkew", + proposed: nowMilli + skewMilli + 1, + prev: nowMilli, + err: errTimestampTooFarInFuture, + }, + { + name: "proposed exceeds math.MaxInt64", + proposed: uint64(math.MaxInt64) + 1, + prev: nowMilli, + err: errTimestampTooBig, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + block := &StateMachineBlock{Metadata: StateMachineMetadata{Timestamp: tt.proposed}} + prev := &StateMachineBlock{Metadata: StateMachineMetadata{Timestamp: tt.prev}} + err := verifyTimestamp(block, prev, now, maxSkew) + if tt.err == nil { + require.NoError(t, err) + return + } + require.ErrorIs(t, err, tt.err) + }) + } +} + func TestComputePrevVMBlockSeq(t *testing.T) { t.Run("parent has no inner block", func(t *testing.T) { parent := StateMachineBlock{ diff --git a/msm/util_test.go b/msm/util_test.go index 17cb53a7..4675b600 100644 --- a/msm/util_test.go +++ b/msm/util_test.go @@ -40,7 +40,7 @@ func (i *InnerBlock) Timestamp() time.Time { return i.TS } -func (i *InnerBlock) Verify(_ context.Context) error { +func (i *InnerBlock) Verify(_ context.Context, _ uint64) error { return nil } @@ -49,10 +49,10 @@ type fakeVMBlock struct { height uint64 } -func (f *fakeVMBlock) Digest() [32]byte { return [32]byte{} } -func (f *fakeVMBlock) Height() uint64 { return f.height } -func (f *fakeVMBlock) Timestamp() time.Time { return time.Time{} } -func (f *fakeVMBlock) Verify(_ context.Context) error { return nil } +func (f *fakeVMBlock) Digest() [32]byte { return [32]byte{} } +func (f *fakeVMBlock) Height() uint64 { return f.height } +func (f *fakeVMBlock) Timestamp() time.Time { return time.Time{} } +func (f *fakeVMBlock) Verify(_ context.Context, _ uint64) error { return nil } type outerBlock struct { finalization *simplex.Finalization @@ -279,6 +279,10 @@ type testConfig struct { } func newStateMachine(t *testing.T) (*StateMachine, *testConfig) { + return newStateMachineWithLogger(t, testutil.MakeLogger(t)) +} + +func newStateMachineWithLogger(tb testing.TB, logger simplex.Logger) (*StateMachine, *testConfig) { bs := make(blockStore) bs[0] = &outerBlock{block: genesisBlock} @@ -295,7 +299,7 @@ func newStateMachine(t *testing.T) (*StateMachine, *testConfig) { LastNonSimplexBlockPChainHeight: 100, GetTime: time.Now, TimeSkewLimit: time.Second * 5, - Logger: testutil.MakeLogger(t), + Logger: logger, GetBlock: testConfig.blockStore.getBlock, MaxBlockBuildingWaitTime: time.Second, ApprovalsRetriever: &testConfig.approvalsRetriever, @@ -306,18 +310,35 @@ func newStateMachine(t *testing.T) (*StateMachine, *testConfig) { GetPChainHeight: func() uint64 { return 100 }, - GetUpgrades: func() any { - return nil - }, GetValidatorSet: testConfig.validatorSetRetriever.getValidatorSet, PChainProgressListener: &noOpPChainListener{}, LastNonSimplexInnerBlock: genesisBlock.InnerBlock, MyNodeID: myNodeID[:], Signer: &testutil.TestSigner{}, + ComputeICMEpoch: func(input ICMEpochInput) ICMEpochInfo { + // This is just the ACP-181 implementation from avalanchego + var zeroEpoch ICMEpochInfo + if input.ParentEpoch == zeroEpoch { + return ICMEpochInfo{ + PChainEpochHeight: input.ParentPChainHeight, + EpochNumber: 1, + EpochStartTime: uint64(input.ParentTimestamp.Unix()), + } + } + endTime := time.Unix(int64(input.ParentEpoch.EpochStartTime), 0).Add(time.Second) + if input.ParentTimestamp.Before(endTime) { + return input.ParentEpoch + } + return ICMEpochInfo{ + PChainEpochHeight: input.ParentPChainHeight, + EpochNumber: input.ParentEpoch.EpochNumber + 1, + EpochStartTime: uint64(input.ParentTimestamp.Unix()), + } + }, } sm, err := NewStateMachine(&smConfig) - require.NoError(t, err) + require.NoError(tb, err) return sm, &testConfig } @@ -381,7 +402,7 @@ func (b *testVMBlock) Timestamp() time.Time { return time.Now() } -func (b *testVMBlock) Verify(_ context.Context) error { +func (b *testVMBlock) Verify(_ context.Context, _ uint64) error { return nil }