mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user