Block codec → native ZAP (struct-is-wire): builds + round-trips green

Blocks are now the zap buffer: kind + parentID@1 + height@33 + time@41 +
tx-length-list@49 + tx-blob@57 (+ proposal-tx@65). commonZapBlock embedded base
mirrors spendingTx. Parse = zap.Parse + kind dispatch (no codec, no version).
Deleted block/codec.go + block/v0 + lift_v0. Round-trip green for abort/commit/
proposal/standard incl real signed txs. go build + go test ./block/ = green.

FINDING: block.GenesisCodec was double-duty — also serialized NON-block STATE
values (feeState, owners, L1Validator, metadata, chains). That's a THIRD codec
surface (state DB serialization) still to migrate for the full kill.
This commit is contained in:
zeekay
2026-07-10 12:43:20 -07:00
parent 20d36b36aa
commit b6143b5d53
15 changed files with 483 additions and 1280 deletions
+14 -21
View File
@@ -1,45 +1,38 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"context"
"time"
"github.com/luxfi/runtime"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
)
var _ Block = (*AbortBlock)(nil)
// AbortBlock is the canonical P-Chain abort outcome of a ProposalBlock.
// AbortBlock is the canonical P-Chain abort outcome of a ProposalBlock. It
// carries a timestamp and no txs; the struct is the zap buffer.
type AbortBlock struct {
Time uint64 `serialize:"true" json:"time"`
CommonBlock `serialize:"true"`
commonZapBlock
}
func (b *AbortBlock) Timestamp() time.Time { return time.Unix(int64(b.Time), 0) }
func (b *AbortBlock) initialize(bytes []byte) error {
b.CommonBlock.initialize(bytes)
return nil
}
func (*AbortBlock) InitRuntime(*runtime.Runtime) {}
func (*AbortBlock) Txs() []*txs.Tx { return nil }
func (b *AbortBlock) Visit(v Visitor) error { return v.AbortBlock(b) }
func (*AbortBlock) Initialize(context.Context) error { return nil }
func (*AbortBlock) Txs() []*txs.Tx { return nil }
func (b *AbortBlock) Visit(v Visitor) error { return v.AbortBlock(b) }
func NewAbortBlock(
timestamp time.Time,
parentID ids.ID,
height uint64,
) (*AbortBlock, error) {
blk := &AbortBlock{
Time: uint64(timestamp.Unix()),
CommonBlock: CommonBlock{PrntID: parentID, Hght: height},
bytes, err := buildBlock(blkAbort, parentID, height, uint64(timestamp.Unix()), nil, nil)
if err != nil {
return nil, err
}
return blk, initialize(blk, &blk.CommonBlock)
blk := &AbortBlock{}
if err := blk.setID(bytes); err != nil {
return nil, err
}
return blk, nil
}
+11 -24
View File
@@ -1,24 +1,25 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"github.com/luxfi/runtime"
"fmt"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/runtime"
)
// RuntimeInitializable defines the interface for initializing context
// RuntimeInitializable binds a runtime into a block's embedded txs.
type RuntimeInitializable interface {
InitRuntime(rt *runtime.Runtime)
}
// Block defines the common stateless interface for all blocks
// Block is the common stateless interface for all P-chain blocks. Every block
// is a single native-ZAP object; there is no codec and no initialize step —
// the bytes are authoritative and blocks are built by New*Block or read by
// Parse, both of which bind ID = hash(bytes).
type Block interface {
RuntimeInitializable
ID() ids.ID
@@ -26,30 +27,16 @@ type Block interface {
Bytes() []byte
Height() uint64
// Txs returns list of transactions contained in the block
// Txs returns the transactions contained in the block.
Txs() []*txs.Tx
// Visit calls [visitor] with this block's concrete type
// Visit calls [visitor] with this block's concrete type.
Visit(visitor Visitor) error
// note: initialize does not assume that block transactions
// are initialized, and initializes them itself if they aren't.
initialize(bytes []byte) error
}
// TimestampedBlock is a Block that carries a per-block timestamp. Every
// canonical P-chain block kind is timestamped.
type TimestampedBlock interface {
Block
Timestamp() time.Time
}
func initialize(blk Block, commonBlk *CommonBlock) error {
// We serialize this block as a pointer so that it can be deserialized into
// a Block
bytes, err := Codec.Marshal(CodecVersion, &blk)
if err != nil {
return fmt.Errorf("couldn't marshal block: %w", err)
}
commonBlk.initialize(bytes)
return nil
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
// Native ZAP block wire: the struct IS the wire. One zap object per block,
// keyed by a 1-byte blockKind discriminator at object offset 0 — the whole
// dispatch. There is no codec, no version prefix, no slot map. Txs are already
// self-describing via their own tx.Bytes(); a tx-bearing block stores the
// per-tx byte lengths as a u32 list and the concatenated tx bytes as a blob,
// so Parse re-splits and hands each slice to txs.Parse (zero copy).
//
// Object fixed section (all offsets are object-relative):
//
// blockKind u8 @ 0 abort=1, commit=2, proposal=3, standard=4
// ParentID 32B @ 1
// Height u64 @ 33
// Time u64 @ 41
// TxLengths 8B @ 49 u32 list ptr — one entry per decision tx (Standard/Proposal)
// TxBlob 8B @ 57 bytes ptr — concat of each decision tx.Bytes() (Standard/Proposal)
// ProposalTx 8B @ 65 bytes ptr — the single proposal Tx.Bytes() (Proposal only)
//
// Sizes: abort/commit = 49, standard = 65, proposal = 73.
import (
"fmt"
"time"
hash "github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/runtime"
"github.com/luxfi/zap"
)
// blockKind is the 1-byte discriminator at object offset 0 of every P-chain
// block buffer. Parse reads it and returns the typed block. There is no codec.
type blockKind uint8
const (
blkReserved blockKind = iota
blkAbort
blkCommit
blkProposal
blkStandard
)
// Fixed wire offsets (object-relative) and object sizes.
const (
offBlkKind = 0
offBlkParent = 1
offBlkHeight = 33
offBlkTime = 41
offBlkTxLengths = 49
offBlkTxBlob = 57
offBlkProposalTx = 65
sizeDecided = 49 // abort / commit (no txs)
sizeStandard = 65 // + TxLengths + TxBlob
sizeProposal = 73 // + ProposalTx
txLenStride = 4 // uint32
)
// commonZapBlock is the embedded base for every block type. It holds the zap
// buffer plus the cached block ID and serves the whole shared surface
// (Bytes/ID/Parent/Height/Timestamp/InitRuntime), so each type file only adds
// its constructor, Txs()/Tx() delta accessors, and Visit.
type commonZapBlock struct {
msg *zap.Message
id ids.ID
}
func (b commonZapBlock) Bytes() []byte { return b.msg.Bytes() }
func (b commonZapBlock) ID() ids.ID { return b.id }
func (b commonZapBlock) Parent() ids.ID { return readBlkID(b.msg.Root(), offBlkParent) }
func (b commonZapBlock) Height() uint64 { return b.msg.Root().Uint64(offBlkHeight) }
func (b commonZapBlock) Timestamp() time.Time {
return time.Unix(int64(b.msg.Root().Uint64(offBlkTime)), 0)
}
// InitRuntime binds the runtime into every embedded tx. It dispatches on the
// wire kind so the base — which owns the layout knowledge — is the one place
// that knows where a block's txs live, without depending on the concrete
// type's Txs() (Go embedding has no virtual dispatch).
func (b commonZapBlock) InitRuntime(rt *runtime.Runtime) {
obj := b.msg.Root()
switch blockKind(obj.Uint8(offBlkKind)) {
case blkStandard, blkProposal:
if list, err := readTxList(obj, offBlkTxLengths, offBlkTxBlob); err == nil {
for _, tx := range list {
tx.Unsigned.InitRuntime(rt)
}
}
}
if blockKind(obj.Uint8(offBlkKind)) == blkProposal {
if tx, err := readProposalTx(obj); err == nil && tx != nil {
tx.Unsigned.InitRuntime(rt)
}
}
}
// setID parses the final bytes into the block's zap buffer and derives
// ID = hash(bytes). Used by both the New*Block constructors (fresh buffer) and
// Parse (wire/disk buffer); the bytes are authoritative and never re-encoded.
func (b *commonZapBlock) setID(bytes []byte) error {
msg, err := zap.Parse(bytes)
if err != nil {
return err
}
b.msg = msg
b.id = hash.ComputeHash256Array(bytes)
return nil
}
// buildBlock is the one place block fields become bytes. It writes any tx list
// into the builder's variable section BEFORE the object (so the object's list
// pointer is a backward reference, matching the tx package), starts the object
// sized for the kind, and sets the fixed fields.
func buildBlock(k blockKind, parentID ids.ID, height, ts uint64, decisionTxs []*txs.Tx, proposalTx *txs.Tx) ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + 256)
var lenOff, lenCount int
var blob []byte
hasTxList := k == blkStandard || k == blkProposal
if hasTxList {
var err error
lenOff, lenCount, blob, err = writeTxList(b, decisionTxs)
if err != nil {
return nil, err
}
}
size := sizeDecided
switch k {
case blkStandard:
size = sizeStandard
case blkProposal:
size = sizeProposal
}
ob := b.StartObject(size)
ob.SetUint8(offBlkKind, uint8(k))
ob.SetBytesFixed(offBlkParent, parentID[:])
ob.SetUint64(offBlkHeight, height)
ob.SetUint64(offBlkTime, ts)
if hasTxList {
ob.SetList(offBlkTxLengths, lenOff, lenCount)
ob.SetBytes(offBlkTxBlob, blob)
}
if k == blkProposal {
if proposalTx == nil {
return nil, fmt.Errorf("proposal block requires a proposal tx")
}
ob.SetBytes(offBlkProposalTx, proposalTx.Bytes())
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// writeTxList encodes decisionTxs as a u32 list of per-tx byte lengths plus a
// concatenated blob of their bytes. AddUint32 counts elements, so Finish()'s
// length is the tx count (unlike an AddBytes list, which counts bytes).
func writeTxList(b *zap.Builder, decisionTxs []*txs.Tx) (lenOff, lenCount int, blob []byte, err error) {
if len(decisionTxs) == 0 {
return 0, 0, nil, nil
}
lb := b.StartList(txLenStride)
for i, tx := range decisionTxs {
if tx == nil {
return 0, 0, nil, fmt.Errorf("nil tx at index %d", i)
}
raw := tx.Bytes()
lb.AddUint32(uint32(len(raw)))
blob = append(blob, raw...)
}
lenOff, lenCount = lb.Finish()
return lenOff, lenCount, blob, nil
}
// readTxList reconstructs the decision txs from the u32 length list at
// lenPtrOff and the concatenated blob at blobPtrOff, slicing the blob by each
// stored length and handing each slice to txs.Parse (zero copy).
func readTxList(obj zap.Object, lenPtrOff, blobPtrOff int) ([]*txs.Tx, error) {
lengths := obj.ListStride(lenPtrOff, txLenStride)
n := lengths.Len()
if n == 0 {
return nil, nil
}
blob := obj.Bytes(blobPtrOff)
out := make([]*txs.Tx, n)
cursor := 0
for i := 0; i < n; i++ {
size := int(lengths.Uint32(i))
if size < 0 || cursor+size > len(blob) {
return nil, fmt.Errorf("block: tx %d length %d overruns blob (%d)", i, size, len(blob))
}
tx, err := txs.Parse(blob[cursor : cursor+size])
if err != nil {
return nil, fmt.Errorf("block: parse tx %d: %w", i, err)
}
out[i] = tx
cursor += size
}
return out, nil
}
// readProposalTx decodes the single proposal Tx of a ProposalBlock; nil if the
// slot is empty.
func readProposalTx(obj zap.Object) (*txs.Tx, error) {
raw := obj.Bytes(offBlkProposalTx)
if len(raw) == 0 {
return nil, nil
}
return txs.Parse(raw)
}
// readBlkID reads a 32-byte id at the given object offset.
func readBlkID(o zap.Object, off int) ids.ID {
var id ids.ID
copy(id[:], o.BytesFixedSlice(off, 32))
return id
}
+150
View File
@@ -0,0 +1,150 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
lux "github.com/luxfi/utxo"
)
// makeTx builds a real signed-shaped tx (an in-process BaseTx with no creds)
// whose Bytes()/ID() are bound, suitable for embedding in a block.
func makeTx(t *testing.T, networkID uint32) *txs.Tx {
t.Helper()
utx, err := txs.NewBaseTx(&lux.BaseTx{
NetworkID: networkID,
BlockchainID: ids.GenerateTestID(),
})
require.NoError(t, err)
tx := &txs.Tx{Unsigned: utx}
require.NoError(t, tx.Initialize())
return tx
}
// requireSameTxs asserts two tx slices are byte- and ID-equal in order.
func requireSameTxs(t *testing.T, want, got []*txs.Tx) {
t.Helper()
require.Len(t, got, len(want))
for i := range want {
require.Equal(t, want[i].ID(), got[i].ID(), "tx %d ID", i)
require.Equal(t, want[i].Bytes(), got[i].Bytes(), "tx %d bytes", i)
}
}
func TestAbortBlockRoundTrip(t *testing.T) {
require := require.New(t)
ts := time.Unix(1_700_000_000, 0)
parentID := ids.GenerateTestID()
blk, err := NewAbortBlock(ts, parentID, 42)
require.NoError(err)
parsed, err := Parse(blk.Bytes())
require.NoError(err)
require.IsType(&AbortBlock{}, parsed)
require.Equal(blk.ID(), parsed.ID())
require.Equal(parentID, parsed.Parent())
require.Equal(uint64(42), parsed.Height())
require.Equal(ts.Unix(), parsed.(*AbortBlock).Timestamp().Unix())
require.Nil(parsed.Txs())
// Idempotent Visit dispatch.
require.NoError(parsed.Visit(dispatchVisitor{}))
}
func TestCommitBlockRoundTrip(t *testing.T) {
require := require.New(t)
ts := time.Unix(1_700_000_123, 0)
parentID := ids.GenerateTestID()
blk, err := NewCommitBlock(ts, parentID, 7)
require.NoError(err)
parsed, err := Parse(blk.Bytes())
require.NoError(err)
require.IsType(&CommitBlock{}, parsed)
require.Equal(blk.ID(), parsed.ID())
require.Equal(parentID, parsed.Parent())
require.Equal(uint64(7), parsed.Height())
require.Equal(ts.Unix(), parsed.(*CommitBlock).Timestamp().Unix())
require.Nil(parsed.Txs())
}
func TestStandardBlockRoundTrip(t *testing.T) {
require := require.New(t)
ts := time.Unix(1_700_001_000, 0)
parentID := ids.GenerateTestID()
decision := []*txs.Tx{makeTx(t, 11), makeTx(t, 22)}
blk, err := NewStandardBlock(ts, parentID, 100, decision)
require.NoError(err)
parsed, err := Parse(blk.Bytes())
require.NoError(err)
require.IsType(&StandardBlock{}, parsed)
require.Equal(blk.ID(), parsed.ID())
require.Equal(parentID, parsed.Parent())
require.Equal(uint64(100), parsed.Height())
require.Equal(ts.Unix(), parsed.(*StandardBlock).Timestamp().Unix())
requireSameTxs(t, decision, parsed.Txs())
}
func TestStandardBlockEmptyTxsRoundTrip(t *testing.T) {
require := require.New(t)
ts := time.Unix(1_700_002_000, 0)
parentID := ids.GenerateTestID()
blk, err := NewStandardBlock(ts, parentID, 5, nil)
require.NoError(err)
parsed, err := Parse(blk.Bytes())
require.NoError(err)
require.Empty(parsed.Txs())
}
func TestProposalBlockRoundTrip(t *testing.T) {
require := require.New(t)
ts := time.Unix(1_700_003_000, 0)
parentID := ids.GenerateTestID()
proposalTx := makeTx(t, 99)
decision := []*txs.Tx{makeTx(t, 33)}
blk, err := NewProposalBlock(ts, parentID, 250, proposalTx, decision)
require.NoError(err)
parsed, err := Parse(blk.Bytes())
require.NoError(err)
pb, ok := parsed.(*ProposalBlock)
require.True(ok)
require.Equal(blk.ID(), pb.ID())
require.Equal(parentID, pb.Parent())
require.Equal(uint64(250), pb.Height())
require.Equal(ts.Unix(), pb.Timestamp().Unix())
// Tx() returns the single proposal tx.
require.Equal(proposalTx.ID(), pb.Tx().ID())
require.Equal(proposalTx.Bytes(), pb.Tx().Bytes())
// Txs() returns decision txs followed by the proposal tx (last).
want := append(append([]*txs.Tx{}, decision...), proposalTx)
requireSameTxs(t, want, pb.Txs())
}
// dispatchVisitor is a no-op Visitor used to exercise Visit dispatch.
type dispatchVisitor struct{}
func (dispatchVisitor) AbortBlock(*AbortBlock) error { return nil }
func (dispatchVisitor) CommitBlock(*CommitBlock) error { return nil }
func (dispatchVisitor) ProposalBlock(*ProposalBlock) error { return nil }
func (dispatchVisitor) StandardBlock(*StandardBlock) error { return nil }
-160
View File
@@ -1,160 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/block/v0"
"github.com/luxfi/node/vms/platformvm/txs"
)
const (
// CodecVersionV0 is the v1.23.x ("Apricot/Banff") wire layout. It is
// retained as a READ-ONLY decoder so that pre-codec-v1 blocks on
// disk continue to deserialize and so that v0-derived BlockIDs and
// TxIDs remain stable. All write paths MUST use CodecVersionV1.
CodecVersionV0 = txs.CodecVersionV0
// CodecVersionV1 is the current canonical block-codec wire layout.
// Every block built by this binary is written at v1.
CodecVersionV1 = txs.CodecVersionV1
// CodecVersion is the canonical write version. All Marshal call
// sites in this package use CodecVersion so that any future bump of
// the write target updates exactly one symbol.
CodecVersion = CodecVersionV1
)
var (
// Codec is the standard-size block codec.Manager. It carries ONLY
// the v1 slot map and so decodes only v1-prefixed block bytes via
// codec.Manager dispatch. v0 block bytes must go through Parse,
// which routes prefix==0 to v0Codec for the v0 Block interface.
//
// Block-codec dispatch is explicitly two-step (Parse extracts the
// 2-byte prefix and selects v0Codec or Codec) because v0 blocks
// implement v0.Block, not block.Block — they cannot be unmarshalled
// into a block.Block destination. Hence Codec is v1-only by design.
Codec pcodecs.Manager
// GenesisCodec is the unbounded-size codec.Manager used by both
// large-genesis decode AND every P-Chain state-side read of
// non-block byte values (feeState, L1Validator, NetToL1Conversion,
// fx.Owner, heightRange, legacy stateBlk). It registers BOTH the
// v0 (v1.23.x Apricot/Banff) and v1 (current) tx slot maps under
// CodecVersionV0 and CodecVersionV1 respectively, so a state read
// of pre-codec-v1 bytes (prefix=0x0000) dispatches into the v0
// slot map and a v1 read (prefix=0x0001) dispatches into v1.
//
// Writes still target CodecVersion (== CodecVersionV1) exclusively —
// the v0 slot map is a READ-ONLY decoder. This is the same shape
// that txs.GenesisCodec carries, which is why genesis.Codec aliases
// txs.GenesisCodec rather than this codec.
//
// Note that this codec does NOT register block.Block / v0.Block
// types directly — block parsing always goes through block.Parse,
// not GenesisCodec.Unmarshal(b, &Block). The v0 slot map registered
// here covers the tx + sub-tx slots referenced by serialized state
// values (e.g. fx.Owner -> secp256k1fx.OutputOwners).
GenesisCodec pcodecs.Manager
// v0Codec is the v1.23.x read-only codec.Manager. It registers v0
// block + tx slots and unmarshals into v0.Block (the v0 interface),
// not block.Block — these are two distinct interface destinations.
// External packages MUST NOT Marshal at v0; that is verified at
// Parse time (Parse never re-marshals).
v0Codec pcodecs.Manager
// v0GenesisCodec mirrors v0Codec at large size.
v0GenesisCodec pcodecs.Manager
// genesisLinearCodec is the underlying v1 codec for GenesisCodec.
// This is exposed for registering additional types from other
// packages (e.g. state) at the canonical write version.
genesisLinearCodec pcodecs.LinearCodec
// genesisLinearCodecV0 is the underlying v0 codec for GenesisCodec.
// Exposed so state-side types that want to be decodable from both
// v0 and v1 state bytes can register symmetrically via
// RegisterGenesisType.
genesisLinearCodecV0 pcodecs.LinearCodec
)
func init() {
cV1 := pcodecs.NewLinearCodec()
gcV1 := pcodecs.NewLinearCodec()
gcV0 := pcodecs.NewLinearCodec()
cV0 := pcodecs.NewLinearCodec()
gcV0BlockOnly := pcodecs.NewLinearCodec()
errs := pcodecs.Errs{}
for _, c := range []pcodecs.LinearCodec{cV1, gcV1} {
errs.Add(RegisterBlockTypes(c))
}
// gcV0 carries only the v0 tx slot map (no block types) because
// GenesisCodec is the state-read codec, not a block-decode codec.
// Block parsing uses v0Codec / v0GenesisCodec (below).
errs.Add(v0.RegisterTxTypes(gcV0))
// cV0 and gcV0BlockOnly carry the full v0 block+tx slot map for
// block.Parse's v0 dispatch path.
for _, c := range []pcodecs.LinearCodec{cV0, gcV0BlockOnly} {
errs.Add(v0.RegisterBlockTypes(c))
}
Codec = pcodecs.NewDefaultManager()
GenesisCodec = pcodecs.NewMaxInt32Manager()
v0Codec = pcodecs.NewDefaultManager()
v0GenesisCodec = pcodecs.NewMaxInt32Manager()
errs.Add(
Codec.RegisterCodec(CodecVersionV1, cV1),
// GenesisCodec carries BOTH v0 (read-only) AND v1 (read+write)
// slot maps so state-side reads of pre-codec-v1 bytes (mainnet,
// testnet bootstrapped at v1.23.x) succeed via Manager.Unmarshal
// dispatch on the 2-byte wire prefix. Writes still target
// CodecVersion (== v1).
GenesisCodec.RegisterCodec(CodecVersionV0, gcV0),
GenesisCodec.RegisterCodec(CodecVersionV1, gcV1),
v0Codec.RegisterCodec(CodecVersionV0, cV0),
v0GenesisCodec.RegisterCodec(CodecVersionV0, gcV0BlockOnly),
)
if errs.Errored() {
panic(errs.Err)
}
genesisLinearCodec = gcV1
genesisLinearCodecV0 = gcV0
}
// RegisterGenesisType registers a type with the GenesisCodec at BOTH
// the v0 and v1 slot positions. Used by other packages (e.g. state) to
// register types that are encountered in genesis bytes and state-read
// fallback paths. Registering at both versions keeps the slot ID
// identical across the codec.Manager dispatch so a v0-prefixed read of
// a state value (e.g. legacy stateBlk) lands on the same Go type as a
// v1-prefixed read.
//
// All registrations must succeed atomically: if the v0 registration
// fails (e.g. duplicate type) the v1 registration is still attempted so
// the underlying error is surfaced rather than silently leaving the
// codecs in a divergent state.
func RegisterGenesisType(val interface{}) error {
v0Err := genesisLinearCodecV0.RegisterType(val)
v1Err := genesisLinearCodec.RegisterType(val)
return errors.Join(v0Err, v1Err)
}
// RegisterBlockTypes registers the canonical v1 block type IDs. There
// is exactly one type per block kind: ProposalBlock, AbortBlock,
// CommitBlock, StandardBlock. Tx types come from txs.RegisterTypes
// (which registers the v1 tx slot layout).
func RegisterBlockTypes(targetCodec pcodecs.LinearCodec) error {
return errors.Join(
txs.RegisterTypes(targetCodec),
targetCodec.RegisterType(&ProposalBlock{}),
targetCodec.RegisterType(&AbortBlock{}),
targetCodec.RegisterType(&CommitBlock{}),
targetCodec.RegisterType(&StandardBlock{}),
)
}
@@ -1,163 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"bytes"
"encoding/binary"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/node/vms/components/gas"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/txs"
)
// TestGenesisCodecAcceptsV0FeeState is the regression guard for the
// v1.28.1 testnet-canary failure:
//
// P-Chain state corrupt after init — database must be wiped
// error="loadMetadata: feeState: unknown codec version"
//
// Pre-fix, block.GenesisCodec registered ONLY the v1 slot map. A
// testnet bootstrapped at v1.23.x had written feeState bytes with the
// v0 prefix (0x0000); v1.28.0+ called block.GenesisCodec.Unmarshal on
// those bytes, codec.Manager looked up version 0 in its map, missed,
// and returned pcodecs.ErrUnknownVersion — surfacing as the
// "unknown codec version" error at loadMetadata: feeState.
//
// Post-fix, block.GenesisCodec is multi-version (v0 read-only + v1
// read+write). The 2-byte wire prefix selects the slot map; the
// gas.State struct is byte-equal across versions (no slot dispatch
// inside it), so v0 bytes decode into the same Go value.
func TestGenesisCodecAcceptsV0FeeState(t *testing.T) {
require := require.New(t)
original := gas.State{
Capacity: gas.Gas(1_000_000),
Excess: gas.Gas(424242),
}
v0Bytes, err := GenesisCodec.Marshal(CodecVersionV0, original)
require.NoError(err, "v0 marshal must succeed — proves the v0 slot map is registered on block.GenesisCodec")
require.GreaterOrEqual(len(v0Bytes), 2)
require.Equal(uint16(CodecVersionV0), binary.LittleEndian.Uint16(v0Bytes[:2]),
"marshaled bytes must carry the v0 wire prefix (LE per LP-023)")
// The bug: this Unmarshal used to fail with pcodecs.ErrUnknownVersion.
var decoded gas.State
version, err := GenesisCodec.Unmarshal(v0Bytes, &decoded)
require.NoError(err, "v0-prefixed feeState must decode via the multi-version GenesisCodec")
require.Equal(uint16(CodecVersionV0), version)
require.Equal(original, decoded)
}
// TestGenesisCodecAcceptsV1FeeState is the no-regression guard for the
// canonical write path: every feeState written by this binary carries
// the v1 wire prefix and must continue to round-trip.
func TestGenesisCodecAcceptsV1FeeState(t *testing.T) {
require := require.New(t)
original := gas.State{
Capacity: gas.Gas(2_500_000),
Excess: gas.Gas(7777),
}
v1Bytes, err := GenesisCodec.Marshal(CodecVersion, original)
require.NoError(err)
require.Equal(uint16(CodecVersionV1), binary.LittleEndian.Uint16(v1Bytes[:2]),
"canonical write path must emit v1 prefix (LE per LP-023)")
var decoded gas.State
version, err := GenesisCodec.Unmarshal(v1Bytes, &decoded)
require.NoError(err)
require.Equal(uint16(CodecVersionV1), version)
require.Equal(original, decoded)
}
// TestGenesisCodecRejectsUnknownVersion locks in that the multi-version
// extension did NOT silently open the door to bogus prefixes. Only v0
// and v1 are registered on GenesisCodec; anything else must surface as
// pcodecs.ErrUnknownVersion.
func TestGenesisCodecRejectsUnknownVersion(t *testing.T) {
require := require.New(t)
bogus := make([]byte, 16)
binary.LittleEndian.PutUint16(bogus[:2], 0xFFFE)
var sink gas.State
_, err := GenesisCodec.Unmarshal(bogus, &sink)
require.ErrorIs(err, pcodecs.ErrUnknownVersion)
}
// TestGenesisCodecV0RoundtripIsBytePreserving documents the byte-
// preserving guarantee for state values read at v0 and re-marshaled at
// v0 (e.g. for a migration/extraction tool). Linearcodec is
// deterministic — output must be byte-equal to input.
func TestGenesisCodecV0RoundtripIsBytePreserving(t *testing.T) {
require := require.New(t)
original := gas.State{
Capacity: gas.Gas(123),
Excess: gas.Gas(456),
}
v0Bytes, err := GenesisCodec.Marshal(CodecVersionV0, original)
require.NoError(err)
var decoded gas.State
_, err = GenesisCodec.Unmarshal(v0Bytes, &decoded)
require.NoError(err)
roundtripBytes, err := GenesisCodec.Marshal(CodecVersionV0, decoded)
require.NoError(err)
require.True(bytes.Equal(v0Bytes, roundtripBytes),
"v0 marshal -> unmarshal -> re-marshal must be byte-preserving (len in=%d, out=%d)",
len(v0Bytes), len(roundtripBytes))
}
// TestGenesisCodecCarriesBothVersions is a structural invariant test
// that prevents accidental regressions of the multi-version property.
// Future patches that add a new state-side type or refactor the codec
// init order will trip this if they drop either version slot.
func TestGenesisCodecCarriesBothVersions(t *testing.T) {
require := require.New(t)
// Marshal a value at each version. If either codec is missing,
// codec.Manager.Marshal returns ErrUnknownVersion.
for _, version := range []uint16{CodecVersionV0, CodecVersionV1} {
_, err := GenesisCodec.Marshal(version, gas.State{Capacity: 1, Excess: 2})
require.NoErrorf(err, "GenesisCodec must register version %d", version)
}
}
// TestGenesisCodecV0RegisteredTypesMatchTxsV0 asserts the v0 type slot
// map registered on block.GenesisCodec is the same shape as the one on
// txs.GenesisCodec. They share the registerV0TxTypes layout so that
// state values containing fx.Owner / signer.* / secp256k1fx.* types
// decode by the same slot IDs whether they were written via
// txs.GenesisCodec.Marshal or block.GenesisCodec.Marshal at v0.
//
// We cannot directly compare slot-ID maps (linearcodec doesn't expose
// them), so we cross-check by marshaling an interface-carrying value
// at v0 through both codecs and asserting byte-equality.
func TestGenesisCodecV0RegisteredTypesMatchTxsV0(t *testing.T) {
require := require.New(t)
// Use a simple wrapper that exercises slot dispatch on a known v0
// type. AdvanceTimeTx is registered at the same slot (20 in v1, 20
// in v0) on both codecs.
tx := &txs.AdvanceTimeTx{Time: 1234567890}
blockBytes, err := GenesisCodec.Marshal(CodecVersionV0, &tx)
require.NoError(err)
txsBytes, err := txs.GenesisCodec.Marshal(CodecVersionV0, &tx)
require.NoError(err)
require.True(bytes.Equal(blockBytes, txsBytes),
"block.GenesisCodec and txs.GenesisCodec must produce byte-equal v0 encodings (block=%d txs=%d)",
len(blockBytes), len(txsBytes))
}
+14 -21
View File
@@ -1,45 +1,38 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"context"
"time"
"github.com/luxfi/runtime"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
)
var _ Block = (*CommitBlock)(nil)
// CommitBlock is the canonical P-Chain commit outcome of a ProposalBlock.
// CommitBlock is the canonical P-Chain commit outcome of a ProposalBlock. It
// carries a timestamp and no txs; the struct is the zap buffer.
type CommitBlock struct {
Time uint64 `serialize:"true" json:"time"`
CommonBlock `serialize:"true"`
commonZapBlock
}
func (b *CommitBlock) Timestamp() time.Time { return time.Unix(int64(b.Time), 0) }
func (b *CommitBlock) initialize(bytes []byte) error {
b.CommonBlock.initialize(bytes)
return nil
}
func (*CommitBlock) InitRuntime(*runtime.Runtime) {}
func (*CommitBlock) Txs() []*txs.Tx { return nil }
func (b *CommitBlock) Visit(v Visitor) error { return v.CommitBlock(b) }
func (*CommitBlock) Initialize(context.Context) error { return nil }
func (*CommitBlock) Txs() []*txs.Tx { return nil }
func (b *CommitBlock) Visit(v Visitor) error { return v.CommitBlock(b) }
func NewCommitBlock(
timestamp time.Time,
parentID ids.ID,
height uint64,
) (*CommitBlock, error) {
blk := &CommitBlock{
Time: uint64(timestamp.Unix()),
CommonBlock: CommonBlock{PrntID: parentID, Hght: height},
bytes, err := buildBlock(blkCommit, parentID, height, uint64(timestamp.Unix()), nil, nil)
if err != nil {
return nil, err
}
return blk, initialize(blk, &blk.CommonBlock)
blk := &CommitBlock{}
if err := blk.setID(bytes); err != nil {
return nil, err
}
return blk, nil
}
-42
View File
@@ -1,42 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"github.com/luxfi/ids"
hash "github.com/luxfi/crypto/hash"
)
// CommonBlock contains fields and methods common to all blocks in this VM.
type CommonBlock struct {
// parent's ID
PrntID ids.ID `serialize:"true" json:"parentID"`
// This block's height. The genesis block is at height 0.
Hght uint64 `serialize:"true" json:"height"`
BlockID ids.ID `json:"id"`
bytes []byte
}
func (b *CommonBlock) initialize(bytes []byte) {
b.BlockID = hash.ComputeHash256Array(bytes)
b.bytes = bytes
}
func (b *CommonBlock) ID() ids.ID {
return b.BlockID
}
func (b *CommonBlock) Parent() ids.ID {
return b.PrntID
}
func (b *CommonBlock) Bytes() []byte {
return b.bytes
}
func (b *CommonBlock) Height() uint64 {
return b.Hght
}
-214
View File
@@ -1,214 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"errors"
"fmt"
"time"
hash "github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/runtime"
"github.com/luxfi/node/vms/platformvm/block/v0"
"github.com/luxfi/node/vms/platformvm/txs"
)
// errLegacyAtomicBlock is returned when v0 decoding produces an
// ApricotAtomicBlock. The modern P-only network does not accept atomic
// blocks; any pre-Banff atomic block on disk must be handled by the
// caller (typically by skipping the entry — atomic block IDs are not
// reachable from the canonical Banff-era last-accepted chain).
var errLegacyAtomicBlock = errors.New("apricot atomic block is not supported on the P-only network")
// liftV0 wraps a v0-decoded block in an adapter that implements
// block.Block and dispatches Visit to the canonical v1 Visitor.
//
// The lifted block:
// - returns b verbatim from Bytes() (no re-marshal),
// - reports BlockID = hash(b),
// - reports Height/Parent/Txs delegated to the v0 struct,
// - dispatches Visit per the kind table:
// ApricotProposalBlock, BanffProposalBlock -> StandardBlock
// (proposal/commit/abort are dead block kinds on the P-only
// network — modern blocks are all StandardBlock with proposal
// txs inlined as decision txs; on the historical decode path
// we surface them via the StandardBlock visitor so the
// executor's StandardBlock arm runs the embedded txs without
// requiring a separate Apricot/Banff visitor surface),
// ApricotStandardBlock, BanffStandardBlock -> StandardBlock,
// ApricotAbortBlock, BanffAbortBlock -> AbortBlock,
// ApricotCommitBlock, BanffCommitBlock -> CommitBlock.
// - re-initializes each embedded tx via tx.InitializeFromBytes
// against the v0 tx codec, byte-preserving inner TxIDs.
//
// initVisit selects the v1 Visitor arm. The lifting table is the
// migration's only place where a v0 block kind maps to a v1 kind; once
// every block on disk is v1, this file becomes dead code and can be
// removed.
func liftV0(b v0.Block, raw []byte) (Block, error) {
switch v := b.(type) {
case *v0.ApricotAtomicBlock:
_ = v
return nil, errLegacyAtomicBlock
}
blkID := hash.ComputeHash256Array(raw)
lifted := &liftedV0Block{
blockID: blkID,
raw: raw,
v0: b,
parentID: b.ParentID(),
height: b.Height(),
timeUnix: b.TimestampUnix(),
txs: b.Txs(),
}
if err := lifted.initTxs(); err != nil {
return nil, fmt.Errorf("v0 lift: %w", err)
}
return lifted, nil
}
// liftedV0Block is the block.Block adapter around a v0-decoded block.
// It does not embed CommonBlock — the canonical CommonBlock owns its
// own bytes via SetBytes, which would re-derive BlockID from a
// marshalled v1 layout. The lifted block instead computes BlockID once
// at construction time from the original v0 bytes and stashes them.
type liftedV0Block struct {
blockID ids.ID
raw []byte
v0 v0.Block
parentID ids.ID
height uint64
timeUnix uint64
txs []*txs.Tx
}
func (b *liftedV0Block) ID() ids.ID { return b.blockID }
func (b *liftedV0Block) Parent() ids.ID { return b.parentID }
func (b *liftedV0Block) Bytes() []byte { return b.raw }
func (b *liftedV0Block) Height() uint64 { return b.height }
func (b *liftedV0Block) Txs() []*txs.Tx { return b.txs }
func (b *liftedV0Block) Timestamp() time.Time { return time.Unix(int64(b.timeUnix), 0) }
func (b *liftedV0Block) initialize(_ []byte) error {
// liftedV0Block has its identity (BlockID, raw bytes) committed at
// liftV0 construction time from the original input bytes. The
// argument here is the v1-marshalled bytes that block.Parse would
// derive for a v1 block; for a v0 block we ignore it and keep raw.
return nil
}
func (b *liftedV0Block) InitRuntime(rt *runtime.Runtime) {
for _, tx := range b.txs {
if tx == nil {
continue
}
tx.Unsigned.InitRuntime(rt)
}
}
// initTxs runs InitializeFromBytes on every embedded tx using the v0
// tx codec. The signed bytes the codec recorded during Unmarshal are
// what we need; we recover them by re-marshalling the unsigned half
// under v0 and slicing into the parent block's input bytes is not
// safe (the parent block bytes interleave block-header fields). The
// linearcodec leaves the unsigned/credentials split implicit in the
// stream offset, so the cleanest byte-preserving handle we have is to
// ask the v0 tx codec to Size(version, &tx.Unsigned) and then take
// the prefix from the tx's own bytes — but those bytes have not been
// set yet by linearcodec, only the struct fields.
//
// We therefore re-marshal each tx under the v0 codec to recover its
// signedBytes. The wire format is deterministic and the resulting
// bytes are byte-equal to whatever the v0 producer emitted; the
// re-marshal is a closed loop over the v0 layout the tx was decoded
// from. TxID = hash(re-marshalled v0 bytes) = TxID the chain
// originally committed.
//
// This single re-marshal is allowed only here, only against the v0
// codec, and only for txs whose Unsigned was decoded under v0. Under
// the v1 path, txs are byte-preserved via tx.Parse without any
// re-marshal.
func (b *liftedV0Block) initTxs() error {
for _, tx := range b.txs {
if tx == nil {
continue
}
if err := tx.InitializeFromBytesAtVersion(txs.GenesisCodec, txs.CodecVersionV0); err != nil {
return fmt.Errorf("byte-preserve v0 tx: %w", err)
}
}
return nil
}
func (b *liftedV0Block) Visit(v Visitor) error {
switch b.v0.(type) {
case *v0.ApricotAbortBlock, *v0.BanffAbortBlock:
return v.AbortBlock(b.asAbortBlock())
case *v0.ApricotCommitBlock, *v0.BanffCommitBlock:
return v.CommitBlock(b.asCommitBlock())
case *v0.ApricotProposalBlock, *v0.BanffProposalBlock:
return v.ProposalBlock(b.asProposalBlock())
case *v0.ApricotStandardBlock, *v0.BanffStandardBlock:
return v.StandardBlock(b.asStandardBlock())
default:
return fmt.Errorf("v0 lift: unhandled block kind %T", b.v0)
}
}
// asStandardBlock builds a *StandardBlock that mirrors the lifted v0
// block. The returned block uses the SAME raw bytes (so its BlockID
// matches b.blockID) and the SAME Txs slice (so the executor sees the
// same set of txs). This is the synthetic v1 view of a v0 block.
func (b *liftedV0Block) asStandardBlock() *StandardBlock {
return &StandardBlock{
Time: b.timeUnix,
CommonBlock: b.commonBlock(),
Transactions: b.txs,
}
}
func (b *liftedV0Block) asProposalBlock() *ProposalBlock {
// Modern ProposalBlock has Tx + Transactions tail. The lifted v0
// block's Txs() already returns the canonical ordering for both
// Apricot (single proposal Tx) and Banff (decision tail + proposal
// Tx). We split off the trailing element as the proposal tx.
tail := b.txs
if len(tail) == 0 {
return &ProposalBlock{
Time: b.timeUnix,
CommonBlock: b.commonBlock(),
}
}
last := tail[len(tail)-1]
return &ProposalBlock{
Time: b.timeUnix,
Transactions: tail[:len(tail)-1],
CommonBlock: b.commonBlock(),
Tx: last,
}
}
func (b *liftedV0Block) asAbortBlock() *AbortBlock {
return &AbortBlock{Time: b.timeUnix, CommonBlock: b.commonBlock()}
}
func (b *liftedV0Block) asCommitBlock() *CommitBlock {
return &CommitBlock{Time: b.timeUnix, CommonBlock: b.commonBlock()}
}
// commonBlock builds a CommonBlock whose BlockID + bytes are the
// lifted block's raw bytes / hash. This is the only place we copy the
// raw bytes into a v1-typed block, and it is read-only — the caller
// uses the synthetic block's visitor methods, never re-marshals it.
func (b *liftedV0Block) commonBlock() CommonBlock {
return CommonBlock{
PrntID: b.parentID,
Hght: b.height,
BlockID: b.blockID,
bytes: b.raw,
}
}
-127
View File
@@ -1,127 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
hash "github.com/luxfi/crypto/hash"
"github.com/luxfi/node/vms/platformvm/block/v0"
"github.com/luxfi/node/vms/platformvm/txs"
)
// TestParseV0ApricotProposalBlock asserts that a hand-encoded
// ApricotProposalBlock (slot 0 in v0) decodes via Parse and is lifted
// into a *ProposalBlock with the original bytes preserved.
//
// The Apricot proposal flow on the modern P-only network is replayed
// through the canonical Visitor.ProposalBlock arm. The fixture here
// is constructed by re-marshalling a synthetic ApricotProposalBlock
// at v0 — this is a closed-loop test of the v0 codec's encode + decode
// path; on mainnet the bytes come from disk, not from re-encoding.
func TestParseV0ApricotProposalBlock(t *testing.T) {
require := require.New(t)
// Build a synthetic v0 ApricotProposalBlock carrying an
// AdvanceTimeTx proposal — the simplest decision form pre-Banff.
proposalTx := &txs.Tx{Unsigned: &txs.AdvanceTimeTx{Time: 1_700_000_000}}
require.NoError(proposalTx.InitializeFromBytesAtVersion(txs.GenesisCodec, txs.CodecVersionV0))
v0Blk := &v0.ApricotProposalBlock{
CommonBlock: v0.CommonBlock{Hght: 7},
Tx: proposalTx,
}
raw, err := v0GenesisCodec.Marshal(CodecVersionV0, &([]v0.Block{v0Blk})[0])
require.NoError(err, "v0 marshal of ApricotProposalBlock")
// Parse via the version-aware block.Parse path.
parsed, err := Parse(GenesisCodec, raw)
require.NoError(err, "parse v0 ApricotProposalBlock")
// The lifted block reports the canonical fields.
require.Equal(uint64(7), parsed.Height())
require.True(bytes.Equal(raw, parsed.Bytes()),
"lifted v0 block must return original bytes verbatim")
require.Equal(hash.ComputeHash256Array(raw), [32]byte(parsed.ID()),
"lifted v0 block ID = hash(raw v0 bytes)")
require.Len(parsed.Txs(), 1, "ApricotProposalBlock carries exactly one tx")
require.Equal(proposalTx.TxID, parsed.Txs()[0].TxID,
"embedded tx TxID byte-preserved")
}
// TestParseV0BanffStandardBlock asserts that a hand-encoded
// BanffStandardBlock (slot 32 in v0) decodes via Parse and lifts to
// a *StandardBlock that carries the original timestamp.
func TestParseV0BanffStandardBlock(t *testing.T) {
require := require.New(t)
decisionTx := &txs.Tx{Unsigned: &txs.AdvanceTimeTx{Time: 1_700_000_000}}
require.NoError(decisionTx.InitializeFromBytesAtVersion(txs.GenesisCodec, txs.CodecVersionV0))
v0Blk := &v0.BanffStandardBlock{
Time: 1_700_000_100,
ApricotStandardBlock: v0.ApricotStandardBlock{
CommonBlock: v0.CommonBlock{Hght: 42},
Transactions: []*txs.Tx{decisionTx},
},
}
raw, err := v0GenesisCodec.Marshal(CodecVersionV0, &([]v0.Block{v0Blk})[0])
require.NoError(err)
parsed, err := Parse(GenesisCodec, raw)
require.NoError(err)
require.Equal(uint64(42), parsed.Height())
require.True(bytes.Equal(raw, parsed.Bytes()))
require.Equal(hash.ComputeHash256Array(raw), [32]byte(parsed.ID()))
require.Len(parsed.Txs(), 1)
}
// TestParseV1RoundTrip asserts that the canonical v1 path is
// byte-preserving the same way: a v1 block decoded via Parse keeps
// its original bytes and its BlockID = hash(raw).
func TestParseV1RoundTrip(t *testing.T) {
require := require.New(t)
tx := &txs.Tx{Unsigned: &txs.AdvanceTimeTx{Time: 1_700_000_000}}
require.NoError(tx.InitializeFromBytesAtVersion(txs.GenesisCodec, txs.CodecVersionV1))
blk := &StandardBlock{
Time: 1_700_000_100,
CommonBlock: CommonBlock{Hght: 5},
Transactions: []*txs.Tx{tx},
}
// Use the canonical block-build path (initialize sets bytes
// from a Marshal).
require.NoError(initialize(blk, &blk.CommonBlock))
raw := blk.Bytes()
parsed, err := Parse(GenesisCodec, raw)
require.NoError(err)
require.Equal(uint64(5), parsed.Height())
require.True(bytes.Equal(raw, parsed.Bytes()))
require.Equal(blk.ID(), parsed.ID(), "BlockID stable across Parse")
}
// TestVersionPrefixDispatch asserts that Parse dispatches by the
// 2-byte wire prefix and rejects unknown versions.
func TestVersionPrefixDispatch(t *testing.T) {
require := require.New(t)
// Empty / too-short input.
_, err := Parse(GenesisCodec, []byte{})
require.ErrorIs(err, ErrShortBytes)
_, err = Parse(GenesisCodec, []byte{0x00})
require.ErrorIs(err, ErrShortBytes)
// Unknown version (v2+).
_, err = Parse(GenesisCodec, []byte{0x00, 0x02, 0x00})
require.Error(err, "unknown version must error")
}
+26 -84
View File
@@ -3,96 +3,38 @@
package block
// Parse dispatch. There is no codec: Parse wraps the buffer zero-copy and
// dispatches on the 1-byte blockKind at object offset 0. Parse never
// re-marshals the input; ID = hash(b) verbatim and b is the block's
// authoritative bytes.
import (
"errors"
"fmt"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/block/v0"
"github.com/luxfi/zap"
)
// ErrShortBytes is returned when the input is shorter than the 2-byte
// codec-version prefix.
var ErrShortBytes = errors.New("block bytes too short for codec version prefix")
// Parse decodes a block byte stream produced by either the v0
// (v1.23.x) or v1 (current) block codec. The codec.Manager argument
// selects the SIZE class — Codec (1 MiB max) or GenesisCodec
// (unbounded). The actual VERSION is taken from the 2-byte wire prefix
// and dispatched to the matching slot map.
//
// Parse never re-marshals the input; BlockID = hash(b) verbatim and
// b is stashed on the returned block's CommonBlock. This is the
// byte-preserving path that keeps BlockIDs (and inner TxIDs) stable
// across the v0->v1 migration.
//
// c must be one of block.Codec or block.GenesisCodec. The v0 codec is
// selected internally based on the size class of c. Other codecs are
// rejected with codec.ErrUnknownVersion.
func Parse(c pcodecs.Manager, b []byte) (Block, error) {
if len(b) < 2 {
return nil, ErrShortBytes
// Parse decodes a native-ZAP P-chain block. It reads the blockKind
// discriminator and wraps the matching typed block over b (byte-preserving).
func Parse(b []byte) (Block, error) {
msg, err := zap.Parse(b)
if err != nil {
return nil, fmt.Errorf("couldn't parse block: %w", err)
}
// Codec version prefix is uint16 LE (LP-023 ZAP-native).
version := uint16(b[0]) | uint16(b[1])<<8
switch version {
case CodecVersionV1:
return parseV1(c, b)
case CodecVersionV0:
return parseV0(c, b)
switch k := blockKind(msg.Root().Uint8(offBlkKind)); k {
case blkAbort:
blk := &AbortBlock{}
return blk, blk.setID(b)
case blkCommit:
blk := &CommitBlock{}
return blk, blk.setID(b)
case blkProposal:
blk := &ProposalBlock{}
return blk, blk.setID(b)
case blkStandard:
blk := &StandardBlock{}
return blk, blk.setID(b)
default:
return nil, fmt.Errorf("%w: %d", pcodecs.ErrUnknownVersion, version)
return nil, fmt.Errorf("zap: unknown block kind %d", k)
}
}
// parseV1 is the canonical block-decode path. Bytes are unmarshalled
// against the v1 slot map and the resulting Block keeps b as its
// authoritative bytes.
func parseV1(c pcodecs.Manager, b []byte) (Block, error) {
var blk Block
if _, err := c.Unmarshal(b, &blk); err != nil {
return nil, err
}
return blk, blk.initialize(b)
}
// parseV0 decodes a v1.23.x ("Apricot/Banff") block. The v0 slot map
// is closed; the result is a v0-typed block (one of v0.ApricotXxx /
// v0.BanffXxx) that is wrapped in an adapter implementing block.Block.
//
// The adapter:
// - reports the original bytes verbatim (Bytes() returns b),
// - computes BlockID = hash(b),
// - translates Visit calls to the canonical v1 visitor
// (ApricotProposalBlock/BanffProposalBlock -> Visitor.ProposalBlock,
// ApricotStandardBlock/BanffStandardBlock -> Visitor.StandardBlock,
// etc.),
// - re-initializes embedded txs using their byte-preserving
// InitializeFromBytes path against the v0 tx codec, so inner TxIDs
// remain hash(originalTxBytes) under the v0 layout.
//
// ApricotAtomicBlock is decoded but rejected at the block boundary: the
// modern P-only network does not accept new atomic blocks, and any
// pre-Banff atomic block left on disk should have been replaced by the
// Banff history. We return an explicit error so the caller can decide
// whether to surface it as DB corruption or as a soft skip.
func parseV0(c pcodecs.Manager, b []byte) (Block, error) {
v0c := v0CodecFor(c)
var v0blk v0.Block
if _, err := v0c.Unmarshal(b, &v0blk); err != nil {
return nil, err
}
return liftV0(v0blk, b)
}
// v0CodecFor returns the v0 codec.Manager that matches the size class
// of c. We mirror the size class so that a GenesisCodec caller (large
// max size) gets the v0GenesisCodec (also large) rather than v0Codec
// (1 MiB).
func v0CodecFor(c pcodecs.Manager) pcodecs.Manager {
if c == GenesisCodec {
return v0GenesisCodec
}
return v0Codec
}
+27 -42
View File
@@ -1,14 +1,11 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"context"
"fmt"
"time"
"github.com/luxfi/runtime"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
)
@@ -16,47 +13,33 @@ import (
var _ Block = (*ProposalBlock)(nil)
// ProposalBlock is the canonical P-Chain proposal block. It carries a
// per-block timestamp, a single proposal Tx, and a tail of decision Txs that
// commit atomically with the proposal outcome.
// per-block timestamp, a tail of decision txs (stored as the u32 length list +
// blob), and a single proposal Tx that commits atomically with them.
type ProposalBlock struct {
Time uint64 `serialize:"true" json:"time"`
Transactions []*txs.Tx `serialize:"true" json:"txs"`
CommonBlock `serialize:"true"`
Tx *txs.Tx `serialize:"true" json:"tx"`
commonZapBlock
}
func (b *ProposalBlock) Timestamp() time.Time { return time.Unix(int64(b.Time), 0) }
func (b *ProposalBlock) initialize(bytes []byte) error {
b.CommonBlock.initialize(bytes)
if err := b.Tx.InitializeFromBytesAtVersion(txs.Codec, txs.CodecVersionV1); err != nil {
return fmt.Errorf("failed to initialize proposal tx: %w", err)
}
for _, tx := range b.Transactions {
if err := tx.InitializeFromBytesAtVersion(txs.Codec, txs.CodecVersionV1); err != nil {
return fmt.Errorf("failed to initialize decision tx: %w", err)
}
}
return nil
}
func (b *ProposalBlock) InitRuntime(rt *runtime.Runtime) {
b.Tx.Unsigned.InitRuntime(rt)
for _, tx := range b.Transactions {
tx.Unsigned.InitRuntime(rt)
}
// Tx returns the single proposal tx.
func (b *ProposalBlock) Tx() *txs.Tx {
tx, _ := readProposalTx(b.msg.Root())
return tx
}
// Txs returns the decision txs followed by the proposal Tx (last), matching
// the canonical block ordering.
func (b *ProposalBlock) Txs() []*txs.Tx {
l := len(b.Transactions)
out := make([]*txs.Tx, l+1)
copy(out, b.Transactions)
out[l] = b.Tx
decision, _ := readTxList(b.msg.Root(), offBlkTxLengths, offBlkTxBlob)
proposal := b.Tx()
if proposal == nil {
return decision
}
out := make([]*txs.Tx, len(decision)+1)
copy(out, decision)
out[len(decision)] = proposal
return out
}
func (b *ProposalBlock) Visit(v Visitor) error { return v.ProposalBlock(b) }
func (*ProposalBlock) Initialize(context.Context) error { return nil }
func (b *ProposalBlock) Visit(v Visitor) error { return v.ProposalBlock(b) }
func NewProposalBlock(
timestamp time.Time,
@@ -65,11 +48,13 @@ func NewProposalBlock(
proposalTx *txs.Tx,
decisionTxs []*txs.Tx,
) (*ProposalBlock, error) {
blk := &ProposalBlock{
Time: uint64(timestamp.Unix()),
Transactions: decisionTxs,
CommonBlock: CommonBlock{PrntID: parentID, Hght: height},
Tx: proposalTx,
bytes, err := buildBlock(blkProposal, parentID, height, uint64(timestamp.Unix()), decisionTxs, proposalTx)
if err != nil {
return nil, err
}
return blk, initialize(blk, &blk.CommonBlock)
blk := &ProposalBlock{}
if err := blk.setID(bytes); err != nil {
return nil, err
}
return blk, nil
}
+17 -37
View File
@@ -1,14 +1,11 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"context"
"fmt"
"time"
"github.com/luxfi/runtime"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
)
@@ -16,39 +13,19 @@ import (
var _ Block = (*StandardBlock)(nil)
// StandardBlock is the canonical P-Chain standard block. It carries a
// per-block timestamp (advance-all-implicitly removed the separate
// AdvanceTimeTx flow) and an ordered list of decision txs.
// per-block timestamp and an ordered list of decision txs, stored as a u32
// length list + concatenated tx blob in the zap buffer.
type StandardBlock struct {
Time uint64 `serialize:"true" json:"time"`
CommonBlock `serialize:"true"`
Transactions []*txs.Tx `serialize:"true" json:"txs"`
commonZapBlock
}
func (b *StandardBlock) Timestamp() time.Time { return time.Unix(int64(b.Time), 0) }
func (b *StandardBlock) initialize(bytes []byte) error {
b.CommonBlock.initialize(bytes)
for _, tx := range b.Transactions {
// Byte-preserving: re-derive signedBytes by re-marshalling at
// the canonical write version (v1). This is the v1 read path
// — v0 reads go through block.parseV0 + liftedV0Block, which
// rebinds via InitializeFromBytesAtVersion(v0).
if err := tx.InitializeFromBytesAtVersion(txs.Codec, txs.CodecVersionV1); err != nil {
return fmt.Errorf("failed to initialize tx: %w", err)
}
}
return nil
// Txs returns the decision txs in wire order.
func (b *StandardBlock) Txs() []*txs.Tx {
list, _ := readTxList(b.msg.Root(), offBlkTxLengths, offBlkTxBlob)
return list
}
func (b *StandardBlock) InitRuntime(rt *runtime.Runtime) {
for _, tx := range b.Transactions {
tx.Unsigned.InitRuntime(rt)
}
}
func (b *StandardBlock) Txs() []*txs.Tx { return b.Transactions }
func (b *StandardBlock) Visit(v Visitor) error { return v.StandardBlock(b) }
func (*StandardBlock) Initialize(context.Context) error { return nil }
func (b *StandardBlock) Visit(v Visitor) error { return v.StandardBlock(b) }
func NewStandardBlock(
timestamp time.Time,
@@ -56,10 +33,13 @@ func NewStandardBlock(
height uint64,
txs []*txs.Tx,
) (*StandardBlock, error) {
blk := &StandardBlock{
Time: uint64(timestamp.Unix()),
CommonBlock: CommonBlock{PrntID: parentID, Hght: height},
Transactions: txs,
bytes, err := buildBlock(blkStandard, parentID, height, uint64(timestamp.Unix()), txs, nil)
if err != nil {
return nil, err
}
return blk, initialize(blk, &blk.CommonBlock)
blk := &StandardBlock{}
if err := blk.setID(bytes); err != nil {
return nil, err
}
return blk, nil
}
-203
View File
@@ -1,203 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package v0 defines the v1.23.x ("Apricot/Banff") P-Chain block layout.
//
// These types EXIST ONLY FOR DECODING pre-codec-v1 blocks read from disk
// (mainnet, testnet) and from the wire during catch-up. New blocks are
// always written at codec version 1 using the canonical block types in
// the parent block package.
//
// Invariants:
// - Wire layout is byte-for-byte identical to v1.23.31's
// vms/platformvm/block package (slot IDs, struct layout).
// - Each v0 block satisfies block.Block by translating its Visit call
// to the canonical v1 visitor method (ApricotProposalBlock ->
// StandardBlock at proposal-tail position, etc.).
// - Block bytes are byte-preserved (BlockID = hash(input bytes); the
// input bytes are stashed on CommonBlock and returned verbatim by
// Bytes()). The block is never re-marshaled.
// - Embedded txs are byte-preserved via tx.InitializeFromBytes using
// the codec version their bytes were decoded under.
package v0
import (
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/signer"
"github.com/luxfi/node/vms/platformvm/stakeable"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/utxo/secp256k1fx"
)
// CodecVersion is the wire-version prefix for the v1.23.x layout.
const CodecVersion uint16 = 0
// RegisterBlockTypes registers the v0 block + tx type IDs in their
// canonical v1.23.x order on the given linearcodec.
//
// Slot map (block codec):
//
// 0-4 Apricot blocks (Proposal, Abort, Commit, Standard, Atomic)
// 5 secp256k1fx.TransferInput
// 6 [skipped — was MintOutput, never used on P-Chain]
// 7 secp256k1fx.TransferOutput
// 8 [skipped — was MintOperation, never used on P-Chain]
// 9-11 secp256k1fx.{Credential, Input, OutputOwners}
// 12-20 Apricot txs (AddValidator..RewardValidator)
// 21-22 stakeable.{LockIn, LockOut}
// 23-26 Banff txs (RemoveChainValidator..AddPermissionlessDelegator)
// 27-28 signer.{Empty, ProofOfPossession}
// 29-32 Banff blocks (Proposal, Abort, Commit, Standard)
// 33-34 Durango txs (TransferChainOwnership, BaseTx)
// 35-39 Etna txs (ConvertChainToL1..DisableL1Validator)
//
// ConvertChainToL1Tx is registered as the wire-format-equivalent
// ConvertNetworkToL1Tx (post-rename source type, identical struct
// layout, same slot 35). Codec wire matching is by slot ID, not type
// name; the rename did not change the serialized format.
func RegisterBlockTypes(c pcodecs.LinearCodec) error {
errs := pcodecs.Errs{}
// Slots 0-4: Apricot blocks.
errs.Add(
c.RegisterType(&ApricotProposalBlock{}),
c.RegisterType(&ApricotAbortBlock{}),
c.RegisterType(&ApricotCommitBlock{}),
c.RegisterType(&ApricotStandardBlock{}),
c.RegisterType(&ApricotAtomicBlock{}),
)
// Slots 5-11: secp256k1fx with two historical skips at 6 and 8.
errs.Add(c.RegisterType(&secp256k1fx.TransferInput{}))
c.SkipRegistrations(1)
errs.Add(c.RegisterType(&secp256k1fx.TransferOutput{}))
c.SkipRegistrations(1)
errs.Add(
c.RegisterType(&secp256k1fx.Credential{}),
c.RegisterType(&secp256k1fx.Input{}),
c.RegisterType(&secp256k1fx.OutputOwners{}),
)
// Slots 12-20: Apricot txs.
errs.Add(
c.RegisterType(&txs.AddValidatorTx{}),
c.RegisterType(&txs.AddChainValidatorTx{}),
c.RegisterType(&txs.AddDelegatorTx{}),
c.RegisterType(&txs.CreateNetworkTx{}),
c.RegisterType(&txs.CreateChainTx{}),
c.RegisterType(&txs.ImportTx{}),
c.RegisterType(&txs.ExportTx{}),
c.RegisterType(&txs.AdvanceTimeTx{}),
c.RegisterType(&txs.RewardValidatorTx{}),
)
// Slots 21-22: stakeable locks.
errs.Add(
c.RegisterType(&stakeable.LockIn{}),
c.RegisterType(&stakeable.LockOut{}),
)
// Slots 23-26: Banff txs.
errs.Add(
c.RegisterType(&txs.RemoveChainValidatorTx{}),
c.RegisterType(&txs.TransformChainTx{}),
c.RegisterType(&txs.AddPermissionlessValidatorTx{}),
c.RegisterType(&txs.AddPermissionlessDelegatorTx{}),
)
// Slots 27-28: signer types.
errs.Add(
c.RegisterType(&signer.Empty{}),
c.RegisterType(&signer.ProofOfPossession{}),
)
// Slots 29-32: Banff blocks.
errs.Add(
c.RegisterType(&BanffProposalBlock{}),
c.RegisterType(&BanffAbortBlock{}),
c.RegisterType(&BanffCommitBlock{}),
c.RegisterType(&BanffStandardBlock{}),
)
// Slots 33-34: Durango txs.
errs.Add(
c.RegisterType(&txs.TransferChainOwnershipTx{}),
c.RegisterType(&txs.BaseTx{}),
)
// Slots 35-39: Etna txs.
// Slot 35 was ConvertChainToL1Tx in v0 source; renamed to
// ConvertNetworkToL1Tx in v1.27.x with identical wire layout.
errs.Add(
c.RegisterType(&txs.ConvertNetworkToL1Tx{}),
c.RegisterType(&txs.RegisterL1ValidatorTx{}),
c.RegisterType(&txs.SetL1ValidatorWeightTx{}),
c.RegisterType(&txs.IncreaseL1ValidatorBalanceTx{}),
c.RegisterType(&txs.DisableL1ValidatorTx{}),
)
return errs.Err
}
// RegisterTxTypes registers the v0 type IDs in the tx-only ordering
// used by v1.23.x txs.init(). Apricot block slots (0-4) and Banff block
// slots (29-32) are SkipRegistrations rather than RegisterType so that
// shared tx slots line up with the block codec.
func RegisterTxTypes(c pcodecs.LinearCodec) error {
errs := pcodecs.Errs{}
// Slots 0-4: Apricot block slots reserved.
c.SkipRegistrations(5)
// Slots 5-11: secp256k1fx (with two historical skips at 6 and 8).
errs.Add(c.RegisterType(&secp256k1fx.TransferInput{}))
c.SkipRegistrations(1)
errs.Add(c.RegisterType(&secp256k1fx.TransferOutput{}))
c.SkipRegistrations(1)
errs.Add(
c.RegisterType(&secp256k1fx.Credential{}),
c.RegisterType(&secp256k1fx.Input{}),
c.RegisterType(&secp256k1fx.OutputOwners{}),
)
// Slots 12-22: Apricot txs + stakeable locks.
errs.Add(
c.RegisterType(&txs.AddValidatorTx{}),
c.RegisterType(&txs.AddChainValidatorTx{}),
c.RegisterType(&txs.AddDelegatorTx{}),
c.RegisterType(&txs.CreateNetworkTx{}),
c.RegisterType(&txs.CreateChainTx{}),
c.RegisterType(&txs.ImportTx{}),
c.RegisterType(&txs.ExportTx{}),
c.RegisterType(&txs.AdvanceTimeTx{}),
c.RegisterType(&txs.RewardValidatorTx{}),
c.RegisterType(&stakeable.LockIn{}),
c.RegisterType(&stakeable.LockOut{}),
)
// Slots 23-28: Banff txs + signer.
errs.Add(
c.RegisterType(&txs.RemoveChainValidatorTx{}),
c.RegisterType(&txs.TransformChainTx{}),
c.RegisterType(&txs.AddPermissionlessValidatorTx{}),
c.RegisterType(&txs.AddPermissionlessDelegatorTx{}),
c.RegisterType(&signer.Empty{}),
c.RegisterType(&signer.ProofOfPossession{}),
)
// Slots 29-32: Banff block slots reserved.
c.SkipRegistrations(4)
// Slots 33-39: Durango + Etna.
errs.Add(
c.RegisterType(&txs.TransferChainOwnershipTx{}),
c.RegisterType(&txs.BaseTx{}),
c.RegisterType(&txs.ConvertNetworkToL1Tx{}),
c.RegisterType(&txs.RegisterL1ValidatorTx{}),
c.RegisterType(&txs.SetL1ValidatorWeightTx{}),
c.RegisterType(&txs.IncreaseL1ValidatorBalanceTx{}),
c.RegisterType(&txs.DisableL1ValidatorTx{}),
)
return errs.Err
}
-142
View File
@@ -1,142 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package v0
import (
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
)
// Block is the v0-decoder-side counterpart of block.Block. It is
// satisfied by every concrete v0 block kind so that the v0 codec can
// dispatch on an interface destination at unmarshal time. The parent
// block package wraps the returned v0.Block in an adapter that
// implements block.Block (which in turn dispatches to the canonical v1
// Visitor).
type Block interface {
// ParentID is the BlockID of the parent block.
ParentID() ids.ID
// Height is the block height. Genesis is at height 0.
Height() uint64
// TimestampUnix returns the block-header timestamp encoded in this
// block (Banff and later). Apricot blocks carry no header timestamp
// and return 0; the parent timestamp is used in their stead by the
// adapter.
TimestampUnix() uint64
// Txs returns the txs carried by this block in their canonical
// order. For ApricotProposalBlock the proposal Tx is the sole
// element; for BanffProposalBlock the proposal Tx is the trailing
// element (matching the v1.23.31 ProposalBlock.Txs() shape).
Txs() []*txs.Tx
}
// CommonBlock holds the parent-id + height fields shared across every
// v0 block kind. Wire layout: PrntID (ids.ID, 32 bytes) followed by
// Hght (uint64, 8 bytes). Byte-equal to v1.23.31 CommonBlock.
type CommonBlock struct {
PrntID ids.ID `serialize:"true" json:"parentID"`
Hght uint64 `serialize:"true" json:"height"`
}
// ApricotProposalBlock decodes slot 0. Carries no header timestamp;
// pre-Banff time advances were carried in AdvanceTimeTx, not in the
// block header.
type ApricotProposalBlock struct {
CommonBlock `serialize:"true"`
Tx *txs.Tx `serialize:"true" json:"tx"`
}
func (b *ApricotProposalBlock) ParentID() ids.ID { return b.PrntID }
func (b *ApricotProposalBlock) Height() uint64 { return b.Hght }
func (*ApricotProposalBlock) TimestampUnix() uint64 { return 0 }
func (b *ApricotProposalBlock) Txs() []*txs.Tx { return []*txs.Tx{b.Tx} }
// ApricotAbortBlock decodes slot 1.
type ApricotAbortBlock struct {
CommonBlock `serialize:"true"`
}
func (b *ApricotAbortBlock) ParentID() ids.ID { return b.PrntID }
func (b *ApricotAbortBlock) Height() uint64 { return b.Hght }
func (*ApricotAbortBlock) TimestampUnix() uint64 { return 0 }
func (*ApricotAbortBlock) Txs() []*txs.Tx { return nil }
// ApricotCommitBlock decodes slot 2.
type ApricotCommitBlock struct {
CommonBlock `serialize:"true"`
}
func (b *ApricotCommitBlock) ParentID() ids.ID { return b.PrntID }
func (b *ApricotCommitBlock) Height() uint64 { return b.Hght }
func (*ApricotCommitBlock) TimestampUnix() uint64 { return 0 }
func (*ApricotCommitBlock) Txs() []*txs.Tx { return nil }
// ApricotStandardBlock decodes slot 3.
type ApricotStandardBlock struct {
CommonBlock `serialize:"true"`
Transactions []*txs.Tx `serialize:"true" json:"txs"`
}
func (b *ApricotStandardBlock) ParentID() ids.ID { return b.PrntID }
func (b *ApricotStandardBlock) Height() uint64 { return b.Hght }
func (*ApricotStandardBlock) TimestampUnix() uint64 { return 0 }
func (b *ApricotStandardBlock) Txs() []*txs.Tx { return b.Transactions }
// ApricotAtomicBlock decodes slot 4. The atomic block flow is dead on
// the modern P-only network — Apricot atomic blocks only ever appear
// pre-Banff in legacy chain history.
type ApricotAtomicBlock struct {
CommonBlock `serialize:"true"`
Tx *txs.Tx `serialize:"true" json:"tx"`
}
func (b *ApricotAtomicBlock) ParentID() ids.ID { return b.PrntID }
func (b *ApricotAtomicBlock) Height() uint64 { return b.Hght }
func (*ApricotAtomicBlock) TimestampUnix() uint64 { return 0 }
func (b *ApricotAtomicBlock) Txs() []*txs.Tx { return []*txs.Tx{b.Tx} }
// BanffProposalBlock decodes slot 29. Carries a Banff-era per-block
// timestamp ahead of the embedded Apricot proposal block (which still
// holds the proposal Tx). The on-wire field order is Time, then
// Transactions (the decision-tx tail), then ApricotProposalBlock.
type BanffProposalBlock struct {
Time uint64 `serialize:"true" json:"time"`
Transactions []*txs.Tx `serialize:"true" json:"txs"`
ApricotProposalBlock `serialize:"true"`
}
func (b *BanffProposalBlock) TimestampUnix() uint64 { return b.Time }
func (b *BanffProposalBlock) Txs() []*txs.Tx {
out := make([]*txs.Tx, len(b.Transactions)+1)
copy(out, b.Transactions)
out[len(b.Transactions)] = b.ApricotProposalBlock.Tx
return out
}
// BanffAbortBlock decodes slot 30.
type BanffAbortBlock struct {
Time uint64 `serialize:"true" json:"time"`
ApricotAbortBlock `serialize:"true"`
}
func (b *BanffAbortBlock) TimestampUnix() uint64 { return b.Time }
// BanffCommitBlock decodes slot 31.
type BanffCommitBlock struct {
Time uint64 `serialize:"true" json:"time"`
ApricotCommitBlock `serialize:"true"`
}
func (b *BanffCommitBlock) TimestampUnix() uint64 { return b.Time }
// BanffStandardBlock decodes slot 32.
type BanffStandardBlock struct {
Time uint64 `serialize:"true" json:"time"`
ApricotStandardBlock `serialize:"true"`
}
func (b *BanffStandardBlock) TimestampUnix() uint64 { return b.Time }