diff --git a/adapters.go b/adapters.go index 76f083c6..5679929b 100644 --- a/adapters.go +++ b/adapters.go @@ -215,7 +215,7 @@ func (bw *BlockBuilderWaiter) WaitForPendingBlock(ctx context.Context) { } func (bw *BlockBuilderWaiter) BuildBlock(ctx context.Context, metadata common.ProtocolMetadata, blacklist common.Blacklist) (common.VerifiedBlock, bool) { - block, err := bw.msm.BuildBlock(ctx, metadata, &blacklist) + block, err := bw.msm.BuildBlock(ctx, metadata, blacklist) if err != nil { return nil, false } diff --git a/common/blacklist.canoto.go b/common/blacklist.canoto.go new file mode 100644 index 00000000..4ec91732 --- /dev/null +++ b/common/blacklist.canoto.go @@ -0,0 +1,765 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.19.0 +// source: blacklist.go + +package common + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that the generated code is compatible with the library version. +const ( + _ uint = canoto.VersionCompatibility - 1 + _ uint = 1 - canoto.VersionCompatibility +) + +// Ensure that unused imports do not error +var _ = io.ErrUnexpectedEOF + +const ( + canotoNumber_Blacklist__NodeCount = 1 + canotoNumber_Blacklist__SuspectedNodes = 2 + canotoNumber_Blacklist__Updates = 3 + + canotoTag_Blacklist__NodeCount = "\x08" // canoto.Tag(canotoNumber_Blacklist__NodeCount, canoto.Varint) + canotoTag_Blacklist__SuspectedNodes = "\x12" // canoto.Tag(canotoNumber_Blacklist__SuspectedNodes, canoto.Len) + canotoTag_Blacklist__Updates = "\x1a" // canoto.Tag(canotoNumber_Blacklist__Updates, canoto.Len) +) + +type canotoData_Blacklist struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*Blacklist) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeFor[Blacklist]()) + var zero Blacklist + s := &canoto.Spec{ + Name: "Blacklist", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_Blacklist__NodeCount, + Name: "NodeCount", + OneOf: "", + TypeUint: canoto.SizeOf(zero.NodeCount), + }, + canoto.FieldTypeFromField( + /*type inference:*/ (canoto.MakeEntryNilPointer(zero.SuspectedNodes)), + /*FieldNumber: */ canotoNumber_Blacklist__SuspectedNodes, + /*Name: */ "SuspectedNodes", + /*FixedLength: */ 0, + /*Repeated: */ true, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (canoto.MakeEntryNilPointer(zero.Updates)), + /*FieldNumber: */ canotoNumber_Blacklist__Updates, + /*Name: */ "Updates", + /*FixedLength: */ 0, + /*Repeated: */ true, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *Blacklist) 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 *Blacklist) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = Blacklist{} + 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_Blacklist__NodeCount: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.NodeCount); err != nil { + return err + } + if canoto.IsZero(c.NodeCount) { + return canoto.ErrZeroValue + } + case canotoNumber_Blacklist__SuspectedNodes: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the first entry manually because the tag is already + // stripped. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canotoTag_Blacklist__SuspectedNodes) + if err != nil { + return err + } + + c.SuspectedNodes = canoto.MakeSlice(c.SuspectedNodes, countMinus1+1) + field := c.SuspectedNodes + additionalField := field[1:] + if len(msgBytes) != 0 { + remainingBytes := r.B + r.B = msgBytes + if err := (&field[0]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + + // Read the rest of the entries, stripping the tag each time. + for i := range additionalField { + r.B = r.B[len(canotoTag_Blacklist__SuspectedNodes):] + r.Unsafe = true + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + if len(msgBytes) == 0 { + continue + } + + remainingBytes := r.B + r.B = msgBytes + if err := (&additionalField[i]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + case canotoNumber_Blacklist__Updates: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the first entry manually because the tag is already + // stripped. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canotoTag_Blacklist__Updates) + if err != nil { + return err + } + + c.Updates = canoto.MakeSlice(c.Updates, countMinus1+1) + field := c.Updates + additionalField := field[1:] + if len(msgBytes) != 0 { + remainingBytes := r.B + r.B = msgBytes + if err := (&field[0]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + + // Read the rest of the entries, stripping the tag each time. + for i := range additionalField { + r.B = r.B[len(canotoTag_Blacklist__Updates):] + r.Unsafe = true + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + if len(msgBytes) == 0 { + continue + } + + remainingBytes := r.B + r.B = msgBytes + if err := (&additionalField[i]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + 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 *Blacklist) ValidCanoto() bool { + { + field := c.SuspectedNodes + for i := range field { + if !(&field[i]).ValidCanoto() { + return false + } + } + } + { + field := c.Updates + for i := range field { + if !(&field[i]).ValidCanoto() { + return false + } + } + } + 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 *Blacklist) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.NodeCount) { + size += uint64(len(canotoTag_Blacklist__NodeCount)) + canoto.SizeUint(c.NodeCount) + } + { + field := c.SuspectedNodes + for i := range field { + (&field[i]).CalculateCanotoCache() + fieldSize := (&field[i]).CachedCanotoSize() + size += uint64(len(canotoTag_Blacklist__SuspectedNodes)) + canoto.SizeUint(fieldSize) + fieldSize + } + } + { + field := c.Updates + for i := range field { + (&field[i]).CalculateCanotoCache() + fieldSize := (&field[i]).CachedCanotoSize() + size += uint64(len(canotoTag_Blacklist__Updates)) + canoto.SizeUint(fieldSize) + fieldSize + } + } + 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 *Blacklist) 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 *Blacklist) 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 *Blacklist) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.NodeCount) { + canoto.Append(&w, canotoTag_Blacklist__NodeCount) + canoto.AppendUint(&w, c.NodeCount) + } + { + field := c.SuspectedNodes + for i := range field { + canoto.Append(&w, canotoTag_Blacklist__SuspectedNodes) + canoto.AppendUint(&w, (&field[i]).CachedCanotoSize()) + w = (&field[i]).MarshalCanotoInto(w) + } + } + { + field := c.Updates + for i := range field { + canoto.Append(&w, canotoTag_Blacklist__Updates) + canoto.AppendUint(&w, (&field[i]).CachedCanotoSize()) + w = (&field[i]).MarshalCanotoInto(w) + } + } + return w +} + +const ( + canotoNumber_BlacklistUpdate__Type = 1 + canotoNumber_BlacklistUpdate__NodeIndex = 2 + + canotoTag_BlacklistUpdate__Type = "\x08" // canoto.Tag(canotoNumber_BlacklistUpdate__Type, canoto.Varint) + canotoTag_BlacklistUpdate__NodeIndex = "\x10" // canoto.Tag(canotoNumber_BlacklistUpdate__NodeIndex, canoto.Varint) +) + +type canotoData_BlacklistUpdate struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*BlacklistUpdate) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero BlacklistUpdate + s := &canoto.Spec{ + Name: "BlacklistUpdate", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_BlacklistUpdate__Type, + Name: "Type", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Type), + }, + { + FieldNumber: canotoNumber_BlacklistUpdate__NodeIndex, + Name: "NodeIndex", + OneOf: "", + TypeUint: canoto.SizeOf(zero.NodeIndex), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *BlacklistUpdate) 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 *BlacklistUpdate) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = BlacklistUpdate{} + 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_BlacklistUpdate__Type: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Type); err != nil { + return err + } + if canoto.IsZero(c.Type) { + return canoto.ErrZeroValue + } + case canotoNumber_BlacklistUpdate__NodeIndex: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.NodeIndex); err != nil { + return err + } + if canoto.IsZero(c.NodeIndex) { + 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 *BlacklistUpdate) 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 *BlacklistUpdate) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.Type) { + size += uint64(len(canotoTag_BlacklistUpdate__Type)) + canoto.SizeUint(c.Type) + } + if !canoto.IsZero(c.NodeIndex) { + size += uint64(len(canotoTag_BlacklistUpdate__NodeIndex)) + canoto.SizeUint(c.NodeIndex) + } + 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 *BlacklistUpdate) 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 *BlacklistUpdate) 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 *BlacklistUpdate) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.Type) { + canoto.Append(&w, canotoTag_BlacklistUpdate__Type) + canoto.AppendUint(&w, c.Type) + } + if !canoto.IsZero(c.NodeIndex) { + canoto.Append(&w, canotoTag_BlacklistUpdate__NodeIndex) + canoto.AppendUint(&w, c.NodeIndex) + } + return w +} + +const ( + canotoNumber_SuspectedNode__NodeIndex = 1 + canotoNumber_SuspectedNode__SuspectingCount = 2 + canotoNumber_SuspectedNode__RedeemingCount = 3 + canotoNumber_SuspectedNode__OrbitSuspected = 4 + canotoNumber_SuspectedNode__OrbitToRedeem = 5 + + canotoTag_SuspectedNode__NodeIndex = "\x08" // canoto.Tag(canotoNumber_SuspectedNode__NodeIndex, canoto.Varint) + canotoTag_SuspectedNode__SuspectingCount = "\x10" // canoto.Tag(canotoNumber_SuspectedNode__SuspectingCount, canoto.Varint) + canotoTag_SuspectedNode__RedeemingCount = "\x18" // canoto.Tag(canotoNumber_SuspectedNode__RedeemingCount, canoto.Varint) + canotoTag_SuspectedNode__OrbitSuspected = "\x20" // canoto.Tag(canotoNumber_SuspectedNode__OrbitSuspected, canoto.Varint) + canotoTag_SuspectedNode__OrbitToRedeem = "\x28" // canoto.Tag(canotoNumber_SuspectedNode__OrbitToRedeem, canoto.Varint) +) + +type canotoData_SuspectedNode struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*SuspectedNode) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero SuspectedNode + s := &canoto.Spec{ + Name: "SuspectedNode", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_SuspectedNode__NodeIndex, + Name: "NodeIndex", + OneOf: "", + TypeUint: canoto.SizeOf(zero.NodeIndex), + }, + { + FieldNumber: canotoNumber_SuspectedNode__SuspectingCount, + Name: "SuspectingCount", + OneOf: "", + TypeUint: canoto.SizeOf(zero.SuspectingCount), + }, + { + FieldNumber: canotoNumber_SuspectedNode__RedeemingCount, + Name: "RedeemingCount", + OneOf: "", + TypeUint: canoto.SizeOf(zero.RedeemingCount), + }, + { + FieldNumber: canotoNumber_SuspectedNode__OrbitSuspected, + Name: "OrbitSuspected", + OneOf: "", + TypeUint: canoto.SizeOf(zero.OrbitSuspected), + }, + { + FieldNumber: canotoNumber_SuspectedNode__OrbitToRedeem, + Name: "OrbitToRedeem", + OneOf: "", + TypeUint: canoto.SizeOf(zero.OrbitToRedeem), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *SuspectedNode) 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 *SuspectedNode) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = SuspectedNode{} + 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_SuspectedNode__NodeIndex: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.NodeIndex); err != nil { + return err + } + if canoto.IsZero(c.NodeIndex) { + return canoto.ErrZeroValue + } + case canotoNumber_SuspectedNode__SuspectingCount: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.SuspectingCount); err != nil { + return err + } + if canoto.IsZero(c.SuspectingCount) { + return canoto.ErrZeroValue + } + case canotoNumber_SuspectedNode__RedeemingCount: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.RedeemingCount); err != nil { + return err + } + if canoto.IsZero(c.RedeemingCount) { + return canoto.ErrZeroValue + } + case canotoNumber_SuspectedNode__OrbitSuspected: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.OrbitSuspected); err != nil { + return err + } + if canoto.IsZero(c.OrbitSuspected) { + return canoto.ErrZeroValue + } + case canotoNumber_SuspectedNode__OrbitToRedeem: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.OrbitToRedeem); err != nil { + return err + } + if canoto.IsZero(c.OrbitToRedeem) { + 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 *SuspectedNode) 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 *SuspectedNode) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.NodeIndex) { + size += uint64(len(canotoTag_SuspectedNode__NodeIndex)) + canoto.SizeUint(c.NodeIndex) + } + if !canoto.IsZero(c.SuspectingCount) { + size += uint64(len(canotoTag_SuspectedNode__SuspectingCount)) + canoto.SizeUint(c.SuspectingCount) + } + if !canoto.IsZero(c.RedeemingCount) { + size += uint64(len(canotoTag_SuspectedNode__RedeemingCount)) + canoto.SizeUint(c.RedeemingCount) + } + if !canoto.IsZero(c.OrbitSuspected) { + size += uint64(len(canotoTag_SuspectedNode__OrbitSuspected)) + canoto.SizeUint(c.OrbitSuspected) + } + if !canoto.IsZero(c.OrbitToRedeem) { + size += uint64(len(canotoTag_SuspectedNode__OrbitToRedeem)) + canoto.SizeUint(c.OrbitToRedeem) + } + 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 *SuspectedNode) 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 *SuspectedNode) 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 *SuspectedNode) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.NodeIndex) { + canoto.Append(&w, canotoTag_SuspectedNode__NodeIndex) + canoto.AppendUint(&w, c.NodeIndex) + } + if !canoto.IsZero(c.SuspectingCount) { + canoto.Append(&w, canotoTag_SuspectedNode__SuspectingCount) + canoto.AppendUint(&w, c.SuspectingCount) + } + if !canoto.IsZero(c.RedeemingCount) { + canoto.Append(&w, canotoTag_SuspectedNode__RedeemingCount) + canoto.AppendUint(&w, c.RedeemingCount) + } + if !canoto.IsZero(c.OrbitSuspected) { + canoto.Append(&w, canotoTag_SuspectedNode__OrbitSuspected) + canoto.AppendUint(&w, c.OrbitSuspected) + } + if !canoto.IsZero(c.OrbitToRedeem) { + canoto.Append(&w, canotoTag_SuspectedNode__OrbitToRedeem) + canoto.AppendUint(&w, c.OrbitToRedeem) + } + return w +} diff --git a/common/blacklist.go b/common/blacklist.go index 7f925490..749de1ab 100644 --- a/common/blacklist.go +++ b/common/blacklist.go @@ -39,15 +39,17 @@ func Orbit(round uint64, nodeIndex uint16, nodeCount uint16) uint64 { // It can be derived by applying the recorded Updates to the parent block's blacklist. type Blacklist struct { // NodeCount is the configuration of the blacklist. - NodeCount uint16 + NodeCount uint16 `canoto:"uint,1"` // SuspectedNodes is the list of nodes that are currently suspected. // it's the inner state of the blacklist. - SuspectedNodes SuspectedNodes + SuspectedNodes SuspectedNodes `canoto:"repeated value,2"` // Updates is the list of modifications that a block builder is proposing // to perform to the blacklist. - Updates BlacklistUpdates + Updates BlacklistUpdates `canoto:"repeated value,3"` + + canotoData canotoData_Blacklist } func NewBlacklist(nodeCount uint16) Blacklist { @@ -115,49 +117,11 @@ const ( type BlacklistUpdates []BlacklistUpdate -func (updates BlacklistUpdates) Len() int { - return len(updates) * 3 -} - -func (updates BlacklistUpdates) Bytes() []byte { - // Each update is encoded as: - // 1 byte for the type - // 2 bytes for the node index - if len(updates) == 0 { - return []byte{} - } - - buff := make([]byte, len(updates)*3) - for i, blu := range updates { - pos := i * 3 - buff[pos] = byte(blu.Type) - binary.BigEndian.PutUint16(buff[pos+1:pos+3], blu.NodeIndex) - } - return buff -} - -func (updates *BlacklistUpdates) FromBytes(buff []byte) error { - if len(buff) == 0 { - return nil - } - if len(buff)%3 != 0 { - return fmt.Errorf("buffer length is not a multiple of 3") - } - numUpdates := len(buff) / 3 - *updates = make([]BlacklistUpdate, numUpdates) - for i := 0; i < numUpdates; i++ { - pos := i * 3 - (*updates)[i] = BlacklistUpdate{ - Type: BlacklistOpType(buff[pos]), - NodeIndex: binary.BigEndian.Uint16(buff[pos+1 : pos+3]), - } - } - return nil -} - type BlacklistUpdate struct { - Type BlacklistOpType - NodeIndex uint16 + Type BlacklistOpType `canoto:"uint,1"` + NodeIndex uint16 `canoto:"uint,2"` + + canotoData canotoData_BlacklistUpdate } func (bu *BlacklistUpdate) Equals(bu2 *BlacklistUpdate) bool { @@ -360,75 +324,38 @@ func (bl *Blacklist) IsNodeSuspected(nodeIndex uint16) bool { return false } -// Bytes returns the byte representation of the blacklist. -// The bytes of a blacklist are encoded as follows: -// 2 bytes for the node count -// 2 bytes for the number of updates -// N bytes for the suspected nodes section -// The rest of the bytes encode the updates section. -func (bl *Blacklist) Bytes() []byte { - buff := make([]byte, 2+2+bl.Updates.Len()+bl.SuspectedNodes.Len()) - binary.BigEndian.PutUint16(buff[0:2], bl.NodeCount) - if len(bl.SuspectedNodes) == 0 && len(bl.Updates) == 0 { - buff = buff[:2] - return buff +// Clone returns a deep copy of the blacklist. +func (bl *Blacklist) Clone() Blacklist { + clone := Blacklist{ + NodeCount: bl.NodeCount, } - binary.BigEndian.PutUint16(buff[2:4], uint16(len(bl.Updates))) - if bl.SuspectedNodes.Len() > 0 { - copy(buff[4:4+bl.SuspectedNodes.Len()], bl.SuspectedNodes.Bytes()) + for _, sn := range bl.SuspectedNodes { + clone.SuspectedNodes = append(clone.SuspectedNodes, SuspectedNode{ + NodeIndex: sn.NodeIndex, + SuspectingCount: sn.SuspectingCount, + RedeemingCount: sn.RedeemingCount, + OrbitSuspected: sn.OrbitSuspected, + OrbitToRedeem: sn.OrbitToRedeem, + }) } - if bl.Updates.Len() > 0 { - copy(buff[4+bl.SuspectedNodes.Len():], bl.Updates.Bytes()) + for _, update := range bl.Updates { + clone.Updates = append(clone.Updates, BlacklistUpdate{ + Type: update.Type, + NodeIndex: update.NodeIndex, + }) } - return buff + return clone +} + +// Bytes returns the byte representation of the blacklist. +func (bl *Blacklist) Bytes() []byte { + clone := bl.Clone() + return clone.MarshalCanoto() } // FromBytes populates the blacklist from the given bytes. func (bl *Blacklist) FromBytes(buff []byte) error { - if len(buff) == 2 { - bl.NodeCount = binary.BigEndian.Uint16(buff[0:2]) - bl.SuspectedNodes = SuspectedNodes{} - bl.Updates = BlacklistUpdates{} - return nil - } - - if len(buff) < 4 { - return fmt.Errorf("buffer too short (%d) to contain blacklist", len(buff)) - } - - bl.NodeCount = binary.BigEndian.Uint16(buff[0:2]) - numUpdates := binary.BigEndian.Uint16(buff[2:4]) - - originalBuffSize := len(buff) - buff = buff[4:] - - updateLen := int(numUpdates) * 3 - if len(buff) < updateLen { - return fmt.Errorf("buffer too short (%d) to contain %d bytes for updates", len(buff), 4+3*numUpdates) - } - - suspectedNodesLen := len(buff) - updateLen - - if numUpdates == 0 && suspectedNodesLen == 0 { - return fmt.Errorf("buffer too large (%d) to contain no updates and no suspected nodes", originalBuffSize) - } - - var suspectedNodes SuspectedNodes - if err := suspectedNodes.FromBytes(buff[:suspectedNodesLen]); err != nil { - return fmt.Errorf("failed to parse suspected nodes: %w", err) - } - - bl.SuspectedNodes = suspectedNodes - - if err := bl.Updates.FromBytes(buff[suspectedNodesLen:]); err != nil { - return fmt.Errorf("failed to parse blacklist updates: %w", err) - } - - if updateLen != len(buff[suspectedNodesLen:]) { - return fmt.Errorf("expected %d bytes for updates, but got %d", updateLen, len(buff[suspectedNodesLen:])) - } - - return nil + return bl.UnmarshalCanoto(buff) } func (bl *Blacklist) VerifyProposedBlacklist(candidateBlacklist Blacklist, round uint64) error { @@ -556,81 +483,26 @@ func (bl *Blacklist) ComputeBlacklistUpdates(round uint64, nodeCount uint16, tim type SuspectedNodes []SuspectedNode -func (sns *SuspectedNodes) FromBytes(buff []byte) error { - pos := 0 - for pos < len(buff) { - var sn SuspectedNode - bytesRead, err := sn.Read(buff[pos:]) - if err != nil { - return fmt.Errorf("failed to read suspected node at position %d: %w", pos, err) - } - *sns = append(*sns, sn) - pos += bytesRead - } - return nil -} - -func (sns *SuspectedNodes) Len() int { - var totalLen int - for _, sn := range *sns { - totalLen += sn.Len() - } - return totalLen -} - -func (sns *SuspectedNodes) Bytes() []byte { - // Suspected nodes are encoded by concatenating all suspected nodes one after the other. - - if len(*sns) == 0 { - return []byte{} - } - - var buffSize int - for _, sn := range *sns { - buffSize += sn.Len() - } - - buff := make([]byte, buffSize) - - var pos int - for _, sn := range *sns { - bytesWritten := sn.write(buff[pos:]) - pos += bytesWritten - } - - return buff -} - // SuspectedNode is the information we keep for each suspected node. // A suspected node records the number of accusations and when it was accused in. // A suspected node that has above f+1 accusations is considered as blacklisted. // A suspected node that is blacklisted can be redeemed by gathering f+1 redeeming votes. // A suspected node that has been redeemed by f+1 votes or more is removed from the blacklist. -// Each suspected node is encoded in the following manner: -// A bitmask byte for the fields except the node index that are non-zero. -// 4 top bits of the bitmask are the bitmask for { suspectingCount, redeemingCount, orbitSuspected, orbitToRedeem }. -// A bit is set to 1 if the corresponding field is non-zero. -// The next 2 bytes are the node index. -// The next bytes correspond to the non-zero fields in the order of the bitmask. -// [bitmask byte] -// [node index (2 bytes)] -// [suspectingCount (2 bytes, if non-zero)] -// [redeemingCount (2 bytes, if non-zero)] -// [orbitSuspected (8 bytes, if non-zero)] -// [orbitToRedeem (8 bytes, if non-zero)] type SuspectedNode struct { // NodeIndex is the index of the suspected node among the nodes of the validator set. - NodeIndex uint16 + NodeIndex uint16 `canoto:"uint,1"` // SuspectingCount is the number of nodes that have suspected this node in the current orbit denoted by OrbitSuspected. // If this count is >= f+1, then the node is considered blacklisted. - SuspectingCount uint16 + SuspectingCount uint16 `canoto:"uint,2"` // RedeemingCount is the number of nodes that have redeemed this node in the current orbit denoted by OrbitToRedeem. // If this count reaches >= f+1, the node is removed from the blacklist. - RedeemingCount uint16 + RedeemingCount uint16 `canoto:"uint,3"` // OrbitSuspected is the orbit in which the node was last suspected. - OrbitSuspected uint64 + OrbitSuspected uint64 `canoto:"uint,4"` // OrbitToRedeem is the orbit in which the node was last redeemed. - OrbitToRedeem uint64 + OrbitToRedeem uint64 `canoto:"uint,5"` + + canotoData canotoData_SuspectedNode } func (sn *SuspectedNode) Equals(sn2 *SuspectedNode) bool { diff --git a/common/blacklist_test.go b/common/blacklist_test.go index b233b84f..549c5138 100644 --- a/common/blacklist_test.go +++ b/common/blacklist_test.go @@ -222,41 +222,43 @@ func TestOrbit(t *testing.T) { } func TestBlacklistFromBytes(t *testing.T) { + populated := Blacklist{ + NodeCount: 4, + SuspectedNodes: SuspectedNodes{{NodeIndex: 3, SuspectingCount: 1, OrbitSuspected: 1}}, + Updates: BlacklistUpdates{{NodeIndex: 3, Type: BlacklistOpType_NodeSuspected}}, + } + for _, testCase := range []struct { name string data []byte - expectedErr string + expectErr bool expectedBlacklist Blacklist }{ { - name: "empty blacklist", - data: []byte{0, 0x64}, - expectedBlacklist: Blacklist{ - NodeCount: 100, - SuspectedNodes: SuspectedNodes{}, - Updates: BlacklistUpdates{}, - }, + name: "empty blacklist", + data: (&Blacklist{NodeCount: 100}).Bytes(), + expectedBlacklist: Blacklist{NodeCount: 100}, }, { - name: "too short data", - data: []byte{0, 1, 2}, - expectedErr: "buffer too short (3) to contain blacklist", + name: "populated blacklist", + data: populated.Bytes(), + expectedBlacklist: populated, }, { - name: "invalid suspected nodes", - data: []byte{0, 4, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, - expectedErr: "failed to parse suspected nodes: failed to read suspected node at position 0: invalid bitmask: lower 4 bits must be zero, got 00000010", + name: "malformed data", + data: []byte{0, 1, 2}, + expectErr: true, }, } { t.Run(testCase.name, func(t *testing.T) { var bl Blacklist err := bl.FromBytes(testCase.data) - if testCase.expectedErr == "" { - require.NoError(t, err) - require.Equal(t, testCase.expectedBlacklist, bl) + if testCase.expectErr { + require.Error(t, err) return } - require.EqualError(t, err, testCase.expectedErr) + require.NoError(t, err) + require.True(t, testCase.expectedBlacklist.Equals(&bl)) }) } } @@ -296,67 +298,6 @@ func FuzzBlacklistFromBytes(f *testing.F) { }) } -func FuzzSuspectedNodes(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - var sn SuspectedNodes - err := sn.FromBytes(data) - if err == nil { - require.Equal(t, data, sn.Bytes()) - return - } - require.Error(t, err) - }) -} - -func FuzzBlacklistUpdates(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - var updates BlacklistUpdates - err := updates.FromBytes(data) - if err == nil { - require.Equal(t, data, updates.Bytes()) - require.Len(t, updates.Bytes(), len(updates)*3) - return - } - require.Error(t, err) - }) -} - -func TestSuspectedNodesTrailer(t *testing.T) { - sns := SuspectedNodes{ - {NodeIndex: 1, SuspectingCount: 2, OrbitSuspected: 3, RedeemingCount: 4, OrbitToRedeem: 5}, - {NodeIndex: 6, SuspectingCount: 7, OrbitSuspected: 8, RedeemingCount: 9, OrbitToRedeem: 10}, - } - bytes := sns.Bytes() - - suspectedNode := SuspectedNode{ - NodeIndex: uint16(mathrand.Intn(1000)), - SuspectingCount: uint16(mathrand.Intn(10)), - OrbitSuspected: uint64(mathrand.Intn(100)), - RedeemingCount: uint16(mathrand.Intn(10)), - OrbitToRedeem: uint64(mathrand.Intn(100)), - } - - trailer := make([]byte, suspectedNode.Len()) - - for { - _, err := rand.Read(trailer) - require.NoError(t, err) - - // Is this a valid encoding of a SuspectedNode? - var sn SuspectedNode - _, err = sn.Read(trailer) - if err != nil { - break - } - } - - data := append(bytes, trailer...) - - var sns2 SuspectedNodes - err := sns2.FromBytes(data) - require.Error(t, err) -} - func TestBlacklistBytes(t *testing.T) { bl := Blacklist{ NodeCount: 5, @@ -371,15 +312,11 @@ func TestBlacklistBytes(t *testing.T) { } bytes := bl.Bytes() - var sns SuspectedNodes - er := sns.FromBytes(bl.SuspectedNodes.Bytes()) - require.NoError(t, er) - var bl2 Blacklist err := bl2.FromBytes(bytes) require.NoError(t, err) - require.Equal(t, bl, bl2) + require.True(t, bl.Equals(&bl2)) } func TestComputeBlacklistUpdates(t *testing.T) { @@ -557,29 +494,6 @@ func TestAdvanceRound(t *testing.T) { } } -func TestUpdateBytesEqualsLen(t *testing.T) { - var updates BlacklistUpdates - update := BlacklistUpdate{} - update1 := BlacklistUpdate{ - Type: BlacklistOpType_NodeRedeemed, - NodeIndex: 1, - } - update2 := BlacklistUpdate{ - Type: BlacklistOpType_NodeSuspected, - NodeIndex: 2, - } - update3 := BlacklistUpdate{ - NodeIndex: 3, - } - - updates = append(updates, update) - updates = append(updates, update1) - updates = append(updates, update2) - updates = append(updates, update3) - - require.Equal(t, len(updates.Bytes()), updates.Len()) -} - func TestVerifyBlacklistUpdates(t *testing.T) { testBlacklist := Blacklist{ NodeCount: 4, diff --git a/common/encoding.go b/common/encoding.go index 2d78b017..edae1e15 100644 --- a/common/encoding.go +++ b/common/encoding.go @@ -10,6 +10,10 @@ import ( "fmt" ) +const ( + maxAllocationSize = 100_000_000 // Arbitrary limit to prevent excessive memory allocation, should never happen in practice +) + type QuorumRecord struct { QC []byte Vote []byte @@ -96,7 +100,7 @@ func NewQuorumRecord(qc []byte, rawVote []byte, recordType uint16) []byte { return buff } -// ParseNotarizationRecordBytes parses a notarization record into the bytes of the QC and the vote +// ParseNotarizationRecord parses a notarization record into the bytes of the QC and the vote func ParseNotarizationRecord(r []byte) ([]byte, ToBeSignedVote, error) { recordType := binary.BigEndian.Uint16(r) if recordType != NotarizationRecordType { @@ -133,15 +137,23 @@ func NotarizationFromRecord(record []byte, qd QCDeserializer) (Notarization, err }, nil } -func BlockRecord(bh BlockHeader, blockData []byte) []byte { +func BlockRecord(bh BlockHeader, blockData []byte) ([]byte, error) { mdBytes := bh.Bytes() + mdSize := len(mdBytes) + buffSize := mdSize + len(blockData) + 2 + 4 + + if buffSize > maxAllocationSize { // Arbitrary limit to prevent excessive memory allocation, should never happen in practice + return nil, fmt.Errorf("block record size exceeds allowed size: %d", buffSize) + } - buff := make([]byte, len(mdBytes)+len(blockData)+2) + buff := make([]byte, buffSize) + // 2 bytes for record type, 4 bytes for block header length, the rest is for the block header and block data binary.BigEndian.PutUint16(buff, BlockRecordType) - copy(buff[2:], mdBytes) - copy(buff[2+BlockHeaderLen:], blockData) + binary.BigEndian.PutUint32(buff[2:], uint32(mdSize)) + copy(buff[6:], mdBytes) + copy(buff[6+mdSize:], blockData) - return buff + return buff, nil } func BlockFromRecord(ctx context.Context, blockDeserializer BlockDeserializer, record []byte) (Block, error) { @@ -154,28 +166,46 @@ func BlockFromRecord(ctx context.Context, blockDeserializer BlockDeserializer, r } func ParseBlockRecord(buff []byte) (BlockHeader, []byte, error) { + initialSize := len(buff) + + if len(buff) < 2 { + return BlockHeader{}, nil, fmt.Errorf("buffer too small, expected at least 2 bytes for record type") + } recordType := binary.BigEndian.Uint16(buff) if recordType != BlockRecordType { return BlockHeader{}, nil, fmt.Errorf("expected record type %d, got %d", BlockRecordType, recordType) } buff = buff[2:] - if len(buff) < BlockHeaderLen { - return BlockHeader{}, nil, fmt.Errorf("buffer too small, expected %d bytes", BlockHeaderLen+2) + + if len(buff) < 4 { + return BlockHeader{}, nil, fmt.Errorf("buffer too small, expected at least 4 bytes for metadata size") + } + + mdSize := binary.BigEndian.Uint32(buff[0:4]) // Read metadata size + if mdSize > maxAllocationSize { // Arbitrary limit to prevent excessive memory allocation + return BlockHeader{}, nil, fmt.Errorf("metadata size too large: %d bytes", mdSize) + } + + buff = buff[4:] // Move past the metadata size field + + if len(buff) < int(mdSize) { + return BlockHeader{}, nil, fmt.Errorf("buffer too small, expected %d bytes for block metadata but got %d", 6+mdSize, initialSize) } - var bh BlockHeader - if err := bh.FromBytes(buff[:BlockHeaderLen]); err != nil { + var blockHeader BlockHeader + if err := blockHeader.FromBytes(buff[:mdSize]); err != nil { return BlockHeader{}, nil, fmt.Errorf("failed to deserialize block metadata: %w", err) } - buff = buff[BlockHeaderLen:] + // The rest of the buffer is the block data + buff = buff[mdSize:] if len(buff) == 0 { return BlockHeader{}, nil, fmt.Errorf("buffer too small, expected block data but gone none") } - return bh, buff, nil + return blockHeader, buff, nil } func ParseEmptyNotarizationRecord(buff []byte) ([]byte, ToBeSignedEmptyVote, error) { @@ -227,21 +257,75 @@ func ParseEmptyVoteRecord(rawEmptyVote []byte) (ToBeSignedEmptyVote, error) { } func BlockRecordRetentionTerm(record []byte) (uint64, error) { - if len(record) < 19 { - return 0, fmt.Errorf("record too short to extract round, expected at least 19 bytes, got %d", len(record)) + initialSize := len(record) + + var pos int + // First 2 bytes are for record type + pos += 2 + // The next 4 bytes are for metadata size + pos += 4 + + if len(record) < pos { + return 0, fmt.Errorf("record too short to extract metadata size, expected at least %d bytes, got %d", pos, initialSize) + } + + metadataSize := binary.BigEndian.Uint32(record[2:6]) + + if metadataSize > maxAllocationSize { + return 0, fmt.Errorf("metadata size too large: %d bytes", metadataSize) + } + + record = record[6:] // Move the slice to start after the metadata size field + + if len(record) < int(metadataSize) { + return 0, fmt.Errorf("record too short to extract round, expected at least %d bytes, got %d", 6+int(metadataSize), initialSize) } + var blockHeader BlockHeader + if err := blockHeader.FromBytes(record[0:metadataSize]); err != nil { + return 0, fmt.Errorf("failed to deserialize block metadata: %w", err) + } + + return blockHeader.Round, nil +} + +func notarizationQuorumRecordRetentionTerm(record []byte) (uint64, error) { + initialSize := len(record) + var pos int // First 2 bytes are for record type pos += 2 - // The next 9 bytes are for version and epoch. - pos += 9 - // The next 8 bytes are for round. - round := binary.BigEndian.Uint64(record[pos : pos+8]) - return round, nil + + // Next 4 bytes are for the size of the vote + pos += 4 + + if len(record) < pos { + return 0, fmt.Errorf("record too short to extract vote size, expected at least %d bytes, got %d", pos, initialSize) + } + + voteSize := binary.BigEndian.Uint32(record[2:6]) + + if voteSize > maxAllocationSize { + return 0, fmt.Errorf("vote size too large: %d bytes", voteSize) + } + + record = record[pos:] // Move the slice to start after the vote size field + + if len(record) < int(voteSize) { + return 0, fmt.Errorf("record too short to extract vote, expected at least %d bytes, got %d", 6+int(voteSize), initialSize) + } + + record = record[:voteSize] // Trim everything but the vote, we don't need anything else to extract the round + + var vote ToBeSignedVote + if err := vote.FromBytes(record); err != nil { + return 0, fmt.Errorf("failed to deserialize vote: %w", err) + } + + return vote.Round, nil } -func QuorumRecordRetentionTerm(record []byte) (uint64, error) { +func emptyNotarizationQuorumRecordRetentionTerm(record []byte) (uint64, error) { if len(record) < 23 { return 0, fmt.Errorf("record too short to extract round, expected at least 23 bytes, got %d", len(record)) } @@ -285,9 +369,9 @@ func (wrr *WALRetentionReader) RetentionTerm(entry []byte) (uint64, error) { case BlockRecordType: return BlockRecordRetentionTerm(entry) case NotarizationRecordType: - return QuorumRecordRetentionTerm(entry) + return notarizationQuorumRecordRetentionTerm(entry) case EmptyNotarizationRecordType: - return QuorumRecordRetentionTerm(entry) + return emptyNotarizationQuorumRecordRetentionTerm(entry) case EmptyVoteRecordType: return EmptyVoteRecordRetentionTerm(entry) default: diff --git a/common/encoding_maxalloc_test.go b/common/encoding_maxalloc_test.go new file mode 100644 index 00000000..4b5eaffd --- /dev/null +++ b/common/encoding_maxalloc_test.go @@ -0,0 +1,138 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package common + +import ( + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" +) + +func sizePrefixedRecord(recordType uint16, size uint32, tail ...byte) []byte { + b := make([]byte, 6+len(tail)) + binary.BigEndian.PutUint16(b, recordType) + binary.BigEndian.PutUint32(b[2:], size) + copy(b[6:], tail) + return b +} + +func TestBlockRecordAllocationLimit(t *testing.T) { + bh := BlockHeader{ProtocolMetadata: ProtocolMetadata{Version: 1, Round: 7, Seq: 2, Epoch: 3}} + + // Happy path: a well-formed record round-trips. + rec, err := BlockRecord(bh, []byte{1, 2, 3}) + require.NoError(t, err) + + gotBH, data, err := ParseBlockRecord(rec) + require.NoError(t, err) + require.True(t, bh.Equals(&gotBH)) + require.Equal(t, []byte{1, 2, 3}, data) + + // Block data large enough that the total record would exceed the allocation + // limit: BlockRecord must reject it before allocating the output buffer. + oversized := make([]byte, maxAllocationSize) + _, err = BlockRecord(bh, oversized) + require.ErrorContains(t, err, "block record size exceeds") +} + +func TestParseBlockRecordErrors(t *testing.T) { + bh := BlockHeader{ProtocolMetadata: ProtocolMetadata{Version: 1, Round: 7, Seq: 2, Epoch: 3}} + valid, err := BlockRecord(bh, []byte{9, 9, 9}) + require.NoError(t, err) + mdSize := int(binary.BigEndian.Uint32(valid[2:6])) + + // A valid header followed by no block data. + noData := append([]byte(nil), valid[:6+mdSize]...) + + // Total length of 3: record type present, but fewer than 4 bytes remain for + // the metadata size field. + shortSize := make([]byte, 3) + binary.BigEndian.PutUint16(shortSize, BlockRecordType) + + for _, tc := range []struct { + name string + input []byte + errContains string + }{ + {"valid round-trips", valid, ""}, + {"nil buffer", nil, "at least 2 bytes for record type"}, + {"single byte", []byte{0x00}, "at least 2 bytes for record type"}, + {"wrong record type", sizePrefixedRecord(NotarizationRecordType, 0), "expected record type"}, + {"missing metadata size", shortSize, "at least 4 bytes for metadata size"}, + {"metadata size too large", sizePrefixedRecord(BlockRecordType, maxAllocationSize+1), "metadata size too large"}, + {"metadata size at limit but truncated", sizePrefixedRecord(BlockRecordType, maxAllocationSize), "buffer too small, expected"}, + {"declared size exceeds buffer", sizePrefixedRecord(BlockRecordType, 50, 0x01, 0x02), "buffer too small, expected"}, + {"invalid metadata bytes", sizePrefixedRecord(BlockRecordType, 1, 0x80, 0x00), "failed to deserialize block metadata"}, + {"no block data", noData, "expected block data but gone none"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, _, err := ParseBlockRecord(tc.input) + if tc.errContains == "" { + require.NoError(t, err) + return + } + require.ErrorContains(t, err, tc.errContains) + }) + } +} + +func TestBlockRecordRetentionTermErrors(t *testing.T) { + bh := BlockHeader{ProtocolMetadata: ProtocolMetadata{Version: 1, Round: 42, Seq: 2, Epoch: 3}} + valid, err := BlockRecord(bh, []byte{9}) + require.NoError(t, err) + + for _, tc := range []struct { + name string + input []byte + expectedTerm uint64 + errContains string + }{ + {"valid returns round", valid, 42, ""}, + {"too short for metadata size", []byte{0, 0, 0}, 0, "too short to extract metadata size"}, + {"metadata size too large", sizePrefixedRecord(BlockRecordType, maxAllocationSize+1), 0, "metadata size too large"}, + {"metadata size at limit but truncated", sizePrefixedRecord(BlockRecordType, maxAllocationSize), 0, "too short to extract round"}, + {"declared size exceeds buffer", sizePrefixedRecord(BlockRecordType, 40, 0x01), 0, "too short to extract round"}, + {"invalid metadata bytes", sizePrefixedRecord(BlockRecordType, 1, 0x80), 0, "failed to deserialize block metadata"}, + } { + t.Run(tc.name, func(t *testing.T) { + term, err := BlockRecordRetentionTerm(tc.input) + if tc.errContains == "" { + require.NoError(t, err) + require.Equal(t, tc.expectedTerm, term) + return + } + require.ErrorContains(t, err, tc.errContains) + }) + } +} + +func TestNotarizationQuorumRecordRetentionTermErrors(t *testing.T) { + vote := ToBeSignedVote{BlockHeader{ProtocolMetadata: ProtocolMetadata{Version: 1, Round: 99, Seq: 2, Epoch: 3}}} + valid := NewQuorumRecord([]byte{1, 2, 3}, vote.Bytes(), NotarizationRecordType) + + for _, tc := range []struct { + name string + input []byte + expectedTerm uint64 + errContains string + }{ + {"valid returns round", valid, 99, ""}, + {"too short for vote size", []byte{0, 0, 0}, 0, "too short to extract vote size"}, + {"vote size too large", sizePrefixedRecord(NotarizationRecordType, maxAllocationSize+1), 0, "vote size too large"}, + {"vote size at limit but truncated", sizePrefixedRecord(NotarizationRecordType, maxAllocationSize), 0, "too short to extract vote,"}, + {"declared size exceeds buffer", sizePrefixedRecord(NotarizationRecordType, 40, 0x01), 0, "too short to extract vote,"}, + {"invalid vote bytes", sizePrefixedRecord(NotarizationRecordType, 1, 0x80), 0, "failed to deserialize vote"}, + } { + t.Run(tc.name, func(t *testing.T) { + term, err := notarizationQuorumRecordRetentionTerm(tc.input) + if tc.errContains == "" { + require.NoError(t, err) + require.Equal(t, tc.expectedTerm, term) + return + } + require.ErrorContains(t, err, tc.errContains) + }) + } +} diff --git a/common/encoding_test.go b/common/encoding_test.go index 1202f807..714bfa24 100644 --- a/common/encoding_test.go +++ b/common/encoding_test.go @@ -77,7 +77,8 @@ func TestBlockRecord(t *testing.T) { require.NoError(t, err) payload := []byte{11, 12, 13, 14, 15, 16} - record := BlockRecord(bh, payload) + record, err := BlockRecord(bh, payload) + require.NoError(t, err) retentionTerm, err := BlockRecordRetentionTerm(record) require.NoError(t, err) @@ -86,7 +87,7 @@ func TestBlockRecord(t *testing.T) { md2, payload2, err := ParseBlockRecord(record) require.NoError(t, err) - require.Equal(t, bh, md2) + require.True(t, bh.Equals(&md2)) require.Equal(t, payload, payload2) } @@ -104,7 +105,8 @@ func FuzzBlockRecord(f *testing.F) { }, Digest: digest, } - record := BlockRecord(bh, payload) + record, err := BlockRecord(bh, payload) + require.NoError(t, err) retentionTerm, err := BlockRecordRetentionTerm(record) require.NoError(t, err) require.Equal(t, round, retentionTerm) @@ -140,14 +142,14 @@ func TestNotarizationRecord(t *testing.T) { require.NoError(t, err) record := NewQuorumRecord([]byte{1, 2, 3}, vote.Bytes(), NotarizationRecordType) - retentionTerm, err := QuorumRecordRetentionTerm(record) + retentionTerm, err := notarizationQuorumRecordRetentionTerm(record) require.NoError(t, err) require.Equal(t, uint64(666), retentionTerm) qc, vote2, err := ParseNotarizationRecord(record) require.NoError(t, err) require.Equal(t, []byte{1, 2, 3}, qc) - require.Equal(t, vote, vote2) + require.True(t, vote.BlockHeader.Equals(&vote2.BlockHeader)) } func FuzzNotarizationRecord(f *testing.F) { @@ -174,14 +176,14 @@ func FuzzNotarizationRecord(f *testing.F) { } record := NewQuorumRecord([]byte{1, 2, 3}, vote.Bytes(), NotarizationRecordType) - retentionTerm, err := QuorumRecordRetentionTerm(record) + retentionTerm, err := notarizationQuorumRecordRetentionTerm(record) require.NoError(t, err) require.Equal(t, round, retentionTerm) qc, vote2, err := ParseNotarizationRecord(record) require.NoError(t, err) require.Equal(t, []byte{1, 2, 3}, qc) - require.Equal(t, vote, vote2) + require.True(t, vote.BlockHeader.Equals(&vote2.BlockHeader)) }) } @@ -227,7 +229,7 @@ func TestRetentionTerm(t *testing.T) { } emptyNotarizationRecord := NewQuorumRecord([]byte{1, 2, 3}, en.Vote.Bytes(), EmptyNotarizationRecordType) notarizationRecord := NewQuorumRecord([]byte{1, 2, 3}, vote.Bytes(), NotarizationRecordType) - blockRecord := BlockRecord(BlockHeader{ + blockRecord, err := BlockRecord(BlockHeader{ ProtocolMetadata: ProtocolMetadata{ Version: 1, Round: 668, @@ -235,6 +237,7 @@ func TestRetentionTerm(t *testing.T) { Epoch: 4, }, }, []byte{11, 12, 13, 14, 15, 16}) + require.NoError(t, err) finalizationRecord := NewQuorumRecord([]byte{1, 2, 3}, en.Vote.Bytes(), FinalizationRecordType) diff --git a/common/metadata.canoto.go b/common/metadata.canoto.go new file mode 100644 index 00000000..b736aaa2 --- /dev/null +++ b/common/metadata.canoto.go @@ -0,0 +1,494 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.19.0 +// source: metadata.go + +package common + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that the generated code is compatible with the library version. +const ( + _ uint = canoto.VersionCompatibility - 1 + _ uint = 1 - canoto.VersionCompatibility +) + +// Ensure that unused imports do not error +var _ = io.ErrUnexpectedEOF + +const ( + canotoNumber_ProtocolMetadata__Version = 1 + canotoNumber_ProtocolMetadata__Epoch = 2 + canotoNumber_ProtocolMetadata__Round = 3 + canotoNumber_ProtocolMetadata__Seq = 4 + canotoNumber_ProtocolMetadata__Prev = 5 + + canotoTag_ProtocolMetadata__Version = "\x08" // canoto.Tag(canotoNumber_ProtocolMetadata__Version, canoto.Varint) + canotoTag_ProtocolMetadata__Epoch = "\x10" // canoto.Tag(canotoNumber_ProtocolMetadata__Epoch, canoto.Varint) + canotoTag_ProtocolMetadata__Round = "\x18" // canoto.Tag(canotoNumber_ProtocolMetadata__Round, canoto.Varint) + canotoTag_ProtocolMetadata__Seq = "\x20" // canoto.Tag(canotoNumber_ProtocolMetadata__Seq, canoto.Varint) + canotoTag_ProtocolMetadata__Prev = "\x2a" // canoto.Tag(canotoNumber_ProtocolMetadata__Prev, canoto.Len) +) + +type canotoData_ProtocolMetadata struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*ProtocolMetadata) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero ProtocolMetadata + s := &canoto.Spec{ + Name: "ProtocolMetadata", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_ProtocolMetadata__Version, + Name: "Version", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Version), + }, + { + FieldNumber: canotoNumber_ProtocolMetadata__Epoch, + Name: "Epoch", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Epoch), + }, + { + FieldNumber: canotoNumber_ProtocolMetadata__Round, + Name: "Round", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Round), + }, + { + FieldNumber: canotoNumber_ProtocolMetadata__Seq, + Name: "Seq", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Seq), + }, + { + FieldNumber: canotoNumber_ProtocolMetadata__Prev, + Name: "Prev", + OneOf: "", + TypeFixedBytes: uint64(len(zero.Prev)), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *ProtocolMetadata) 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 *ProtocolMetadata) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = ProtocolMetadata{} + 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_ProtocolMetadata__Version: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Version); err != nil { + return err + } + if canoto.IsZero(c.Version) { + return canoto.ErrZeroValue + } + case canotoNumber_ProtocolMetadata__Epoch: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Epoch); err != nil { + return err + } + if canoto.IsZero(c.Epoch) { + return canoto.ErrZeroValue + } + case canotoNumber_ProtocolMetadata__Round: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Round); err != nil { + return err + } + if canoto.IsZero(c.Round) { + return canoto.ErrZeroValue + } + case canotoNumber_ProtocolMetadata__Seq: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Seq); err != nil { + return err + } + if canoto.IsZero(c.Seq) { + return canoto.ErrZeroValue + } + case canotoNumber_ProtocolMetadata__Prev: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.Prev) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.Prev)[:], r.B) + if canoto.IsZero(c.Prev) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + 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 *ProtocolMetadata) 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 *ProtocolMetadata) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.Version) { + size += uint64(len(canotoTag_ProtocolMetadata__Version)) + canoto.SizeUint(c.Version) + } + if !canoto.IsZero(c.Epoch) { + size += uint64(len(canotoTag_ProtocolMetadata__Epoch)) + canoto.SizeUint(c.Epoch) + } + if !canoto.IsZero(c.Round) { + size += uint64(len(canotoTag_ProtocolMetadata__Round)) + canoto.SizeUint(c.Round) + } + if !canoto.IsZero(c.Seq) { + size += uint64(len(canotoTag_ProtocolMetadata__Seq)) + canoto.SizeUint(c.Seq) + } + if !canoto.IsZero(c.Prev) { + size += uint64(len(canotoTag_ProtocolMetadata__Prev)) + canoto.SizeBytes((&c.Prev)[:]) + } + 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 *ProtocolMetadata) 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 *ProtocolMetadata) 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 *ProtocolMetadata) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.Version) { + canoto.Append(&w, canotoTag_ProtocolMetadata__Version) + canoto.AppendUint(&w, c.Version) + } + if !canoto.IsZero(c.Epoch) { + canoto.Append(&w, canotoTag_ProtocolMetadata__Epoch) + canoto.AppendUint(&w, c.Epoch) + } + if !canoto.IsZero(c.Round) { + canoto.Append(&w, canotoTag_ProtocolMetadata__Round) + canoto.AppendUint(&w, c.Round) + } + if !canoto.IsZero(c.Seq) { + canoto.Append(&w, canotoTag_ProtocolMetadata__Seq) + canoto.AppendUint(&w, c.Seq) + } + if !canoto.IsZero(c.Prev) { + canoto.Append(&w, canotoTag_ProtocolMetadata__Prev) + canoto.AppendBytes(&w, (&c.Prev)[:]) + } + return w +} + +const ( + canotoNumber_BlockHeader__ProtocolMetadata = 1 + canotoNumber_BlockHeader__Digest = 2 + + canotoTag_BlockHeader__ProtocolMetadata = "\x0a" // canoto.Tag(canotoNumber_BlockHeader__ProtocolMetadata, canoto.Len) + canotoTag_BlockHeader__Digest = "\x12" // canoto.Tag(canotoNumber_BlockHeader__Digest, canoto.Len) +) + +type canotoData_BlockHeader struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*BlockHeader) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeFor[BlockHeader]()) + var zero BlockHeader + s := &canoto.Spec{ + Name: "BlockHeader", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.ProtocolMetadata), + /*FieldNumber: */ canotoNumber_BlockHeader__ProtocolMetadata, + /*Name: */ "ProtocolMetadata", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + { + FieldNumber: canotoNumber_BlockHeader__Digest, + Name: "Digest", + OneOf: "", + TypeFixedBytes: uint64(len(zero.Digest)), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *BlockHeader) 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 *BlockHeader) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = BlockHeader{} + 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_BlockHeader__ProtocolMetadata: + 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.ProtocolMetadata).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + case canotoNumber_BlockHeader__Digest: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.Digest) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.Digest)[:], r.B) + if canoto.IsZero(c.Digest) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + 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 *BlockHeader) ValidCanoto() bool { + if !(&c.ProtocolMetadata).ValidCanoto() { + return false + } + 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 *BlockHeader) CalculateCanotoCache() { + var size uint64 + (&c.ProtocolMetadata).CalculateCanotoCache() + if fieldSize := (&c.ProtocolMetadata).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canotoTag_BlockHeader__ProtocolMetadata)) + canoto.SizeUint(fieldSize) + fieldSize + } + if !canoto.IsZero(c.Digest) { + size += uint64(len(canotoTag_BlockHeader__Digest)) + canoto.SizeBytes((&c.Digest)[:]) + } + 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 *BlockHeader) 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 *BlockHeader) 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 *BlockHeader) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if fieldSize := (&c.ProtocolMetadata).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canotoTag_BlockHeader__ProtocolMetadata) + canoto.AppendUint(&w, fieldSize) + w = (&c.ProtocolMetadata).MarshalCanotoInto(w) + } + if !canoto.IsZero(c.Digest) { + canoto.Append(&w, canotoTag_BlockHeader__Digest) + canoto.AppendBytes(&w, (&c.Digest)[:]) + } + return w +} diff --git a/common/metadata.go b/common/metadata.go index 5c3f3c5f..319561c7 100644 --- a/common/metadata.go +++ b/common/metadata.go @@ -5,20 +5,11 @@ package common import ( "bytes" - "encoding/binary" "fmt" ) const ( - metadataVersionLen = 1 - metadataEpochLen = 8 - metadataRoundLen = 8 - metadataSeqLen = 8 - metadataPrevLen = 32 - metadataDigestLen = 32 - - ProtocolMetadataLen = metadataVersionLen + metadataEpochLen + metadataRoundLen + metadataSeqLen + metadataPrevLen - BlockHeaderLen = ProtocolMetadataLen + metadataDigestLen + metadataDigestLen = 32 ) const ( @@ -28,27 +19,31 @@ const ( // ProtocolMetadata encodes information about the protocol state at a given point in time. type ProtocolMetadata struct { // Version defines the version of the protocol this block was created with. - Version uint8 + Version uint8 `canoto:"uint,1"` // Epoch returns the epoch in which the block was proposed - Epoch uint64 + Epoch uint64 `canoto:"uint,2"` // Round returns the round number in which the block was proposed. // Can also be an empty block. - Round uint64 + Round uint64 `canoto:"uint,3"` // Seq is the order of the block among all blocks in the blockchain. // Cannot correspond to an empty block. - Seq uint64 + Seq uint64 `canoto:"uint,4"` // Prev returns the digest of the previous data block - Prev Digest + Prev Digest `canoto:"fixed bytes,5"` + + canotoData canotoData_ProtocolMetadata } // BlockHeader encodes a succinct and collision-free representation of a block. // It's included in votes and finalizations in order to convey which block is voted on, // or which block is finalized. type BlockHeader struct { - ProtocolMetadata + ProtocolMetadata `canoto:"value,1"` // Digest returns a collision resistant short representation of the block's bytes - Digest Digest + Digest Digest `canoto:"fixed bytes,2"` + + canotoData canotoData_BlockHeader } type Digest [metadataDigestLen]byte @@ -57,7 +52,7 @@ func (d Digest) String() string { return fmt.Sprintf("%x...", (d)[:digestFormatSize]) } -func (bh BlockHeader) String() string { +func (bh *BlockHeader) String() string { return fmt.Sprintf("BlockHeader{Digest: %s, Prev: %s, Epoch: %d, Round: %d, Seq: %d, Version: %d}", bh.Digest.String(), bh.Prev.String(), bh.Epoch, bh.Round, bh.Seq, bh.Version) } @@ -69,72 +64,36 @@ func (bh *BlockHeader) Equals(other *BlockHeader) bool { } func (bh *BlockHeader) Bytes() []byte { - buff := make([]byte, BlockHeaderLen) - - mdBytes := bh.ProtocolMetadata.Bytes() - copy(buff, mdBytes) - copy(buff[ProtocolMetadataLen:], bh.Digest[:]) - - return buff + clone := BlockHeader{ + ProtocolMetadata: bh.ProtocolMetadata.Clone(), + Digest: bh.Digest, + } + return clone.MarshalCanoto() } func (bh *BlockHeader) FromBytes(buff []byte) error { - if len(buff) != BlockHeaderLen { - return fmt.Errorf("invalid buffer length %d, expected %d", len(buff), BlockHeaderLen) - } - - md, err := ProtocolMetadataFromBytes(buff[:ProtocolMetadataLen]) - if err != nil { - return fmt.Errorf("failed to parse ProtocolMetadata: %w", err) - } - bh.ProtocolMetadata = *md - - copy(bh.Digest[:], buff[ProtocolMetadataLen:ProtocolMetadataLen+metadataDigestLen]) - return nil + return bh.UnmarshalCanoto(buff) } // Serializes a ProtocolMetadata from a byte slice. func ProtocolMetadataFromBytes(buff []byte) (*ProtocolMetadata, error) { - if len(buff) != ProtocolMetadataLen { - return nil, fmt.Errorf("invalid buffer length %d, expected %d", len(buff), ProtocolMetadataLen) - } - md := &ProtocolMetadata{} - var pos int - md.Version = buff[pos] - pos++ - md.Epoch = binary.BigEndian.Uint64(buff[pos:]) - pos += metadataEpochLen - md.Round = binary.BigEndian.Uint64(buff[pos:]) - pos += metadataRoundLen - md.Seq = binary.BigEndian.Uint64(buff[pos:]) - pos += metadataSeqLen - copy(md.Prev[:], buff[pos:pos+metadataPrevLen]) - - return md, nil + return md, md.UnmarshalCanoto(buff) +} + +// Clone returns a copy of the ProtocolMetadata. +func (md *ProtocolMetadata) Clone() ProtocolMetadata { + return ProtocolMetadata{ + Version: md.Version, + Epoch: md.Epoch, + Round: md.Round, + Seq: md.Seq, + Prev: md.Prev, + } } // Bytes returns a byte encoding of the ProtocolMetadata. -// it is encoded as follows: -// [Version (1 byte), Epoch (8 bytes), Round (8 bytes), -// Seq (8 bytes), Prev (32 bytes)] func (md *ProtocolMetadata) Bytes() []byte { - buff := make([]byte, ProtocolMetadataLen) - var pos int - - buff[pos] = md.Version - pos++ - - binary.BigEndian.PutUint64(buff[pos:], md.Epoch) - pos += metadataEpochLen - - binary.BigEndian.PutUint64(buff[pos:], md.Round) - pos += metadataRoundLen - - binary.BigEndian.PutUint64(buff[pos:], md.Seq) - pos += metadataSeqLen - - copy(buff[pos:], md.Prev[:]) - - return buff + clone := md.Clone() + return clone.MarshalCanoto() } diff --git a/common/metadata_test.go b/common/metadata_test.go index 577a1386..70b2e85f 100644 --- a/common/metadata_test.go +++ b/common/metadata_test.go @@ -34,7 +34,7 @@ func TestMetadata(t *testing.T) { var bh2 BlockHeader require.NoError(t, bh2.FromBytes(bh.Bytes())) - require.Equal(t, bh, bh2) + require.True(t, bh.Equals(&bh2)) } func FuzzMetadata(f *testing.F) { @@ -56,6 +56,6 @@ func FuzzMetadata(f *testing.F) { var bh2 BlockHeader require.NoError(t, bh2.FromBytes(bh.Bytes())) - require.Equal(t, bh, bh2) + require.True(t, bh.Equals(&bh2)) }) } diff --git a/common/notarization.go b/common/notarization.go index 3e900a74..d5ad6dbb 100644 --- a/common/notarization.go +++ b/common/notarization.go @@ -48,7 +48,7 @@ func NewNotarization(logger Logger, signatureAggregator SignatureAggregator, vot for _, vote := range votesForCurrentRound { if !vote.Vote.BlockHeader.Equals(&blockHeader) { logger.Debug("Skipping vote for block header different than the proposal block header", - zap.Stringer("voteDigest", vote.Vote.BlockHeader), zap.Stringer("NodeID", vote.Signature.Signer)) + zap.Stringer("voteDigest", &vote.Vote.BlockHeader), zap.Stringer("NodeID", vote.Signature.Signer)) continue } logger.Debug("Collected vote from node", zap.Stringer("NodeID", vote.Signature.Signer), diff --git a/external.go b/external.go index 22d65ace..23343728 100644 --- a/external.go +++ b/external.go @@ -39,28 +39,16 @@ func (p *ParsedBlock) Bytes() ([]byte, error) { } func (p *ParsedBlock) BlockHeader() common.BlockHeader { - var md *common.ProtocolMetadata - var err error - if len(p.Metadata.SimplexProtocolMetadata) > 0 { - md, err = common.ProtocolMetadataFromBytes(p.Metadata.SimplexProtocolMetadata) - if err != nil { - panic(err) // TODO: handle error - } - } else { - md = &common.ProtocolMetadata{} - } - + md := p.Metadata.SimplexProtocolMetadata digest := p.StateMachineBlock.Digest() return common.BlockHeader{ - ProtocolMetadata: *md, + ProtocolMetadata: md, Digest: digest, } } func (p *ParsedBlock) Blacklist() common.Blacklist { - var blacklist common.Blacklist - _ = blacklist.FromBytes(p.Metadata.SimplexBlacklist) // TODO: encode blacklist with Canoto - return blacklist + return p.Metadata.SimplexBlacklist } func (p *ParsedBlock) Verify(ctx context.Context) (common.VerifiedBlock, error) { diff --git a/instance.go b/instance.go index d8666232..724fc70c 100644 --- a/instance.go +++ b/instance.go @@ -518,10 +518,7 @@ func (i *Instance) maybeGarbageCollectWAL(lastBlock metadata.StateMachineBlock) // TODO: We need to test a scenario where an epoch change occurred and then a few notarizations have been persisted to WAL, // but no block has been finalized. So the WAL contains entries from previous epochs as well as from the current epoch. // TODO: We need to test a scenario where an epoch change occurred but the node has crashed after notarizing some Telocks. - md, err := common.ProtocolMetadataFromBytes(lastBlock.Metadata.SimplexProtocolMetadata) - if err != nil { - return fmt.Errorf("error parsing protocol metadata from last block: %w", err) - } + md := lastBlock.Metadata.SimplexProtocolMetadata if err := i.wal.GarbageCollect(md.Round); err != nil { return fmt.Errorf("error garbage collecting WALs: %w", err) } diff --git a/msm/encoding.canoto.go b/msm/encoding.canoto.go index 1c6a5e03..a196d7d2 100644 --- a/msm/encoding.canoto.go +++ b/msm/encoding.canoto.go @@ -1,7 +1,7 @@ // Code generated by canoto. DO NOT EDIT. // versions: // canoto v0.19.0 -// source: encoding.go +// source: msm/encoding.go package metadata @@ -61,18 +61,26 @@ func (*StateMachineMetadata) CanotoSpec(types ...reflect.Type) *canoto.Spec { /*Pointer: */ false, /*types: */ types, ), - { - FieldNumber: canotoNumber_StateMachineMetadata__SimplexProtocolMetadata, - Name: "SimplexProtocolMetadata", - OneOf: "", - TypeBytes: true, - }, - { - FieldNumber: canotoNumber_StateMachineMetadata__SimplexBlacklist, - Name: "SimplexBlacklist", - OneOf: "", - TypeBytes: true, - }, + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.SimplexProtocolMetadata), + /*FieldNumber: */ canotoNumber_StateMachineMetadata__SimplexProtocolMetadata, + /*Name: */ "SimplexProtocolMetadata", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.SimplexBlacklist), + /*FieldNumber: */ canotoNumber_StateMachineMetadata__SimplexBlacklist, + /*Name: */ "SimplexBlacklist", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), { FieldNumber: canotoNumber_StateMachineMetadata__PChainHeight, Name: "PChainHeight", @@ -172,23 +180,49 @@ func (c *StateMachineMetadata) UnmarshalCanotoFrom(r canoto.Reader) error { return canoto.ErrUnexpectedWireType } - if err := canoto.ReadBytes(&r, &c.SimplexProtocolMetadata); err != nil { + // 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(c.SimplexProtocolMetadata) == 0 { + 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.SimplexProtocolMetadata).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes case canotoNumber_StateMachineMetadata__SimplexBlacklist: if wireType != canoto.Len { return canoto.ErrUnexpectedWireType } - if err := canoto.ReadBytes(&r, &c.SimplexBlacklist); err != nil { + // 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(c.SimplexBlacklist) == 0 { + 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.SimplexBlacklist).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes case canotoNumber_StateMachineMetadata__PChainHeight: if wireType != canoto.Varint { return canoto.ErrUnexpectedWireType @@ -277,6 +311,12 @@ func (c *StateMachineMetadata) ValidCanoto() bool { if !(&c.SimplexEpochInfo).ValidCanoto() { return false } + if !(&c.SimplexProtocolMetadata).ValidCanoto() { + return false + } + if !(&c.SimplexBlacklist).ValidCanoto() { + return false + } if !(&c.ICMEpochInfo).ValidCanoto() { return false } @@ -296,11 +336,13 @@ func (c *StateMachineMetadata) CalculateCanotoCache() { if fieldSize := (&c.SimplexEpochInfo).CachedCanotoSize(); fieldSize != 0 { size += uint64(len(canotoTag_StateMachineMetadata__SimplexEpochInfo)) + canoto.SizeUint(fieldSize) + fieldSize } - if len(c.SimplexProtocolMetadata) != 0 { - size += uint64(len(canotoTag_StateMachineMetadata__SimplexProtocolMetadata)) + canoto.SizeBytes(c.SimplexProtocolMetadata) + (&c.SimplexProtocolMetadata).CalculateCanotoCache() + if fieldSize := (&c.SimplexProtocolMetadata).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canotoTag_StateMachineMetadata__SimplexProtocolMetadata)) + canoto.SizeUint(fieldSize) + fieldSize } - if len(c.SimplexBlacklist) != 0 { - size += uint64(len(canotoTag_StateMachineMetadata__SimplexBlacklist)) + canoto.SizeBytes(c.SimplexBlacklist) + (&c.SimplexBlacklist).CalculateCanotoCache() + if fieldSize := (&c.SimplexBlacklist).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canotoTag_StateMachineMetadata__SimplexBlacklist)) + canoto.SizeUint(fieldSize) + fieldSize } if !canoto.IsZero(c.PChainHeight) { size += uint64(len(canotoTag_StateMachineMetadata__PChainHeight)) + canoto.SizeUint(c.PChainHeight) @@ -360,13 +402,15 @@ func (c *StateMachineMetadata) MarshalCanotoInto(w canoto.Writer) canoto.Writer canoto.AppendUint(&w, fieldSize) w = (&c.SimplexEpochInfo).MarshalCanotoInto(w) } - if len(c.SimplexProtocolMetadata) != 0 { + if fieldSize := (&c.SimplexProtocolMetadata).CachedCanotoSize(); fieldSize != 0 { canoto.Append(&w, canotoTag_StateMachineMetadata__SimplexProtocolMetadata) - canoto.AppendBytes(&w, c.SimplexProtocolMetadata) + canoto.AppendUint(&w, fieldSize) + w = (&c.SimplexProtocolMetadata).MarshalCanotoInto(w) } - if len(c.SimplexBlacklist) != 0 { + if fieldSize := (&c.SimplexBlacklist).CachedCanotoSize(); fieldSize != 0 { canoto.Append(&w, canotoTag_StateMachineMetadata__SimplexBlacklist) - canoto.AppendBytes(&w, c.SimplexBlacklist) + canoto.AppendUint(&w, fieldSize) + w = (&c.SimplexBlacklist).MarshalCanotoInto(w) } if !canoto.IsZero(c.PChainHeight) { canoto.Append(&w, canotoTag_StateMachineMetadata__PChainHeight) diff --git a/msm/encoding.go b/msm/encoding.go index b45c8735..193699cd 100644 --- a/msm/encoding.go +++ b/msm/encoding.go @@ -21,10 +21,10 @@ type StateMachineMetadata struct { // SimplexEpochInfo is the metadata that the StateMachine uses for its own epoching. SimplexEpochInfo SimplexEpochInfo `canoto:"value,1"` // SimplexProtocolMetadata is the metadata that Simplex uses for its protocol, such as sequence and round number. - SimplexProtocolMetadata []byte `canoto:"bytes,2"` + SimplexProtocolMetadata common.ProtocolMetadata `canoto:"value,2"` // SimplexBlacklist is the metadata that Simplex uses to keep track of blacklisted nodes. // Blacklisted nodes do not become leaders. - SimplexBlacklist []byte `canoto:"bytes,3"` + SimplexBlacklist common.Blacklist `canoto:"value,3"` // PChainHeight is the P-Chain height that the StateMachine sampled at the time of building the block. // It's used for ICM epoching, not for Simplex epoching. // For Simplex epoching, the P-Chain height that matters is the PChainReferenceHeight in the SimplexEpochInfo. diff --git a/msm/fake_node_test.go b/msm/fake_node_test.go index 04f6ac9a..6f7f292d 100644 --- a/msm/fake_node_test.go +++ b/msm/fake_node_test.go @@ -282,10 +282,7 @@ func newFakeNode(t *testing.T) *fakeNode { for _, bs := range fn.blocks { match := bs.block.Digest() == digest if !match { - md, err := common.ProtocolMetadataFromBytes(bs.block.Metadata.SimplexProtocolMetadata) - if err != nil { - return StateMachineBlock{}, nil, err - } + md := bs.block.Metadata.SimplexProtocolMetadata match = md.Seq == seq } if match { @@ -365,8 +362,7 @@ func (fn *fakeNode) tryFinalizeNextBlock() { fn.blocks[nextIndex].finalized = true block := fn.blocks[nextIndex].block - md, err := common.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) - require.NoError(fn.t, err) + md := block.Metadata.SimplexProtocolMetadata fn.sm.LatestPersistedHeight = md.Seq fn.t.Logf("Finalized block at height %d with epoch %d", md.Seq, block.Metadata.SimplexEpochInfo.EpochNumber) @@ -423,7 +419,7 @@ func (fn *fakeNode) buildBlock() (avalanchego.VMBlock, *StateMachineBlock) { Round: lastMD.Round + 1, Epoch: fn.epoch, Prev: prevBlockDigest, - }, nil) + }, common.Blacklist{}) require.NoError(fn.t, err) return block.InnerBlock, block @@ -431,13 +427,11 @@ func (fn *fakeNode) buildBlock() (avalanchego.VMBlock, *StateMachineBlock) { func (fn *fakeNode) prepareMetadataAndPrevBlockDigest() (*common.ProtocolMetadata, [32]byte) { var lastMD *common.ProtocolMetadata - var err error lastBlockDigest := genesisBlock.Digest() if len(fn.blocks) > 0 { lastBlock := fn.blocks[len(fn.blocks)-1].block lastBlockDigest = lastBlock.Digest() - lastMD, err = common.ProtocolMetadataFromBytes(lastBlock.Metadata.SimplexProtocolMetadata) - require.NoError(fn.t, err) + lastMD = &lastBlock.Metadata.SimplexProtocolMetadata } else { lastMD = &common.ProtocolMetadata{ Prev: lastBlockDigest, diff --git a/msm/fuzz_test.go b/msm/fuzz_test.go index d44a5a20..65e474c4 100644 --- a/msm/fuzz_test.go +++ b/msm/fuzz_test.go @@ -223,7 +223,7 @@ func buildEpochChain(tb testing.TB, logger common.Logger) ([]*StateMachineBlock, prevDigest = genesis.Digest() } md := common.ProtocolMetadata{Seq: seq, Round: round, Epoch: epoch, Prev: prevDigest} - block, err := sm.BuildBlock(ctx, md, nil) + block, err := sm.BuildBlock(ctx, md, emptyBlacklist) require.NoError(tb, err) return block } diff --git a/msm/msm.go b/msm/msm.go index 13764abc..cf7fbb99 100644 --- a/msm/msm.go +++ b/msm/msm.go @@ -277,7 +277,7 @@ func (sm *StateMachine) maybeInitializeApprovalStore(validatorSet NodeBLSMapping } // BuildBlock constructs the next block on top of the given parent block, and passes in the provided simplex metadata and blacklist. -func (sm *StateMachine) BuildBlock(ctx context.Context, metadata common.ProtocolMetadata, blacklist *common.Blacklist) (*StateMachineBlock, error) { +func (sm *StateMachine) BuildBlock(ctx context.Context, metadata common.ProtocolMetadata, blacklist common.Blacklist) (*StateMachineBlock, error) { // The zero sequence number is reserved for the genesis block, which should never be built. if metadata.Seq == 0 { return nil, fmt.Errorf("%w: got %d", errInvalidProtocolMetadataSeq, metadata.Seq) @@ -307,26 +307,19 @@ func (sm *StateMachine) BuildBlock(ctx context.Context, metadata common.Protocol ) }() - var simplexBlacklistBytes []byte - if blacklist != nil { - simplexBlacklistBytes = blacklist.Bytes() - } - // In order to know where in the epoch change process we are, // we identify the current state by looking at the parent block's epoch info. currentState := parentBlock.Metadata.SimplexEpochInfo.NextState() - simplexMetadataBytes := metadata.Bytes() - switch currentState { case stateFirstSimplexBlock: - return sm.buildBlockZero(parentBlock, simplexMetadataBytes, simplexBlacklistBytes) + return sm.buildBlockZero(parentBlock, metadata, blacklist) case stateBuildBlockNormalOp: - return sm.buildBlockNormalOp(ctx, &parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) + return sm.buildBlockNormalOp(ctx, &parentBlock, metadata, blacklist, prevBlockSeq) case stateBuildCollectingApprovals: - return sm.buildBlockCollectingApprovals(ctx, &parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) + return sm.buildBlockCollectingApprovals(ctx, &parentBlock, metadata, blacklist, prevBlockSeq) case stateBuildBlockEpochSealed: - return sm.buildBlockEpochSealed(ctx, &parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) + return sm.buildBlockEpochSealed(ctx, &parentBlock, metadata, blacklist, prevBlockSeq) default: return nil, fmt.Errorf("%w: %d", errUnknownState, currentState) } @@ -347,11 +340,7 @@ func (sm *StateMachine) VerifyBlock(ctx context.Context, block *StateMachineBloc return block.InnerBlock.Verify(ctx, block.Metadata.ICMEpochInfo.PChainEpochHeight) } - pmd, err := common.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) - if err != nil { - return fmt.Errorf("failed to parse ProtocolMetadata: %w", err) - } - + pmd := block.Metadata.SimplexProtocolMetadata seq := pmd.Seq if seq == 0 { @@ -427,10 +416,7 @@ func verifyTimestamp(block *StateMachineBlock, prevBlock *StateMachineBlock, now } func (sm *StateMachine) verifyEpochNumber(block *StateMachineBlock) error { - md, err := common.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) - if err != nil { - return fmt.Errorf("failed to parse ProtocolMetadata: %w", err) - } + md := block.Metadata.SimplexProtocolMetadata if md.Epoch != block.Metadata.SimplexEpochInfo.EpochNumber { return fmt.Errorf("%w: got %d, expected %d", errInvalidProtocolMetadataEpoch, md.Epoch, block.Metadata.SimplexEpochInfo.EpochNumber) } @@ -449,7 +435,7 @@ func (sm *StateMachine) verifyEpochNumber(block *StateMachineBlock) error { // │ NPCH = 0 │ │ NPCH = 0 │ │ NPCH = p' (> 0) │ // └─────────────────┘ └─────────────────┘ └─────────────────┘ // → stays NormalOp → CollectingApprovals -func (sm *StateMachine) buildBlockNormalOp(ctx context.Context, parentBlock *StateMachineBlock, simplexMetadata, simplexBlacklist []byte, prevBlockSeq uint64) (*StateMachineBlock, error) { +func (sm *StateMachine) buildBlockNormalOp(ctx context.Context, parentBlock *StateMachineBlock, simplexMetadata common.ProtocolMetadata, simplexBlacklist common.Blacklist, prevBlockSeq uint64) (*StateMachineBlock, error) { // Since in the previous block, we were not transitioning to a new epoch, // the P-chain reference height and epoch of the new block should remain the same. newSimplexEpochInfo := SimplexEpochInfo{ @@ -462,7 +448,7 @@ func (sm *StateMachine) buildBlockNormalOp(ctx context.Context, parentBlock *Sta } // buildBlockOrTransitionEpoch builds a block and decides whether to transition to a new epoch based on the P-chain height and validator set changes. -func (sm *StateMachine) buildBlockOrTransitionEpoch(ctx context.Context, parentBlock *StateMachineBlock, simplexMetadata, simplexBlacklist []byte, newSimplexEpochInfo SimplexEpochInfo) (*StateMachineBlock, error) { +func (sm *StateMachine) buildBlockOrTransitionEpoch(ctx context.Context, parentBlock *StateMachineBlock, simplexMetadata common.ProtocolMetadata, simplexBlacklist common.Blacklist, newSimplexEpochInfo SimplexEpochInfo) (*StateMachineBlock, error) { var isSealingBlockFinalized bool sealingBlockSeq := parentBlock.Metadata.SimplexEpochInfo.EpochNumber _, finalization, err := sm.GetBlock(sealingBlockSeq, [32]byte{}) @@ -759,7 +745,7 @@ func (sm *StateMachine) createBlockBuildingDecider(pChainReferenceHeight uint64) // otherwise it points to the previous epoch's sealing block. // - SBS : 0 except on Telocks of a sealed-but-not-yet-finalized epoch, where // it equals the sequence number of that epoch's sealing block. -func (sm *StateMachine) buildBlockZero(parentBlock StateMachineBlock, simplexMetadata, simplexBlacklist []byte) (*StateMachineBlock, error) { +func (sm *StateMachine) buildBlockZero(parentBlock StateMachineBlock, simplexMetadata common.ProtocolMetadata, simplexBlacklist common.Blacklist) (*StateMachineBlock, error) { pChainHeight := sm.LastNonSimplexBlockPChainHeight var validatorSet NodeBLSMappings @@ -788,10 +774,7 @@ func (sm *StateMachine) buildBlockZero(parentBlock StateMachineBlock, simplexMet timestamp := sm.LastNonSimplexInnerBlock.Timestamp().UnixMilli() simplexEpochInfo := constructSimplexZeroBlockSimplexEpochInfo(pChainHeight, validatorSet, prevVMBlockSeq) - md, err := common.ProtocolMetadataFromBytes(simplexMetadata) - if err != nil { - return nil, fmt.Errorf("failed to parse simplex metadata: %w", err) - } + md := simplexMetadata md.Prev = sm.LastNonSimplexInnerBlock.Digest() md.Seq = sm.LastNonSimplexInnerBlock.Height() @@ -898,7 +881,7 @@ func (sm *StateMachine) verifyBlockZero(block *StateMachineBlock, prevBlock Stat // │ │ │ │ │ sealing block) │ // └──────────────────┘ └────────────────────┘ └────────────────────────────┘ // → stays Collecting → BuildBlockEpochSealed -func (sm *StateMachine) buildBlockCollectingApprovals(ctx context.Context, parentBlock *StateMachineBlock, simplexMetadata, simplexBlacklist []byte, prevBlockSeq uint64) (*StateMachineBlock, error) { +func (sm *StateMachine) buildBlockCollectingApprovals(ctx context.Context, parentBlock *StateMachineBlock, simplexMetadata common.ProtocolMetadata, simplexBlacklist common.Blacklist, prevBlockSeq uint64) (*StateMachineBlock, error) { // We prepare information that is needed to compute the approvals for the new epoch, // such as the validator set for the next epoch, and the approvals from peers. prevBlockNextPChainReferenceHeight := parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight @@ -1208,8 +1191,8 @@ func collectAuxiliaryInfo(block *StateMachineBlock, startSeq uint64, getBlock Bl // so that we can continue making progress and not get stuck waiting for the VM. func (sm *StateMachine) buildBlockImpatiently(ctx context.Context, timestamp time.Time, - simplexMetadata []byte, - simplexBlacklist []byte, + simplexMetadata common.ProtocolMetadata, + simplexBlacklist common.Blacklist, simplexEpochInfo SimplexEpochInfo, pChainHeight uint64, icmEpochInfo ICMEpochInfo, @@ -1235,8 +1218,8 @@ func (sm *StateMachine) buildBlockImpatiently(ctx context.Context, func (sm *StateMachine) createSealingBlock(ctx context.Context, timestamp time.Time, - simplexMetadata []byte, - simplexBlacklist []byte, + simplexMetadata common.ProtocolMetadata, + simplexBlacklist common.Blacklist, simplexEpochInfo SimplexEpochInfo, pChainHeight uint64, icmEpochInfo ICMEpochInfo, @@ -1277,8 +1260,8 @@ func wrapBlock( childBlock avalanchego.VMBlock, newSimplexEpochInfo SimplexEpochInfo, pChainHeight uint64, - simplexMetadata, - simplexBlacklist []byte, + simplexMetadata common.ProtocolMetadata, + simplexBlacklist common.Blacklist, timestamp time.Time, icmEpochInfo ICMEpochInfo, auxiliaryInfo *AuxiliaryInfo) *StateMachineBlock { @@ -1333,7 +1316,7 @@ func (sm *StateMachine) areWeReadyToTransitionEpoch(parentBlock *StateMachineBlo // │ BVD = vset@p' │ │ BVD = nil │ │ BVD = nil │ // └──────────────────┘ └──────────────────┘ └──────────────────────────┘ // → stays EpochSealed → NormalOp (new epoch) -func (sm *StateMachine) buildBlockEpochSealed(ctx context.Context, parentBlock *StateMachineBlock, simplexMetadata, simplexBlacklist []byte, prevBlockSeq uint64) (*StateMachineBlock, error) { +func (sm *StateMachine) buildBlockEpochSealed(ctx context.Context, parentBlock *StateMachineBlock, simplexMetadata common.ProtocolMetadata, simplexBlacklist common.Blacklist, prevBlockSeq uint64) (*StateMachineBlock, error) { // We check if the sealing block has already been finalized. // If not, we build a Telock block. readyToTransitionEpoch, sealingBlockSeq, sealingBlock, err := sm.areWeReadyToTransitionEpoch(parentBlock, prevBlockSeq) diff --git a/msm/msm_test.go b/msm/msm_test.go index cea2cfb9..83ae9d6c 100644 --- a/msm/msm_test.go +++ b/msm/msm_test.go @@ -19,6 +19,10 @@ import ( "github.com/stretchr/testify/require" ) +var ( + emptyBlacklist = common.Blacklist{} +) + func TestMSMBuildAndVerifyBlocksAfterGenesis(t *testing.T) { validMD := common.ProtocolMetadata{ Round: 1, @@ -42,10 +46,9 @@ func TestMSMBuildAndVerifyBlocksAfterGenesis(t *testing.T) { name: "verifying a genesis block", md: validMD, mutateBlock: func(block *StateMachineBlock) { - md, err := common.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) - require.NoError(t, err) + md := block.Metadata.SimplexProtocolMetadata md.Seq = 0 - block.Metadata.SimplexProtocolMetadata = md.Bytes() + block.Metadata.SimplexProtocolMetadata = md }, err: errBuiltGenesisInnerBlock, }, @@ -126,7 +129,7 @@ func TestMSMBuildAndVerifyBlocksAfterGenesis(t *testing.T) { testCase.configure(sm2, testConfig2) } - block, err := sm1.BuildBlock(context.Background(), testCase.md, nil) + block, err := sm1.BuildBlock(context.Background(), testCase.md, emptyBlacklist) require.NoError(t, err) require.NotNil(t, block) @@ -188,7 +191,7 @@ func TestMSMFirstSimplexBlockAfterPreSimplexBlocks(t *testing.T) { Content: []byte{7, 8, 9}, } - block, err := sm1.BuildBlock(context.Background(), md, nil) + block, err := sm1.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.NotNil(t, block) @@ -196,7 +199,7 @@ func TestMSMFirstSimplexBlockAfterPreSimplexBlocks(t *testing.T) { Metadata: StateMachineMetadata{ Timestamp: uint64(preSimplexParent.InnerBlock.Timestamp().UnixMilli()), PChainHeight: 100, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: 100, EpochNumber: 43, @@ -221,7 +224,7 @@ func TestMSMBuildBlockRejectsZeroSeq(t *testing.T) { // Seq 0 is reserved for the genesis block, which should never be built. sm, _ := newStateMachine(t) - block, err := sm.BuildBlock(context.Background(), common.ProtocolMetadata{Seq: 0}, nil) + block, err := sm.BuildBlock(context.Background(), common.ProtocolMetadata{Seq: 0}, emptyBlacklist) require.ErrorIs(t, err, errInvalidProtocolMetadataSeq) require.Nil(t, block) } @@ -249,20 +252,18 @@ func TestMSMNormalOp(t *testing.T) { { name: "trying to build a genesis block", mutateBlock: func(block *StateMachineBlock) { - md, err := common.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) - require.NoError(t, err) + md := block.Metadata.SimplexProtocolMetadata md.Seq = 0 - block.Metadata.SimplexProtocolMetadata = md.Bytes() + block.Metadata.SimplexProtocolMetadata = md }, err: errBuiltGenesisInnerBlock, }, { name: "previous block not found", mutateBlock: func(block *StateMachineBlock) { - md, err := common.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) - require.NoError(t, err) + md := block.Metadata.SimplexProtocolMetadata md.Seq = 999 - block.Metadata.SimplexProtocolMetadata = md.Bytes() + block.Metadata.SimplexProtocolMetadata = md }, err: common.ErrBlockNotFound, }, @@ -347,8 +348,7 @@ func TestMSMNormalOp(t *testing.T) { } lastBlock := chain[len(chain)-1] - md, err := common.ProtocolMetadataFromBytes(lastBlock.Metadata.SimplexProtocolMetadata) - require.NoError(t, err) + md := lastBlock.Metadata.SimplexProtocolMetadata md.Seq++ md.Round++ @@ -364,7 +364,7 @@ func TestMSMNormalOp(t *testing.T) { sm2.GetTime = fixedTime content := make([]byte, 10) - _, err = rand.Read(content) + _, err := rand.Read(content) require.NoError(t, err) testConfig1.blockBuilder.Block = &testutil.InnerBlock{ @@ -378,7 +378,7 @@ func TestMSMNormalOp(t *testing.T) { testCase.setup(sm2, testConfig2) } - block1, err := sm1.BuildBlock(context.Background(), *md, &blacklist) + block1, err := sm1.BuildBlock(context.Background(), md, blacklist) require.NoError(t, err) require.NotNil(t, block1) @@ -400,10 +400,10 @@ func TestMSMNormalOp(t *testing.T) { Content: content, }, Metadata: StateMachineMetadata{ - SimplexBlacklist: blacklist.Bytes(), + SimplexBlacklist: blacklist, Timestamp: uint64(blockTime.UnixMilli()), PChainHeight: testCase.expectedPChainHeight, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: 100, EpochNumber: 1, @@ -615,13 +615,13 @@ func TestMSMFullEpochLifecycle(t *testing.T) { Prev: testCase.firstBlockBeforeSimplex.Digest(), } - block1, err := sm.BuildBlock(context.Background(), md, nil) + block1, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ Metadata: StateMachineMetadata{ Timestamp: uint64(startTime.UnixMilli()), PChainHeight: pChainHeight1, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainHeight1, EpochNumber: testCase.epochNum, @@ -649,14 +649,14 @@ func TestMSMFullEpochLifecycle(t *testing.T) { currentTime = startTime.Add(2 * time.Millisecond) tc.blockBuilder.Block = nextBlock(2) md = common.ProtocolMetadata{Seq: baseSeq + 2, Round: 1, Epoch: testCase.epochNum, Prev: block1.Digest()} - block2, err := sm.BuildBlock(context.Background(), md, nil) + block2, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(2), Metadata: StateMachineMetadata{ Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight1, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainHeight1, EpochNumber: testCase.epochNum, @@ -679,14 +679,14 @@ func TestMSMFullEpochLifecycle(t *testing.T) { currentTime = startTime.Add(time.Second + 3*time.Millisecond) tc.blockBuilder.Block = nextBlock(3) md = common.ProtocolMetadata{Seq: baseSeq + 3, Round: 2, Epoch: testCase.epochNum, Prev: block2.Digest()} - block3, err := sm.BuildBlock(context.Background(), md, nil) + block3, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(3), Metadata: StateMachineMetadata{ Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainHeight1, EpochNumber: testCase.epochNum, @@ -718,14 +718,14 @@ func TestMSMFullEpochLifecycle(t *testing.T) { currentTime = startTime.Add(time.Second + 4*time.Millisecond) tc.blockBuilder.Block = nextBlock(4) md = common.ProtocolMetadata{Seq: baseSeq + 4, Round: 3, Epoch: testCase.epochNum, Prev: block3.Digest()} - block4, err := sm.BuildBlock(context.Background(), md, nil) + block4, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(4), Metadata: StateMachineMetadata{ Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainHeight1, EpochNumber: testCase.epochNum, @@ -760,14 +760,14 @@ func TestMSMFullEpochLifecycle(t *testing.T) { currentTime = startTime.Add(time.Second + 5*time.Millisecond) tc.blockBuilder.Block = nextBlock(5) md = common.ProtocolMetadata{Seq: baseSeq + 5, Round: 4, Epoch: testCase.epochNum, Prev: block4.Digest()} - block5, err := sm.BuildBlock(context.Background(), md, nil) + block5, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(5), Metadata: StateMachineMetadata{ Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainHeight1, EpochNumber: testCase.epochNum, @@ -802,14 +802,14 @@ func TestMSMFullEpochLifecycle(t *testing.T) { currentTime = startTime.Add(time.Second + 6*time.Millisecond) tc.blockBuilder.Block = nextBlock(6) md = common.ProtocolMetadata{Seq: baseSeq + 6, Round: 5, Epoch: testCase.epochNum, Prev: block5.Digest()} - block6, err := sm.BuildBlock(context.Background(), md, nil) + block6, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(6), Metadata: StateMachineMetadata{ Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainHeight1, EpochNumber: testCase.epochNum, @@ -874,7 +874,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { 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) + telock, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ @@ -882,7 +882,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { Metadata: StateMachineMetadata{ Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainHeight1, EpochNumber: testCase.epochNum, @@ -906,14 +906,14 @@ func TestMSMFullEpochLifecycle(t *testing.T) { md.Epoch = sealingSeq currentTime = startTime.Add(time.Second + 7*time.Millisecond) - block7, err := sm.BuildBlock(context.Background(), md, nil) + block7, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.Equal(t, &StateMachineBlock{ InnerBlock: nextBlock(7), Metadata: StateMachineMetadata{ Timestamp: uint64(currentTime.UnixMilli()), PChainHeight: pChainHeight2, - SimplexProtocolMetadata: md.Bytes(), + SimplexProtocolMetadata: md, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainHeight2, EpochNumber: sealingSeq, @@ -1496,9 +1496,9 @@ func TestBuildBlockCollectingApprovalsDedupsOwnApprovalAcrossRounds(t *testing.T InnerBlock: &testutil.InnerBlock{TS: time.Now(), BlockHeight: 1, Content: []byte{0xAA}}, Metadata: StateMachineMetadata{ PChainHeight: 200, - SimplexProtocolMetadata: (&common.ProtocolMetadata{ + SimplexProtocolMetadata: common.ProtocolMetadata{ Seq: parentSeq, Round: 5, Epoch: 1, - }).Bytes(), + }, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: 100, EpochNumber: 1, @@ -1512,7 +1512,7 @@ func TestBuildBlockCollectingApprovalsDedupsOwnApprovalAcrossRounds(t *testing.T // ----- Round 1: first collecting-approvals block ----- tc.blockBuilder.Block = &testutil.InnerBlock{TS: time.Now(), BlockHeight: 2, Content: []byte{0x01}} md1 := common.ProtocolMetadata{Seq: parentSeq + 1, Round: 6, Epoch: 1, Prev: parent.Digest()} - block1, err := sm.BuildBlock(context.Background(), md1, nil) + block1, err := sm.BuildBlock(context.Background(), md1, emptyBlacklist) require.NoError(t, err) require.NotNil(t, block1.Metadata.SimplexEpochInfo.NextEpochApprovals, "first block in collecting-approvals state must carry NextEpochApprovals") @@ -1528,7 +1528,7 @@ func TestBuildBlockCollectingApprovalsDedupsOwnApprovalAcrossRounds(t *testing.T // ----- Round 2: another collecting-approvals block, still no peer approvals ----- tc.blockBuilder.Block = &testutil.InnerBlock{TS: time.Now(), BlockHeight: 3, Content: []byte{0x02}} md2 := common.ProtocolMetadata{Seq: md1.Seq + 1, Round: 7, Epoch: 1, Prev: block1.Digest()} - block2, err := sm.BuildBlock(context.Background(), md2, nil) + block2, err := sm.BuildBlock(context.Background(), md2, emptyBlacklist) require.NoError(t, err) require.NotNil(t, block2.Metadata.SimplexEpochInfo.NextEpochApprovals) @@ -1568,9 +1568,9 @@ func TestVerifyCollectingApprovalsNotReady(t *testing.T) { InnerBlock: &testutil.InnerBlock{TS: time.Now(), BlockHeight: 1, Content: []byte{0xAA}}, Metadata: StateMachineMetadata{ PChainHeight: nextPChainRefHeight, - SimplexProtocolMetadata: (&common.ProtocolMetadata{ + SimplexProtocolMetadata: common.ProtocolMetadata{ Seq: parentSeq, Round: 5, Epoch: 1, - }).Bytes(), + }, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainRefHeight, EpochNumber: 1, @@ -1587,7 +1587,7 @@ func TestVerifyCollectingApprovalsNotReady(t *testing.T) { build := func(t *testing.T, sm *StateMachine, tc *testConfig, parent StateMachineBlock) *StateMachineBlock { tc.blockBuilder.Block = &testutil.InnerBlock{TS: time.Now(), BlockHeight: 2, Content: []byte{0x01}} md := common.ProtocolMetadata{Seq: parentSeq + 1, Round: 6, Epoch: 1, Prev: parent.Digest()} - block, err := sm.BuildBlock(context.Background(), md, nil) + block, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) return block } @@ -1673,9 +1673,9 @@ func TestCollectingApprovalsAuxInfoGating(t *testing.T) { InnerBlock: &testutil.InnerBlock{TS: time.Now(), BlockHeight: 1, Content: []byte{0xAA}}, Metadata: StateMachineMetadata{ PChainHeight: nextPChainRefHeight, - SimplexProtocolMetadata: (&common.ProtocolMetadata{ + SimplexProtocolMetadata: common.ProtocolMetadata{ Seq: parentSeq, Round: 5, Epoch: 1, - }).Bytes(), + }, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainRefHeight, EpochNumber: 1, @@ -1691,7 +1691,7 @@ func TestCollectingApprovalsAuxInfoGating(t *testing.T) { build := func(seq uint64, prev StateMachineBlock) *StateMachineBlock { tc.blockBuilder.Block = &testutil.InnerBlock{TS: time.Now(), BlockHeight: seq, Content: []byte{byte(seq)}} md := common.ProtocolMetadata{Seq: seq, Round: seq, Epoch: 1, Prev: prev.Digest()} - block, err := sm.BuildBlock(context.Background(), md, nil) + block, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.NoError(t, sm.VerifyBlock(context.Background(), block)) tc.blockStore[seq] = &outerBlock{block: *block} @@ -1797,9 +1797,9 @@ func TestCollectingApprovalsAuxInfoVersionIDIsBackwardCompatible(t *testing.T) { InnerBlock: &testutil.InnerBlock{TS: time.Now(), BlockHeight: 1, Content: []byte{0xAA}}, Metadata: StateMachineMetadata{ PChainHeight: nextPChainRefHeight, - SimplexProtocolMetadata: (&common.ProtocolMetadata{ + SimplexProtocolMetadata: common.ProtocolMetadata{ Seq: parentSeq, Round: 5, Epoch: 1, - }).Bytes(), + }, SimplexEpochInfo: SimplexEpochInfo{ PChainReferenceHeight: pChainRefHeight, EpochNumber: 1, @@ -1820,7 +1820,7 @@ func TestCollectingApprovalsAuxInfoVersionIDIsBackwardCompatible(t *testing.T) { buildAndVerify := func(seq uint64, prev StateMachineBlock) *StateMachineBlock { tc.blockBuilder.Block = &testutil.InnerBlock{TS: time.Now(), BlockHeight: seq, Content: []byte{byte(seq)}} md := common.ProtocolMetadata{Seq: seq, Round: seq, Epoch: 1, Prev: prev.Digest()} - block, err := sm.BuildBlock(context.Background(), md, nil) + block, err := sm.BuildBlock(context.Background(), md, emptyBlacklist) require.NoError(t, err) require.NoError(t, sm.VerifyBlock(context.Background(), block)) tc.blockStore[seq] = &outerBlock{block: *block} diff --git a/msm/util_test.go b/msm/util_test.go index 86b45f5c..c588b97e 100644 --- a/msm/util_test.go +++ b/msm/util_test.go @@ -285,12 +285,12 @@ func makeNormalSimplexBlock(t *testing.T, index int, blocks []StateMachineBlock, }, Metadata: StateMachineMetadata{ PChainHeight: 100, - SimplexProtocolMetadata: (&common.ProtocolMetadata{ + SimplexProtocolMetadata: common.ProtocolMetadata{ Round: round, Seq: seq, Epoch: 1, Prev: prev, - }).Bytes(), + }, SimplexEpochInfo: SimplexEpochInfo{ PrevSealingBlockHash: [32]byte{}, PChainReferenceHeight: 100, diff --git a/simplex/epoch.go b/simplex/epoch.go index ab58f7f4..5cfc4ebd 100644 --- a/simplex/epoch.go +++ b/simplex/epoch.go @@ -2089,7 +2089,11 @@ func (e *Epoch) createBlockVerificationTask(block common.Block, from common.Node return md.Digest } - blockRecord := common.BlockRecord(md, blockBytes) + blockRecord, err := common.BlockRecord(md, blockBytes) + if err != nil { + e.Logger.Error("Failed to create block record for WAL", zap.Error(err)) + return md.Digest + } if err := e.WAL.Append(blockRecord); err != nil { e.haltedError = err e.Logger.Error("Failed to append block record to WAL", zap.Error(err)) @@ -2359,8 +2363,8 @@ func (e *Epoch) verifyProposalMetadataAndBlacklist(block common.Block) bool { if !equals { e.Logger.Debug("Received block with an incorrect header", - zap.Stringer("expected", expectedBH), - zap.Stringer("received", bh)) + zap.Stringer("expected", &expectedBH), + zap.Stringer("received", &bh)) } return equals @@ -2556,7 +2560,11 @@ func (e *Epoch) proposeBlock(block common.VerifiedBlock) error { return errors.New("failed to store block proposed by me") } - blockRecord := common.BlockRecord(block.BlockHeader(), rawBlock) + blockRecord, err := common.BlockRecord(block.BlockHeader(), rawBlock) + if err != nil { + e.Logger.Error("Failed creating block record for WAL", zap.Error(err)) + return err + } if err := e.WAL.Append(blockRecord); err != nil { e.Logger.Error("Failed appending block to WAL", zap.Error(err)) return err diff --git a/simplex/epoch_failover_test.go b/simplex/epoch_failover_test.go index 480002f1..9d6af53f 100644 --- a/simplex/epoch_failover_test.go +++ b/simplex/epoch_failover_test.go @@ -200,7 +200,7 @@ func TestEpochLeaderFailoverReceivesEmptyVotesEarly(t *testing.T) { } block2 := storage.WaitForBlockCommit(3) - require.Equal(t, block, block2) + require.Equal(t, block.BlockHeader().Digest, block2.BlockHeader().Digest) require.Equal(t, uint64(4), storage.NumBlocks()) require.Equal(t, uint64(4), block2.BlockHeader().Round) require.Equal(t, uint64(3), block2.BlockHeader().Seq) @@ -986,11 +986,13 @@ func TestEpochBlacklist(t *testing.T) { require.Equal(t, nodes[0], LeaderForRound(nodes, block.BlockHeader().Round)) // our node will propose the empty block // Make sure our node properly constructed the blacklist - require.Equal(t, Blacklist{ + actual := block.Blacklist() + expected := Blacklist{ NodeCount: 4, SuspectedNodes: SuspectedNodes{{NodeIndex: 3, SuspectingCount: 1, OrbitSuspected: 1}}, Updates: []BlacklistUpdate{{Type: BlacklistOpType_NodeSuspected, NodeIndex: 3}}, - }, block.Blacklist()) + } + require.True(t, expected.Equals(&actual)) // Compute next blacklist blacklist := NewBlacklist(4) @@ -1004,7 +1006,8 @@ func TestEpochBlacklist(t *testing.T) { block, _ = notarizeAndFinalizeRound(t, e, bb) // It should have the blacklist we have previously constructed - require.Equal(t, blacklist, block.Blacklist()) + actual = block.Blacklist() + require.True(t, blacklist.Equals(&actual)) // Do it again but preserve the blacklist until we get to the round of the last node. // This time, there are no updates to the blacklist, it just carries over. @@ -1082,7 +1085,7 @@ func TestEpochBlacklist(t *testing.T) { block = bb.GetBuiltBlock() - require.Equal(t, Blacklist{ + expected = Blacklist{ NodeCount: 4, SuspectedNodes: SuspectedNodes{ { @@ -1094,7 +1097,9 @@ func TestEpochBlacklist(t *testing.T) { }, }, Updates: []BlacklistUpdate{{Type: BlacklistOpType_NodeRedeemed, NodeIndex: 3}}, - }, block.Blacklist(), "Node should vote to redeem the previously failed node") + } + actual = block.Blacklist() + require.True(t, expected.Equals(&actual), "Node should vote to redeem the previously failed node") blacklist = Blacklist{ NodeCount: 4, diff --git a/simplex/epoch_multinode_test.go b/simplex/epoch_multinode_test.go index 74a88ea8..3740f935 100644 --- a/simplex/epoch_multinode_test.go +++ b/simplex/epoch_multinode_test.go @@ -188,11 +188,12 @@ func TestSimplexMultiNodeBlacklist(t *testing.T) { for _, n := range net.Instances[:3] { block := n.Storage.WaitForBlockCommit(uint64(3)) blacklist := block.Blacklist() - require.Equal(t, common.Blacklist{ + expectedBlacklist := common.Blacklist{ NodeCount: 4, SuspectedNodes: common.SuspectedNodes{{NodeIndex: 3, SuspectingCount: 1, OrbitSuspected: 1}}, Updates: []common.BlacklistUpdate{{NodeIndex: 3, Type: common.BlacklistOpType_NodeSuspected}}, - }, blacklist) + } + require.True(t, expectedBlacklist.Equals(&blacklist)) } // Reconnect the fourth node but don't let it un-blacklist itself prematurely. @@ -204,21 +205,24 @@ func TestSimplexMultiNodeBlacklist(t *testing.T) { for _, n := range net.Instances[:3] { block := n.Storage.WaitForBlockCommit(uint64(4)) blacklist := block.Blacklist() - require.Equal(t, common.Blacklist{ + expectedBlacklist := common.Blacklist{ NodeCount: 4, SuspectedNodes: common.SuspectedNodes{{NodeIndex: 3, SuspectingCount: 2, OrbitSuspected: 1}}, Updates: []common.BlacklistUpdate{{NodeIndex: 3, Type: common.BlacklistOpType_NodeSuspected}}, - }, blacklist) + } + require.True(t, expectedBlacklist.Equals(&blacklist)) } // Wait for the node to replicate the missing blocks. net.Instances[3].TriggerNewBlock() block := net.Instances[3].Storage.WaitForBlockCommit(4) - require.Equal(t, common.Blacklist{ + blacklist := block.Blacklist() + expectedBlacklist := common.Blacklist{ NodeCount: 4, SuspectedNodes: common.SuspectedNodes{{NodeIndex: 3, SuspectingCount: 2, OrbitSuspected: 1}}, Updates: []common.BlacklistUpdate{{NodeIndex: 3, Type: common.BlacklistOpType_NodeSuspected}}, - }, block.Blacklist()) + } + require.True(t, expectedBlacklist.Equals(&blacklist)) // Make another block. net.Instances[2].TriggerNewBlock() diff --git a/simplex/epoch_test.go b/simplex/epoch_test.go index 1e000d03..97baebdc 100644 --- a/simplex/epoch_test.go +++ b/simplex/epoch_test.go @@ -1688,7 +1688,7 @@ func TestBlockDeserializer(t *testing.T) { require.NoError(t, err) tb2, err := blockDeserializer.DeserializeBlock(ctx, tbBytes) require.NoError(t, err) - require.Equal(t, tb, tb2) + require.Equal(t, tb.BlockHeader().Digest, tb2.BlockHeader().Digest) } // advanceRound progresses [e] to a new round. If [notarize] is set, the round will progress due to a notarization. diff --git a/simplex/recovery_test.go b/simplex/recovery_test.go index a1b29a81..1ff52025 100644 --- a/simplex/recovery_test.go +++ b/simplex/recovery_test.go @@ -34,7 +34,8 @@ func TestRecoverFromWALProposed(t *testing.T) { require.True(t, ok) fBytes, err := firstBlock.Bytes() require.NoError(t, err) - record := BlockRecord(firstBlock.BlockHeader(), fBytes) + record, err := BlockRecord(firstBlock.BlockHeader(), fBytes) + require.NoError(t, err) // write block record to wal require.NoError(t, wal.Append(record)) @@ -87,7 +88,7 @@ func TestRecoverFromWALProposed(t *testing.T) { block2 := storage.WaitForBlockCommit(i) - require.Equal(t, block, block2) + require.Equal(t, block.BlockHeader().Digest, block2.BlockHeader().Digest) } require.Equal(t, rounds, e.Storage.NumBlocks()) @@ -112,7 +113,8 @@ func TestRecoverFromNotarization(t *testing.T) { require.True(t, ok) bBytes, err := block.Bytes() require.NoError(t, err) - blockRecord := BlockRecord(block.BlockHeader(), bBytes) + blockRecord, err := BlockRecord(block.BlockHeader(), bBytes) + require.NoError(t, err) // write block blockRecord to wal require.NoError(t, wal.Append(blockRecord)) @@ -171,7 +173,8 @@ func TestRecoverFromWalWithStorage(t *testing.T) { require.True(t, ok) bBytes, err := block.Bytes() require.NoError(t, err) - record := BlockRecord(block.BlockHeader(), bBytes) + record, err := BlockRecord(block.BlockHeader(), bBytes) + require.NoError(t, err) // write block record to wal require.NoError(t, wal.Append(record)) @@ -239,7 +242,7 @@ func TestWalCreatedProperly(t *testing.T) { blockFromWal, err := BlockFromRecord(ctx, conf.BlockDeserializer, records[0]) require.NoError(t, err) block := bb.GetBuiltBlock() - require.Equal(t, blockFromWal, block) + require.Equal(t, blockFromWal.BlockHeader().Digest, block.BlockHeader().Digest) // start at one since our node has already voted for i := 1; i < quorum; i++ { @@ -319,7 +322,7 @@ func TestWalWritesBlockRecord(t *testing.T) { require.Len(t, records, 1) blockFromWal, err := BlockFromRecord(ctx, conf.BlockDeserializer, records[0]) require.NoError(t, err) - require.Equal(t, block, blockFromWal) + require.Equal(t, block.BlockHeader().Digest, blockFromWal.BlockHeader().Digest) } func TestWalWritesFinalization(t *testing.T) { @@ -346,7 +349,7 @@ func TestWalWritesFinalization(t *testing.T) { blockFromWal, err := BlockFromRecord(ctx, conf.BlockDeserializer, records[0]) require.NoError(t, err) - require.Equal(t, firstBlock, blockFromWal) + require.Equal(t, firstBlock.BlockHeader().Digest, blockFromWal.BlockHeader().Digest) expectedNotarizationRecord, err := testutil.NewNotarizationRecord(conf.Logger, sigAggregrator, firstBlock, nodes[0:quorum]) require.NoError(t, err) require.Equal(t, expectedNotarizationRecord, records[1]) @@ -385,7 +388,7 @@ func TestWalWritesFinalization(t *testing.T) { require.Len(t, records, 4) blockFromWal, err = BlockFromRecord(ctx, conf.BlockDeserializer, records[2]) require.NoError(t, err) - require.Equal(t, secondBlock, blockFromWal) + require.Equal(t, secondBlock.BlockHeader().Digest, blockFromWal.BlockHeader().Digest) expectedNotarizationRecord, err = testutil.NewNotarizationRecord(conf.Logger, sigAggregrator, secondBlock, nodes[0:quorum]) require.NoError(t, err) require.Equal(t, expectedNotarizationRecord, records[3]) @@ -430,7 +433,8 @@ func TestRecoverFromMultipleNotarizations(t *testing.T) { require.True(t, ok) fBytes, err := firstBlock.Bytes() require.NoError(t, err) - record := BlockRecord(firstBlock.BlockHeader(), fBytes) + record, err := BlockRecord(firstBlock.BlockHeader(), fBytes) + require.NoError(t, err) wal.Append(record) firstNotarizationRecord, err := testutil.NewNotarizationRecord(conf.Logger, sigAggr, firstBlock, nodes[0:quorum]) @@ -443,7 +447,8 @@ func TestRecoverFromMultipleNotarizations(t *testing.T) { require.True(t, ok) sBytes, err := secondBlock.Bytes() require.NoError(t, err) - record = BlockRecord(secondBlock.BlockHeader(), sBytes) + record, err = BlockRecord(secondBlock.BlockHeader(), sBytes) + require.NoError(t, err) wal.Append(record) // Add notarization for second block @@ -480,8 +485,10 @@ func TestRecoverFromMultipleNotarizations(t *testing.T) { storageBytes, err = secondBlockRetrieved.Bytes() require.NoError(t, err) require.Equal(t, sBytes, storageBytes) - require.Equal(t, finalization1, finalizationRetrieved1) - require.Equal(t, finalization2, finalizationRetrieved2) + require.True(t, finalization1.Finalization.BlockHeader.Equals(&finalizationRetrieved1.Finalization.BlockHeader)) + require.Equal(t, finalization1.QC, finalizationRetrieved1.QC) + require.True(t, finalization2.Finalization.BlockHeader.Equals(&finalizationRetrieved2.Finalization.BlockHeader)) + require.Equal(t, finalization2.QC, finalizationRetrieved2.QC) } // TestRecoveryBlocksIndexed tests that the epoch properly skips @@ -498,7 +505,8 @@ func TestRecoveryBlocksIndexed(t *testing.T) { fBytes, err := firstBlock.Bytes() require.NoError(t, err) require.True(t, ok) - record := BlockRecord(firstBlock.BlockHeader(), fBytes) + record, err := BlockRecord(firstBlock.BlockHeader(), fBytes) + require.NoError(t, err) wal.Append(record) sigAggr := conf.SignatureAggregatorCreator(conf.Comm.Validators()) @@ -515,7 +523,8 @@ func TestRecoveryBlocksIndexed(t *testing.T) { require.True(t, ok) sBytes, err := secondBlock.Bytes() require.NoError(t, err) - record = BlockRecord(secondBlock.BlockHeader(), sBytes) + record, err = BlockRecord(secondBlock.BlockHeader(), sBytes) + require.NoError(t, err) wal.Append(record) protocolMetadata.Round = 2 @@ -524,7 +533,8 @@ func TestRecoveryBlocksIndexed(t *testing.T) { require.True(t, ok) tBytes, err := thirdBlock.Bytes() require.NoError(t, err) - record = BlockRecord(thirdBlock.BlockHeader(), tBytes) + record, err = BlockRecord(thirdBlock.BlockHeader(), tBytes) + require.NoError(t, err) wal.Append(record) finalization1, _ := testutil.NewFinalizationRecord(t, sigAggr, firstBlock, nodes[0:quorum]) @@ -621,7 +631,8 @@ func TestRecoveryReVerifiesBlocks(t *testing.T) { require.True(t, ok) fBytes, err := firstBlock.Bytes() require.NoError(t, err) - record := BlockRecord(firstBlock.BlockHeader(), fBytes) + record, err := BlockRecord(firstBlock.BlockHeader(), fBytes) + require.NoError(t, err) wal.Append(record) deserializer.DelayedVerification <- struct{}{} @@ -652,7 +663,8 @@ func TestWalRecoveryTriggersEmptyVoteTimeout(t *testing.T) { require.True(t, ok) bBytes, err := block.Bytes() require.NoError(t, err) - blockRecord := BlockRecord(block.BlockHeader(), bBytes) + blockRecord, err := BlockRecord(block.BlockHeader(), bBytes) + require.NoError(t, err) // write block blockRecord to wal require.NoError(t, wal.Append(blockRecord)) @@ -717,7 +729,8 @@ func TestWalRecoveryMonitorsProgress(t *testing.T) { require.True(t, ok) bBytes, err := block.Bytes() require.NoError(t, err) - blockRecord := BlockRecord(block.BlockHeader(), bBytes) + blockRecord, err := BlockRecord(block.BlockHeader(), bBytes) + require.NoError(t, err) // write block blockRecord to wal require.NoError(t, wal.Append(blockRecord)) @@ -763,7 +776,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes, err := block.Bytes() require.NoError(t, err) - blockRecord := BlockRecord(block.BlockHeader(), bBytes) + blockRecord, err := BlockRecord(block.BlockHeader(), bBytes) + require.NoError(t, err) notarizationRecord, err := testutil.NewNotarizationRecord(conf.Logger, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block, nodes[0:quorum]) require.NoError(t, err) @@ -781,7 +795,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes1, err := block1.Bytes() require.NoError(t, err) - blockRecord1 := BlockRecord(block1.BlockHeader(), bBytes1) + blockRecord1, err := BlockRecord(block1.BlockHeader(), bBytes1) + require.NoError(t, err) _, finalizationRecord1 := testutil.NewFinalizationRecord(t, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block1, nodes[0:quorum]) @@ -803,7 +818,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes0, err := block0.Bytes() require.NoError(t, err) - blockRecord0 := BlockRecord(block0.BlockHeader(), bBytes0) + blockRecord0, err := BlockRecord(block0.BlockHeader(), bBytes0) + require.NoError(t, err) notarizationRecord0, err := testutil.NewNotarizationRecord(conf.Logger, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block0, nodes[0:quorum]) require.NoError(t, err) @@ -811,7 +827,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes1, err := block1.Bytes() require.NoError(t, err) - blockRecord1 := BlockRecord(block1.BlockHeader(), bBytes1) + blockRecord1, err := BlockRecord(block1.BlockHeader(), bBytes1) + require.NoError(t, err) notarizationRecord1, err := testutil.NewNotarizationRecord(conf.Logger, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block1, nodes[0:quorum]) require.NoError(t, err) @@ -819,7 +836,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes2, err := block2.Bytes() require.NoError(t, err) - blockRecord2 := BlockRecord(block2.BlockHeader(), bBytes2) + blockRecord2, err := BlockRecord(block2.BlockHeader(), bBytes2) + require.NoError(t, err) notarizationRecord2, err := testutil.NewNotarizationRecord(conf.Logger, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block2, nodes[0:quorum]) require.NoError(t, err) @@ -846,7 +864,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes3, err := block3.Bytes() require.NoError(t, err) - blockRecord3 := BlockRecord(block3.BlockHeader(), bBytes3) + blockRecord3, err := BlockRecord(block3.BlockHeader(), bBytes3) + require.NoError(t, err) _, finalizationRecord3 := testutil.NewFinalizationRecord(t, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block3, nodes[0:quorum]) // Create empty notarization for round 2 @@ -859,7 +878,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes1, err := block1.Bytes() require.NoError(t, err) - blockRecord1 := BlockRecord(block1.BlockHeader(), bBytes1) + blockRecord1, err := BlockRecord(block1.BlockHeader(), bBytes1) + require.NoError(t, err) notarizationRecord1, err := testutil.NewNotarizationRecord(conf.Logger, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block1, nodes[0:quorum]) require.NoError(t, err) @@ -881,7 +901,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes0, err := block0.Bytes() require.NoError(t, err) - blockRecord0 := BlockRecord(block0.BlockHeader(), bBytes0) + blockRecord0, err := BlockRecord(block0.BlockHeader(), bBytes0) + require.NoError(t, err) notarizationRecord0, err := testutil.NewNotarizationRecord(conf.Logger, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block0, nodes[0:quorum]) require.NoError(t, err) @@ -890,7 +911,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes10, err := block10.Bytes() require.NoError(t, err) - blockRecord10 := BlockRecord(block10.BlockHeader(), bBytes10) + blockRecord10, err := BlockRecord(block10.BlockHeader(), bBytes10) + require.NoError(t, err) _, finalizationRecord10 := testutil.NewFinalizationRecord(t, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block10, nodes[0:quorum]) // Create empty notarization for round 5 @@ -915,7 +937,8 @@ func TestWalRecoverySetsRoundCorrectly(t *testing.T) { require.True(t, ok) bBytes2, err := block2.Bytes() require.NoError(t, err) - blockRecord2 := BlockRecord(block2.BlockHeader(), bBytes2) + blockRecord2, err := BlockRecord(block2.BlockHeader(), bBytes2) + require.NoError(t, err) notarizationRecord2, err := testutil.NewNotarizationRecord(conf.Logger, conf.SignatureAggregatorCreator(conf.Comm.Validators()), block2, nodes[0:quorum]) require.NoError(t, err) diff --git a/simplex/replication_request_test.go b/simplex/replication_request_test.go index b5a66303..e7a569c7 100644 --- a/simplex/replication_request_test.go +++ b/simplex/replication_request_test.go @@ -439,7 +439,9 @@ func TestReplicationRequestSeqsAndRoundsTruncated(t *testing.T) { for _, data := range blocks[numIndexed:] { blockBytes, err := data.VerifiedBlock.Bytes() require.NoError(t, err) - require.NoError(t, wal.Append(common.BlockRecord(data.VerifiedBlock.BlockHeader(), blockBytes))) + br, err := common.BlockRecord(data.VerifiedBlock.BlockHeader(), blockBytes) + require.NoError(t, err) + require.NoError(t, wal.Append(br)) notarization, err := testutil.NewNotarization(conf.Logger, &testutil.TestSignatureAggregator{N: len(nodes)}, data.VerifiedBlock, nodes[:quorom]) require.NoError(t, err) @@ -489,6 +491,35 @@ func TestReplicationRequestSeqsAndRoundsTruncated(t *testing.T) { msg := <-comm.in resp := msg.VerifiedReplicationResponse - require.Equal(t, expected, resp.Data) + requireEqualQuorumRounds(t, expected, resp.Data) + +} + +// requireEqualQuorumRounds compares quorum rounds semantically, avoiding +// reflect.DeepEqual on canoto-encoded values (whose unexported size cache and +// nil-vs-empty slices differ between freshly-built and round-tripped values). +func requireEqualQuorumRounds(t *testing.T, expected, actual []common.VerifiedQuorumRound) { + require.Equal(t, len(expected), len(actual)) + for i := range expected { + exp, act := expected[i], actual[i] + require.Equal(t, exp.VerifiedBlock.BlockHeader().Digest, act.VerifiedBlock.BlockHeader().Digest) + + if exp.Finalization != nil { + require.NotNil(t, act.Finalization) + require.True(t, exp.Finalization.Finalization.BlockHeader.Equals(&act.Finalization.Finalization.BlockHeader)) + require.Equal(t, exp.Finalization.QC, act.Finalization.QC) + } else { + require.Nil(t, act.Finalization) + } + + if exp.Notarization != nil { + require.NotNil(t, act.Notarization) + require.True(t, exp.Notarization.Vote.BlockHeader.Equals(&act.Notarization.Vote.BlockHeader)) + require.Equal(t, exp.Notarization.QC, act.Notarization.QC) + } else { + require.Nil(t, act.Notarization) + } + require.Equal(t, exp.EmptyNotarization, act.EmptyNotarization) + } } diff --git a/simplex/replication_test.go b/simplex/replication_test.go index 4fbdcee0..593fe614 100644 --- a/simplex/replication_test.go +++ b/simplex/replication_test.go @@ -373,7 +373,8 @@ func TestReplicationStartsBeforeCurrentRound(t *testing.T) { firstBlock := storageData[0].VerifiedBlock fBytes, err := firstBlock.Bytes() require.NoError(t, err) - record := common.BlockRecord(firstBlock.BlockHeader(), fBytes) + record, err := common.BlockRecord(firstBlock.BlockHeader(), fBytes) + require.NoError(t, err) laggingNode.WAL.Append(record) sigAggr := laggingNode.E.SignatureAggregatorCreator(laggingNode.E.Comm.Validators()) @@ -384,7 +385,8 @@ func TestReplicationStartsBeforeCurrentRound(t *testing.T) { secondBlock := storageData[1].VerifiedBlock sBytes, err := secondBlock.Bytes() require.NoError(t, err) - record = common.BlockRecord(secondBlock.BlockHeader(), sBytes) + record, err = common.BlockRecord(secondBlock.BlockHeader(), sBytes) + require.NoError(t, err) laggingNode.WAL.Append(record) secondNotarizationRecord, err := NewNotarizationRecord(laggingNode.E.Logger, sigAggr, secondBlock, nodes[0:quorum])