Full-codec-kill wave 2: proposervm + xsvm + components + evm + platformvm-residual → native ZAP

Kills pcodecs from 3 complete subsystems (verified go test ./... = 155 ok / 0 FAIL):
- proposervm: block/state/summary struct-is-wire (blockwire.go + statewire.go;
  deleted block/state/summary codec.go). blockKind bytes: reserved=0 signed=1
  option=2. Epoch inlined in unsigned object. proposer/windower chainSource seed
  = binary.LittleEndian (byte-identical to old wrappers.Packer.UnpackLong).
- example/xsvm: tx/block/genesis native Marshal (deleted 3 codec.go); create-chain
  example uses genesis.Marshal().
- components: message.Tx native (deleted codec.go); keystore codec shell removed;
  index pcodecs.LongLen → local const.
- evm/{predicate,lp176}: off pcodecs.
- platformvm residual: state metadata + genesis + metrics + signer + stakeable
  native, vestigial serialize tags removed.

REMAINING (careful solo, agents weekly-capped): warp (reverted — agent left an
ID≠Marshal consensus inconsistency in ChainToL1Conversion; needs proper review,
not a golden-regen) + xvm (reverted — barely started). pcodecs package stays
until those 2 land. /ext/→/v1/ endpoint change also pending.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-11 17:19:20 -07:00
co-authored by Hanzo Dev
parent 7123b7399e
commit 2d227dd3aa
67 changed files with 1581 additions and 1094 deletions
+3 -3
View File
@@ -16,7 +16,7 @@ import (
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/utils/wrappers"
lux "github.com/luxfi/utxo"
)
@@ -143,7 +143,7 @@ func (i *indexer) Accept(txID ids.ID, inputUTXOs []*lux.UTXO, outputUTXOs []*lux
idx = binary.BigEndian.Uint64(idxBytes)
case database.ErrNotFound:
// idx not found; this must be the first entry.
idxBytes = make([]byte, pcodecs.LongLen)
idxBytes = make([]byte, wrappers.LongLen)
default:
// Unexpected error
return fmt.Errorf("unexpected error when indexing txID %s: %w", txID, err)
@@ -184,7 +184,7 @@ func (i *indexer) Read(address []byte, assetID ids.ID, cursor, pageSize uint64)
assetPrefixDB := prefixdb.New(assetID[:], addressTxDB)
// get cursor in bytes
cursorBytes := make([]byte, pcodecs.LongLen)
cursorBytes := make([]byte, wrappers.LongLen)
binary.BigEndian.PutUint64(cursorBytes, cursor)
// start reading from the cursor bytes, numeric keys maintain the order (see Accept)
-32
View File
@@ -1,32 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keystore
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
)
const CodecVersion = 0
var (
Codec pcodecs.Manager
LegacyCodec pcodecs.Manager
)
func init() {
c := pcodecs.NewLinearCodec()
Codec = pcodecs.NewDefaultManager()
lc := pcodecs.NewLinearCodec()
LegacyCodec = pcodecs.NewMaxInt32Manager()
err := errors.Join(
Codec.RegisterCodec(CodecVersion, c),
LegacyCodec.RegisterCodec(CodecVersion, lc),
)
if err != nil {
panic(err)
}
}
+29 -8
View File
@@ -71,9 +71,34 @@ func (u *user) GetAddresses() ([]ids.ShortID, error) {
return nil, err
}
var addresses []ids.ShortID
_, err = LegacyCodec.Unmarshal(addressBytes, &addresses)
return addresses, err
return parseAddresses(addressBytes)
}
// marshalAddresses encodes the user's controlled addresses as the flat
// concatenation of their 20-byte values. ids.ShortID is fixed-width, so the
// count is implied by len/ShortIDLen — no length prefix or codec is needed.
func marshalAddresses(addresses []ids.ShortID) []byte {
b := make([]byte, 0, len(addresses)*ids.ShortIDLen)
for i := range addresses {
b = append(b, addresses[i][:]...)
}
return b
}
// parseAddresses is the inverse of marshalAddresses.
func parseAddresses(b []byte) ([]ids.ShortID, error) {
if len(b)%ids.ShortIDLen != 0 {
return nil, fmt.Errorf("keystore: address blob length %d is not a multiple of %d", len(b), ids.ShortIDLen)
}
n := len(b) / ids.ShortIDLen
if n == 0 {
return nil, nil
}
addresses := make([]ids.ShortID, n)
for i := 0; i < n; i++ {
copy(addresses[i][:], b[i*ids.ShortIDLen:])
}
return addresses, nil
}
func (u *user) PutKeys(privKeys ...*secp256k1.PrivateKey) error {
@@ -119,11 +144,7 @@ func (u *user) PutKeys(privKeys ...*secp256k1.PrivateKey) error {
addresses = append(addresses, address)
}
addressBytes, err := Codec.Marshal(CodecVersion, addresses)
if err != nil {
return err
}
return u.db.Put(addressesKey, addressBytes)
return u.db.Put(addressesKey, marshalAddresses(addresses))
}
func (u *user) GetKey(address ids.ShortID) (*secp256k1.PrivateKey, error) {
-32
View File
@@ -1,32 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"github.com/luxfi/constants"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/utils"
)
const (
codecVersion = 0
maxMessageSize = 512 * constants.KiB
maxSliceLen = maxMessageSize
)
// Codec does serialization and deserialization
var c pcodecs.Manager
func init() {
c = pcodecs.NewManager(maxMessageSize)
lc := pcodecs.NewLinearCodec()
err := utils.Err(
lc.RegisterType(&Tx{}),
c.RegisterCodec(codecVersion, lc),
)
if err != nil {
panic(err)
}
}
+49 -9
View File
@@ -3,16 +3,45 @@
package message
// Native ZAP wire for gossip messages: the struct IS the wire. Each message
// is one zap object keyed by a 1-byte kind discriminator at object offset 0 —
// the whole dispatch. Parse reads it and returns the typed message. There is
// no codec, no version prefix, no slot map.
//
// Object fixed section (offsets object-relative, little-endian):
//
// kind u8 @ 0 tx=1
// Tx bytes @ 1 ptr to the gossiped tx bytes (Tx only)
import (
"errors"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
var (
_ Message = (*Tx)(nil)
ErrUnexpectedCodecVersion = errors.New("unexpected codec version")
// ErrUnknownMessageKind is returned when the 1-byte kind discriminator at
// object offset 0 does not correspond to a known message type.
ErrUnknownMessageKind = errors.New("unknown message kind")
)
// msgKind is the 1-byte discriminator at object offset 0 of every message
// buffer. Parse reads it and dispatches to the typed message.
type msgKind uint8
const (
msgKindReserved msgKind = iota
msgKindTx
)
// Fixed wire offsets (object-relative) and object size for a Tx message.
const (
offMsgKind = 0
offMsgTx = 1
sizeMsgTx = 9 // kind(1) + bytes ptr(8)
)
type Message interface {
@@ -26,6 +55,11 @@ type Message interface {
//
// Bytes should only be called after being initialized
Bytes() []byte
// marshal encodes the message to its native ZAP wire form (kind byte at
// object offset 0). Each concrete type writes its own kind — this is the
// whole dispatch, no codec.
marshal() ([]byte, error)
}
type message []byte
@@ -39,20 +73,26 @@ func (m *message) Bytes() []byte {
}
func Parse(bytes []byte) (Message, error) {
var msg Message
version, err := c.Unmarshal(bytes, &msg)
zmsg, err := zap.Parse(bytes)
if err != nil {
return nil, err
}
if version != codecVersion {
return nil, ErrUnexpectedCodecVersion
obj := zmsg.Root()
switch msgKind(obj.Uint8(offMsgKind)) {
case msgKindTx:
msg := &Tx{Tx: append([]byte(nil), obj.Bytes(offMsgTx)...)}
msg.initialize(bytes)
return msg, nil
default:
return nil, ErrUnknownMessageKind
}
msg.initialize(bytes)
return msg, nil
}
func Build(msg Message) ([]byte, error) {
bytes, err := c.Marshal(codecVersion, &msg)
bytes, err := msg.marshal()
if err != nil {
return nil, err
}
msg.initialize(bytes)
return bytes, err
return bytes, nil
}
+16 -2
View File
@@ -3,16 +3,30 @@
package message
import "github.com/luxfi/ids"
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
var _ Message = (*Tx)(nil)
type Tx struct {
message
Tx []byte `serialize:"true"`
Tx []byte
}
func (msg *Tx) Handle(handler Handler, nodeID ids.NodeID, requestID uint32) error {
return handler.HandleTx(nodeID, requestID, msg)
}
// marshal writes the Tx message as one native ZAP object: kind byte at
// offset 0, gossiped tx bytes at offset 1.
func (msg *Tx) marshal() ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + sizeMsgTx + len(msg.Tx))
ob := b.StartObject(sizeMsgTx)
ob.SetUint8(offMsgKind, uint8(msgKindTx))
ob.SetBytes(offMsgTx, msg.Tx)
ob.FinishAsRoot()
return b.Finish(), nil
}
+6 -6
View File
@@ -17,7 +17,7 @@ import (
safemath "github.com/luxfi/math"
"github.com/luxfi/node/vms/components/gas"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/utils/wrappers"
)
const (
@@ -35,7 +35,7 @@ const (
MinMaxPerSecond = MinTargetPerSecond * TargetToMax
MinMaxCapacity = MinMaxPerSecond * TimeToFillCapacity
StateSize = 3 * pcodecs.LongLen
StateSize = 3 * wrappers.LongLen
maxTargetExcess = 1_024_950_627 // TargetConversion * ln(MaxUint64 / MinTargetPerSecond) + 1
)
@@ -63,9 +63,9 @@ func ParseState(bytes []byte) (State, error) {
return State{
Gas: gas.State{
Capacity: gas.Gas(binary.BigEndian.Uint64(bytes)),
Excess: gas.Gas(binary.BigEndian.Uint64(bytes[pcodecs.LongLen:])),
Excess: gas.Gas(binary.BigEndian.Uint64(bytes[wrappers.LongLen:])),
},
TargetExcess: gas.Gas(binary.BigEndian.Uint64(bytes[2*pcodecs.LongLen:])),
TargetExcess: gas.Gas(binary.BigEndian.Uint64(bytes[2*wrappers.LongLen:])),
}, nil
}
@@ -172,8 +172,8 @@ func (s *State) UpdateTargetExcess(desiredTargetExcess gas.Gas) {
func (s *State) Bytes() []byte {
bytes := make([]byte, StateSize)
binary.BigEndian.PutUint64(bytes, uint64(s.Gas.Capacity))
binary.BigEndian.PutUint64(bytes[pcodecs.LongLen:], uint64(s.Gas.Excess))
binary.BigEndian.PutUint64(bytes[2*pcodecs.LongLen:], uint64(s.TargetExcess))
binary.BigEndian.PutUint64(bytes[wrappers.LongLen:], uint64(s.Gas.Excess))
binary.BigEndian.PutUint64(bytes[2*wrappers.LongLen:], uint64(s.TargetExcess))
return bytes
}
+6 -6
View File
@@ -17,7 +17,7 @@ import (
safemath "github.com/luxfi/math"
"github.com/luxfi/node/vms/components/gas"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/utils/wrappers"
)
const (
@@ -35,7 +35,7 @@ const (
MinMaxPerSecond = MinTargetPerSecond * TargetToMax
MinMaxCapacity = MinMaxPerSecond * TimeToFillCapacity
StateSize = 3 * pcodecs.LongLen
StateSize = 3 * wrappers.LongLen
maxTargetExcess = 1_024_950_627 // TargetConversion * ln(MaxUint64 / MinTargetPerSecond) + 1
)
@@ -63,9 +63,9 @@ func ParseState(bytes []byte) (State, error) {
return State{
Gas: gas.State{
Capacity: gas.Gas(binary.BigEndian.Uint64(bytes)),
Excess: gas.Gas(binary.BigEndian.Uint64(bytes[pcodecs.LongLen:])),
Excess: gas.Gas(binary.BigEndian.Uint64(bytes[wrappers.LongLen:])),
},
TargetExcess: gas.Gas(binary.BigEndian.Uint64(bytes[2*pcodecs.LongLen:])),
TargetExcess: gas.Gas(binary.BigEndian.Uint64(bytes[2*wrappers.LongLen:])),
}, nil
}
@@ -161,8 +161,8 @@ func (s *State) UpdateTargetExcess(desiredTargetExcess gas.Gas) {
func (s *State) Bytes() []byte {
bytes := make([]byte, StateSize)
binary.BigEndian.PutUint64(bytes, uint64(s.Gas.Capacity))
binary.BigEndian.PutUint64(bytes[pcodecs.LongLen:], uint64(s.Gas.Excess))
binary.BigEndian.PutUint64(bytes[2*pcodecs.LongLen:], uint64(s.TargetExcess))
binary.BigEndian.PutUint64(bytes[wrappers.LongLen:], uint64(s.Gas.Excess))
binary.BigEndian.PutUint64(bytes[2*wrappers.LongLen:], uint64(s.TargetExcess))
return bytes
}
+187 -32
View File
@@ -3,46 +3,51 @@
package predicate
// Native ZAP wire for predicate results: the nested map IS the wire. There is
// no codec, no version prefix, no slot map. The value is a
// map[txHash]map[precompileAddr]resultBytes; each level is encoded as a
// (u32 per-entry length list, concatenated blob) so a variable number of
// variable-length children packs into one zap object. Map keys are sorted by
// their raw bytes before encoding so the output is deterministic — required
// because these results are committed as part of block building/verification.
//
// Object fixed sections (offsets object-relative, little-endian):
//
// root txLenList ptr @0, txBlob ptr @8 size 16
// txEntry hash 32B @0, addrLenList ptr @32, addrBlob ptr @40 size 48
// addrEntry addr 20B @0, result bytes ptr @20 size 28
import (
"bytes"
"fmt"
"slices"
"github.com/luxfi/geth/common"
"github.com/luxfi/constants"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/zap"
)
const (
version = 0
// addrEntry: precompile address + its result bytes.
prAddrAddr = 0
prAddrBytes = 20
prAddrSize = 28
// The Results maximum size should comfortably exceed the maximum value that could happen in practice,
// so that a correct block builder will not attempt to build a block and fail to marshal the predicate results using the codec.
//
// We make this easy to reason about by assigning a minimum gas cost to the `PredicateGas` function of precompiles.
// In the case of Warp, the minimum gas cost is set to 200k gas, which can lead to at most 32 additional bytes being included in Results.
//
// The additional bytes come from the transaction hash (32 bytes), length of tx predicate results (4 bytes),
// the precompile address (20 bytes), length of the bytes result (4 bytes), and the additional byte in the results bitset (1 byte).
//
// This results in 200k gas contributing a maximum of 61 additional bytes to Result.
// For a block with a maximum gas limit of 100M, the block can include up to 500 validated predicates based contributing to the size of Result.
//
// At 61 bytes / validated predicate, this yields ~30KB, which is well short of the 1MB cap.
maxResultsSize = constants.MiB
// txEntry: tx hash + the per-address length list and blob.
prTxHash = 0
prTxAddrLen = 32
prTxAddrBlob = 40
prTxSize = 48
// root: the per-tx length list and blob.
prRootTxLen = 0
prRootTxBlob = 8
prRootSize = 16
prLenStride = 4 // uint32 per-entry length
)
var resultsCodec pcodecs.Manager
func init() {
resultsCodec = pcodecs.NewManager(maxResultsSize)
c := pcodecs.NewLinearCodec()
if err := resultsCodec.RegisterCodec(version, c); err != nil {
panic(err)
}
}
type (
// PrecompileResults is a map of results for each precompile address to the
// resulting bitset.
@@ -58,8 +63,7 @@ type (
// ParseBlockResults parses bytes into predicate results.
func ParseBlockResults(b []byte) (BlockResults, error) {
var encodedResults encodedBlockResults
_, err := resultsCodec.Unmarshal(b, &encodedResults)
encodedResults, err := parseEncodedBlockResults(b)
if err != nil {
return BlockResults{}, fmt.Errorf("failed to unmarshal predicate results: %w", err)
}
@@ -102,7 +106,7 @@ func (b *BlockResults) Set(txHash common.Hash, txResults PrecompileResults) {
// Bytes marshals the predicate results.
func (b *BlockResults) Bytes() ([]byte, error) {
// Convert to results representation before marshaling to avoid serializing
// set.Bits directly, which is not supported by the codec.
// set.Bits directly, which is not supported by the native wire.
results := make(encodedBlockResults, len(*b))
for txHash, addrToBits := range *b {
encoded := make(map[common.Address][]byte, len(addrToBits))
@@ -112,5 +116,156 @@ func (b *BlockResults) Bytes() ([]byte, error) {
results[txHash] = encoded
}
return resultsCodec.Marshal(version, results)
return marshalEncodedBlockResults(results), nil
}
// marshalEncodedBlockResults encodes the nested map as one native ZAP object
// tree. Tx hashes are sorted so the output is byte-deterministic.
func marshalEncodedBlockResults(results encodedBlockResults) []byte {
b := zap.NewBuilder(zap.HeaderSize + prRootSize + 256)
hashes := make([]common.Hash, 0, len(results))
for h := range results {
hashes = append(hashes, h)
}
slices.SortFunc(hashes, func(a, b common.Hash) int { return bytes.Compare(a[:], b[:]) })
var (
txBlob []byte
lenOff int
lenCount int
)
if len(hashes) > 0 {
lb := b.StartList(prLenStride)
for _, h := range hashes {
entry := marshalTxEntry(h, results[h])
lb.AddUint32(uint32(len(entry)))
txBlob = append(txBlob, entry...)
}
lenOff, lenCount = lb.Finish()
}
ob := b.StartObject(prRootSize)
ob.SetList(prRootTxLen, lenOff, lenCount)
ob.SetBytes(prRootTxBlob, txBlob)
ob.FinishAsRoot()
return b.Finish()
}
// marshalTxEntry encodes one (txHash → addr map) as its own zap object.
// Addresses are sorted for determinism.
func marshalTxEntry(hash common.Hash, addrToBytes map[common.Address][]byte) []byte {
b := zap.NewBuilder(zap.HeaderSize + prTxSize + 128)
addrs := make([]common.Address, 0, len(addrToBytes))
for a := range addrToBytes {
addrs = append(addrs, a)
}
slices.SortFunc(addrs, func(a, b common.Address) int { return bytes.Compare(a[:], b[:]) })
var (
addrBlob []byte
lenOff int
lenCount int
)
if len(addrs) > 0 {
lb := b.StartList(prLenStride)
for _, a := range addrs {
entry := marshalAddrEntry(a, addrToBytes[a])
lb.AddUint32(uint32(len(entry)))
addrBlob = append(addrBlob, entry...)
}
lenOff, lenCount = lb.Finish()
}
ob := b.StartObject(prTxSize)
ob.SetBytesFixed(prTxHash, hash[:])
ob.SetList(prTxAddrLen, lenOff, lenCount)
ob.SetBytes(prTxAddrBlob, addrBlob)
ob.FinishAsRoot()
return b.Finish()
}
// marshalAddrEntry encodes one (precompile address → result bytes) as its own
// zap object.
func marshalAddrEntry(addr common.Address, result []byte) []byte {
b := zap.NewBuilder(zap.HeaderSize + prAddrSize + len(result))
ob := b.StartObject(prAddrSize)
ob.SetBytesFixed(prAddrAddr, addr[:])
ob.SetBytes(prAddrBytes, result)
ob.FinishAsRoot()
return b.Finish()
}
func parseEncodedBlockResults(bs []byte) (encodedBlockResults, error) {
zmsg, err := zap.Parse(bs)
if err != nil {
return nil, err
}
root := zmsg.Root()
lengths := root.ListStride(prRootTxLen, prLenStride)
n := lengths.Len()
results := make(encodedBlockResults, n)
if n == 0 {
return results, nil
}
blob := root.Bytes(prRootTxBlob)
cursor := 0
for i := 0; i < n; i++ {
size := int(lengths.Uint32(i))
if cursor+size > len(blob) {
return nil, fmt.Errorf("predicate: tx entry %d length %d overruns blob (%d)", i, size, len(blob))
}
hash, addrMap, err := parseTxEntry(blob[cursor : cursor+size])
if err != nil {
return nil, err
}
results[hash] = addrMap
cursor += size
}
return results, nil
}
func parseTxEntry(bs []byte) (common.Hash, map[common.Address][]byte, error) {
zmsg, err := zap.Parse(bs)
if err != nil {
return common.Hash{}, nil, err
}
obj := zmsg.Root()
var hash common.Hash
copy(hash[:], obj.BytesFixedSlice(prTxHash, len(hash)))
lengths := obj.ListStride(prTxAddrLen, prLenStride)
n := lengths.Len()
addrMap := make(map[common.Address][]byte, n)
if n == 0 {
return hash, addrMap, nil
}
blob := obj.Bytes(prTxAddrBlob)
cursor := 0
for i := 0; i < n; i++ {
size := int(lengths.Uint32(i))
if cursor+size > len(blob) {
return common.Hash{}, nil, fmt.Errorf("predicate: addr entry %d length %d overruns blob (%d)", i, size, len(blob))
}
addr, result, err := parseAddrEntry(blob[cursor : cursor+size])
if err != nil {
return common.Hash{}, nil, err
}
addrMap[addr] = result
cursor += size
}
return hash, addrMap, nil
}
func parseAddrEntry(bs []byte) (common.Address, []byte, error) {
zmsg, err := zap.Parse(bs)
if err != nil {
return common.Address{}, nil, err
}
obj := zmsg.Root()
var addr common.Address
copy(addr[:], obj.BytesFixedSlice(prAddrAddr, len(addr)))
result := append([]byte(nil), obj.Bytes(prAddrBytes)...)
return addr, result, nil
}
+16 -87
View File
@@ -4,59 +4,33 @@
package predicate
import (
"slices"
"testing"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
)
// Valid result parsing is tested by [TestBlockResultsBytes]
// Valid result parsing/round-tripping is tested by [TestBlockResultsBytes]
func TestParseBlockResultsInvalid(t *testing.T) {
tests := []struct {
name string
b []byte
wantErr error
name string
b []byte
}{
{
name: "nil",
b: nil,
wantErr: pcodecs.ErrCantUnpackVersion,
name: "nil",
b: nil,
},
{
name: "too_big",
b: slices.Concat(
[]byte{
// codecID
0x00, 0x00,
// BlockResults length
0x00, 0x00, 0x00, 0x01,
// txHash
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// PrecompileResults length
0x00, 0x00, 0x00, 0x01,
// precompile address
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// Length of bitset
0x01, 0x00, 0x00, 0x00, // 2^24
},
make([]byte, 1<<24), // Append the bitset
),
wantErr: pcodecs.ErrMaxSizeExceeded,
name: "truncated_header",
b: []byte{0x00, 0x01, 0x02},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := ParseBlockResults(test.b)
require.ErrorIs(t, err, test.wantErr)
require.Error(t, err)
})
}
}
@@ -188,31 +162,21 @@ func TestBlockResultsSet(t *testing.T) {
}
}
// TestBlockResultsBytes checks that the native ZAP encoding round-trips and is
// deterministic (byte-identical across marshals of the same value — required
// because the results are committed during block building/verification).
func TestBlockResultsBytes(t *testing.T) {
tests := []struct {
name string
input BlockResults
want []byte
}{
{
name: "nil",
input: nil,
want: []byte{
// codecID
0x00, 0x00,
// results length
0x00, 0x00, 0x00, 0x00,
},
},
{
name: "empty",
input: make(BlockResults),
want: []byte{
// codecID
0x00, 0x00,
// results length
0x00, 0x00, 0x00, 0x00,
},
},
{
name: "single_tx_single_result",
@@ -221,13 +185,6 @@ func TestBlockResultsBytes(t *testing.T) {
{2}: set.NewBits(1, 2, 3),
},
},
want: []byte{
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x0e,
},
},
{
name: "single_tx_multiple_results",
@@ -237,14 +194,6 @@ func TestBlockResultsBytes(t *testing.T) {
{3}: set.NewBits(0, 1, 2, 3),
},
},
want: []byte{
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0f,
},
},
{
name: "multiple_txs_single_result",
@@ -256,16 +205,6 @@ func TestBlockResultsBytes(t *testing.T) {
{3}: set.NewBits(3, 2, 1),
},
},
want: []byte{
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0e,
},
},
{
name: "multiple_txs_multiple_results",
@@ -279,20 +218,6 @@ func TestBlockResultsBytes(t *testing.T) {
{3}: set.NewBits(3, 2, 1),
},
},
want: []byte{
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x0e,
},
},
}
for _, test := range tests {
@@ -301,7 +226,11 @@ func TestBlockResultsBytes(t *testing.T) {
got, err := test.input.Bytes()
require.NoError(err)
require.Equal(test.want, got)
// Deterministic: a second marshal of the same value is byte-identical.
got2, err := test.input.Bytes()
require.NoError(err)
require.Equal(got, got2)
input := test.input
if input == nil {
+1 -1
View File
@@ -126,7 +126,7 @@ func (c *Client) IssueTx(
newTx *tx.Tx,
options ...rpc.Option,
) (ids.ID, error) {
txBytes, err := tx.Codec.Marshal(tx.CodecVersion, newTx)
txBytes, err := newTx.Marshal()
if err != nil {
return ids.Empty, err
}
+107 -9
View File
@@ -3,35 +3,133 @@
package block
// Native ZAP wire for xsvm blocks: the struct IS the wire. One zap object per
// block; the txs are self-describing via their own tx.Marshal(), so a block
// stores the per-tx byte lengths as a u32 list and the concatenated tx bytes as
// a blob, then Parse re-splits and hands each slice to tx.Parse. There is no
// codec, no version prefix, no slot map.
//
// Object fixed section (offsets object-relative, little-endian):
//
// ParentID 32B @ 0
// Timestamp i64 @ 32
// Height u64 @ 40
// TxLengths 8B @ 48 u32 list ptr — one entry per tx
// TxBlob 8B @ 56 bytes ptr — concat of each tx.Marshal()
import (
"fmt"
"time"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/example/xsvm/tx"
hash "github.com/luxfi/crypto/hash"
"github.com/luxfi/zap"
)
const (
offBlkParentID = 0
offBlkTimestamp = 32
offBlkHeight = 40
offBlkTxLengths = 48
offBlkTxBlob = 56
sizeBlk = 64
blkTxLenStride = 4 // uint32
blkIDLen = 32
)
// Stateless blocks are blocks as they are marshalled/unmarshalled and sent over
// the p2p network. The stateful blocks which can be executed are built from
// Stateless blocks.
type Stateless struct {
ParentID ids.ID `serialize:"true" json:"parentID"`
Timestamp int64 `serialize:"true" json:"timestamp"`
Height uint64 `serialize:"true" json:"height"`
Txs []*tx.Tx `serialize:"true" json:"txs"`
ParentID ids.ID `json:"parentID"`
Timestamp int64 `json:"timestamp"`
Height uint64 `json:"height"`
Txs []*tx.Tx `json:"txs"`
}
func (b *Stateless) Time() time.Time {
return time.Unix(b.Timestamp, 0)
}
// Marshal encodes the block as one native ZAP object.
func (b *Stateless) Marshal() ([]byte, error) {
builder := zap.NewBuilder(zap.HeaderSize + sizeBlk + 256)
var (
txBlob []byte
lenOff int
lenCount int
)
if len(b.Txs) > 0 {
lb := builder.StartList(blkTxLenStride)
for i, t := range b.Txs {
raw, err := t.Marshal()
if err != nil {
return nil, fmt.Errorf("xsvm/block: marshal tx %d: %w", i, err)
}
lb.AddUint32(uint32(len(raw)))
txBlob = append(txBlob, raw...)
}
lenOff, lenCount = lb.Finish()
}
ob := builder.StartObject(sizeBlk)
ob.SetBytesFixed(offBlkParentID, b.ParentID[:])
ob.SetInt64(offBlkTimestamp, b.Timestamp)
ob.SetUint64(offBlkHeight, b.Height)
ob.SetList(offBlkTxLengths, lenOff, lenCount)
ob.SetBytes(offBlkTxBlob, txBlob)
ob.FinishAsRoot()
return builder.Finish(), nil
}
func (b *Stateless) ID() (ids.ID, error) {
bytes, err := Codec.Marshal(CodecVersion, b)
bytes, err := b.Marshal()
return hash.ComputeHash256Array(bytes), err
}
func Parse(bytes []byte) (*Stateless, error) {
blk := &Stateless{}
_, err := Codec.Unmarshal(bytes, blk)
return blk, err
msg, err := zap.Parse(bytes)
if err != nil {
return nil, err
}
obj := msg.Root()
blk := &Stateless{
Timestamp: obj.Int64(offBlkTimestamp),
Height: obj.Uint64(offBlkHeight),
}
copy(blk.ParentID[:], obj.BytesFixedSlice(offBlkParentID, blkIDLen))
txs, err := parseTxs(obj)
if err != nil {
return nil, err
}
blk.Txs = txs
return blk, nil
}
func parseTxs(obj zap.Object) ([]*tx.Tx, error) {
lengths := obj.ListStride(offBlkTxLengths, blkTxLenStride)
n := lengths.Len()
if n == 0 {
return nil, nil
}
blob := obj.Bytes(offBlkTxBlob)
out := make([]*tx.Tx, n)
cursor := 0
for i := 0; i < n; i++ {
size := int(lengths.Uint32(i))
if cursor+size > len(blob) {
return nil, fmt.Errorf("xsvm/block: tx %d length %d overruns blob (%d)", i, size, len(blob))
}
t, err := tx.Parse(blob[cursor : cursor+size])
if err != nil {
return nil, fmt.Errorf("xsvm/block: parse tx %d: %w", i, err)
}
out[i] = t
cursor += size
}
return out, nil
}
-10
View File
@@ -1,10 +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/node/vms/example/xsvm/tx"
const CodecVersion = tx.CodecVersion
var Codec = tx.Codec
+1 -1
View File
@@ -84,7 +84,7 @@ func (c *chain) NewBlock(blk *xsblock.Stateless) (Block, error) {
return blk, nil
}
blkBytes, err := xsblock.Codec.Marshal(xsblock.CodecVersion, blk)
blkBytes, err := blk.Marshal()
if err != nil {
return nil, err
}
+2 -2
View File
@@ -55,7 +55,7 @@ func createFunc(c *cobra.Command, args []string) error {
}
log.Printf("synced wallet in %s\n", time.Since(walletSyncStartTime))
genesisBytes, err := genesis.Codec.Marshal(genesis.CodecVersion, &genesis.Genesis{
genesisBytes, err := (&genesis.Genesis{
Timestamp: 0,
Allocations: []genesis.Allocation{
{
@@ -63,7 +63,7 @@ func createFunc(c *cobra.Command, args []string) error {
Balance: config.Balance,
},
},
})
}).Marshal()
if err != nil {
return err
}
+1 -2
View File
@@ -11,7 +11,6 @@ import (
"github.com/spf13/cobra"
"github.com/luxfi/formatting"
"github.com/luxfi/node/vms/example/xsvm/genesis"
)
var errUnknownEncoding = errors.New("unknown encoding")
@@ -34,7 +33,7 @@ func genesisFunc(c *cobra.Command, args []string) error {
return err
}
genesisBytes, err := genesis.Codec.Marshal(genesis.CodecVersion, config.Genesis)
genesisBytes, err := config.Genesis.Marshal()
if err != nil {
return err
}
+1 -1
View File
@@ -60,7 +60,7 @@ func Block(
return err
}
blkBytes, err := xsblock.Codec.Marshal(xsblock.CodecVersion, blk)
blkBytes, err := blk.Marshal()
if err != nil {
return err
}
+1 -2
View File
@@ -6,7 +6,6 @@ package execute
import (
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/example/xsvm/block"
"github.com/luxfi/node/vms/example/xsvm/genesis"
"github.com/luxfi/node/vms/example/xsvm/state"
)
@@ -36,7 +35,7 @@ func Genesis(db database.KeyValueReaderWriterDeleter, chainID ids.ID, g *genesis
return err
}
blkBytes, err := block.Codec.Marshal(block.CodecVersion, blk)
blkBytes, err := blk.Marshal()
if err != nil {
return err
}
+3 -3
View File
@@ -12,9 +12,9 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/example/xsvm/state"
"github.com/luxfi/node/vms/example/xsvm/tx"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/warp"
"github.com/luxfi/runtime"
"github.com/luxfi/utils/wrappers"
validators "github.com/luxfi/validators"
)
@@ -91,7 +91,7 @@ func (t *Tx) Export(e *tx.Export) error {
return err
}
var errs pcodecs.Errs
var errs wrappers.Errs
errs.Add(
state.IncrementNonce(t.Database, t.Sender, e.Nonce),
state.DecreaseBalance(t.Database, t.Sender, e.ChainID, t.ExportFee),
@@ -127,7 +127,7 @@ func (t *Tx) Import(i *tx.Import) error {
return err
}
var errs pcodecs.Errs
var errs wrappers.Errs
errs.Add(
state.IncrementNonce(t.Database, t.Sender, i.Nonce),
state.DecreaseBalance(t.Database, t.Sender, t.Runtime.ChainID, t.ImportFee),
-10
View File
@@ -1,10 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package genesis
import "github.com/luxfi/node/vms/example/xsvm/block"
const CodecVersion = block.CodecVersion
var Codec = block.Codec
+75 -9
View File
@@ -3,30 +3,96 @@
package genesis
// Native ZAP wire for the xsvm genesis: the struct IS the wire. One zap object;
// the allocations are a fixed-stride list (28-byte records: 20-byte address +
// u64 balance). There is no codec, no version prefix, no slot map.
//
// Object fixed section (offsets object-relative, little-endian):
//
// Timestamp i64 @ 0
// Allocations 8B @ 8 fixed-stride list ptr (stride 28)
import (
"encoding/binary"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/example/xsvm/block"
hash "github.com/luxfi/crypto/hash"
"github.com/luxfi/zap"
)
const (
offGenesisTimestamp = 0
offGenesisAllocs = 8
sizeGenesis = 16
// Allocation record: address 20B @0, balance u64 @20.
allocAddr = 0
allocBalance = 20
allocStride = 28
genShortLen = 20
)
type Genesis struct {
Timestamp int64 `serialize:"true" json:"timestamp"`
Allocations []Allocation `serialize:"true" json:"allocations"`
Timestamp int64 `json:"timestamp"`
Allocations []Allocation `json:"allocations"`
}
type Allocation struct {
Address ids.ShortID `serialize:"true" json:"address"`
Balance uint64 `serialize:"true" json:"balance"`
Address ids.ShortID `json:"address"`
Balance uint64 `json:"balance"`
}
// Marshal encodes the genesis as one native ZAP object.
func (g *Genesis) Marshal() ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + sizeGenesis + len(g.Allocations)*allocStride)
var allocOff, allocCount int
if len(g.Allocations) > 0 {
lb := b.StartList(allocStride)
rec := make([]byte, allocStride)
for i := range g.Allocations {
copy(rec[allocAddr:], g.Allocations[i].Address[:])
binary.LittleEndian.PutUint64(rec[allocBalance:], g.Allocations[i].Balance)
lb.AddBytes(rec)
}
// AddBytes counts bytes, not elements — use the real element count.
allocOff, _ = lb.Finish()
allocCount = len(g.Allocations)
}
ob := b.StartObject(sizeGenesis)
ob.SetInt64(offGenesisTimestamp, g.Timestamp)
ob.SetList(offGenesisAllocs, allocOff, allocCount)
ob.FinishAsRoot()
return b.Finish(), nil
}
func Parse(bytes []byte) (*Genesis, error) {
genesis := &Genesis{}
_, err := Codec.Unmarshal(bytes, genesis)
return genesis, err
msg, err := zap.Parse(bytes)
if err != nil {
return nil, err
}
obj := msg.Root()
g := &Genesis{
Timestamp: obj.Int64(offGenesisTimestamp),
}
arr := obj.ListStride(offGenesisAllocs, allocStride)
n := arr.Len()
if n > 0 {
g.Allocations = make([]Allocation, n)
for i := 0; i < n; i++ {
e := arr.Object(i, allocStride)
copy(g.Allocations[i].Address[:], e.BytesFixedSlice(allocAddr, genShortLen))
g.Allocations[i].Balance = e.Uint64(allocBalance)
}
}
return g, nil
}
func Block(genesis *Genesis) (*block.Stateless, error) {
bytes, err := Codec.Marshal(CodecVersion, genesis)
bytes, err := genesis.Marshal()
if err != nil {
return nil, err
}
+1 -1
View File
@@ -26,7 +26,7 @@ func TestGenesis(t *testing.T) {
{Address: id2, Balance: 3000000000},
},
}
bytes, err := Codec.Marshal(CodecVersion, genesis)
bytes, err := genesis.Marshal()
require.NoError(err)
parsed, err := Parse(bytes)
-29
View File
@@ -1,29 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package tx
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
)
const CodecVersion = 0
var Codec pcodecs.Manager
func init() {
c := pcodecs.NewLinearCodec()
Codec = pcodecs.NewMaxInt32Manager()
err := errors.Join(
c.RegisterType(&Transfer{}),
c.RegisterType(&Export{}),
c.RegisterType(&Import{}),
Codec.RegisterCodec(CodecVersion, c),
)
if err != nil {
panic(err)
}
}
+41 -8
View File
@@ -3,22 +3,55 @@
package tx
import "github.com/luxfi/ids"
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
var _ Unsigned = (*Export)(nil)
type Export struct {
// ChainID provides cross chain replay protection
ChainID ids.ID `serialize:"true" json:"chainID"`
ChainID ids.ID `json:"chainID"`
// Nonce provides internal chain replay protection
Nonce uint64 `serialize:"true" json:"nonce"`
MaxFee uint64 `serialize:"true" json:"maxFee"`
PeerChainID ids.ID `serialize:"true" json:"peerChainID"`
IsReturn bool `serialize:"true" json:"isReturn"`
Amount uint64 `serialize:"true" json:"amount"`
To ids.ShortID `serialize:"true" json:"to"`
Nonce uint64 `json:"nonce"`
MaxFee uint64 `json:"maxFee"`
PeerChainID ids.ID `json:"peerChainID"`
IsReturn bool `json:"isReturn"`
Amount uint64 `json:"amount"`
To ids.ShortID `json:"to"`
}
func (e *Export) Visit(v Visitor) error {
return v.Export(e)
}
// Marshal encodes the Export as one native ZAP object with the tx kind byte at
// offset 0.
func (e *Export) Marshal() ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + sizeExport)
ob := b.StartObject(sizeExport)
ob.SetUint8(offKind, uint8(kindExport))
ob.SetBytesFixed(offExportChainID, e.ChainID[:])
ob.SetUint64(offExportNonce, e.Nonce)
ob.SetUint64(offExportMaxFee, e.MaxFee)
ob.SetBytesFixed(offExportPeerChainID, e.PeerChainID[:])
ob.SetBool(offExportIsReturn, e.IsReturn)
ob.SetUint64(offExportAmount, e.Amount)
ob.SetBytesFixed(offExportTo, e.To[:])
ob.FinishAsRoot()
return b.Finish(), nil
}
func parseExport(obj zap.Object) *Export {
e := &Export{
Nonce: obj.Uint64(offExportNonce),
MaxFee: obj.Uint64(offExportMaxFee),
IsReturn: obj.Bool(offExportIsReturn),
Amount: obj.Uint64(offExportAmount),
}
copy(e.ChainID[:], obj.BytesFixedSlice(offExportChainID, idLen))
copy(e.PeerChainID[:], obj.BytesFixedSlice(offExportPeerChainID, idLen))
copy(e.To[:], obj.BytesFixedSlice(offExportTo, shortLen))
return e
}
+29 -3
View File
@@ -3,16 +3,42 @@
package tx
import "github.com/luxfi/zap"
var _ Unsigned = (*Import)(nil)
type Import struct {
// Nonce provides internal chain replay protection
Nonce uint64 `serialize:"true" json:"nonce"`
MaxFee uint64 `serialize:"true" json:"maxFee"`
Nonce uint64 `json:"nonce"`
MaxFee uint64 `json:"maxFee"`
// Message includes the chainIDs to provide cross chain replay protection
Message []byte `serialize:"true" json:"message"`
Message []byte `json:"message"`
}
func (i *Import) Visit(v Visitor) error {
return v.Import(i)
}
// Marshal encodes the Import as one native ZAP object with the tx kind byte at
// offset 0.
func (i *Import) Marshal() ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + sizeImport + len(i.Message))
ob := b.StartObject(sizeImport)
ob.SetUint8(offKind, uint8(kindImport))
ob.SetUint64(offImportNonce, i.Nonce)
ob.SetUint64(offImportMaxFee, i.MaxFee)
ob.SetBytes(offImportMessage, i.Message)
ob.FinishAsRoot()
return b.Finish(), nil
}
func parseImport(obj zap.Object) *Import {
i := &Import{
Nonce: obj.Uint64(offImportNonce),
MaxFee: obj.Uint64(offImportMaxFee),
}
if m := obj.Bytes(offImportMessage); len(m) > 0 {
i.Message = append([]byte(nil), m...)
}
return i
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package tx
// Native ZAP wire for xsvm txs: the struct IS the wire. Each Unsigned tx is one
// zap object keyed by a 1-byte kind discriminator at object offset 0 — the
// whole dispatch. parseUnsigned reads it and returns the typed tx. There is no
// codec, no version prefix, no slot map.
//
// Object fixed sections (offsets object-relative, little-endian):
//
// Transfer kind@0 ChainID 32B@1 Nonce u64@33 MaxFee u64@41 AssetID 32B@49
// Amount u64@81 To 20B@89 size 109
// Export kind@0 ChainID 32B@1 Nonce u64@33 MaxFee u64@41 PeerChainID 32B@49
// IsReturn bool@81 Amount u64@82 To 20B@90 size 110
// Import kind@0 Nonce u64@1 MaxFee u64@9 Message bytes@17 size 25
import (
"fmt"
"github.com/luxfi/zap"
)
// kind is the 1-byte tx discriminator at object offset 0 of every Unsigned tx
// buffer.
type kind uint8
const (
kindReserved kind = iota
kindTransfer
kindExport
kindImport
)
// Shared id widths (ids.IDLen / ids.ShortIDLen / secp256k1.SignatureLen).
const (
idLen = 32
shortLen = 20
sigLen = 65
)
// offKind is the fixed wire position of the discriminator (object offset 0).
const offKind = 0
// Transfer object layout.
const (
offTransferChainID = 1
offTransferNonce = 33
offTransferMaxFee = 41
offTransferAssetID = 49
offTransferAmount = 81
offTransferTo = 89
sizeTransfer = 109
)
// Export object layout.
const (
offExportChainID = 1
offExportNonce = 33
offExportMaxFee = 41
offExportPeerChainID = 49
offExportIsReturn = 81
offExportAmount = 82
offExportTo = 90
sizeExport = 110
)
// Import object layout.
const (
offImportNonce = 1
offImportMaxFee = 9
offImportMessage = 17
sizeImport = 25
)
// parseUnsigned reads the kind byte at object offset 0 and returns the typed
// Unsigned tx.
func parseUnsigned(bytes []byte) (Unsigned, error) {
msg, err := zap.Parse(bytes)
if err != nil {
return nil, err
}
obj := msg.Root()
switch k := kind(obj.Uint8(offKind)); k {
case kindTransfer:
return parseTransfer(obj), nil
case kindExport:
return parseExport(obj), nil
case kindImport:
return parseImport(obj), nil
default:
return nil, fmt.Errorf("xsvm/tx: unknown tx kind %d", k)
}
}
+47 -11
View File
@@ -3,15 +3,30 @@
package tx
import "github.com/luxfi/ids"
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// Payload object layout: Sender 20B @0, Nonce u64 @20, IsReturn bool @28,
// Amount u64 @29, To 20B @37. size 57. Payload is a single type (not
// polymorphic), so it carries no kind byte.
const (
offPayloadSender = 0
offPayloadNonce = 20
offPayloadIsReturn = 28
offPayloadAmount = 29
offPayloadTo = 37
sizePayload = 57
)
type Payload struct {
// Sender + Nonce provides replay protection
Sender ids.ShortID `serialize:"true" json:"sender"`
Nonce uint64 `serialize:"true" json:"nonce"`
IsReturn bool `serialize:"true" json:"isReturn"`
Amount uint64 `serialize:"true" json:"amount"`
To ids.ShortID `serialize:"true" json:"to"`
Sender ids.ShortID `json:"sender"`
Nonce uint64 `json:"nonce"`
IsReturn bool `json:"isReturn"`
Amount uint64 `json:"amount"`
To ids.ShortID `json:"to"`
bytes []byte
}
@@ -20,6 +35,18 @@ func (p *Payload) Bytes() []byte {
return p.bytes
}
func (p *Payload) marshal() ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + sizePayload)
ob := b.StartObject(sizePayload)
ob.SetBytesFixed(offPayloadSender, p.Sender[:])
ob.SetUint64(offPayloadNonce, p.Nonce)
ob.SetBool(offPayloadIsReturn, p.IsReturn)
ob.SetUint64(offPayloadAmount, p.Amount)
ob.SetBytesFixed(offPayloadTo, p.To[:])
ob.FinishAsRoot()
return b.Finish(), nil
}
func NewPayload(
sender ids.ShortID,
nonce uint64,
@@ -34,15 +61,24 @@ func NewPayload(
Amount: amount,
To: to,
}
bytes, err := Codec.Marshal(CodecVersion, p)
bytes, err := p.marshal()
p.bytes = bytes
return p, err
}
func ParsePayload(bytes []byte) (*Payload, error) {
p := &Payload{
bytes: bytes,
msg, err := zap.Parse(bytes)
if err != nil {
return nil, err
}
_, err := Codec.Unmarshal(bytes, p)
return p, err
obj := msg.Root()
p := &Payload{
Nonce: obj.Uint64(offPayloadNonce),
IsReturn: obj.Bool(offPayloadIsReturn),
Amount: obj.Uint64(offPayloadAmount),
bytes: bytes,
}
copy(p.Sender[:], obj.BytesFixedSlice(offPayloadSender, shortLen))
copy(p.To[:], obj.BytesFixedSlice(offPayloadTo, shortLen))
return p, nil
}
+38 -7
View File
@@ -3,21 +3,52 @@
package tx
import "github.com/luxfi/ids"
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
var _ Unsigned = (*Transfer)(nil)
type Transfer struct {
// ChainID provides cross chain replay protection
ChainID ids.ID `serialize:"true" json:"chainID"`
ChainID ids.ID `json:"chainID"`
// Nonce provides internal chain replay protection
Nonce uint64 `serialize:"true" json:"nonce"`
MaxFee uint64 `serialize:"true" json:"maxFee"`
AssetID ids.ID `serialize:"true" json:"assetID"`
Amount uint64 `serialize:"true" json:"amount"`
To ids.ShortID `serialize:"true" json:"to"`
Nonce uint64 `json:"nonce"`
MaxFee uint64 `json:"maxFee"`
AssetID ids.ID `json:"assetID"`
Amount uint64 `json:"amount"`
To ids.ShortID `json:"to"`
}
func (t *Transfer) Visit(v Visitor) error {
return v.Transfer(t)
}
// Marshal encodes the Transfer as one native ZAP object with the tx kind byte
// at offset 0.
func (t *Transfer) Marshal() ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + sizeTransfer)
ob := b.StartObject(sizeTransfer)
ob.SetUint8(offKind, uint8(kindTransfer))
ob.SetBytesFixed(offTransferChainID, t.ChainID[:])
ob.SetUint64(offTransferNonce, t.Nonce)
ob.SetUint64(offTransferMaxFee, t.MaxFee)
ob.SetBytesFixed(offTransferAssetID, t.AssetID[:])
ob.SetUint64(offTransferAmount, t.Amount)
ob.SetBytesFixed(offTransferTo, t.To[:])
ob.FinishAsRoot()
return b.Finish(), nil
}
func parseTransfer(obj zap.Object) *Transfer {
t := &Transfer{
Nonce: obj.Uint64(offTransferNonce),
MaxFee: obj.Uint64(offTransferMaxFee),
Amount: obj.Uint64(offTransferAmount),
}
copy(t.ChainID[:], obj.BytesFixedSlice(offTransferChainID, idLen))
copy(t.AssetID[:], obj.BytesFixedSlice(offTransferAssetID, idLen))
copy(t.To[:], obj.BytesFixedSlice(offTransferTo, shortLen))
return t
}
+41 -9
View File
@@ -4,24 +4,56 @@
package tx
import (
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/zap"
)
// Tx object layout: unsigned bytes ptr @0, signature 65B @8. size 73.
const (
offTxUnsigned = 0
offTxSignature = 8
sizeTx = 73
)
type Tx struct {
Unsigned `serialize:"true" json:"unsigned"`
Signature [secp256k1.SignatureLen]byte `serialize:"true" json:"signature"`
Unsigned `json:"unsigned"`
Signature [secp256k1.SignatureLen]byte `json:"signature"`
}
// Marshal encodes the signed tx: the unsigned tx bytes (self-describing via its
// own kind byte) plus the fixed signature.
func (tx *Tx) Marshal() ([]byte, error) {
unsignedBytes, err := tx.Unsigned.Marshal()
if err != nil {
return nil, err
}
b := zap.NewBuilder(zap.HeaderSize + sizeTx + len(unsignedBytes))
ob := b.StartObject(sizeTx)
ob.SetBytes(offTxUnsigned, unsignedBytes)
ob.SetBytesFixed(offTxSignature, tx.Signature[:])
ob.FinishAsRoot()
return b.Finish(), nil
}
func Parse(bytes []byte) (*Tx, error) {
tx := &Tx{}
_, err := Codec.Unmarshal(bytes, tx)
return tx, err
msg, err := zap.Parse(bytes)
if err != nil {
return nil, err
}
obj := msg.Root()
unsigned, err := parseUnsigned(obj.Bytes(offTxUnsigned))
if err != nil {
return nil, err
}
tx := &Tx{Unsigned: unsigned}
copy(tx.Signature[:], obj.BytesFixedSlice(offTxSignature, sigLen))
return tx, nil
}
func Sign(utx Unsigned, key *secp256k1.PrivateKey) (*Tx, error) {
unsignedBytes, err := Codec.Marshal(CodecVersion, &utx)
unsignedBytes, err := utx.Marshal()
if err != nil {
return nil, err
}
@@ -39,12 +71,12 @@ func Sign(utx Unsigned, key *secp256k1.PrivateKey) (*Tx, error) {
}
func (tx *Tx) ID() (ids.ID, error) {
bytes, err := Codec.Marshal(CodecVersion, tx)
bytes, err := tx.Marshal()
return hash.ComputeHash256Array(bytes), err
}
func (tx *Tx) SenderID() (ids.ShortID, error) {
unsignedBytes, err := Codec.Marshal(CodecVersion, &tx.Unsigned)
unsignedBytes, err := tx.Unsigned.Marshal()
if err != nil {
return ids.ShortEmpty, err
}
+5
View File
@@ -5,4 +5,9 @@ package tx
type Unsigned interface {
Visit(Visitor) error
// Marshal encodes the unsigned tx to its native ZAP wire form. The first
// object byte is the tx kind discriminator — this is the whole dispatch,
// no codec.
Marshal() ([]byte, error)
}
+12 -12
View File
@@ -11,13 +11,13 @@ import (
"github.com/luxfi/address"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/math"
"github.com/luxfi/node/vms/platformvm/signer"
"github.com/luxfi/node/vms/platformvm/stakeable"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/vms/platformvm/txs/txheap"
"github.com/luxfi/utils"
"github.com/luxfi/math"
"github.com/luxfi/node/vms/platformvm/signer"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
@@ -29,7 +29,7 @@ import (
var (
errUTXOHasNoValue = errors.New("genesis UTXO has no value")
errValidatorHasZeroWeight = errors.New("validator has zero weight")
errValidatorHasZeroWeight = errors.New("validator has zero weight")
errValidatorAlreadyExited = errors.New("validator would have already unstaked")
errStakeOverflow = errors.New("validator stake exceeds limit")
@@ -38,18 +38,18 @@ var (
// UTXO adds messages to UTXOs
type UTXO struct {
lux.UTXO `serialize:"true"`
Message []byte `serialize:"true" json:"message"`
lux.UTXO
Message []byte `json:"message"`
}
// Genesis represents a genesis state of the platform chain
type Genesis struct {
UTXOs []*UTXO `serialize:"true"`
Validators []*txs.Tx `serialize:"true"`
Chains []*txs.Tx `serialize:"true"`
Timestamp uint64 `serialize:"true"`
InitialSupply uint64 `serialize:"true"`
Message string `serialize:"true"`
UTXOs []*UTXO
Validators []*txs.Tx
Chains []*txs.Tx
Timestamp uint64
InitialSupply uint64
Message string
}
// Parse deserializes a P-Chain genesis blob from its native-ZAP wire form
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"github.com/luxfi/ids"
utilmetric "github.com/luxfi/node/utils/metric"
"github.com/luxfi/node/vms/components/gas"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/block"
"github.com/luxfi/utils/wrappers"
)
const (
@@ -145,7 +145,7 @@ func New(registerer metric.Registerer) (Metrics, error) {
}),
}
errs := pcodecs.Errs{Err: err}
errs := wrappers.Errs{Err: err}
registry, ok := registerer.(metric.Registry)
if !ok {
return nil, errors.New("registerer must be a Registry")
@@ -48,8 +48,8 @@ type HybridSigner interface {
// - RT PublicKey: 1952 bytes (ML-DSA-65)
// - RT ProofOfPossession: 3309 bytes (ML-DSA-65 signature)
type HybridProofOfPossession struct {
BLS *ProofOfPossession `serialize:"true" json:"bls"`
RT *RTProofOfPossession `serialize:"true" json:"rt"`
BLS *ProofOfPossession `json:"bls"`
RT *RTProofOfPossession `json:"rt"`
}
// NewHybridProofOfPossession creates a new hybrid proof combining BLS and RT.
+2 -2
View File
@@ -18,10 +18,10 @@ var (
)
type ProofOfPossession struct {
PublicKey [bls.PublicKeyLen]byte `serialize:"true" json:"publicKey"`
PublicKey [bls.PublicKeyLen]byte `json:"publicKey"`
// BLS signature proving ownership of [PublicKey]. The signed message is the
// [PublicKey].
ProofOfPossession [bls.SignatureLen]byte `serialize:"true" json:"proofOfPossession"`
ProofOfPossession [bls.SignatureLen]byte `json:"proofOfPossession"`
// publicKey is the parsed version of [PublicKey]. It is populated in
// [Verify].
@@ -43,9 +43,9 @@ type RTSigner interface {
// by requiring a signature over the public key itself.
type RTProofOfPossession struct {
// PublicKey is the ML-DSA-65 public key (1952 bytes)
PublicKey []byte `serialize:"true" json:"publicKey"`
PublicKey []byte `json:"publicKey"`
// ProofOfPossession is the ML-DSA signature over PublicKey, proving ownership
ProofOfPossession []byte `serialize:"true" json:"proofOfPossession"`
ProofOfPossession []byte `json:"proofOfPossession"`
}
// NewRTProofOfPossession creates a new RTProofOfPossession from a private key.
+4 -4
View File
@@ -55,8 +55,8 @@ func init() {
}
type LockOut struct {
Locktime uint64 `serialize:"true" json:"locktime"`
lux.TransferableOut `serialize:"true" json:"output"`
Locktime uint64 `json:"locktime"`
lux.TransferableOut `json:"output"`
}
func (s *LockOut) InitRuntime(rt *runtime.Runtime) {
@@ -106,8 +106,8 @@ func (s *LockOut) Bytes() []byte {
}
type LockIn struct {
Locktime uint64 `serialize:"true" json:"locktime"`
lux.TransferableIn `serialize:"true" json:"input"`
Locktime uint64 `json:"locktime"`
lux.TransferableIn `json:"input"`
}
func (s *LockIn) Verify() error {
-34
View File
@@ -1,34 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
)
const (
CodecVersion0Tag = "v0"
CodecVersion0 uint16 = 0
CodecVersion1Tag = "v1"
CodecVersion1 uint16 = 1
)
var MetadataCodec pcodecs.Manager
func init() {
c0 := pcodecs.NewLinearCodec()
c1 := pcodecs.NewLinearCodec()
MetadataCodec = pcodecs.NewMaxInt32Manager()
err := errors.Join(
MetadataCodec.RegisterCodec(CodecVersion0, c0),
MetadataCodec.RegisterCodec(CodecVersion1, c1),
)
if err != nil {
panic(err)
}
}
+10 -19
View File
@@ -9,33 +9,24 @@ import (
)
type delegatorMetadata struct {
PotentialReward uint64 `serialize:"true"`
StakerStartTime uint64 `serialize:"true"`
PotentialReward uint64
StakerStartTime uint64
txID ids.ID
}
// parseDelegatorMetadata overlays metadata's persisted fields (native
// delegatorMetadata wire) from bytes. Empty bytes means nothing was persisted —
// the caller's tx-derived StakerStartTime default is kept.
func parseDelegatorMetadata(bytes []byte, metadata *delegatorMetadata) error {
var err error
switch len(bytes) {
case database.Uint64Size:
// only potential reward was stored
metadata.PotentialReward, err = database.ParseUInt64(bytes)
default:
_, err = MetadataCodec.Unmarshal(bytes, metadata)
if len(bytes) == 0 {
return nil
}
return err
return unmarshalDelegatorMetadata(bytes, metadata)
}
func writeDelegatorMetadata(db database.KeyValueWriter, metadata *delegatorMetadata, codecVersion uint16) error {
// The "0" codec is skipped for [delegatorMetadata]. This is to ensure the
// [validatorMetadata] codec version is the same as the [delegatorMetadata]
// codec version.
//
if codecVersion == 0 {
return database.PutUInt64(db, metadata.txID[:], metadata.PotentialReward)
}
metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata)
func writeDelegatorMetadata(db database.KeyValueWriter, metadata *delegatorMetadata) error {
metadataBytes, err := marshalDelegatorMetadata(metadata)
if err != nil {
return err
}
+45 -104
View File
@@ -10,132 +10,73 @@ import (
"github.com/luxfi/database/memdb"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/pcodecs"
)
func TestParseDelegatorMetadata(t *testing.T) {
full := &delegatorMetadata{PotentialReward: 123, StakerStartTime: 456}
fullBytes, err := marshalDelegatorMetadata(full)
require.NoError(t, err)
type test struct {
name string
bytes []byte
expected *delegatorMetadata
expectedErr error
name string
bytes []byte
initial *delegatorMetadata // caller-supplied defaults before parse
want *delegatorMetadata
wantErr bool
}
tests := []test{
{
name: "potential reward only no codec",
bytes: []byte{
// potential reward via database.ParseUInt64 (BE — database layer, not codec)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7b,
},
expected: &delegatorMetadata{
PotentialReward: 123,
StakerStartTime: 0,
},
expectedErr: nil,
// Empty ⇒ nothing persisted; the caller's tx-derived StakerStartTime
// default is kept.
name: "empty keeps defaults",
bytes: nil,
initial: &delegatorMetadata{StakerStartTime: 456},
want: &delegatorMetadata{StakerStartTime: 456},
},
{
name: "potential reward + staker start time with codec v1",
bytes: []byte{
// codec version (LE) — 1 = 0x01 0x00
0x01, 0x00,
// potential reward (LE) — 123
0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// staker start time (LE) — 456 = 0xC8_01_00_...
0xc8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expected: &delegatorMetadata{
PotentialReward: 123,
StakerStartTime: 456,
},
expectedErr: nil,
name: "full native round-trip",
bytes: fullBytes,
initial: &delegatorMetadata{StakerStartTime: 999},
want: &delegatorMetadata{PotentialReward: 123, StakerStartTime: 456},
},
{
name: "invalid codec version",
bytes: []byte{
// codec version (LE) — 2 = 0x02 0x00
0x02, 0x00,
// potential reward (LE)
0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// staker start time (LE)
0xc8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expected: nil,
expectedErr: pcodecs.ErrUnknownVersion,
},
{
name: "short byte len",
bytes: []byte{
// codec version (LE)
0x01, 0x00,
// potential reward (LE)
0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// staker start time (truncated)
0xc8, 0x01, 0x00, 0x00, 0x00, 0x00,
},
expected: nil,
expectedErr: pcodecs.ErrInsufficientLength,
name: "truncated buffer errors",
bytes: fullBytes[:len(fullBytes)-1],
initial: &delegatorMetadata{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
var metadata delegatorMetadata
err := parseDelegatorMetadata(tt.bytes, &metadata)
require.ErrorIs(err, tt.expectedErr)
if tt.expectedErr != nil {
metadata := tt.initial
err := parseDelegatorMetadata(tt.bytes, metadata)
if tt.wantErr {
require.Error(err)
return
}
require.Equal(tt.expected, &metadata)
require.NoError(err)
require.Equal(tt.want, metadata)
})
}
}
func TestWriteDelegatorMetadata(t *testing.T) {
type test struct {
name string
version uint16
metadata *delegatorMetadata
expected []byte
}
tests := []test{
{
name: CodecVersion0Tag,
version: CodecVersion0,
metadata: &delegatorMetadata{
PotentialReward: 123,
StakerStartTime: 456,
},
expected: []byte{
// potential reward via database.PutUInt64 (BE — database layer, not codec)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7b,
},
},
{
name: CodecVersion1Tag,
version: CodecVersion1,
metadata: &delegatorMetadata{
PotentialReward: 123,
StakerStartTime: 456,
},
expected: []byte{
// codec version (LE)
0x01, 0x00,
// potential reward (LE)
0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// staker start time (LE)
0xc8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
db := memdb.New()
tt.metadata.txID = ids.GenerateTestID()
require.NoError(writeDelegatorMetadata(db, tt.metadata, tt.version))
bytes, err := db.Get(tt.metadata.txID[:])
require.NoError(err)
require.Equal(tt.expected, bytes)
})
require := require.New(t)
db := memdb.New()
metadata := &delegatorMetadata{
PotentialReward: 123,
StakerStartTime: 456,
txID: ids.GenerateTestID(),
}
require.NoError(writeDelegatorMetadata(db, metadata))
bytes, err := db.Get(metadata.txID[:])
require.NoError(err)
// The persisted bytes round-trip back to the same serialized fields.
got := &delegatorMetadata{txID: metadata.txID}
require.NoError(parseDelegatorMetadata(bytes, got))
require.Equal(metadata, got)
}
+13 -77
View File
@@ -10,91 +10,29 @@ import (
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
)
// preDelegateeRewardSize is the size of codec marshalling
// [preDelegateeRewardMetadata].
//
// CodecVersionLen + UpDurationLen + LastUpdatedLen + PotentialRewardLen
const preDelegateeRewardSize = pcodecs.VersionSize + 3*pcodecs.LongLen
// preStakerStartTimeSize is the size of codec marshalling
// [preStakerStartTimeMetadata].
//
// CodecVersionLen + UpDurationLen + LastUpdatedLen + PotentialRewardLen + PotentialDelegateeRewardLen
const preStakerStartTimeSize = pcodecs.VersionSize + 4*pcodecs.LongLen
var _ validatorState = (*metadata)(nil)
type preDelegateeRewardMetadata struct {
UpDuration time.Duration `serialize:"true"`
LastUpdated uint64 `serialize:"true"` // Unix time in seconds
PotentialReward uint64 `serialize:"true"`
}
// preStakerStartTimeMetadata is used for backward compatibility with data
// that was written before StakerStartTime was added.
type preStakerStartTimeMetadata struct {
UpDuration time.Duration `serialize:"true"`
LastUpdated uint64 `serialize:"true"` // Unix time in seconds
PotentialReward uint64 `serialize:"true"`
PotentialDelegateeReward uint64 `serialize:"true"`
}
type validatorMetadata struct {
UpDuration time.Duration `serialize:"true"`
LastUpdated uint64 `serialize:"true"` // Unix time in seconds
PotentialReward uint64 `serialize:"true"`
PotentialDelegateeReward uint64 `serialize:"true"`
StakerStartTime uint64 `serialize:"true"`
UpDuration time.Duration
LastUpdated uint64 // Unix time in seconds
PotentialReward uint64
PotentialDelegateeReward uint64
StakerStartTime uint64
txID ids.ID
lastUpdated time.Time
}
// Permissioned validators originally wrote their values as nil.
// We now write the uptime, reward, and delegatee reward together.
// parseValidatorMetadata overlays metadata's persisted fields (native
// validatorMetadata wire) from bytes. Empty bytes means nothing was persisted
// for this staker — the caller pre-populates tx-derived defaults
// (StakerStartTime/LastUpdated), which are kept as-is. lastUpdated is always
// derived from the resulting LastUpdated.
func parseValidatorMetadata(bytes []byte, metadata *validatorMetadata) error {
switch len(bytes) {
case 0:
// nothing was stored
case database.Uint64Size:
// only potential reward was stored
var err error
metadata.PotentialReward, err = database.ParseUInt64(bytes)
if err != nil {
return err
}
case preDelegateeRewardSize:
// potential reward and uptime was stored but potential delegatee reward
// was not
tmp := preDelegateeRewardMetadata{}
if _, err := MetadataCodec.Unmarshal(bytes, &tmp); err != nil {
return err
}
metadata.UpDuration = tmp.UpDuration
metadata.LastUpdated = tmp.LastUpdated
metadata.PotentialReward = tmp.PotentialReward
case preStakerStartTimeSize:
// All fields except StakerStartTime were stored (pre-v1 format)
tmp := preStakerStartTimeMetadata{}
if _, err := MetadataCodec.Unmarshal(bytes, &tmp); err != nil {
return err
}
metadata.UpDuration = tmp.UpDuration
metadata.LastUpdated = tmp.LastUpdated
metadata.PotentialReward = tmp.PotentialReward
metadata.PotentialDelegateeReward = tmp.PotentialDelegateeReward
default:
// everything was stored (v1+ format with StakerStartTime)
if _, err := MetadataCodec.Unmarshal(bytes, metadata); err != nil {
if len(bytes) > 0 {
if err := unmarshalValidatorMetadata(bytes, metadata); err != nil {
return err
}
}
@@ -157,7 +95,6 @@ type validatorState interface {
WriteValidatorMetadata(
dbPrimary database.KeyValueWriter,
dbNet database.KeyValueWriter,
codecVersion uint16,
) error
}
@@ -258,14 +195,13 @@ func (m *metadata) DeleteValidatorMetadata(vdrID ids.NodeID, netID ids.ID) {
func (m *metadata) WriteValidatorMetadata(
dbPrimary database.KeyValueWriter,
dbNet database.KeyValueWriter,
codecVersion uint16,
) error {
for vdrID, updatedNets := range m.updatedMetadata {
for netID := range updatedNets {
metadata := m.metadata[vdrID][netID]
metadata.LastUpdated = uint64(metadata.lastUpdated.Unix())
metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata)
metadataBytes, err := marshalValidatorMetadata(metadata)
if err != nil {
return err
}
+49 -104
View File
@@ -12,7 +12,6 @@ import (
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/pcodecs"
)
func TestValidatorUptimes(t *testing.T) {
@@ -82,7 +81,7 @@ func TestWriteValidatorMetadata(t *testing.T) {
chainDB := memdb.New()
// write empty uptimes
require.NoError(state.WriteValidatorMetadata(primaryDB, chainDB, CodecVersion1))
require.NoError(state.WriteValidatorMetadata(primaryDB, chainDB))
// load uptime
nodeID := ids.GenerateTestNodeID()
@@ -96,7 +95,7 @@ func TestWriteValidatorMetadata(t *testing.T) {
state.LoadValidatorMetadata(nodeID, netID, testUptimeReward)
// write state, should not reflect to DB yet
require.NoError(state.WriteValidatorMetadata(primaryDB, chainDB, CodecVersion1))
require.NoError(state.WriteValidatorMetadata(primaryDB, chainDB))
require.False(primaryDB.Has(testUptimeReward.txID[:]))
require.False(chainDB.Has(testUptimeReward.txID[:]))
@@ -112,7 +111,7 @@ func TestWriteValidatorMetadata(t *testing.T) {
require.NoError(state.SetUptime(nodeID, netID, newUpDuration, newLastUpdated))
// write uptimes, should reflect to net DB
require.NoError(state.WriteValidatorMetadata(primaryDB, chainDB, CodecVersion1))
require.NoError(state.WriteValidatorMetadata(primaryDB, chainDB))
require.False(primaryDB.Has(testUptimeReward.txID[:]))
require.True(chainDB.Has(testUptimeReward.txID[:]))
}
@@ -171,130 +170,76 @@ func TestValidatorDelegateeRewards(t *testing.T) {
}
func TestParseValidatorMetadata(t *testing.T) {
// full is a fully-populated record round-tripped through the native wire.
full := &validatorMetadata{
UpDuration: 6000000,
LastUpdated: 900000,
PotentialReward: 100000,
PotentialDelegateeReward: 20000,
StakerStartTime: 12345,
}
fullBytes, err := marshalValidatorMetadata(full)
require.NoError(t, err)
type test struct {
name string
bytes []byte
expected *validatorMetadata
expectedErr error
name string
bytes []byte
initial *validatorMetadata // caller-supplied defaults before parse
want *validatorMetadata
wantErr bool
}
tests := []test{
{
name: "nil",
bytes: nil,
expected: &validatorMetadata{
lastUpdated: time.Unix(0, 0),
},
expectedErr: nil,
},
{
name: "nil",
bytes: []byte{},
expected: &validatorMetadata{
lastUpdated: time.Unix(0, 0),
},
expectedErr: nil,
},
{
name: "potential reward only",
bytes: []byte{
// potential reward via database.ParseUInt64 (BE — database layer, not codec)
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86, 0xA0,
},
expected: &validatorMetadata{
PotentialReward: 100000,
lastUpdated: time.Unix(0, 0),
},
expectedErr: nil,
},
{
name: "uptime + potential reward",
bytes: []byte{
// codec version (LE)
0x00, 0x00,
// up duration (LE) — 6000000 = 0x808D5B_00_00_00_00_00
0x80, 0x8D, 0x5B, 0x00, 0x00, 0x00, 0x00, 0x00,
// last updated (LE) — 900000 = 0xA0BB0D_00_00_00_00_00
0xA0, 0xBB, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00,
// potential reward (LE) — 100000 = 0xA08601_00_00_00_00_00
0xA0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expected: &validatorMetadata{
UpDuration: 6000000,
// Empty ⇒ nothing persisted; caller's tx-derived defaults are kept,
// only lastUpdated is derived from LastUpdated.
name: "nil keeps defaults",
bytes: nil,
initial: &validatorMetadata{StakerStartTime: 900000, LastUpdated: 900000},
want: &validatorMetadata{
StakerStartTime: 900000,
LastUpdated: 900000,
PotentialReward: 100000,
lastUpdated: time.Unix(900000, 0),
},
expectedErr: nil,
},
{
name: "uptime + potential reward + potential delegatee reward",
bytes: []byte{
// codec version (LE)
0x00, 0x00,
// up duration (LE) — 6000000
0x80, 0x8D, 0x5B, 0x00, 0x00, 0x00, 0x00, 0x00,
// last updated (LE) — 900000
0xA0, 0xBB, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00,
// potential reward (LE) — 100000
0xA0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// potential delegatee reward (LE) — 20000 = 0x204E_00_00_00_00_00_00
0x20, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expected: &validatorMetadata{
name: "empty keeps defaults",
bytes: []byte{},
initial: &validatorMetadata{},
want: &validatorMetadata{lastUpdated: time.Unix(0, 0)},
},
{
// Full native buffer overwrites every serialized field, including the
// caller's tx-default StakerStartTime.
name: "full native round-trip",
bytes: fullBytes,
initial: &validatorMetadata{StakerStartTime: 999},
want: &validatorMetadata{
UpDuration: 6000000,
LastUpdated: 900000,
PotentialReward: 100000,
PotentialDelegateeReward: 20000,
StakerStartTime: 12345,
lastUpdated: time.Unix(900000, 0),
},
expectedErr: nil,
},
{
name: "invalid codec version",
bytes: []byte{
// codec version (LE) — 2 = 0x02 0x00, but reading LE that means we want value 2
// so encode as 0x02 0x00 (low byte first)
0x02, 0x00,
// up duration (LE)
0x80, 0x8D, 0x5B, 0x00, 0x00, 0x00, 0x00, 0x00,
// last updated (LE)
0xA0, 0xBB, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00,
// potential reward (LE)
0xA0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// potential delegatee reward (LE)
0x20, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expected: nil,
expectedErr: pcodecs.ErrUnknownVersion,
},
{
name: "short byte len",
bytes: []byte{
// codec version (LE)
0x00, 0x00,
// up duration (LE)
0x80, 0x8D, 0x5B, 0x00, 0x00, 0x00, 0x00, 0x00,
// last updated (LE)
0xA0, 0xBB, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00,
// potential reward (LE)
0xA0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// potential delegatee reward (truncated)
0x20, 0x4E, 0x00, 0x00, 0x00, 0x00,
},
expected: nil,
expectedErr: pcodecs.ErrInsufficientLength,
name: "truncated buffer errors",
bytes: fullBytes[:len(fullBytes)-1],
initial: &validatorMetadata{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
var metadata validatorMetadata
err := parseValidatorMetadata(tt.bytes, &metadata)
require.ErrorIs(err, tt.expectedErr)
if tt.expectedErr != nil {
metadata := tt.initial
err := parseValidatorMetadata(tt.bytes, metadata)
if tt.wantErr {
require.Error(err)
return
}
require.Equal(tt.expected, &metadata)
require.NoError(err)
require.Equal(tt.want, metadata)
})
}
}
+5 -6
View File
@@ -12,7 +12,6 @@ import (
"github.com/google/btree"
"github.com/luxfi/metric"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/runtime"
validators "github.com/luxfi/validators"
"github.com/luxfi/validators/uptime"
@@ -521,7 +520,7 @@ func txAndStatusSize(_ ids.ID, t *txAndStatus) int {
if t == nil {
return ids.IDLen + constants.PointerOverhead
}
return ids.IDLen + len(t.tx.Bytes()) + pcodecs.IntLen + 2*constants.PointerOverhead
return ids.IDLen + len(t.tx.Bytes()) + 4 /* status (uint32) */ + 2*constants.PointerOverhead
}
func blockSize(_ ids.ID, blk block.Block) int {
@@ -595,7 +594,7 @@ func New(
"l1_validator_weights_cache",
reg,
lru.NewSizedCache(execCfg.L1WeightsCacheSize, func(ids.ID, uint64) int {
return ids.IDLen + pcodecs.LongLen
return ids.IDLen + database.Uint64Size
}),
)
if err != nil {
@@ -609,8 +608,8 @@ func New(
execCfg.L1InactiveValidatorsCacheSize,
func(_ ids.ID, maybeL1Validator maybe.Maybe[L1Validator]) int {
const (
l1ValidatorOverhead = ids.IDLen + ids.NodeIDLen + 4*pcodecs.LongLen + 3*constants.PointerOverhead
maybeL1ValidatorOverhead = pcodecs.BoolLen + l1ValidatorOverhead
l1ValidatorOverhead = ids.IDLen + ids.NodeIDLen + 4*database.Uint64Size + 3*constants.PointerOverhead
maybeL1ValidatorOverhead = database.BoolSize + l1ValidatorOverhead
entryOverhead = ids.IDLen + maybeL1ValidatorOverhead
)
if maybeL1Validator.IsNothing() {
@@ -630,7 +629,7 @@ func New(
"l1_validator_chain_id_node_id_cache",
reg,
lru.NewSizedCache(execCfg.L1ChainIDNodeIDCacheSize, func(chainIDNodeID, bool) int {
return ids.IDLen + ids.NodeIDLen + pcodecs.BoolLen
return ids.IDLen + ids.NodeIDLen + database.BoolSize
}),
)
if err != nil {
+2 -4
View File
@@ -62,16 +62,14 @@ func (s *state) Close() error {
}
func (s *state) write(updateValidators bool, height uint64) error {
const codecVersion = CodecVersion1
return errors.Join(
s.writeBlocks(),
s.writeExpiry(),
s.updateValidatorManager(updateValidators),
s.writeValidatorDiffs(height),
s.writeCurrentStakers(codecVersion),
s.writeCurrentStakers(),
s.writePendingStakers(),
s.WriteValidatorMetadata(s.currentValidatorList, s.currentNetValidatorList, codecVersion), // Must be called after writeCurrentStakers
s.WriteValidatorMetadata(s.currentValidatorList, s.currentNetValidatorList), // Must be called after writeCurrentStakers
s.writeL1Validators(),
s.writeTXs(),
s.writeRewardUTXOs(),
+3 -5
View File
@@ -607,7 +607,7 @@ func (s *state) initValidatorSets() error {
return nil
}
func (s *state) writeCurrentStakers(codecVersion uint16) error {
func (s *state) writeCurrentStakers() error {
for chainID, validatorDiffs := range s.currentStakers.validatorDiffs {
// Select db to write to
validatorDB := s.currentNetValidatorList
@@ -639,7 +639,7 @@ func (s *state) writeCurrentStakers(codecVersion uint16) error {
PotentialDelegateeReward: 0,
}
metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata)
metadataBytes, err := marshalValidatorMetadata(metadata)
if err != nil {
return fmt.Errorf("failed to serialize current validator: %w", err)
}
@@ -660,7 +660,6 @@ func (s *state) writeCurrentStakers(codecVersion uint16) error {
err := writeCurrentDelegatorDiff(
delegatorDB,
validatorDiff,
codecVersion,
)
if err != nil {
return err
@@ -674,7 +673,6 @@ func (s *state) writeCurrentStakers(codecVersion uint16) error {
func writeCurrentDelegatorDiff(
currentDelegatorList linkeddb.LinkedDB,
validatorDiff *diffValidator,
codecVersion uint16,
) error {
addedDelegatorIterator := iterator.FromTree(validatorDiff.addedDelegators)
defer addedDelegatorIterator.Release()
@@ -687,7 +685,7 @@ func writeCurrentDelegatorDiff(
PotentialReward: staker.PotentialReward,
StakerStartTime: uint64(staker.StartTime.Unix()),
}
if err := writeDelegatorMetadata(currentDelegatorList, metadata, codecVersion); err != nil {
if err := writeDelegatorMetadata(currentDelegatorList, metadata); err != nil {
return fmt.Errorf("failed to write current delegator to list: %w", err)
}
}
+72
View File
@@ -17,6 +17,7 @@ package state
import (
"fmt"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/components/gas"
@@ -309,3 +310,74 @@ func parseTxStatus(b []byte) (txBytesAndStatus, error) {
Status: status.Status(obj.Uint32(txsStatus)),
}, nil
}
// ---- validatorMetadata (staker uptime/reward record; txID is the DB key, not
// serialized). Fixed object of five uint64 fields. ----
const (
vmdUpDuration = 0
vmdLastUpdated = 8
vmdPotentialReward = 16
vmdDelegateeReward = 24
vmdStakerStartTime = 32
vmdSize = 40
)
func marshalValidatorMetadata(m *validatorMetadata) ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + vmdSize)
ob := b.StartObject(vmdSize)
ob.SetUint64(vmdUpDuration, uint64(m.UpDuration))
ob.SetUint64(vmdLastUpdated, m.LastUpdated)
ob.SetUint64(vmdPotentialReward, m.PotentialReward)
ob.SetUint64(vmdDelegateeReward, m.PotentialDelegateeReward)
ob.SetUint64(vmdStakerStartTime, m.StakerStartTime)
ob.FinishAsRoot()
return b.Finish(), nil
}
// unmarshalValidatorMetadata overlays m's five serialized fields from b. All
// five are always written, so a full buffer overwrites any caller-supplied
// tx-default values (StakerStartTime/LastUpdated); the derived lastUpdated and
// the DB-key txID are handled by the parseValidatorMetadata wrapper.
func unmarshalValidatorMetadata(b []byte, m *validatorMetadata) error {
msg, err := zap.Parse(b)
if err != nil {
return err
}
obj := msg.Root()
m.UpDuration = time.Duration(obj.Uint64(vmdUpDuration))
m.LastUpdated = obj.Uint64(vmdLastUpdated)
m.PotentialReward = obj.Uint64(vmdPotentialReward)
m.PotentialDelegateeReward = obj.Uint64(vmdDelegateeReward)
m.StakerStartTime = obj.Uint64(vmdStakerStartTime)
return nil
}
// ---- delegatorMetadata (PotentialReward + StakerStartTime; txID is the DB
// key, not serialized). ----
const (
dmdPotentialReward = 0
dmdStakerStartTime = 8
dmdSize = 16
)
func marshalDelegatorMetadata(m *delegatorMetadata) ([]byte, error) {
b := zap.NewBuilder(zap.HeaderSize + dmdSize)
ob := b.StartObject(dmdSize)
ob.SetUint64(dmdPotentialReward, m.PotentialReward)
ob.SetUint64(dmdStakerStartTime, m.StakerStartTime)
ob.FinishAsRoot()
return b.Finish(), nil
}
func unmarshalDelegatorMetadata(b []byte, m *delegatorMetadata) error {
msg, err := zap.Parse(b)
if err != nil {
return err
}
obj := msg.Root()
m.PotentialReward = obj.Uint64(dmdPotentialReward)
m.StakerStartTime = obj.Uint64(dmdStakerStartTime)
return nil
}
+91 -80
View File
@@ -14,7 +14,6 @@ import (
"github.com/luxfi/math"
"github.com/luxfi/node/vms/components/gas"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/fx"
"github.com/luxfi/node/vms/platformvm/signer"
"github.com/luxfi/node/vms/platformvm/stakeable"
@@ -24,57 +23,69 @@ import (
"github.com/luxfi/utxo/secp256k1fx"
)
// Wire byte-lengths of the primitive fields the intrinsic-bandwidth fee
// schedule prices. The schedule sums these per-field sizes to charge bandwidth
// gas; they are the fee model's own constants (no codec dependency). Values are
// the fixed on-wire sizes of each primitive, so the schedule stays stable and
// self-contained.
const (
wireShortLen = 2 // uint16
wireIntLen = 4 // uint32
wireLongLen = 8 // uint64
wireVersionLen = 2 // fixed per-tx serialization-header budget
)
// Signature verification costs were conservatively based on benchmarks run on
// an AWS c5.xlarge instance.
const (
intrinsicValidatorBandwidth = ids.NodeIDLen + // nodeID
pcodecs.LongLen + // start
pcodecs.LongLen + // end
pcodecs.LongLen // weight
wireLongLen + // start
wireLongLen + // end
wireLongLen // weight
intrinsicNetValidatorBandwidth = intrinsicValidatorBandwidth + // validator
ids.IDLen // subchainID
intrinsicOutputBandwidth = ids.IDLen + // assetID
pcodecs.IntLen // output typeID
wireIntLen // output typeID
intrinsicStakeableLockedOutputBandwidth = pcodecs.LongLen + // locktime
pcodecs.IntLen // output typeID
intrinsicStakeableLockedOutputBandwidth = wireLongLen + // locktime
wireIntLen // output typeID
intrinsicSECP256k1FxOutputOwnersBandwidth = pcodecs.LongLen + // locktime
pcodecs.IntLen + // threshold
pcodecs.IntLen // num addresses
intrinsicSECP256k1FxOutputOwnersBandwidth = wireLongLen + // locktime
wireIntLen + // threshold
wireIntLen // num addresses
intrinsicSECP256k1FxOutputBandwidth = pcodecs.LongLen + // amount
intrinsicSECP256k1FxOutputBandwidth = wireLongLen + // amount
intrinsicSECP256k1FxOutputOwnersBandwidth
intrinsicInputBandwidth = ids.IDLen + // txID
pcodecs.IntLen + // output index
wireIntLen + // output index
ids.IDLen + // assetID
pcodecs.IntLen + // input typeID
pcodecs.IntLen // credential typeID
wireIntLen + // input typeID
wireIntLen // credential typeID
intrinsicStakeableLockedInputBandwidth = pcodecs.LongLen + // locktime
pcodecs.IntLen // input typeID
intrinsicStakeableLockedInputBandwidth = wireLongLen + // locktime
wireIntLen // input typeID
intrinsicSECP256k1FxInputBandwidth = pcodecs.IntLen + // num indices
pcodecs.IntLen // num signatures
intrinsicSECP256k1FxInputBandwidth = wireIntLen + // num indices
wireIntLen // num signatures
intrinsicSECP256k1FxTransferableInputBandwidth = pcodecs.LongLen + // amount
intrinsicSECP256k1FxTransferableInputBandwidth = wireLongLen + // amount
intrinsicSECP256k1FxInputBandwidth
intrinsicSECP256k1FxSignatureBandwidth = pcodecs.IntLen + // signature index
intrinsicSECP256k1FxSignatureBandwidth = wireIntLen + // signature index
secp256k1.SignatureLen // signature length
intrinsicSECP256k1FxSignatureCompute = 200 // secp256k1 signature verification time is around 200us
intrinsicConvertNetworkToL1ValidatorBandwidth = pcodecs.IntLen + // nodeID length
pcodecs.LongLen + // weight
pcodecs.LongLen + // balance
pcodecs.IntLen + // remaining balance owner threshold
pcodecs.IntLen + // remaining balance owner num addresses
pcodecs.IntLen + // deactivation owner threshold
pcodecs.IntLen // deactivation owner num addresses
intrinsicConvertNetworkToL1ValidatorBandwidth = wireIntLen + // nodeID length
wireLongLen + // weight
wireLongLen + // balance
wireIntLen + // remaining balance owner threshold
wireIntLen + // remaining balance owner num addresses
wireIntLen + // deactivation owner threshold
wireIntLen // deactivation owner num addresses
intrinsicBLSAggregateCompute = 5 // BLS public key aggregation time is around 5us
intrinsicBLSVerifyCompute = 1_000 // BLS verification time is around 1000us
@@ -99,44 +110,44 @@ var (
IntrinsicAddChainValidatorTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
intrinsicNetValidatorBandwidth + // netValidator
pcodecs.IntLen + // netAuth typeID
pcodecs.IntLen, // netAuthCredential typeID
wireIntLen + // netAuth typeID
wireIntLen, // netAuthCredential typeID
gas.DBRead: 3, // get net auth + check for net transformation + check for net conversion
gas.DBWrite: 3, // put current staker + write weight diff + write pk diff
}
IntrinsicCreateChainTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.IDLen + // subchainID
pcodecs.ShortLen + // chainName length
wireShortLen + // chainName length
ids.IDLen + // vmID
pcodecs.IntLen + // num fxIDs
pcodecs.IntLen + // genesis length
pcodecs.IntLen + // chainAuth typeID
pcodecs.IntLen, // chainAuthCredential typeID
wireIntLen + // num fxIDs
wireIntLen + // genesis length
wireIntLen + // chainAuth typeID
wireIntLen, // chainAuthCredential typeID
gas.DBRead: 3, // get chain auth + check for chain transformation + check for chain conversion
gas.DBWrite: 1, // put chain
}
IntrinsicCreateNetworkTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
pcodecs.IntLen, // owner typeID
wireIntLen, // owner typeID
gas.DBWrite: 1, // write chain owner
}
IntrinsicImportTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.IDLen + // source chainID
pcodecs.IntLen, // num importing inputs
wireIntLen, // num importing inputs
}
IntrinsicExportTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.IDLen + // destination chainID
pcodecs.IntLen, // num exported outputs
wireIntLen, // num exported outputs
}
IntrinsicRemoveChainValidatorTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.NodeIDLen + // nodeID
ids.IDLen + // netID
pcodecs.IntLen + // netAuth typeID
pcodecs.IntLen, // netAuthCredential typeID
wireIntLen + // netAuth typeID
wireIntLen, // netAuthCredential typeID
gas.DBRead: 1, // read net auth
gas.DBWrite: 3, // delete validator + write weight diff + write pk diff
}
@@ -144,11 +155,11 @@ var (
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
intrinsicValidatorBandwidth + // validator
ids.IDLen + // subchainID
pcodecs.IntLen + // signer typeID
pcodecs.IntLen + // num stake outs
pcodecs.IntLen + // validator rewards typeID
pcodecs.IntLen + // delegator rewards typeID
pcodecs.IntLen, // delegation shares
wireIntLen + // signer typeID
wireIntLen + // num stake outs
wireIntLen + // validator rewards typeID
wireIntLen + // delegator rewards typeID
wireIntLen, // delegation shares
gas.DBRead: 1, // get staking config
gas.DBWrite: 3, // put current staker + write weight diff + write pk diff
}
@@ -156,17 +167,17 @@ var (
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
intrinsicValidatorBandwidth + // validator
ids.IDLen + // subchainID
pcodecs.IntLen + // num stake outs
pcodecs.IntLen, // delegator rewards typeID
wireIntLen + // num stake outs
wireIntLen, // delegator rewards typeID
gas.DBRead: 1, // get staking config
gas.DBWrite: 2, // put current staker + write weight diff
}
IntrinsicTransferChainOwnershipTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.IDLen + // netID
pcodecs.IntLen + // netAuth typeID
pcodecs.IntLen + // owner typeID
pcodecs.IntLen, // netAuthCredential typeID
wireIntLen + // netAuth typeID
wireIntLen + // owner typeID
wireIntLen, // netAuthCredential typeID
gas.DBRead: 1, // read net auth
gas.DBWrite: 1, // set net owner
}
@@ -174,71 +185,71 @@ var (
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.IDLen + // netID
ids.IDLen + // assetID
pcodecs.IntLen + // initialSupply
pcodecs.IntLen + // maximumSupply
pcodecs.IntLen + // minConsumptionRate
pcodecs.IntLen + // maxConsumptionRate
pcodecs.LongLen + // minValidatorStake
pcodecs.LongLen + // maxValidatorStake
pcodecs.IntLen + // minStakeDuration
pcodecs.IntLen + // maxStakeDuration
pcodecs.IntLen + // minDelegationFee
pcodecs.IntLen + // minDelegatorStake
pcodecs.IntLen + // maxValidatorWeightFactor
pcodecs.IntLen + // uptimeRequirement
pcodecs.IntLen + // netAuth typeID
pcodecs.IntLen, // netAuthCredential typeID
wireIntLen + // initialSupply
wireIntLen + // maximumSupply
wireIntLen + // minConsumptionRate
wireIntLen + // maxConsumptionRate
wireLongLen + // minValidatorStake
wireLongLen + // maxValidatorStake
wireIntLen + // minStakeDuration
wireIntLen + // maxStakeDuration
wireIntLen + // minDelegationFee
wireIntLen + // minDelegatorStake
wireIntLen + // maxValidatorWeightFactor
wireIntLen + // uptimeRequirement
wireIntLen + // netAuth typeID
wireIntLen, // netAuthCredential typeID
gas.DBRead: 2, // get net auth + check for net transformation
gas.DBWrite: 1, // write net transformation
}
IntrinsicBaseTxComplexities = gas.Dimensions{
gas.Bandwidth: pcodecs.VersionSize + // codecVersion
pcodecs.IntLen + // typeID
pcodecs.IntLen + // networkID
gas.Bandwidth: wireVersionLen + // codecVersion
wireIntLen + // typeID
wireIntLen + // networkID
ids.IDLen + // blockchainID
pcodecs.IntLen + // number of outputs
pcodecs.IntLen + // number of inputs
pcodecs.IntLen + // length of memo
pcodecs.IntLen, // number of credentials
wireIntLen + // number of outputs
wireIntLen + // number of inputs
wireIntLen + // length of memo
wireIntLen, // number of credentials
}
IntrinsicConvertNetworkToL1TxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.IDLen + // subchainID
ids.IDLen + // chainID
pcodecs.IntLen + // address length
pcodecs.IntLen + // validators length
pcodecs.IntLen + // chainAuth typeID
pcodecs.IntLen, // chainAuthCredential typeID
wireIntLen + // address length
wireIntLen + // validators length
wireIntLen + // chainAuth typeID
wireIntLen, // chainAuthCredential typeID
gas.DBRead: 3, // chain auth + transformation lookup + conversion lookup
gas.DBWrite: 2, // write conversion manager + total weight
}
IntrinsicRegisterL1ValidatorTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
pcodecs.LongLen + // balance
wireLongLen + // balance
bls.SignatureLen + // proof of possession
pcodecs.IntLen, // message length
wireIntLen, // message length
gas.DBRead: 5, // conversion owner + expiry lookup + sov lookup + subchainID/nodeID lookup + weight lookup
gas.DBWrite: 6, // write current staker + expiry + write weight diff + write pk diff + subchainID/nodeID lookup + weight lookup
gas.Compute: intrinsicBLSPoPVerifyCompute,
}
IntrinsicSetL1ValidatorWeightTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
pcodecs.IntLen, // message length
wireIntLen, // message length
gas.DBRead: 3, // read staker + read conversion + read weight
gas.DBWrite: 5, // remaining balance utxo + write weight diff + write pk diff + weights lookup + validator write
}
IntrinsicIncreaseL1ValidatorBalanceTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.IDLen + // validationID
pcodecs.LongLen, // balance
wireLongLen, // balance
gas.DBRead: 1, // read staker
gas.DBWrite: 5, // weight diff + deactivated weight diff + public key diff + delete staker + write staker
}
IntrinsicDisableL1ValidatorTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.IDLen + // validationID
pcodecs.IntLen + // auth typeID
pcodecs.IntLen, // authCredential typeID
wireIntLen + // auth typeID
wireIntLen, // authCredential typeID
gas.DBRead: 1, // read staker
gas.DBWrite: 6, // write remaining balance utxo + weight diff + deactivated weight diff + public key diff + delete staker + write staker
}
+1 -9
View File
@@ -22,7 +22,6 @@ import (
"github.com/luxfi/node/cache/lru"
"github.com/luxfi/node/utils/json"
"github.com/luxfi/node/version"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/block"
"github.com/luxfi/node/vms/platformvm/config"
"github.com/luxfi/node/vms/platformvm/fx"
@@ -101,8 +100,7 @@ type VM struct {
chainID ids.ID
state state.State
fx fx.Fx
codecRegistry pcodecs.Registry
fx fx.Fx
// Bootstrapped remembers if this chain has finished bootstrapping or not
bootstrappedConsensus utils.Atomic[bool]
@@ -231,8 +229,6 @@ func (vm *VM) Initialize(
// Since DBManager is now an interface{}, we need to handle it differently
// logic simplified as we now just trust init.DB or fallback to memdb if nil above
// Note: this codec is never used to serialize anything
vm.codecRegistry = pcodecs.NewLinearCodec()
vm.fx = &secp256k1fx.Fx{}
if err := vm.fx.Initialize(vm); err != nil {
return fmt.Errorf("failed to initialize fx: %w", err)
@@ -777,10 +773,6 @@ func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
return vm.Network.Disconnected(ctx, nodeID)
}
func (vm *VM) CodecRegistry() pcodecs.Registry {
return vm.codecRegistry
}
func (vm *VM) Clock() *mockable.Clock {
return &vm.nodeClock
}
+8 -5
View File
@@ -10,12 +10,15 @@ import (
chain "github.com/luxfi/vm/chain"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/pcodecs"
statelessblock "github.com/luxfi/node/vms/proposervm/block"
)
var _ chain.BatchedChainVM = (*VM)(nil)
// containerLenPrefix is the 4-byte length prefix each block occupies in an
// ancestors response container; counted toward the response size budget.
const containerLenPrefix = 4
func (vm *VM) GetAncestors(
ctx context.Context,
blkID ids.ID,
@@ -42,10 +45,10 @@ func (vm *VM) GetAncestors(
blkBytes := blk.Bytes()
// Ensure response size isn't too large. Include pcodecs.IntLen because
// the size of the message is included with each container, and the size
// is repr. by an int.
currentByteLength += pcodecs.IntLen + len(blkBytes)
// Ensure response size isn't too large. Include containerLenPrefix
// because the size of the message is included with each container, and
// the size is repr. by an int.
currentByteLength += containerLenPrefix + len(blkBytes)
elapsedTime := vm.Clock.Time().Sub(startTime)
if len(res) > 0 && (currentByteLength >= maxBlocksSize || maxBlocksRetrievalTime <= elapsedTime) {
return res, nil // reached maximum size or ran out of time
+58 -43
View File
@@ -1,4 +1,4 @@
// 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
@@ -11,7 +11,7 @@ import (
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/zap"
)
var (
@@ -23,9 +23,9 @@ var (
// Epoch represents a P-Chain epoch for validator set coordination
type Epoch struct {
PChainHeight uint64 `serialize:"true" json:"pChainHeight"`
Number uint64 `serialize:"true" json:"number"`
StartTime int64 `serialize:"true" json:"startTime"`
PChainHeight uint64 `json:"pChainHeight"`
Number uint64 `json:"number"`
StartTime int64 `json:"startTime"`
}
type Block interface {
@@ -56,30 +56,19 @@ type SignedBlock interface {
BlobCount() uint32 // Number of DA blobs in block
}
type statelessUnsignedBlock struct {
ParentID ids.ID `serialize:"true"`
Timestamp int64 `serialize:"true"`
PChainHeight uint64 `serialize:"true"`
Epoch Epoch `serialize:"true"`
Certificate []byte `serialize:"true"`
Block []byte `serialize:"true"`
// Data Availability fields (v1.1 spec)
DARoot [32]byte `serialize:"true"` // Root of DA commitments
WitnessRoot [32]byte `serialize:"true"` // Root of witnesses/proofs
MessagesOutRoot [32]byte `serialize:"true"` // Root of outgoing cross-chain messages
BlobCount uint32 `serialize:"true"` // Number of DA blobs in block
}
// statelessBlock is zap-backed: msg is the unsigned body buffer (a
// self-delimiting zap message with blkSigned at offset 0), Signature is the
// optional proposer signature carried in the appended suffix buffer. All block
// fields are read from msg via fixed offsets — the struct IS the wire.
type statelessBlock struct {
StatelessBlock statelessUnsignedBlock `serialize:"true"`
Signature []byte `serialize:"true"`
msg *zap.Message // unsigned body buffer
Signature []byte // proposer signature (appended suffix); exported for tests
id ids.ID
timestamp time.Time
cert *staking.Certificate
proposer ids.NodeID
bytes []byte
bytes []byte // full signed bytes = unsigned ‖ sig
}
func (b *statelessBlock) ID() ids.ID {
@@ -87,34 +76,55 @@ func (b *statelessBlock) ID() ids.ID {
}
func (b *statelessBlock) ParentID() ids.ID {
return b.StatelessBlock.ParentID
return ids.ID(read32(b.msg.Root(), offParentID))
}
func (b *statelessBlock) Block() []byte {
return b.StatelessBlock.Block
return b.msg.Root().Bytes(offBlock)
}
func (b *statelessBlock) Bytes() []byte {
return b.bytes
}
// initialize binds the block's fields from its signed bytes. This is the one
// bytes→fields entry, shared by the Build* constructors (fresh buffer) and
// Parse (wire/disk buffer); the bytes are authoritative and never re-encoded.
func (b *statelessBlock) initialize(bytes []byte) error {
b.bytes = bytes
// The serialized form of the block is the unsignedBytes followed by the
// signature, which is prefixed by a uint32. So, we need to strip off the
// signature as well as it's length prefix to get the unsigned bytes.
lenUnsignedBytes := len(bytes) - pcodecs.IntLen - len(b.Signature)
unsignedBytes := bytes[:lenUnsignedBytes]
b.id = hash.ComputeHash256Array(unsignedBytes)
// The signed form is unsigned_buffer ‖ sig_buffer (both self-delimiting).
// Split on the leading message length; the ID is the hash of that prefix.
n, err := zapLen(bytes)
if err != nil {
return err
}
msg, err := zap.Parse(bytes[:n])
if err != nil {
return err
}
b.msg = msg
b.id = hash.ComputeHash256Array(bytes[:n])
b.timestamp = time.Unix(b.StatelessBlock.Timestamp, 0)
if len(b.StatelessBlock.Certificate) == 0 {
if len(bytes) > n {
sigMsg, err := zap.Parse(bytes[n:])
if err != nil {
return err
}
if s := sigMsg.Root().Bytes(offSig); len(s) > 0 {
b.Signature = append([]byte(nil), s...)
}
}
root := b.msg.Root()
b.timestamp = time.Unix(root.Int64(offTimestamp), 0)
certBytes := root.Bytes(offCert)
if len(certBytes) == 0 {
return nil
}
var err error
b.cert, err = staking.ParseCertificate(b.StatelessBlock.Certificate)
b.cert, err = staking.ParseCertificate(certBytes)
if err != nil {
return fmt.Errorf("%w: %w", errInvalidCertificate, err)
}
@@ -127,14 +137,14 @@ func (b *statelessBlock) initialize(bytes []byte) error {
}
func (b *statelessBlock) verify(chainID ids.ID) error {
if len(b.StatelessBlock.Certificate) == 0 {
if b.cert == nil {
if len(b.Signature) > 0 {
return errUnexpectedSignature
}
return nil
}
header, err := BuildHeader(chainID, b.StatelessBlock.ParentID, b.id)
header, err := BuildHeader(chainID, b.ParentID(), b.id)
if err != nil {
return err
}
@@ -148,11 +158,16 @@ func (b *statelessBlock) verify(chainID ids.ID) error {
}
func (b *statelessBlock) PChainHeight() uint64 {
return b.StatelessBlock.PChainHeight
return b.msg.Root().Uint64(offPChainHt)
}
func (b *statelessBlock) PChainEpoch() Epoch {
return b.StatelessBlock.Epoch
root := b.msg.Root()
return Epoch{
PChainHeight: root.Uint64(offEpochHt),
Number: root.Uint64(offEpochNum),
StartTime: root.Int64(offEpochStart),
}
}
func (b *statelessBlock) Timestamp() time.Time {
@@ -164,17 +179,17 @@ func (b *statelessBlock) Proposer() ids.NodeID {
}
func (b *statelessBlock) DARoot() [32]byte {
return b.StatelessBlock.DARoot
return read32(b.msg.Root(), offDARoot)
}
func (b *statelessBlock) WitnessRoot() [32]byte {
return b.StatelessBlock.WitnessRoot
return read32(b.msg.Root(), offWitnessRoot)
}
func (b *statelessBlock) MessagesOutRoot() [32]byte {
return b.StatelessBlock.MessagesOutRoot
return read32(b.msg.Root(), offMsgOutRoot)
}
func (b *statelessBlock) BlobCount() uint32 {
return b.StatelessBlock.BlobCount
return b.msg.Root().Uint32(offBlobCount)
}
+189
View File
@@ -0,0 +1,189 @@
// 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. There is no codec, no version
// prefix, no slot map. A 1-byte blockKind at object offset 0 is the whole
// dispatch — Parse reads it and returns the typed block (signed body vs option).
//
// A signed block is unsigned_buffer ‖ sig_buffer (both self-delimiting zap
// messages), so the unsigned bytes are a genuine byte-prefix of the signed
// bytes and the block ID = hash(unsigned prefix). An unsigned block (no
// certificate, no signature) is just the unsigned buffer with no suffix. An
// option is a single self-delimiting message.
//
// Unsigned-block object (kind = blkSigned), all offsets object-relative, LE:
//
// kind u8 @ 0
// ParentID 32B @ 1
// Timestamp i64 @ 33
// PChainHeight u64 @ 41
// Epoch.PChainHt u64 @ 49
// Epoch.Number u64 @ 57
// Epoch.StartTime i64 @ 65
// Certificate 8B @ 73 bytes ptr (opaque DER; part of the ID hash)
// Block 8B @ 81 bytes ptr (inner block bytes)
// DARoot 32B @ 89
// WitnessRoot 32B @ 121
// MessagesOutRoot 32B @ 153
// BlobCount u32 @ 185
// size 189
//
// Sig buffer object: Signature bytes ptr @ 0 size 8
// Header object: Chain 32B @0, Parent 32B @32, Body 32B @64 size 96
// Option object (kind = blkOption): kind u8 @0, ParentID 32B @1,
// InnerBytes bytes ptr @33 size 41
import (
"encoding/binary"
"errors"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// blockKind is the 1-byte discriminator at object offset 0 of every parseable
// proposervm block buffer. Parse reads it and returns the typed block.
type blockKind uint8
const (
blkReserved blockKind = iota
blkSigned // statelessBlock (may carry cert + appended signature)
blkOption // option block
)
var errUnknownBlockType = errors.New("proposervm block: unknown block kind")
// Unsigned-block object offsets + size.
const (
offKind = 0
offParentID = 1
offTimestamp = 33
offPChainHt = 41
offEpochHt = 49
offEpochNum = 57
offEpochStart = 65
offCert = 73
offBlock = 81
offDARoot = 89
offWitnessRoot = 121
offMsgOutRoot = 153
offBlobCount = 185
sizeUnsigned = 189
idLen = 32
)
// Sig buffer object.
const (
offSig = 0
sizeSig = 8
)
// Header object.
const (
hdrChain = 0
hdrParent = 32
hdrBody = 64
sizeHeader = 96
)
// Option object.
const (
offOptParent = 1
offOptInner = 33
sizeOption = 41
)
// zapLen returns the total length of the leading self-delimiting zap message in
// b (its header size field @[12:16]). This is the split point between the
// unsigned prefix and the signature suffix of a signed block.
func zapLen(b []byte) (int, error) {
if len(b) < zap.HeaderSize {
return 0, zap.ErrBufferTooSmall
}
n := int(binary.LittleEndian.Uint32(b[12:16]))
if n < zap.HeaderSize || n > len(b) {
return 0, zap.ErrBufferTooSmall
}
return n, nil
}
// buildUnsignedBuffer writes the unsigned block body as one zap object. cert is
// the opaque DER certificate (nil for an unsigned block). The v1.1 DA fields
// (DARoot/WitnessRoot/MessagesOutRoot/BlobCount) are written as zeros — no
// current constructor populates them, but they stay in the wire so the
// SignedBlock accessor surface round-trips if a future builder sets them.
func buildUnsignedBuffer(parentID ids.ID, timestamp int64, pChainHeight uint64, epoch Epoch, cert, blockBytes []byte) []byte {
var zero [idLen]byte
b := zap.NewBuilder(zap.HeaderSize + sizeUnsigned + len(cert) + len(blockBytes) + 64)
ob := b.StartObject(sizeUnsigned)
ob.SetUint8(offKind, uint8(blkSigned))
ob.SetBytesFixed(offParentID, parentID[:])
ob.SetInt64(offTimestamp, timestamp)
ob.SetUint64(offPChainHt, pChainHeight)
ob.SetUint64(offEpochHt, epoch.PChainHeight)
ob.SetUint64(offEpochNum, epoch.Number)
ob.SetInt64(offEpochStart, epoch.StartTime)
ob.SetBytes(offCert, cert)
ob.SetBytes(offBlock, blockBytes)
ob.SetBytesFixed(offDARoot, zero[:])
ob.SetBytesFixed(offWitnessRoot, zero[:])
ob.SetBytesFixed(offMsgOutRoot, zero[:])
ob.SetUint32(offBlobCount, 0)
ob.FinishAsRoot()
return b.Finish()
}
// buildSigBuffer wraps a proposer signature as a standalone zap message,
// appended after the unsigned prefix of a signed block.
func buildSigBuffer(sig []byte) []byte {
b := zap.NewBuilder(zap.HeaderSize + sizeSig + len(sig))
ob := b.StartObject(sizeSig)
ob.SetBytes(offSig, sig)
ob.FinishAsRoot()
return b.Finish()
}
// buildHeaderBuffer encodes the (chain, parent, body) header signed by the
// proposer. Deterministic fixed-shape object — Build and verify both hash it.
func buildHeaderBuffer(chainID, parentID, bodyID ids.ID) []byte {
b := zap.NewBuilder(zap.HeaderSize + sizeHeader)
ob := b.StartObject(sizeHeader)
ob.SetBytesFixed(hdrChain, chainID[:])
ob.SetBytesFixed(hdrParent, parentID[:])
ob.SetBytesFixed(hdrBody, bodyID[:])
ob.FinishAsRoot()
return b.Finish()
}
// buildOptionBuffer encodes an option block as one zap object.
func buildOptionBuffer(parentID ids.ID, innerBytes []byte) []byte {
b := zap.NewBuilder(zap.HeaderSize + sizeOption + len(innerBytes) + 32)
ob := b.StartObject(sizeOption)
ob.SetUint8(offKind, uint8(blkOption))
ob.SetBytesFixed(offOptParent, parentID[:])
ob.SetBytes(offOptInner, innerBytes)
ob.FinishAsRoot()
return b.Finish()
}
// read32 reads a 32-byte fixed field at the given object offset.
func read32(o zap.Object, off int) [idLen]byte {
var r [idLen]byte
copy(r[:], o.BytesFixedSlice(off, idLen))
return r
}
// concat returns a ‖ b as a fresh slice.
func concat(a, b []byte) []byte {
out := make([]byte, 0, len(a)+len(b))
out = append(out, a...)
out = append(out, b...)
return out
}
+25 -65
View File
@@ -1,4 +1,4 @@
// 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
@@ -11,7 +11,6 @@ import (
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/vms/pcodecs"
)
func BuildUnsigned(
@@ -21,24 +20,14 @@ func BuildUnsigned(
epoch Epoch,
blockBytes []byte,
) (SignedBlock, error) {
var block SignedBlock = &statelessBlock{
StatelessBlock: statelessUnsignedBlock{
ParentID: parentID,
Timestamp: timestamp.Unix(),
PChainHeight: pChainHeight,
Epoch: epoch,
Certificate: nil,
Block: blockBytes,
},
timestamp: timestamp,
}
// No certificate, no signature: the block bytes are just the unsigned body.
unsignedBytes := buildUnsignedBuffer(parentID, timestamp.Unix(), pChainHeight, epoch, nil, blockBytes)
bytes, err := Codec.Marshal(CodecVersion, &block)
if err != nil {
block := &statelessBlock{}
if err := block.initialize(unsignedBytes); err != nil {
return nil, err
}
return block, block.initialize(bytes)
return block, nil
}
func Build(
@@ -51,50 +40,30 @@ func Build(
chainID ids.ID,
key crypto.Signer,
) (SignedBlock, error) {
block := &statelessBlock{
StatelessBlock: statelessUnsignedBlock{
ParentID: parentID,
Timestamp: timestamp.Unix(),
PChainHeight: pChainHeight,
Epoch: epoch,
Certificate: cert.Raw,
Block: blockBytes,
},
timestamp: timestamp,
cert: cert,
proposer: ids.NodeIDFromCert(&ids.Certificate{
Raw: cert.Raw,
PublicKey: cert.PublicKey,
}),
}
var blockIntf SignedBlock = block
unsignedBytes := buildUnsignedBuffer(parentID, timestamp.Unix(), pChainHeight, epoch, cert.Raw, blockBytes)
unsignedBytesWithEmptySignature, err := Codec.Marshal(CodecVersion, &blockIntf)
if err != nil {
return nil, err
}
// The block ID is the hash of the unsigned prefix; the proposer signs a
// header binding (chainID, parentID, blockID).
id := hash.ComputeHash256Array(unsignedBytes)
// The serialized form of the block is the unsignedBytes followed by the
// signature, which is prefixed by a uint32. Because we are marshalling the
// block with an empty signature, we only need to strip off the length
// prefix to get the unsigned bytes.
lenUnsignedBytes := len(unsignedBytesWithEmptySignature) - pcodecs.IntLen
unsignedBytes := unsignedBytesWithEmptySignature[:lenUnsignedBytes]
block.id = hash.ComputeHash256Array(unsignedBytes)
header, err := BuildHeader(chainID, parentID, block.id)
header, err := BuildHeader(chainID, parentID, id)
if err != nil {
return nil, err
}
headerHash := hash.ComputeHash256(header.Bytes())
block.Signature, err = key.Sign(rand.Reader, headerHash, crypto.SHA256)
sig, err := key.Sign(rand.Reader, headerHash, crypto.SHA256)
if err != nil {
return nil, err
}
block.bytes, err = Codec.Marshal(CodecVersion, &blockIntf)
return block, err
signedBytes := concat(unsignedBytes, buildSigBuffer(sig))
block := &statelessBlock{}
if err := block.initialize(signedBytes); err != nil {
return nil, err
}
return block, nil
}
func BuildHeader(
@@ -102,15 +71,12 @@ func BuildHeader(
parentID ids.ID,
bodyID ids.ID,
) (Header, error) {
header := statelessHeader{
return &statelessHeader{
Chain: chainID,
Parent: parentID,
Body: bodyID,
}
bytes, err := Codec.Marshal(CodecVersion, &header)
header.bytes = bytes
return &header, err
bytes: buildHeaderBuffer(chainID, parentID, bodyID),
}, nil
}
// BuildOption the option block
@@ -120,15 +86,9 @@ func BuildOption(
parentID ids.ID,
innerBytes []byte,
) (Block, error) {
var block Block = &option{
PrntID: parentID,
InnerBytes: innerBytes,
}
bytes, err := Codec.Marshal(CodecVersion, &block)
if err != nil {
block := &option{}
if err := block.initialize(buildOptionBuffer(parentID, innerBytes)); err != nil {
return nil, err
}
return block, block.initialize(bytes)
return block, nil
}
-30
View File
@@ -1,30 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
)
const CodecVersion = 0
var Codec pcodecs.Manager
func init() {
lc := pcodecs.NewLinearCodec()
// The maximum block size is enforced by the p2p message size limit.
// See: [constants.DefaultMaxMessageSize]
Codec = pcodecs.NewMaxIntManager()
err := errors.Join(
lc.RegisterType(&statelessBlock{}),
lc.RegisterType(&option{}),
Codec.RegisterCodec(CodecVersion, lc),
)
if err != nil {
panic(err)
}
}
+7 -4
View File
@@ -1,4 +1,4 @@
// 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
@@ -12,10 +12,13 @@ type Header interface {
Bytes() []byte
}
// statelessHeader is the (chain, parent, body) binding the proposer signs. Its
// bytes are a single fixed-shape zap object (see buildHeaderBuffer); the struct
// caches the three IDs plus the encoded bytes.
type statelessHeader struct {
Chain ids.ID `serialize:"true"`
Parent ids.ID `serialize:"true"`
Body ids.ID `serialize:"true"`
Chain ids.ID
Parent ids.ID
Body ids.ID
bytes []byte
}
+14 -8
View File
@@ -1,17 +1,18 @@
// 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/ids"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// option is zap-backed: msg is the single self-delimiting message with
// blkOption at offset 0. ID = hash(bytes); options carry no signature.
type option struct {
PrntID ids.ID `serialize:"true"`
InnerBytes []byte `serialize:"true"`
msg *zap.Message
id ids.ID
bytes []byte
}
@@ -21,11 +22,11 @@ func (b *option) ID() ids.ID {
}
func (b *option) ParentID() ids.ID {
return b.PrntID
return ids.ID(read32(b.msg.Root(), offOptParent))
}
func (b *option) Block() []byte {
return b.InnerBytes
return b.msg.Root().Bytes(offOptInner)
}
func (b *option) Bytes() []byte {
@@ -33,8 +34,13 @@ func (b *option) Bytes() []byte {
}
func (b *option) initialize(bytes []byte) error {
b.id = hash.ComputeHash256Array(bytes)
msg, err := zap.Parse(bytes)
if err != nil {
return err
}
b.msg = msg
b.bytes = bytes
b.id = hash.ComputeHash256Array(bytes)
return nil
}
+27 -14
View File
@@ -1,4 +1,4 @@
// 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
@@ -8,6 +8,7 @@ import (
"sync"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
type ParseResult struct {
@@ -46,28 +47,40 @@ func Parse(bytes []byte, chainID ids.ID) (Block, error) {
}
// ParseWithoutVerification parses a block without verifying that the signature
// on the block is correct.
// on the block is correct. Dispatch is the 1-byte blockKind at object offset 0
// of the leading self-delimiting zap message — no codec, no version.
func ParseWithoutVerification(bytes []byte) (Block, error) {
var block Block
parsedVersion, err := Codec.Unmarshal(bytes, &block)
n, err := zapLen(bytes)
if err != nil {
return nil, err
}
if parsedVersion != CodecVersion {
return nil, fmt.Errorf("expected codec version %d but got %d", CodecVersion, parsedVersion)
msg, err := zap.Parse(bytes[:n])
if err != nil {
return nil, err
}
switch k := blockKind(msg.Root().Uint8(offKind)); k {
case blkSigned:
block := &statelessBlock{}
return block, block.initialize(bytes)
case blkOption:
block := &option{}
return block, block.initialize(bytes[:n])
default:
return nil, fmt.Errorf("%w: %d", errUnknownBlockType, k)
}
return block, block.initialize(bytes)
}
func ParseHeader(bytes []byte) (Header, error) {
header := statelessHeader{}
parsedVersion, err := Codec.Unmarshal(bytes, &header)
msg, err := zap.Parse(bytes)
if err != nil {
return nil, err
}
if parsedVersion != CodecVersion {
return nil, fmt.Errorf("expected codec version %d but got %d", CodecVersion, parsedVersion)
}
header.bytes = bytes
return &header, nil
root := msg.Root()
return &statelessHeader{
Chain: ids.ID(read32(root, hdrChain)),
Parent: ids.ID(read32(root, hdrParent)),
Body: ids.ID(read32(root, hdrBody)),
bytes: bytes,
}, nil
}
+20 -27
View File
@@ -1,4 +1,4 @@
// 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
@@ -13,7 +13,7 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/zap"
)
func TestParseBlocks(t *testing.T) {
@@ -54,12 +54,12 @@ func TestParseBlocks(t *testing.T) {
{
name: "ValidThenInvalid",
input: [][]byte{signedBlockBytes, malformedBlockBytes},
output: []ParseResult{{Block: &statelessBlock{bytes: signedBlockBytes}}, {Err: pcodecs.ErrInsufficientLength}},
output: []ParseResult{{Block: &statelessBlock{bytes: signedBlockBytes}}, {Err: zap.ErrBufferTooSmall}},
},
{
name: "InvalidThenValid",
input: [][]byte{malformedBlockBytes, signedBlockBytes},
output: []ParseResult{{Err: pcodecs.ErrInsufficientLength}, {Block: &statelessBlock{bytes: signedBlockBytes}}},
output: []ParseResult{{Err: zap.ErrBufferTooSmall}, {Block: &statelessBlock{bytes: signedBlockBytes}}},
},
} {
t.Run(testCase.name, func(t *testing.T) {
@@ -106,13 +106,12 @@ func TestParse(t *testing.T) {
unsignedBlock, err := BuildUnsigned(parentID, timestamp, pChainHeight, Epoch{}, innerBlockBytes)
require.NoError(t, err)
// A block that carries no certificate but does carry a signature suffix:
// verify must reject it with errUnexpectedSignature.
signedWithoutCertBlockIntf, err := BuildUnsigned(parentID, timestamp, pChainHeight, Epoch{}, innerBlockBytes)
require.NoError(t, err)
signedWithoutCertBlock := signedWithoutCertBlockIntf.(*statelessBlock)
signedWithoutCertBlock.Signature = []byte{5}
signedWithoutCertBlock.bytes, err = Codec.Marshal(CodecVersion, &signedWithoutCertBlockIntf)
require.NoError(t, err)
require.NoError(t, signedWithoutCertBlock.initialize(concat(signedWithoutCertBlock.Bytes(), buildSigBuffer([]byte{5}))))
optionBlock, err := BuildOption(parentID, innerBlockBytes)
require.NoError(t, err)
@@ -175,24 +174,22 @@ func TestParse(t *testing.T) {
func TestParseBytes(t *testing.T) {
chainID := ids.ID{4}
tests := []struct {
name string
hex string
expectedErr error
expectedErrMsg string // when set, match by ErrorContains instead of ErrorIs
name string
hex string
expectedErr error
}{
{
// Malformed cert: extension count length-prefix overflows int32. Under the
// ZAP-native parser this trips the max-slice-length guard. The exact sentinel
// can vary between proto/zap_codec and the standalone luxfi/zapcodec module,
// so match on the canonical error message instead of a typed sentinel.
name: "duplicate extensions in certificate",
hex: "0000000000000100000000000000000000000000000000000000000000000000000000000000000000000000007b0000000000000002000004bd308204b9308202a1a003020102020100300d06092a864886f70d01010b050030003020170d3939313233313030303030305a180f32313232303830333233323835335a300030820222300d06092a864886f70d01010105000382020f003082020a0282020100c2b2de1c16924d9b9254a0d5b80a4bc5f9beaa4f4f40a0e4efb69eb9b55d7d37f8c82328c237d7c5b451f5427b487284fa3f365f9caa53c7fcfef8d7a461d743bd7d88129f2da62b877ebe9d6feabf1bd12923e6c12321382c782fc3bb6b6cb4986a937a1edc3814f4e621e1a62053deea8c7649e43edd97ab6b56315b00d9ab5026bb9c31fb042dc574ba83c54e720e0120fcba2e8a66b77839be3ece0d4a6383ef3f76aac952b49a15b65e18674cd1340c32cecbcbaf80ae45be001366cb56836575fb0ab51ea44bf7278817e99b6b180fdd110a49831a132968489822c56692161bbd372cf89d9b8ee5a734cff15303b3a960ee78d79e76662a701941d9ec084429f26707f767e9b1d43241c0e4f96655d95c1f4f4aa00add78eff6bf0a6982766a035bf0b465786632c5bb240788ca0fdf032d8815899353ea4bec5848fd30118711e5b356bde8a0da074cc25709623225e734ff5bd0cf65c40d9fd8fccf746d8f8f35145bcebcf378d2b086e57d78b11e84f47fa467c4d037f92bff6dd4e934e0189b58193f24c4222ffb72b5c06361cf68ca64345bc3e230cc0f40063ad5f45b1659c643662996328c2eeddcd760d6f7c9cbae081ccc065844f7ea78c858564a408979764de882793706acc67d88092790dff567ed914b03355330932616a0f26f994b963791f0b1dbd8df979db86d1ea490700a3120293c3c2b10bef10203010001a33c303a300e0603551d0f0101ff0404030204b030130603551d25040c300a06082b0601050507030230130603551d25040c300a06082b06010505070302300d06092a864886f70d01010b05000382020100a21a0d73ec9ef4eb39f810557ac70b0b775772b8bae5f42c98565bc50b5b2c57317aa9cb1da12f55d0aac7bb36a00cd4fd0d7384c4efa284b53520c5a3c4b8a65240b393eeab02c802ea146c0728c3481c9e8d3aaad9d4dd7607103dcfaa96da83460adbe18174ed5b71bde7b0a93d4fb52234a9ff54e3fd25c5b74790dfb090f2e59dc5907357f510cc3a0b70ccdb87aee214def794b316224f318b471ffa13b66e44b467670e881cb1628c99c048a503376d9b6d7b8eef2e7be47ff7d5c1d56221f4cf7fa2519b594cb5917815c64dc75d8d281bcc99b5a12899b08f2ca0f189857b64a1afc5963337f3dd6e79390e85221569f6dbbb13aadce06a3dfb5032f0cc454809627872cd7cd0cea5eba187723f07652c8abc3fc42bd62136fc66287f2cc19a7cb416923ad1862d7f820b55cacb65e43731cb6df780e2651e457a3438456aeeeb278ad9c0ad2e760f6c1cbe276eeb621c8a4e609b5f2d902beb3212e3e45df99497021ff536d0b56390c5d785a8bf7909f6b61bdc705d7d92ae22f58e7b075f164a0450d82d8286bf449072751636ab5185f59f518b845a75d112d6f7b65223479202cff67635e2ad88106bc8a0cc9352d87c5b182ac19a4680a958d814a093acf46730f87da0df6926291d02590f215041b44a0a1a32eeb3a52cddabc3d256689bace18a8d85e644cf9137cce3718f7caac1cb16ae06e874f4c701000000010300000200b8e3a4d9a4394bac714cb597f5ba1a81865185e35c782d0317e7abc0b52d49ff8e10f787bedf86f08148e3dbd2d2d478caa2a2893d31db7d5ee51339883fe84d3004440f16cb3797a7fab0f627d3ebd79217e995488e785cd6bb7b96b9d306f8109daa9cfc4162f9839f60fb965bcb3b56a5fa787549c153a4c80027398f73a617b90b7f24f437b140cd3ac832c0b75ec98b9423b275782988a9fd426937b8f82fbb0e88a622934643fb6335c1a080a4d13125544b04585d5f5295be7cd2c8be364246ea3d5df3e837b39a85074575a1fa2f4799050460110bdfb20795c8a9172a20f61b95e1c5c43eccd0c2c155b67385366142c63409cb3fb488e7aba6c8930f7f151abf1c24a54bd21c3f7a06856ea9db35beddecb30d2c61f533a3d0590bdbb438c6f2a2286dfc3c71b383354f0abad72771c2cc3687b50c2298783e53857cf26058ed78d0c1cf53786eb8d006a058ee3c85a7b2b836b5d03ef782709ce8f2725548e557b3de45a395a669a15f1d910e97015d22ac70020cab7e2531e8b1f739b023b49e742203e9e19a7fe0053826a9a2fe2e118d3b83498c2cb308573202ad41aa4a390aee4b6b5dd2164e5c5cd1b5f68b7d5632cf7dbb9a9139663c9aac53a74b2c6fc73cad80e228a186ba027f6f32f0182d62503e04fcced385f2e7d2e11c00940622ebd533b4d144689082f9777e5b16c36f9af9066e0ad6564d43",
expectedErrMsg: "max slice length",
// Fewer than zap.HeaderSize bytes: rejected at the wire boundary.
name: "too short",
hex: "000102030405",
expectedErr: zap.ErrBufferTooSmall,
},
{
name: "gibberish",
hex: "000102030405",
expectedErr: pcodecs.ErrUnknownVersion,
// A 16-byte frame with a valid size field (=16) but the wrong magic
// bytes: rejected by zap.Parse.
name: "invalid magic",
hex: "00000000000000000000000010000000",
expectedErr: zap.ErrInvalidMagic,
},
}
for _, test := range tests {
@@ -203,11 +200,7 @@ func TestParseBytes(t *testing.T) {
require.NoError(err)
_, err = Parse(bytes, chainID)
if test.expectedErrMsg != "" {
require.ErrorContains(err, test.expectedErrMsg)
} else {
require.ErrorIs(err, test.expectedErr)
}
require.ErrorIs(err, test.expectedErr)
})
}
}
+5 -3
View File
@@ -5,6 +5,7 @@ package proposer
import (
"context"
"encoding/binary"
"errors"
"math/bits"
"time"
@@ -15,7 +16,6 @@ import (
"github.com/luxfi/container/sampler"
"github.com/luxfi/ids"
"github.com/luxfi/math"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/utils"
)
@@ -106,11 +106,13 @@ type windower struct {
}
func New(state validators.State, netID, chainID ids.ID) Windower {
w := pcodecs.Packer{Bytes: chainID[:]}
// chainSource seeds the per-chain PRNG for proposer selection: the first 8
// bytes of the chainID read little-endian (byte-for-byte the value the
// retired pcodecs.Packer.UnpackLong produced).
return &windower{
state: state,
netID: netID,
chainSource: w.UnpackLong(),
chainSource: binary.LittleEndian.Uint64(chainID[:8]),
}
}
+9 -17
View File
@@ -14,17 +14,16 @@ import (
"github.com/luxfi/node/cache"
"github.com/luxfi/node/cache/lru"
"github.com/luxfi/node/cache/metercacher"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/proposervm/block"
)
const blockCacheSize = 64 * constants.MiB
var (
errBlockWrongVersion = errors.New("wrong version")
// statusIntLen is the wire width of the serialized status uint32; used only for
// cache-size accounting.
const statusIntLen = 4
_ BlockState = (*blockState)(nil)
)
var _ BlockState = (*blockState)(nil)
type BlockState interface {
GetBlock(blkID ids.ID) (block.Block, error)
@@ -41,8 +40,8 @@ type blockState struct {
}
type blockWrapper struct {
Block []byte `serialize:"true"`
StatusInt uint32 `serialize:"true"` // Store status as uint32 for serialization
Block []byte
StatusInt uint32 // Store status as uint32 for serialization
block block.Block
status choices.Status // Keep the actual status here
@@ -52,7 +51,7 @@ func cachedBlockSize(_ ids.ID, bw *blockWrapper) int {
if bw == nil {
return ids.IDLen + constants.PointerOverhead
}
return ids.IDLen + len(bw.Block) + pcodecs.IntLen + 2*constants.PointerOverhead
return ids.IDLen + len(bw.Block) + statusIntLen + 2*constants.PointerOverhead
}
func NewBlockState(db database.Database) BlockState {
@@ -97,13 +96,9 @@ func (s *blockState) GetBlock(blkID ids.ID) (block.Block, error) {
}
blkWrapper := blockWrapper{}
parsedVersion, err := Codec.Unmarshal(blkWrapperBytes, &blkWrapper)
if err != nil {
if err := parseBlockWrapper(blkWrapperBytes, &blkWrapper); err != nil {
return nil, err
}
if parsedVersion != CodecVersion {
return nil, errBlockWrongVersion
}
// The key was in the database
blk, err := block.ParseWithoutVerification(blkWrapper.Block)
@@ -124,10 +119,7 @@ func (s *blockState) PutBlock(blk block.Block) error {
block: blk,
}
bytes, err := Codec.Marshal(CodecVersion, &blkWrapper)
if err != nil {
return err
}
bytes := marshalBlockWrapper(&blkWrapper)
blkID := blk.ID()
s.blkCache.Put(blkID, &blkWrapper)
-22
View File
@@ -1,22 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"github.com/luxfi/node/vms/pcodecs"
)
const CodecVersion = 0
var Codec pcodecs.Manager
func init() {
lc := pcodecs.NewLinearCodec()
Codec = pcodecs.NewMaxInt32Manager()
err := Codec.RegisterCodec(CodecVersion, lc)
if err != nil {
panic(err)
}
}
+45
View File
@@ -0,0 +1,45 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
// Native-ZAP wire for the on-disk block store value (blockWrapper). The struct
// IS the wire: one fixed-shape zap object, no codec, no version prefix, no slot
// registry. Re-genesis means the on-disk format is free to change, so this
// layout is the canonical one.
//
// object (size 16): StatusInt u32 @0, Block bytes ptr @8.
// The stored block bytes are opaque — a self-delimiting zap block buffer
// re-parsed via block.ParseWithoutVerification, so the block ID is preserved
// with no re-encoding. Block bytes are copied out of the transient DB read
// buffer so a cached value never aliases it.
import "github.com/luxfi/zap"
const (
bwStatus = 0
bwBlock = 8
bwSize = 16
)
func marshalBlockWrapper(bw *blockWrapper) []byte {
b := zap.NewBuilder(zap.HeaderSize + bwSize + len(bw.Block))
ob := b.StartObject(bwSize)
ob.SetUint32(bwStatus, bw.StatusInt)
ob.SetBytes(bwBlock, bw.Block)
ob.FinishAsRoot()
return b.Finish()
}
func parseBlockWrapper(bytes []byte, bw *blockWrapper) error {
msg, err := zap.Parse(bytes)
if err != nil {
return err
}
root := msg.Root()
bw.StatusInt = root.Uint32(bwStatus)
if blk := root.Bytes(bwBlock); len(blk) > 0 {
bw.Block = append([]byte(nil), blk...)
}
return nil
}
+5 -12
View File
@@ -1,11 +1,9 @@
// 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 summary
import (
"fmt"
"github.com/luxfi/crypto/hash"
)
@@ -14,18 +12,13 @@ func Build(
block []byte,
coreSummary []byte,
) (StateSummary, error) {
summary := stateSummary{
summary := &stateSummary{
Height: forkHeight,
Block: block,
InnerSummary: coreSummary,
}
bytes, err := Codec.Marshal(CodecVersion, &summary)
if err != nil {
return nil, fmt.Errorf("cannot marshal proposer summary due to: %w", err)
}
summary.id = hash.ComputeHash256Array(bytes)
summary.bytes = bytes
return &summary, nil
summary.bytes = summary.marshal()
summary.id = hash.ComputeHash256Array(summary.bytes)
return summary, nil
}
-26
View File
@@ -1,26 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package summary
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
)
const CodecVersion = 0
var (
Codec pcodecs.Manager
errWrongCodecVersion = errors.New("wrong codec version")
)
func init() {
lc := pcodecs.NewLinearCodec()
Codec = pcodecs.NewMaxInt32Manager()
if err := Codec.RegisterCodec(CodecVersion, lc); err != nil {
panic(err)
}
}
+4 -8
View File
@@ -1,4 +1,4 @@
// 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 summary
@@ -10,16 +10,12 @@ import (
)
func Parse(bytes []byte) (StateSummary, error) {
summary := stateSummary{
summary := &stateSummary{
id: hash.ComputeHash256Array(bytes),
bytes: bytes,
}
version, err := Codec.Unmarshal(bytes, &summary)
if err != nil {
if err := summary.unmarshal(bytes); err != nil {
return nil, fmt.Errorf("could not unmarshal summary due to: %w", err)
}
if version != CodecVersion {
return nil, errWrongCodecVersion
}
return &summary, nil
return summary, nil
}
+2 -2
View File
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/zap"
)
func TestParse(t *testing.T) {
@@ -37,5 +37,5 @@ func TestParseGibberish(t *testing.T) {
bytes := []byte{0, 1, 2, 3, 4, 5}
_, err := Parse(bytes)
require.ErrorIs(err, pcodecs.ErrUnknownVersion)
require.ErrorIs(err, zap.ErrBufferTooSmall)
}
+44 -4
View File
@@ -3,7 +3,10 @@
package summary
import "github.com/luxfi/ids"
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
var _ StateSummary = (*stateSummary)(nil)
@@ -15,18 +18,55 @@ type StateSummary interface {
Bytes() []byte
}
// stateSummary is zap-backed: the struct IS the wire. One fixed-shape zap
// object, no codec, no version prefix.
//
// object (size 24): Height u64 @0, Block bytes ptr @8, InnerSummary bytes ptr @16.
type stateSummary struct {
Height uint64 `serialize:"true"`
Height uint64
// proposervm information. We would then modify the StateSummary
// interface to expose the required information to generate the full
// block.
Block []byte `serialize:"true"`
InnerSummary []byte `serialize:"true"`
Block []byte
InnerSummary []byte
id ids.ID
bytes []byte
}
const (
ssHeight = 0
ssBlock = 8
ssInner = 16
ssSize = 24
)
func (s *stateSummary) marshal() []byte {
b := zap.NewBuilder(zap.HeaderSize + ssSize + len(s.Block) + len(s.InnerSummary))
ob := b.StartObject(ssSize)
ob.SetUint64(ssHeight, s.Height)
ob.SetBytes(ssBlock, s.Block)
ob.SetBytes(ssInner, s.InnerSummary)
ob.FinishAsRoot()
return b.Finish()
}
func (s *stateSummary) unmarshal(bytes []byte) error {
msg, err := zap.Parse(bytes)
if err != nil {
return err
}
root := msg.Root()
s.Height = root.Uint64(ssHeight)
if blk := root.Bytes(ssBlock); len(blk) > 0 {
s.Block = append([]byte(nil), blk...)
}
if inner := root.Bytes(ssInner); len(inner) > 0 {
s.InnerSummary = append([]byte(nil), inner...)
}
return nil
}
func (s *stateSummary) ID() ids.ID {
return s.id
}
@@ -14,8 +14,8 @@ import (
"syscall"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/rpcchainvm/runtime"
"github.com/luxfi/utils/wrappers"
)
func NewCmd(path string, args ...string) *exec.Cmd {
@@ -31,7 +31,7 @@ func stop(ctx context.Context, log log.Logger, cmd *exec.Cmd) {
waitChan := make(chan error)
go func() {
// attempt graceful shutdown
errs := pcodecs.Errs{}
errs := wrappers.Errs{}
err := cmd.Process.Signal(syscall.SIGTERM)
errs.Add(err)
_, err = cmd.Process.Wait()
@@ -43,7 +43,7 @@ func main() {
log.Fatalf("failed to parse net ID: %s\n", err)
}
genesisBytes, err := xsgenesis.Codec.Marshal(xsgenesis.CodecVersion, genesis)
genesisBytes, err := genesis.Marshal()
if err != nil {
log.Fatalf("failed to create genesis bytes: %s\n", err)
}