xvm -> native ZAP struct-is-wire: node-side codec kill COMPLETE

X-Chain fully off pcodecs (the last node consumer). kind.go (xkind 1-5), tx.go
(wire.SignedTx envelope), parser (reflect-map + dual LinearCodec DELETED, fx
dispatch by envelope TypeKind/ShapeKind), base/create_asset/operation/import/
export tx, initial_state, operation, block/{standard,parser,builder}, genesis
(native genesis_wire.go: marshalGenesis/parseGenesis + txs.ParseUnsignedTx),
vm/service/static_service/wallet_service/utxo-spender, metrics (pcodecs.Errs ->
errors.Join). ALL xvm test codec threading removed; xvm tests green.

Block-build invariant: writeTxList requires Initialized txs (mempool/parse hand
the builder canonical bytes) — errors on empty tx-bytes rather than a hidden
Initialize side-effect; state_test updated to Initialize its fixtures.

Transport foundation: rpcchainvm NewListener unix-socket fast path gated
LUXD_VM_UNIX_SOCKET=1 (default TCP, non-breaking; api/zap 159b27a infers unix
from socket-path addr). Consumes upstream utxo TransferableOut/In (4ce6a6e).

ZERO real node-side pcodecs consumers remain. vms/pcodecs kept only for 3
external luxfi/chains VMs (Wave B: zkvm/bridgevm/mpcvm) pending their migration.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-12 11:26:36 -07:00
co-authored by Hanzo Dev
parent b113618e5e
commit ddb3fbca93
52 changed files with 1804 additions and 1369 deletions
+56 -4
View File
@@ -1,12 +1,64 @@
// 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 zap
import "net"
import (
"fmt"
"net"
"os"
"path/filepath"
"runtime"
)
// NewListener creates a new TCP listener on an available port.
// This is used for the ZAP handshake during VM subprocess bootstrap.
// NewListener creates the listener a plugin VM dials back on during ZAP
// handshake bootstrap.
//
// The same-host fast path is a unix-domain socket — no TCP/UDP stack, no
// ephemeral-port exhaustion, filesystem-namespaced. The api/zap Dial infers
// "unix" from the socket-path addr, so a plugin rebuilt against that api
// connects back over the socket with zero code change on its side.
//
// It is OPT-IN via LUXD_VM_UNIX_SOCKET=1 because a plugin binary built against
// an older api/zap still dials "tcp" and would fail on a socket-path addr —
// activating the socket transport is a coordinated node+plugin rebuild. Default
// stays TCP loopback so existing /data/plugins keep working across the upgrade.
// Windows always uses TCP (no unix sockets).
func NewListener() (net.Listener, error) {
if runtime.GOOS != "windows" && os.Getenv("LUXD_VM_UNIX_SOCKET") == "1" {
if ln, err := newUnixListener(); err == nil {
return ln, nil
}
// fall through to TCP on any unix-socket failure
}
return net.Listen("tcp", "127.0.0.1:0")
}
// newUnixListener binds a unix-domain socket under the OS temp dir. The socket
// dir is removed when the listener closes (unixListener.Close) so plugin churn
// does not leak socket inodes.
func newUnixListener() (net.Listener, error) {
dir, err := os.MkdirTemp("", "luxd-vm-*")
if err != nil {
return nil, err
}
path := filepath.Join(dir, "vm.sock")
ln, err := net.Listen("unix", path)
if err != nil {
_ = os.RemoveAll(dir)
return nil, fmt.Errorf("unix listen: %w", err)
}
return &unixListener{Listener: ln, dir: dir}, nil
}
// unixListener wraps a unix net.Listener to remove its socket dir on Close.
type unixListener struct {
net.Listener
dir string
}
func (u *unixListener) Close() error {
err := u.Listener.Close()
_ = os.RemoveAll(u.dir)
return err
}
+4 -7
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
@@ -7,11 +7,12 @@ import (
"time"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/txs"
)
// Block defines the common stateless interface for all blocks
// Block defines the common stateless interface for all blocks. There is no
// codec: a block is a self-describing native-ZAP buffer whose bytes are
// authoritative (ID = hash(bytes)); see Parser.ParseBlock.
type Block interface {
ID() ids.ID
Parent() ids.ID
@@ -23,8 +24,4 @@ type Block interface {
// Txs returns the transactions contained in the block
Txs() []*txs.Tx
// note: initialize does not assume that the transactions are initialized,
// and initializes them itself.
initialize(bytes []byte, cm pcodecs.Manager) error
}
+5 -7
View File
@@ -12,7 +12,6 @@ import (
"github.com/luxfi/constants"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/node/vms/xvm/txs"
lux "github.com/luxfi/utxo"
@@ -36,7 +35,7 @@ func TestInvalidBlock(t *testing.T) {
require.NoError(err)
_, err = parser.ParseBlock(nil)
require.ErrorIs(err, pcodecs.ErrCantUnpackVersion)
require.Error(err)
}
func TestStandardBlocks(t *testing.T) {
@@ -53,11 +52,10 @@ func TestStandardBlocks(t *testing.T) {
blkTimestamp := time.Now()
parentID := ids.GenerateTestID()
height := uint64(2022)
cm := parser.Codec()
txs, err := createTestTxs(cm)
txs, err := createTestTxs()
require.NoError(err)
standardBlk, err := NewStandardBlock(parentID, height, blkTimestamp, txs, cm)
standardBlk, err := NewStandardBlock(parentID, height, blkTimestamp, txs)
require.NoError(err)
// parse block
@@ -78,7 +76,7 @@ func TestStandardBlocks(t *testing.T) {
require.Equal(parsed.Txs(), parsedStandardBlk.Txs())
}
func createTestTxs(cm pcodecs.Manager) ([]*txs.Tx, error) {
func createTestTxs() ([]*txs.Tx, error) {
countTxs := 1
testTxs := make([]*txs.Tx, 0, countTxs)
for i := 0; i < countTxs; i++ {
@@ -111,7 +109,7 @@ func createTestTxs(cm pcodecs.Manager) ([]*txs.Tx, error) {
}},
Memo: []byte{1, 2, 3, 4, 5, 6, 7, 8},
}}}
if err := tx.SignSECP256K1Fx(cm, [][]*secp256k1.PrivateKey{{keys[0]}}); err != nil {
if err := tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{keys[0]}}); err != nil {
return nil, err
}
testTxs = append(testTxs, tx)
-2
View File
@@ -130,7 +130,6 @@ func (b *builder) BuildBlock(context.Context) (chain.Block, error) {
}
executor := &txexecutor.Executor{
Codec: b.backend.Codec,
State: txDiff,
Tx: tx,
Inputs: set.NewSet[ids.ID](0), // Initialize empty set for imported inputs
@@ -188,7 +187,6 @@ func (b *builder) BuildBlock(context.Context) (chain.Block, error) {
nextTimestamp,
root,
blockTxs,
b.backend.Codec,
)
if err != nil {
return nil, err
@@ -15,7 +15,6 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs/pcodecsmock"
"github.com/luxfi/node/vms/xvm/block"
blkexecutor "github.com/luxfi/node/vms/xvm/block/executor"
"github.com/luxfi/node/vms/xvm/block/executor/executormock"
@@ -104,17 +103,12 @@ func buildOneTxBlock(
require.NoError(err)
require.NoError(memPool.Add(tx))
// Mock codec: the builder serializes the block; the test reads block.Root
// (set before marshal), so fixed marshal output is fine.
codec := pcodecsmock.NewManager(ctrl)
codec.EXPECT().Marshal(gomock.Any(), gomock.Any()).Return([]byte{1, 2, 3}, nil).AnyTimes()
codec.EXPECT().Size(gomock.Any(), gomock.Any()).Return(2, nil).AnyTimes()
// The builder serializes the block natively (ZAP struct-is-wire); there is
// no codec to inject. The test reads block.Root (stamped before serialize).
builder := New(
&txexecutor.Backend{
Ctx: context.Background(),
Runtime: testRuntime(),
Codec: codec,
Config: &config.Config{},
Log: log.NewNoOpLogger(),
},
+12 -20
View File
@@ -20,8 +20,6 @@ import (
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/pcodecs/pcodecsmock"
"github.com/luxfi/node/vms/xvm/block"
blkexecutor "github.com/luxfi/node/vms/xvm/block/executor"
"github.com/luxfi/node/vms/xvm/block/executor/executormock"
@@ -340,13 +338,9 @@ func TestBuilderBuildBlock(t *testing.T) {
require.NoError(t, memPool.Add(tx2))
// To marshal the tx/block
codec := pcodecsmock.NewManager(ctrl)
codec.EXPECT().Marshal(gomock.Any(), gomock.Any()).Return([]byte{1, 2, 3}, nil).AnyTimes()
codec.EXPECT().Size(gomock.Any(), gomock.Any()).Return(2, nil).AnyTimes()
return New(
&txexecutor.Backend{
Codec: codec,
Ctx: context.Background(),
Runtime: testRuntime(),
Log: log.NewNoOpLogger(),
@@ -409,19 +403,19 @@ func TestBuilderBuildBlock(t *testing.T) {
unsignedTx.EXPECT().SetBytes(gomock.Any()).AnyTimes()
unsignedTx.EXPECT().InputIDs().Return(nil)
tx := &txs.Tx{Unsigned: unsignedTx}
// Native-ZAP blocks store each tx's own wire bytes; give the
// mocked tx non-nil bytes so the block builder doesn't try to
// serialize the mock Unsigned (which has no Bytes expectation).
tx.SetBytes(nil, []byte{1, 2, 3})
memPool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
require.NoError(t, memPool.Add(tx))
// To marshal the tx/block
codec := pcodecsmock.NewManager(ctrl)
codec.EXPECT().Marshal(gomock.Any(), gomock.Any()).Return([]byte{1, 2, 3}, nil).AnyTimes()
codec.EXPECT().Size(gomock.Any(), gomock.Any()).Return(2, nil).AnyTimes()
return New(
&txexecutor.Backend{
Codec: codec,
Ctx: context.Background(),
Runtime: testRuntime(),
Log: log.NewNoOpLogger(),
@@ -486,19 +480,19 @@ func TestBuilderBuildBlock(t *testing.T) {
unsignedTx.EXPECT().SetBytes(gomock.Any()).AnyTimes()
unsignedTx.EXPECT().InputIDs().Return(nil)
tx := &txs.Tx{Unsigned: unsignedTx}
// Native-ZAP blocks store each tx's own wire bytes; give the
// mocked tx non-nil bytes so the block builder doesn't try to
// serialize the mock Unsigned (which has no Bytes expectation).
tx.SetBytes(nil, []byte{1, 2, 3})
memPool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
require.NoError(t, memPool.Add(tx))
// To marshal the tx/block
codec := pcodecsmock.NewManager(ctrl)
codec.EXPECT().Marshal(gomock.Any(), gomock.Any()).Return([]byte{1, 2, 3}, nil).AnyTimes()
codec.EXPECT().Size(gomock.Any(), gomock.Any()).Return(2, nil).AnyTimes()
return New(
&txexecutor.Backend{
Codec: codec,
Ctx: context.Background(),
Runtime: testRuntime(),
Log: log.NewNoOpLogger(),
@@ -549,7 +543,6 @@ func TestBlockBuilderAddLocalTx(t *testing.T) {
backend := &txexecutor.Backend{
Ctx: context.Background(),
Runtime: testRuntime(),
Codec: parser.Codec(),
Log: log.NewNoOpLogger(),
}
@@ -563,10 +556,9 @@ func TestBlockBuilderAddLocalTx(t *testing.T) {
now := time.Now()
parentTimestamp := now.Add(-2 * time.Second)
parentID := ids.GenerateTestID()
cm := parser.Codec()
txs, err := createParentTxs(cm)
txs, err := createParentTxs()
require.NoError(err)
parentBlk, err := block.NewStandardBlock(parentID, 0, parentTimestamp, txs, cm)
parentBlk, err := block.NewStandardBlock(parentID, 0, parentTimestamp, txs)
require.NoError(err)
state.AddBlock(parentBlk)
state.SetLastAccepted(parentBlk.ID())
@@ -621,7 +613,7 @@ func createTxs() []*txs.Tx {
}}
}
func createParentTxs(cm pcodecs.Manager) ([]*txs.Tx, error) {
func createParentTxs() ([]*txs.Tx, error) {
countTxs := 1
testTxs := make([]*txs.Tx, 0, countTxs)
for i := 0; i < countTxs; i++ {
@@ -654,7 +646,7 @@ func createParentTxs(cm pcodecs.Manager) ([]*txs.Tx, error) {
}},
Memo: []byte{1, 2, 9, 4, 5, 6, 7, 8},
}}}
if err := tx.SignSECP256K1Fx(cm, [][]*secp256k1.PrivateKey{{keys[0]}}); err != nil {
if err := tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{keys[0]}}); err != nil {
return nil, err
}
testTxs = append(testTxs, tx)
-1
View File
@@ -199,7 +199,6 @@ func (b *Block) Verify(ctx context.Context) error {
// to ensure that semantic verification correctly accounts for
// transactions that occurred earlier in the block.
executor := &executor.Executor{
Codec: b.manager.backend.Codec,
State: stateDiff,
Tx: tx,
Inputs: set.NewSet[ids.ID](0),
-1
View File
@@ -169,7 +169,6 @@ func (m *manager) VerifyTx(tx *txs.Tx) error {
}
executor := &executor.Executor{
Codec: m.backend.Codec,
State: stateDiff,
Tx: tx,
Inputs: set.NewSet[ids.ID](0), // Initialize empty set for imported inputs
+1 -1
View File
@@ -304,7 +304,7 @@ func makeTxs(t *testing.T, n int) []*txs.Tx {
tx := &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
BlockchainID: ids.GenerateTestID(),
}}}
require.NoError(t, tx.Initialize(projTestParser.Codec()))
require.NoError(t, tx.Initialize())
out[i] = tx
}
return out
-15
View File
@@ -14,7 +14,6 @@ import (
time "time"
ids "github.com/luxfi/ids"
pcodecs "github.com/luxfi/node/vms/pcodecs"
txs "github.com/luxfi/node/vms/xvm/txs"
gomock "go.uber.org/mock/gomock"
)
@@ -140,17 +139,3 @@ func (mr *MockBlockMockRecorder) Txs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Txs", reflect.TypeOf((*MockBlock)(nil).Txs))
}
// initialize mocks base method.
func (m *MockBlock) initialize(bytes []byte, cm pcodecs.Manager) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "initialize", bytes, cm)
ret0, _ := ret[0].(error)
return ret0
}
// initialize indicates an expected call of initialize.
func (mr *MockBlockMockRecorder) initialize(bytes, cm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "initialize", reflect.TypeOf((*MockBlock)(nil).initialize), bytes, cm)
}
+13 -38
View File
@@ -1,24 +1,21 @@
// 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 (
"errors"
"reflect"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/timer/mockable"
)
// CodecVersion is the current default codec version
const CodecVersion = txs.CodecVersion
var _ Parser = (*parser)(nil)
// Parser parses both X-chain txs and blocks. It embeds the codec-free
// txs.Parser (for ParseTx / ParseGenesisTx) and adds native-ZAP block parsing.
type Parser interface {
txs.Parser
@@ -35,16 +32,7 @@ func NewParser(fxs []fxs.Fx) (Parser, error) {
if err != nil {
return nil, err
}
c := p.CodecRegistry()
gc := p.GenesisCodecRegistry()
err = errors.Join(
c.RegisterType(&StandardBlock{}),
gc.RegisterType(&StandardBlock{}),
)
return &parser{
Parser: p,
}, err
return &parser{Parser: p}, nil
}
func NewCustomParser(
@@ -57,30 +45,17 @@ func NewCustomParser(
if err != nil {
return nil, err
}
c := p.CodecRegistry()
gc := p.GenesisCodecRegistry()
err = errors.Join(
c.RegisterType(&StandardBlock{}),
gc.RegisterType(&StandardBlock{}),
)
return &parser{
Parser: p,
}, err
return &parser{Parser: p}, nil
}
func (p *parser) ParseBlock(bytes []byte) (Block, error) {
return parse(p.Codec(), bytes)
// ParseBlock decodes a native-ZAP X-chain block (byte-preserving).
func (*parser) ParseBlock(bytes []byte) (Block, error) {
return parseStandardBlock(bytes)
}
func (p *parser) ParseGenesisBlock(bytes []byte) (Block, error) {
return parse(p.GenesisCodec(), bytes)
}
func parse(cm pcodecs.Manager, bytes []byte) (Block, error) {
var blk Block
if _, err := cm.Unmarshal(bytes, &blk); err != nil {
return nil, err
}
return blk, blk.initialize(bytes, cm)
// ParseGenesisBlock decodes the genesis block. Same wire as any other block —
// the genesis/standard distinction was a codec-version artifact that no longer
// exists.
func (*parser) ParseGenesisBlock(bytes []byte) (Block, error) {
return parseStandardBlock(bytes)
}
+137 -34
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
@@ -9,36 +9,50 @@ import (
hash "github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/zap"
)
var _ Block = (*StandardBlock)(nil)
// StandardBlock is the X-chain block. The struct is the source of truth; bytes
// is the cached native-ZAP wire encoding. Txs are self-describing via their own
// tx.Bytes(), so a block stores the per-tx byte lengths as a u32 list plus the
// concatenated tx bytes, and Parse re-splits them through txs.Parse (zero copy).
//
// Object fixed section (all offsets object-relative):
//
// ParentID 32B @ 0
// Height u64 @ 32
// Time u64 @ 40
// Root 32B @ 48 (merkle execution root; ids.Empty pre-activation)
// TxLengths 8B @ 80 (u32 list ptr — one entry per tx)
// TxBlob 8B @ 88 (bytes ptr — concat of each tx.Bytes())
type StandardBlock struct {
// parent's ID
PrntID ids.ID `serialize:"true" json:"parentID"`
PrntID ids.ID `json:"parentID"`
// This block's height. The genesis block is at height 0.
Hght uint64 `serialize:"true" json:"height"`
Time uint64 `serialize:"true" json:"time"`
Root ids.ID `serialize:"true" json:"merkleRoot"`
Hght uint64 `json:"height"`
Time uint64 `json:"time"`
Root ids.ID `json:"merkleRoot"`
// List of transactions contained in this block.
Transactions []*txs.Tx `serialize:"true" json:"txs"`
Transactions []*txs.Tx `json:"txs"`
BlockID ids.ID `json:"id"`
bytes []byte
}
func (b *StandardBlock) initialize(bytes []byte, cm pcodecs.Manager) error {
b.BlockID = hash.ComputeHash256Array(bytes)
b.bytes = bytes
for _, tx := range b.Transactions {
if err := tx.Initialize(cm); err != nil {
return fmt.Errorf("failed to initialize tx: %w", err)
}
}
return nil
}
const (
offBlkParent = 0 // 32B
offBlkHeight = 32 // u64
offBlkTime = 40 // u64
offBlkRoot = 48 // 32B
offBlkTxLen = 80 // list ptr
offBlkTxBlob = 88 // bytes ptr
sizeBlk = 96
txLenStride = 4 // uint32
)
func (b *StandardBlock) ID() ids.ID {
return b.BlockID
@@ -72,43 +86,132 @@ func NewStandardBlock(
parentID ids.ID,
height uint64,
timestamp time.Time,
txs []*txs.Tx,
cm pcodecs.Manager,
txList []*txs.Tx,
) (*StandardBlock, error) {
return NewStandardBlockWithRoot(parentID, height, timestamp, ids.Empty, txs, cm)
return NewStandardBlockWithRoot(parentID, height, timestamp, ids.Empty, txList)
}
// NewStandardBlockWithRoot builds a StandardBlock carrying an explicit merkle
// root and serializes it. NewStandardBlock is the root == ids.Empty special case
// — the historical, pre-activation shape. Above the xvm execution_root
// activation height the builder passes the computed execution_root here so it is
// part of the serialized, hashed block bytes; below activation the empty-root
// path (NewStandardBlock) is used and the bytes are byte-for-byte unchanged.
// root and serializes it. NewStandardBlock is the root == ids.Empty special
// case — the historical, pre-activation shape. Above the xvm execution_root
// activation height the builder passes the computed execution_root here so it
// is part of the serialized, hashed block bytes.
func NewStandardBlockWithRoot(
parentID ids.ID,
height uint64,
timestamp time.Time,
root ids.ID,
txs []*txs.Tx,
cm pcodecs.Manager,
txList []*txs.Tx,
) (*StandardBlock, error) {
blk := &StandardBlock{
PrntID: parentID,
Hght: height,
Time: uint64(timestamp.Unix()),
Root: root,
Transactions: txs,
Transactions: txList,
}
// We serialize this block as a pointer so that it can be deserialized into
// a Block
var blkIntf Block = blk
bytes, err := cm.Marshal(CodecVersion, &blkIntf)
bytes, err := blk.serialize()
if err != nil {
return nil, fmt.Errorf("couldn't marshal block: %w", err)
}
blk.BlockID = hash.ComputeHash256Array(bytes)
blk.bytes = bytes
return blk, nil
}
func (b *StandardBlock) serialize() ([]byte, error) {
bld := zap.NewBuilder(zap.HeaderSize + sizeBlk + 256)
lenOff, lenCount, blob, err := writeTxList(bld, b.Transactions)
if err != nil {
return nil, err
}
ob := bld.StartObject(sizeBlk)
ob.SetBytesFixed(offBlkParent, b.PrntID[:])
ob.SetUint64(offBlkHeight, b.Hght)
ob.SetUint64(offBlkTime, b.Time)
ob.SetBytesFixed(offBlkRoot, b.Root[:])
ob.SetList(offBlkTxLen, lenOff, lenCount)
ob.SetBytes(offBlkTxBlob, blob)
ob.FinishAsRoot()
return bld.Finish(), nil
}
// parseStandardBlock decodes a native-ZAP X-chain block, byte-preserving:
// ID = hash(bytes) and bytes is the block's authoritative encoding.
func parseStandardBlock(bytes []byte) (Block, error) {
msg, err := zap.Parse(bytes)
if err != nil {
return nil, fmt.Errorf("couldn't parse block: %w", err)
}
obj := msg.Root()
txList, err := readTxList(obj, offBlkTxLen, offBlkTxBlob)
if err != nil {
return nil, err
}
var parent, root ids.ID
copy(parent[:], obj.BytesFixedSlice(offBlkParent, 32))
copy(root[:], obj.BytesFixedSlice(offBlkRoot, 32))
return &StandardBlock{
PrntID: parent,
Hght: obj.Uint64(offBlkHeight),
Time: obj.Uint64(offBlkTime),
Root: root,
Transactions: txList,
BlockID: hash.ComputeHash256Array(bytes),
bytes: bytes,
}, nil
}
// writeTxList encodes txList as a u32 list of per-tx byte lengths plus a
// concatenated blob of their bytes. AddUint32 counts elements, so Finish()'s
// length is the tx count.
func writeTxList(b *zap.Builder, txList []*txs.Tx) (lenOff, lenCount int, blob []byte, err error) {
if len(txList) == 0 {
return 0, 0, nil, nil
}
lb := b.StartList(txLenStride)
for i, tx := range txList {
if tx == nil {
return 0, 0, nil, fmt.Errorf("nil tx at index %d", i)
}
// Block txs come from the mempool already initialized (parsed from
// gossip or signed). An empty tx-bytes slot means an uninitialized tx
// reached block construction — a caller bug, not something to paper
// over with a hidden Initialize side-effect.
raw := tx.Bytes()
if len(raw) == 0 {
return 0, 0, nil, fmt.Errorf("tx %d has no wire bytes (not Initialized before block build)", i)
}
lb.AddUint32(uint32(len(raw)))
blob = append(blob, raw...)
}
lenOff, lenCount = lb.Finish()
return lenOff, lenCount, blob, nil
}
// readTxList reconstructs the txs from the u32 length list and concatenated
// blob, slicing the blob by each stored length and handing each slice to
// txs.Parse (zero copy).
func readTxList(obj zap.Object, lenPtrOff, blobPtrOff int) ([]*txs.Tx, error) {
lengths := obj.ListStride(lenPtrOff, txLenStride)
n := lengths.Len()
if n == 0 {
return nil, nil
}
blob := obj.Bytes(blobPtrOff)
out := make([]*txs.Tx, n)
cursor := 0
for i := 0; i < n; i++ {
size := int(lengths.Uint32(i))
if size < 0 || cursor+size > len(blob) {
return nil, fmt.Errorf("block: tx %d length %d overruns blob (%d)", i, size, len(blob))
}
tx, err := txs.Parse(blob[cursor : cursor+size])
if err != nil {
return nil, fmt.Errorf("block: parse tx %d: %w", i, err)
}
out[i] = tx
cursor += size
}
return out, nil
}
+6 -48
View File
@@ -9,18 +9,10 @@ import (
"github.com/luxfi/address"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/utils"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/bls12381fx"
"github.com/luxfi/utxo/ed25519fx"
"github.com/luxfi/utxo/mldsafx"
"github.com/luxfi/utxo/schnorrfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/secp256r1fx"
"github.com/luxfi/utxo/slhdsafx"
)
// Genesis represents the genesis state of the XVM
@@ -130,11 +122,7 @@ func NewGenesis(
}
if len(initialState.Outs) > 0 {
codec, err := newGenesisCodec()
if err != nil {
return nil, err
}
initialState.Sort(codec)
initialState.Sort()
asset.States = append(asset.States, initialState)
}
@@ -146,31 +134,9 @@ func NewGenesis(
return g, nil
}
// Bytes serializes the Genesis to bytes using the XVM genesis codec
// Bytes serializes the Genesis to its canonical native-ZAP bytes.
func (g *Genesis) Bytes() ([]byte, error) {
codec, err := newGenesisCodec()
if err != nil {
return nil, err
}
return codec.Marshal(txs.CodecVersion, g)
}
func newGenesisCodec() (pcodecs.Manager, error) {
parser, err := txs.NewParser(
[]fxs.Fx{
&secp256k1fx.Fx{},
&mldsafx.Fx{},
&slhdsafx.Fx{},
&ed25519fx.Fx{},
&secp256r1fx.Fx{},
&schnorrfx.Fx{},
&bls12381fx.Fx{},
},
)
if err != nil {
return nil, fmt.Errorf("problem creating parser: %w", err)
}
return parser.GenesisCodec(), nil
return marshalGenesis(g)
}
// ParseGenesisBytes decodes the canonical XVM genesis bytes produced by
@@ -187,17 +153,13 @@ func newGenesisCodec() (pcodecs.Manager, error) {
// builder context's UTXOAssetID must be the genesis-derived one or every
// fee-paying tx fails with "insufficient funds, needs N more nLUX".
func ParseGenesisBytes(genesisBytes []byte) (*Genesis, error) {
codec, err := newGenesisCodec()
g, err := parseGenesis(genesisBytes)
if err != nil {
return nil, err
}
g := &Genesis{}
if _, err := codec.Unmarshal(genesisBytes, g); err != nil {
return nil, fmt.Errorf("unmarshal xvm genesis: %w", err)
}
for i := range g.Txs {
tx := &txs.Tx{Unsigned: &g.Txs[i].CreateAssetTx}
if err := tx.Initialize(codec); err != nil {
if err := tx.Initialize(); err != nil {
return nil, fmt.Errorf("initialize genesis asset %d (%s): %w", i, g.Txs[i].Alias, err)
}
}
@@ -221,11 +183,7 @@ func AssetIDFromGenesisBytes(genesisBytes []byte) (ids.ID, error) {
return ids.Empty, fmt.Errorf("xvm genesis has zero asset txs")
}
tx := &txs.Tx{Unsigned: &g.Txs[0].CreateAssetTx}
codec, err := newGenesisCodec()
if err != nil {
return ids.Empty, err
}
if err := tx.Initialize(codec); err != nil {
if err := tx.Initialize(); err != nil {
return ids.Empty, fmt.Errorf("initialize first genesis asset (%s): %w", g.Txs[0].Alias, err)
}
return tx.ID(), nil
+129
View File
@@ -0,0 +1,129 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package xvm
import (
"fmt"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/zap"
)
// Native-ZAP genesis wire. The X-Chain genesis is a list of assets; each asset
// is an alias string paired with its CreateAssetTx unsigned bytes (the struct
// is the source of truth, marshalled via txs.UnsignedBytes). There is no
// pcodecs.Manager, no reflection — the same struct-is-wire discipline as the
// tx layer.
//
// Object layout:
//
// Count u32 @ 0
// AliasLens list @ 4 (u32 per asset — alias byte length)
// AliasBlob bytes @ 12 (concatenated alias bytes)
// TxLens list @ 20 (u32 per asset — unsigned-tx byte length)
// TxBlob bytes @ 28 (concatenated CreateAssetTx unsigned bytes)
const (
genOffCount = 0
genOffAliasLens = 4
genOffAliasBlob = 12
genOffTxLens = 20
genOffTxBlob = 28
genSize = 36
)
// marshalGenesis encodes the Genesis to its canonical native-ZAP bytes.
func marshalGenesis(g *Genesis) ([]byte, error) {
var aliasBlob, txBlob []byte
aliasLens := make([]uint32, 0, len(g.Txs))
txLens := make([]uint32, 0, len(g.Txs))
for i := range g.Txs {
ga := g.Txs[i]
ub, err := txs.UnsignedBytes(&ga.CreateAssetTx)
if err != nil {
return nil, fmt.Errorf("marshal genesis asset %d (%s): %w", i, ga.Alias, err)
}
aliasLens = append(aliasLens, uint32(len(ga.Alias)))
aliasBlob = append(aliasBlob, ga.Alias...)
txLens = append(txLens, uint32(len(ub)))
txBlob = append(txBlob, ub...)
}
b := zap.NewBuilder(zap.HeaderSize + genSize + len(aliasBlob) + len(txBlob) + 8*len(g.Txs) + 64)
aliasLensOff := writeU32List(b, aliasLens)
txLensOff := writeU32List(b, txLens)
ob := b.StartObject(genSize)
ob.SetUint32(genOffCount, uint32(len(g.Txs)))
ob.SetList(genOffAliasLens, aliasLensOff, len(aliasLens))
ob.SetBytes(genOffAliasBlob, aliasBlob)
ob.SetList(genOffTxLens, txLensOff, len(txLens))
ob.SetBytes(genOffTxBlob, txBlob)
ob.FinishAsRoot()
return b.Finish(), nil
}
// parseGenesis decodes canonical genesis bytes into a *Genesis with each
// GenesisAsset's embedded CreateAssetTx re-hydrated from its unsigned bytes.
func parseGenesis(genesisBytes []byte) (*Genesis, error) {
msg, err := zap.Parse(genesisBytes)
if err != nil {
return nil, fmt.Errorf("parse xvm genesis: %w", err)
}
if msg.Size() != len(genesisBytes) {
return nil, fmt.Errorf("parse xvm genesis: trailing bytes (non-canonical)")
}
root := msg.Root()
n := int(root.Uint32(genOffCount))
aliasLens := readU32List(root, genOffAliasLens)
aliasBlob := root.Bytes(genOffAliasBlob)
txLens := readU32List(root, genOffTxLens)
txBlob := root.Bytes(genOffTxBlob)
if len(aliasLens) != n || len(txLens) != n {
return nil, fmt.Errorf("parse xvm genesis: asset count mismatch")
}
g := &Genesis{Txs: make([]*GenesisAsset, 0, n)}
aPos, tPos := 0, 0
for i := 0; i < n; i++ {
aLen, tLen := int(aliasLens[i]), int(txLens[i])
if aPos+aLen > len(aliasBlob) || tPos+tLen > len(txBlob) {
return nil, fmt.Errorf("parse xvm genesis: asset %d out of bounds", i)
}
alias := string(aliasBlob[aPos : aPos+aLen])
aPos += aLen
ub := txBlob[tPos : tPos+tLen]
tPos += tLen
unsigned, err := txs.ParseUnsignedTx(ub)
if err != nil {
return nil, fmt.Errorf("hydrate genesis asset %d (%s): %w", i, alias, err)
}
cat, ok := unsigned.(*txs.CreateAssetTx)
if !ok {
return nil, fmt.Errorf("genesis asset %d (%s): expected CreateAssetTx, got %T", i, alias, unsigned)
}
ga := &GenesisAsset{Alias: alias, CreateAssetTx: *cat}
g.Txs = append(g.Txs, ga)
}
return g, nil
}
func writeU32List(b *zap.Builder, xs []uint32) int {
lb := b.StartList(4)
for _, x := range xs {
lb.AddUint32(x)
}
off, _ := lb.Finish()
return off
}
func readU32List(o zap.Object, ptrOff int) []uint32 {
l := o.ListStride(ptrOff, 4)
n := l.Len()
out := make([]uint32, n)
for i := 0; i < n; i++ {
out[i] = l.Uint32(i)
}
return out
}
+3 -3
View File
@@ -56,7 +56,7 @@ func TestIndexTransaction_Ordered(t *testing.T) {
// make transaction
tx := buildTX(env.consensusRuntime.ChainID, utxoID, txAssetID, addr)
require.NoError(tx.SignSECP256K1Fx(env.vm.parser.Codec(), [][]*secp256k1.PrivateKey{{key}}))
require.NoError(tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{key}}))
issueAndAccept(require, env.vm, tx)
@@ -97,7 +97,7 @@ func TestIndexTransaction_MultipleTransactions(t *testing.T) {
// make transaction
tx := buildTX(env.consensusRuntime.ChainID, utxoID, txAssetID, addr)
require.NoError(tx.SignSECP256K1Fx(env.vm.parser.Codec(), [][]*secp256k1.PrivateKey{{key}}))
require.NoError(tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{key}}))
// issue transaction
issueAndAccept(require, env.vm, tx)
@@ -142,7 +142,7 @@ func TestIndexTransaction_MultipleAddresses(t *testing.T) {
// make transaction
tx := buildTX(env.consensusRuntime.ChainID, utxoID, txAssetID, addrs...)
require.NoError(tx.SignSECP256K1Fx(env.vm.parser.Codec(), [][]*secp256k1.PrivateKey{{key}}))
require.NoError(tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{key}}))
issueAndAccept(require, env.vm, tx)
+3 -4
View File
@@ -9,7 +9,6 @@ import (
"github.com/luxfi/metric"
utilmetric "github.com/luxfi/metric"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/block"
"github.com/luxfi/node/vms/xvm/txs"
)
@@ -74,7 +73,7 @@ func New(registerer metric.Registerer) (Metrics, error) {
return nil, errors.New("registerer must implement metric.Registry")
}
txMetrics, err := newTxMetrics(registry)
errs := pcodecs.Errs{Err: err}
errs := []error{err}
m := &metricsImpl{txMetrics: txMetrics}
@@ -96,7 +95,7 @@ func New(registerer metric.Registerer) (Metrics, error) {
err = errors.New("APIInterceptor is nil")
}
m.APIInterceptor = apiRequestMetric
errs.Add(err)
errs = append(errs, err)
// Metrics are self-registering when created with NewCounter etc.
return m, errs.Err
return m, errors.Join(errs...)
}
+1 -1
View File
@@ -43,7 +43,7 @@ func TestMarshaller(t *testing.T) {
}
want := &txs.Tx{Unsigned: &txs.BaseTx{}}
require.NoError(want.Initialize(parser.Codec()))
require.NoError(want.Initialize())
bytes, err := marhsaller.MarshalGossip(want)
require.NoError(err)
+14 -20
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"github.com/go-json-experiment/json"
apitypes "github.com/luxfi/api/types"
"github.com/luxfi/consensus/core/choices"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/database"
@@ -17,11 +18,10 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
apitypes "github.com/luxfi/api/types"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/utils"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/secp256k1fx"
@@ -892,8 +892,7 @@ func (s *Service) buildCreateAssetTx(args *CreateAssetArgs) (*txs.Tx, ids.ShortI
initialState.Outs = append(initialState.Outs, minter)
}
codec := s.vm.parser.Codec()
initialState.Sort(codec)
initialState.Sort()
tx := &txs.Tx{Unsigned: &txs.CreateAssetTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
@@ -907,7 +906,7 @@ func (s *Service) buildCreateAssetTx(args *CreateAssetArgs) (*txs.Tx, ids.ShortI
Denomination: args.Denomination,
States: []*txs.InitialState{initialState},
}}
return tx, changeAddr, tx.SignSECP256K1Fx(codec, keys)
return tx, changeAddr, tx.SignSECP256K1Fx(keys)
}
// CreateFixedCapAsset returns ID of the newly created asset
@@ -1060,8 +1059,7 @@ func (s *Service) buildCreateNFTAsset(args *CreateNFTAssetArgs) (*txs.Tx, ids.Sh
initialState.Outs = append(initialState.Outs, minter)
}
codec := s.vm.parser.Codec()
initialState.Sort(codec)
initialState.Sort()
tx := &txs.Tx{Unsigned: &txs.CreateAssetTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
@@ -1075,7 +1073,7 @@ func (s *Service) buildCreateNFTAsset(args *CreateNFTAssetArgs) (*txs.Tx, ids.Sh
Denomination: 0, // NFTs are non-fungible
States: []*txs.InitialState{initialState},
}}
return tx, changeAddr, tx.SignSECP256K1Fx(codec, keys)
return tx, changeAddr, tx.SignSECP256K1Fx(keys)
}
// CreateAddress creates an address for the user [args.Username]
@@ -1386,7 +1384,6 @@ func (s *Service) buildSendMultiple(args *SendMultipleArgs) (*txs.Tx, ids.ShortI
}
}
codec := s.vm.parser.Codec()
lux.SortTransferableOutputs(outs)
tx := &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
@@ -1396,7 +1393,7 @@ func (s *Service) buildSendMultiple(args *SendMultipleArgs) (*txs.Tx, ids.ShortI
Ins: ins,
Memo: memoBytes,
}}}
return tx, changeAddr, tx.SignSECP256K1Fx(codec, keys)
return tx, changeAddr, tx.SignSECP256K1Fx(keys)
}
// MintArgs are arguments for passing into Mint requests
@@ -1539,7 +1536,7 @@ func (s *Service) buildMint(args *MintArgs) (*txs.Tx, ids.ShortID, error) {
}},
Ops: ops,
}}
return tx, changeAddr, tx.SignSECP256K1Fx(s.vm.parser.Codec(), keys)
return tx, changeAddr, tx.SignSECP256K1Fx(keys)
}
// SendNFTArgs are arguments for passing into SendNFT requests
@@ -1667,11 +1664,10 @@ func (s *Service) buildSendNFT(args *SendNFTArgs) (*txs.Tx, ids.ShortID, error)
Ops: ops,
}}
codec := s.vm.parser.Codec()
if err := tx.SignSECP256K1Fx(codec, secpKeys); err != nil {
if err := tx.SignSECP256K1Fx(secpKeys); err != nil {
return nil, ids.ShortEmpty, err
}
return tx, changeAddr, tx.SignNFTFx(codec, nftKeys)
return tx, changeAddr, tx.SignNFTFx(nftKeys)
}
// MintNFTArgs are arguments for passing into MintNFT requests
@@ -1809,11 +1805,10 @@ func (s *Service) buildMintNFT(args *MintNFTArgs) (*txs.Tx, ids.ShortID, error)
Ops: ops,
}}
codec := s.vm.parser.Codec()
if err := tx.SignSECP256K1Fx(codec, secpKeys); err != nil {
if err := tx.SignSECP256K1Fx(secpKeys); err != nil {
return nil, ids.ShortEmpty, err
}
return tx, changeAddr, tx.SignNFTFx(codec, nftKeys)
return tx, changeAddr, tx.SignNFTFx(nftKeys)
}
// ImportArgs are arguments for passing into Import requests
@@ -1952,7 +1947,7 @@ func (s *Service) buildImport(args *ImportArgs) (*txs.Tx, error) {
SourceChain: chainID,
ImportedIns: importInputs,
}}
return tx, tx.SignSECP256K1Fx(s.vm.parser.Codec(), keys)
return tx, tx.SignSECP256K1Fx(keys)
}
// ExportArgs are arguments for passing into ExportAVA requests
@@ -2102,7 +2097,6 @@ func (s *Service) buildExport(args *ExportArgs) (*txs.Tx, ids.ShortID, error) {
}
}
codec := s.vm.parser.Codec()
lux.SortTransferableOutputs(outs)
tx := &txs.Tx{Unsigned: &txs.ExportTx{
@@ -2115,5 +2109,5 @@ func (s *Service) buildExport(args *ExportArgs) (*txs.Tx, ids.ShortID, error) {
DestinationChain: chainID,
ExportedOuts: exportOuts,
}}
return tx, changeAddr, tx.SignSECP256K1Fx(codec, keys)
return tx, changeAddr, tx.SignSECP256K1Fx(keys)
}
-1
View File
@@ -396,7 +396,6 @@ func (s *state) initializeChainState(stopVertexID ids.ID, genesisTimestamp time.
0,
genesisTimestamp,
nil,
s.parser.Codec(),
)
if err != nil {
return fmt.Errorf("failed to initialize genesis block: %w", err)
+17 -19
View File
@@ -63,24 +63,26 @@ func init() {
populatedTx = &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
BlockchainID: ids.GenerateTestID(),
}}}
err = populatedTx.Initialize(parser.Codec())
err = populatedTx.Initialize()
if err != nil {
panic(err)
}
populatedTxID = populatedTx.ID()
// Native-ZAP blocks store each tx's own wire bytes, so a block's txs must
// be Initialized (bytes populated) before the block is built — the same
// contract production uses (mempool/parse hand the builder initialized txs).
blkTx := &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
BlockchainID: ids.GenerateTestID(),
}}}
if err = blkTx.Initialize(); err != nil {
panic(err)
}
populatedBlk, err = block.NewStandardBlock(
ids.GenerateTestID(),
1,
time.Now(),
[]*txs.Tx{
{
Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
BlockchainID: ids.GenerateTestID(),
}},
},
},
parser.Codec(),
[]*txs.Tx{blkTx},
)
if err != nil {
panic(err)
@@ -199,7 +201,7 @@ func ChainTxTest(t *testing.T, c Chain) {
tx := &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
BlockchainID: ids.GenerateTestID(),
}}}
require.NoError(tx.Initialize(parser.Codec()))
require.NoError(tx.Initialize())
txID := tx.ID()
_, err = c.GetTx(txID)
@@ -236,18 +238,15 @@ func ChainBlockTest(t *testing.T, c Chain) {
require.NoError(err)
require.Equal(populatedBlk.ID(), fetchedBlk.ID())
inlineTx := &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
BlockchainID: ids.GenerateTestID(),
}}}
require.NoError(inlineTx.Initialize())
blk, err := block.NewStandardBlock(
ids.GenerateTestID(),
10,
time.Now(),
[]*txs.Tx{
{
Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
BlockchainID: ids.GenerateTestID(),
}},
},
},
parser.Codec(),
[]*txs.Tx{inlineTx},
)
if err != nil {
panic(err)
@@ -302,7 +301,6 @@ func TestInitializeChainState(t *testing.T) {
genesis.Height()+1,
genesisTimestamp,
nil,
parser.Codec(),
)
require.NoError(err)
+1 -1
View File
@@ -74,7 +74,7 @@ func TestSetsAndGets(t *testing.T) {
},
}},
}}}
require.NoError(tx.SignSECP256K1Fx(env.vm.parser.Codec(), [][]*secp256k1.PrivateKey{{keys[0]}}))
require.NoError(tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{keys[0]}}))
txID := tx.ID()
+7 -7
View File
@@ -12,11 +12,11 @@ import (
"github.com/luxfi/address"
"github.com/luxfi/formatting"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/utils"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
@@ -77,19 +77,19 @@ type BuildGenesisReply struct {
// BuildGenesis returns the UTXOs such that at least one address in [args.Addresses] is
// referenced in the UTXO.
func (*StaticService) BuildGenesis(_ *http.Request, args *BuildGenesisArgs, reply *BuildGenesisReply) error {
parser, err := txs.NewParser(
// Validate the fx set is well-formed (native genesis marshals without a
// codec; the parser is only constructed here to reject a bad fx set).
if _, err := txs.NewParser(
[]fxs.Fx{
&secp256k1fx.Fx{},
&nftfx.Fx{},
&propertyfx.Fx{},
},
)
if err != nil {
); err != nil {
return err
}
g := Genesis{}
genesisCodec := parser.GenesisCodec()
for assetAlias, assetDefinition := range args.GenesisData {
assetMemo, err := formatting.Decode(args.Encoding, assetDefinition.Memo)
if err != nil {
@@ -175,7 +175,7 @@ func (*StaticService) BuildGenesis(_ *http.Request, args *BuildGenesisArgs, repl
return errUnknownAssetType
}
}
initialState.Sort(genesisCodec)
initialState.Sort()
asset.States = append(asset.States, initialState)
}
utils.Sort(asset.States)
@@ -183,7 +183,7 @@ func (*StaticService) BuildGenesis(_ *http.Request, args *BuildGenesisArgs, repl
}
utils.Sort(g.Txs)
b, err := genesisCodec.Marshal(txs.CodecVersion, &g)
b, err := g.Bytes()
if err != nil {
return fmt.Errorf("problem marshaling genesis: %w", err)
}
+119 -9
View File
@@ -1,15 +1,16 @@
// 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 txs
import (
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/runtime"
"github.com/luxfi/math/set"
"github.com/luxfi/utxo/wire"
"github.com/luxfi/zap"
)
var (
@@ -17,13 +18,26 @@ var (
_ secp256k1fx.UnsignedTx = (*BaseTx)(nil)
)
// BaseTx is the basis of all transactions.
// BaseTx is the basis of all transactions. It carries the multi-asset spending
// envelope (NetworkID/BlockchainID/Outs/Ins/Memo) as an embedded lux.BaseTx —
// X-Chain is a universal multi-fx settlement layer, so Outs/Ins hold arbitrary
// fx primitives, not the fixed secp256k1 stride the P-Chain uses. The struct
// is the source of truth; Bytes() is the cached native-ZAP wire encoding.
//
// Wire (unsigned): zap object { xkind@0, baseTxEnvelope@8 } where the envelope
// is a wire.XVMBaseTx (each Out/In an fx-typed TransferableOut/In envelope).
type BaseTx struct {
lux.BaseTx `serialize:"true"`
lux.BaseTx
bytes []byte
}
// unsigned-tx object layout shared by every X-chain tx type.
const (
offBaseTx = 8 // bytes ptr: the wire.XVMBaseTx envelope
sizeBaseObj = 16
)
func (t *BaseTx) InitRuntime(rt *runtime.Runtime) {
for _, out := range t.Outs {
out.InitRuntime(rt)
@@ -71,8 +85,104 @@ func (t *BaseTx) NumCredentials() int {
}
// InitializeWithRuntime initializes the transaction with Runtime
func (tx *BaseTx) InitializeWithRuntime(rt *runtime.Runtime) error {
// Initialize any context-dependent fields here
tx.InitRuntime(rt)
func (t *BaseTx) InitializeWithRuntime(rt *runtime.Runtime) error {
t.InitRuntime(rt)
return nil
}
// serialize encodes the unsigned tx into its canonical native-ZAP wire bytes.
func (t *BaseTx) serialize() ([]byte, error) {
env, err := t.baseTxWire()
if err != nil {
return nil, err
}
b := zap.NewBuilder(zap.HeaderSize + sizeBaseObj + len(env) + 64)
ob := b.StartObject(sizeBaseObj)
ob.SetUint8(offXKind, uint8(xkindBase))
ob.SetBytes(offBaseTx, env)
ob.FinishAsRoot()
return b.Finish(), nil
}
// baseTxWire builds the wire.XVMBaseTx envelope from the embedded spending
// envelope: each Out/In becomes an fx-typed TransferableOut/In envelope built
// from its inner fx primitive's .Bytes().
func (t *BaseTx) baseTxWire() ([]byte, error) {
outs := make([][]byte, len(t.Outs))
for i, o := range t.Outs {
b, err := transferableOutBytes(o)
if err != nil {
return nil, err
}
outs[i] = b
}
ins := make([][]byte, len(t.Ins))
for i, in := range t.Ins {
b, err := transferableInBytes(in)
if err != nil {
return nil, err
}
ins[i] = b
}
return wire.NewXVMBaseTx(wire.XVMBaseTxInput{
NetworkID: t.NetworkID,
BlockchainID: [32]byte(t.BlockchainID),
Outs: outs,
Ins: ins,
Memo: t.Memo,
}), nil
}
// decodeBaseTxWire reconstructs an embedded lux.BaseTx from the wire.XVMBaseTx
// envelope at offBaseTx of a parsed unsigned-tx object.
func decodeBaseTxWire(obj zap.Object) (lux.BaseTx, error) {
w, err := wire.WrapXVMBaseTx(obj.Bytes(offBaseTx))
if err != nil {
return lux.BaseTx{}, err
}
base := lux.BaseTx{
NetworkID: w.NetworkID(),
BlockchainID: ids.ID(w.BlockchainID()),
}
if n := w.OutsCount(); n > 0 {
base.Outs = make([]*lux.TransferableOutput, n)
for i := uint32(0); i < n; i++ {
wo, err := w.OutAt(i)
if err != nil {
return lux.BaseTx{}, err
}
out, err := outputFromWire(wo)
if err != nil {
return lux.BaseTx{}, err
}
base.Outs[i] = out
}
}
if n := w.InsCount(); n > 0 {
base.Ins = make([]*lux.TransferableInput, n)
for i := uint32(0); i < n; i++ {
wi, err := w.InAt(i)
if err != nil {
return lux.BaseTx{}, err
}
in, err := inputFromWire(wi)
if err != nil {
return lux.BaseTx{}, err
}
base.Ins[i] = in
}
}
if m := w.Memo(); len(m) > 0 {
base.Memo = append([]byte(nil), m...)
}
return base, nil
}
// parseBaseTx wraps a bare BaseTx unsigned buffer into a typed *BaseTx.
func parseBaseTx(unsignedBytes []byte, obj zap.Object) (*BaseTx, error) {
base, err := decodeBaseTxWire(obj)
if err != nil {
return nil, err
}
return &BaseTx{BaseTx: base, bytes: unsignedBytes}, nil
}
+51 -53
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 txs
@@ -11,9 +11,8 @@ import (
"github.com/luxfi/constants"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/fxs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
@@ -23,28 +22,18 @@ var (
keys = secp256k1.TestKeys()
)
func TestBaseTxSerialization(t *testing.T) {
require := require.New(t)
// fxBytes re-encodes a decoded fx primitive to its wire envelope. Comparing
// fxBytes(original) to fxBytes(decoded) is a real decode-fidelity check: it
// exercises the reconstructed struct fields, not the cached input bytes.
func fxBytes(t *testing.T, v any) []byte {
t.Helper()
b, err := childBytes(v)
require.NoError(t, err)
return b
}
expected := []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, 0x05, 0x04, 0x03, 0x02, 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, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02,
0x03, 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, 0x07, 0x00,
0x00, 0x00, 0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xfc, 0xed, 0xa8, 0xf9, 0x0f, 0xcb,
0x5d, 0x30, 0x61, 0x4b, 0x99, 0xd7, 0x9f, 0xc4, 0xba, 0xa2, 0x93, 0x07, 0x76, 0x26, 0x01, 0x00,
0x00, 0x00, 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2,
0xf1, 0xf0, 0xef, 0xee, 0xed, 0xec, 0xeb, 0xea, 0xe9, 0xe8, 0xe7, 0xe6, 0xe5, 0xe4, 0xe3, 0xe2,
0xe1, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 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, 0x05, 0x00, 0x00, 0x00, 0x31, 0xd4, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
0x02, 0x03, 0x00, 0x00, 0x00, 0x00,
}
tx := &Tx{Unsigned: &BaseTx{BaseTx: lux.BaseTx{
func sampleBaseTx() *BaseTx {
return &BaseTx{BaseTx: lux.BaseTx{
NetworkID: constants.UnitTestID,
BlockchainID: chainID,
Outs: []*lux.TransferableOutput{{
@@ -69,43 +58,52 @@ func TestBaseTxSerialization(t *testing.T) {
},
Asset: lux.Asset{ID: assetID},
In: &secp256k1fx.TransferInput{
Amt: 54321,
Input: secp256k1fx.Input{
SigIndices: []uint32{2},
},
Amt: 54321,
Input: secp256k1fx.Input{SigIndices: []uint32{2}},
},
}},
Memo: []byte{0x00, 0x01, 0x02, 0x03},
}}}
}}
}
parser, err := NewParser(
[]fxs.Fx{
&secp256k1fx.Fx{},
},
)
// TestBaseTxRoundTrip proves the native-ZAP struct-is-wire path: build → Bytes
// → Parse reconstructs the same tx (byte-identical, same ID, same fields), and
// signing appends a real fx credential envelope that round-trips.
func TestBaseTxRoundTrip(t *testing.T) {
require := require.New(t)
utx := sampleBaseTx()
tx := &Tx{Unsigned: utx}
require.NoError(tx.Initialize())
require.NotEqual(ids.Empty, tx.ID())
parsed, err := Parse(tx.Bytes())
require.NoError(err)
require.Equal(tx.Bytes(), parsed.Bytes())
require.Equal(tx.ID(), parsed.ID())
require.NoError(tx.Initialize(parser.Codec()))
require.Equal("2Rxg3BTzwShRePp48i9WX8XpYsFkn8eMNo6AatcgEE9yyGqsni", tx.ID().String())
got := parsed.Unsigned.(*BaseTx)
require.Equal(utx.NetworkID, got.NetworkID)
require.Equal(utx.BlockchainID, got.BlockchainID)
require.Equal([]byte(utx.Memo), []byte(got.Memo))
require.Len(got.Outs, 1)
require.Len(got.Ins, 1)
require.Equal(utx.Outs[0].AssetID(), got.Outs[0].AssetID())
require.Equal(fxBytes(t, utx.Outs[0].Out), fxBytes(t, got.Outs[0].Out))
require.Equal(fxBytes(t, utx.Ins[0].In), fxBytes(t, got.Ins[0].In))
require.Equal(utx.Ins[0].UTXOID, got.Ins[0].UTXOID)
result := tx.Bytes()
require.Equal(expected, result)
require.NoError(tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{
{keys[0], keys[0]},
}))
require.Len(tx.Creds, 1)
require.NoError(tx.SignSECP256K1Fx(
parser.Codec(),
[][]*secp256k1.PrivateKey{
{keys[0], keys[0]},
{keys[0], keys[0]},
},
))
// The tx ID will change based on the non-deterministic signatures
// So we just verify it's not empty
require.NotEmpty(tx.ID().String())
// Skip verifying the exact bytes since signatures are non-deterministic
// Just verify the transaction was signed (has credentials)
result = tx.Bytes()
require.Greater(len(result), len(expected)) // Should be longer after adding credentials
signed, err := Parse(tx.Bytes())
require.NoError(err)
require.Equal(tx.ID(), signed.ID())
require.Len(signed.Creds, 1)
cred := signed.Creds[0].Credential.(*secp256k1fx.Credential)
require.Len(cred.Sigs, 2)
}
func TestBaseTxNotState(t *testing.T) {
-55
View File
@@ -1,55 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"reflect"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/timer/mockable"
"github.com/luxfi/utxo/secp256k1fx"
)
var (
_ pcodecs.Registry = (*codecRegistry)(nil)
_ secp256k1fx.VM = (*fxVM)(nil)
)
type codecRegistry struct {
codecs []pcodecs.Registry
index int
typeToIndex map[reflect.Type]int
}
func (cr *codecRegistry) RegisterType(val interface{}) error {
valType := reflect.TypeOf(val)
cr.typeToIndex[valType] = cr.index
errs := pcodecs.Errs{}
for _, c := range cr.codecs {
errs.Add(c.RegisterType(val))
}
return errs.Err
}
type fxVM struct {
typeToFxIndex map[reflect.Type]int
clock *mockable.Clock
log log.Logger
codecRegistry pcodecs.Registry
}
func (vm *fxVM) Clock() *mockable.Clock {
return vm.clock
}
func (vm *fxVM) CodecRegistry() pcodecs.Registry {
return vm.codecRegistry
}
func (vm *fxVM) Logger() log.Logger {
return vm.log
}
+76 -9
View File
@@ -1,12 +1,12 @@
// 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 txs
import (
"github.com/luxfi/runtime"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/zap"
)
var (
@@ -15,14 +15,27 @@ var (
)
// CreateAssetTx is a transaction that creates a new asset.
//
// Wire (unsigned): the shared { xkind@0, baseTxEnvelope@8 } prefix, then
// Name@16 / Symbol@24 (text) + Denomination@32 (u8) + the InitialStates as a
// packed (length list @36, blob @44) of self-delimiting InitialState objects.
type CreateAssetTx struct {
BaseTx `serialize:"true"`
Name string `serialize:"true" json:"name"`
Symbol string `serialize:"true" json:"symbol"`
Denomination byte `serialize:"true" json:"denomination"`
States []*InitialState `serialize:"true" json:"initialStates"`
BaseTx
Name string `json:"name"`
Symbol string `json:"symbol"`
Denomination byte `json:"denomination"`
States []*InitialState `json:"initialStates"`
}
const (
offCAName = 16 // text ptr
offCASymbol = 24 // text ptr
offCADenom = 32 // u8
offCAStatesLen = 36 // list ptr
offCAStatesBlob = 44 // bytes ptr
sizeCA = 52
)
func (t *CreateAssetTx) InitRuntime(rt *runtime.Runtime) {
for _, state := range t.States {
state.InitRuntime(rt)
@@ -47,7 +60,61 @@ func (t *CreateAssetTx) Visit(v Visitor) error {
}
// InitializeWithRuntime initializes the transaction with Runtime
func (tx *CreateAssetTx) InitializeWithRuntime(rt *runtime.Runtime) error {
// Initialize any context-dependent fields here
func (t *CreateAssetTx) InitializeWithRuntime(rt *runtime.Runtime) error {
t.InitRuntime(rt)
return nil
}
func (t *CreateAssetTx) serialize() ([]byte, error) {
env, err := t.baseTxWire()
if err != nil {
return nil, err
}
states := make([][]byte, len(t.States))
for i, s := range t.States {
b, err := s.Bytes()
if err != nil {
return nil, err
}
states[i] = b
}
b := zap.NewBuilder(zap.HeaderSize + sizeCA + len(env) + 256)
statesLenOff, statesLenCount, statesBlob := writeBlobList(b, states)
ob := b.StartObject(sizeCA)
ob.SetUint8(offXKind, uint8(xkindCreateAsset))
ob.SetBytes(offBaseTx, env)
ob.SetText(offCAName, t.Name)
ob.SetText(offCASymbol, t.Symbol)
ob.SetUint8(offCADenom, t.Denomination)
ob.SetList(offCAStatesLen, statesLenOff, statesLenCount)
ob.SetBytes(offCAStatesBlob, statesBlob)
ob.FinishAsRoot()
return b.Finish(), nil
}
func parseCreateAssetTx(unsignedBytes []byte, obj zap.Object) (*CreateAssetTx, error) {
base, err := decodeBaseTxWire(obj)
if err != nil {
return nil, err
}
stateBufs, err := readBlobList(obj, offCAStatesLen, offCAStatesBlob)
if err != nil {
return nil, err
}
states := make([]*InitialState, len(stateBufs))
for i, buf := range stateBufs {
s, err := parseInitialState(buf)
if err != nil {
return nil, err
}
states[i] = s
}
return &CreateAssetTx{
BaseTx: BaseTx{BaseTx: base, bytes: unsignedBytes},
Name: obj.Text(offCAName),
Symbol: obj.Text(offCASymbol),
Denomination: obj.Uint8(offCADenom),
States: states,
}, nil
}
+53 -174
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 txs
@@ -10,108 +10,21 @@ import (
"github.com/luxfi/constants"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/fxs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
func TestCreateAssetTxSerialization(t *testing.T) {
// TestCreateAssetTxRoundTrip round-trips a create-asset tx carrying both a
// TransferOutput and a MintOutput in its initial state — proving the packed
// InitialState list and the fx-state envelope dispatch reconstruct byte-for-byte.
func TestCreateAssetTxRoundTrip(t *testing.T) {
require := require.New(t)
expected := []byte{
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xee, 0xee,
0xee, 0xee, 0xdd, 0xdd, 0xdd, 0xdd, 0xcc, 0xcc, 0xcc, 0xcc, 0xbb, 0xbb, 0xbb, 0xbb, 0xaa, 0xaa,
0xaa, 0xaa, 0x99, 0x99, 0x99, 0x99, 0x88, 0x88, 0x88, 0x88, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01,
0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11,
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x07, 0x00,
0x00, 0x00, 0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xd4, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x51, 0x02, 0x5c, 0x61, 0xfb, 0xcf,
0xc0, 0x78, 0xf6, 0x93, 0x34, 0xf8, 0x34, 0xbe, 0x6d, 0xd2, 0x6d, 0x55, 0xa9, 0x55, 0xc3, 0x34,
0x41, 0x28, 0xe0, 0x60, 0x12, 0x8e, 0xde, 0x35, 0x23, 0xa2, 0x4a, 0x46, 0x1c, 0x89, 0x43, 0xab,
0x08, 0x59, 0x01, 0x00, 0x00, 0x00, 0xf1, 0xe1, 0xd1, 0xc1, 0xb1, 0xa1, 0x91, 0x81, 0x71, 0x61,
0x51, 0x41, 0x31, 0x21, 0x11, 0x01, 0xf0, 0xe0, 0xd0, 0xc0, 0xb0, 0xa0, 0x90, 0x80, 0x70, 0x60,
0x50, 0x40, 0x30, 0x20, 0x10, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x05, 0x00, 0x00, 0x00, 0x15, 0xcd,
0x5b, 0x07, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x10, 0x00, 0x56, 0x6f, 0x6c, 0x61,
0x74, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x20, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x03, 0x00, 0x56, 0x49,
0x58, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xd4, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x51, 0x02, 0x5c, 0x61, 0xfb, 0xcf,
0xc0, 0x78, 0xf6, 0x93, 0x34, 0xf8, 0x34, 0xbe, 0x6d, 0xd2, 0x6d, 0x55, 0xa9, 0x55, 0xc3, 0x34,
0x41, 0x28, 0xe0, 0x60, 0x12, 0x8e, 0xde, 0x35, 0x23, 0xa2, 0x4a, 0x46, 0x1c, 0x89, 0x43, 0xab,
0x08, 0x59, 0x00, 0x00, 0x00, 0x00,
}
tx := &Tx{Unsigned: &CreateAssetTx{
BaseTx: BaseTx{BaseTx: lux.BaseTx{
NetworkID: 2,
BlockchainID: ids.ID{
0xff, 0xff, 0xff, 0xff, 0xee, 0xee, 0xee, 0xee,
0xdd, 0xdd, 0xdd, 0xdd, 0xcc, 0xcc, 0xcc, 0xcc,
0xbb, 0xbb, 0xbb, 0xbb, 0xaa, 0xaa, 0xaa, 0xaa,
0x99, 0x99, 0x99, 0x99, 0x88, 0x88, 0x88, 0x88,
},
Memo: []byte{0x00, 0x01, 0x02, 0x03},
Outs: []*lux.TransferableOutput{{
Asset: lux.Asset{
ID: ids.ID{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
},
},
Out: &secp256k1fx.TransferOutput{
Amt: 12345,
OutputOwners: secp256k1fx.OutputOwners{
Locktime: 54321,
Threshold: 1,
Addrs: []ids.ShortID{
{
0x51, 0x02, 0x5c, 0x61, 0xfb, 0xcf, 0xc0, 0x78,
0xf6, 0x93, 0x34, 0xf8, 0x34, 0xbe, 0x6d, 0xd2,
0x6d, 0x55, 0xa9, 0x55,
},
{
0xc3, 0x34, 0x41, 0x28, 0xe0, 0x60, 0x12, 0x8e,
0xde, 0x35, 0x23, 0xa2, 0x4a, 0x46, 0x1c, 0x89,
0x43, 0xab, 0x08, 0x59,
},
},
},
},
}},
Ins: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{
TxID: ids.ID{
0xf1, 0xe1, 0xd1, 0xc1, 0xb1, 0xa1, 0x91, 0x81,
0x71, 0x61, 0x51, 0x41, 0x31, 0x21, 0x11, 0x01,
0xf0, 0xe0, 0xd0, 0xc0, 0xb0, 0xa0, 0x90, 0x80,
0x70, 0x60, 0x50, 0x40, 0x30, 0x20, 0x10, 0x00,
},
OutputIndex: 5,
},
Asset: lux.Asset{
ID: ids.ID{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
},
},
In: &secp256k1fx.TransferInput{
Amt: 123456789,
Input: secp256k1fx.Input{
SigIndices: []uint32{3, 7},
},
},
}},
}},
Name: "Volatility Index",
Symbol: "VIX",
utx := &CreateAssetTx{
BaseTx: *sampleBaseTx(),
Name: "Volatility Index",
Symbol: "VIX",
Denomination: 2,
States: []*InitialState{
{
@@ -122,68 +35,46 @@ func TestCreateAssetTxSerialization(t *testing.T) {
OutputOwners: secp256k1fx.OutputOwners{
Locktime: 54321,
Threshold: 1,
Addrs: []ids.ShortID{
{
0x51, 0x02, 0x5c, 0x61, 0xfb, 0xcf, 0xc0, 0x78,
0xf6, 0x93, 0x34, 0xf8, 0x34, 0xbe, 0x6d, 0xd2,
0x6d, 0x55, 0xa9, 0x55,
},
{
0xc3, 0x34, 0x41, 0x28, 0xe0, 0x60, 0x12, 0x8e,
0xde, 0x35, 0x23, 0xa2, 0x4a, 0x46, 0x1c, 0x89,
0x43, 0xab, 0x08, 0x59,
},
},
Addrs: []ids.ShortID{keys[0].PublicKey().Address()},
},
},
&secp256k1fx.MintOutput{
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{keys[1].PublicKey().Address()},
},
},
},
},
},
}}
parser, err := NewParser(
[]fxs.Fx{
&secp256k1fx.Fx{},
},
)
require.NoError(err)
require.NoError(tx.Initialize(parser.Codec()))
result := tx.Bytes()
require.Equal(expected, result)
}
func TestCreateAssetTxSerializationAgain(t *testing.T) {
require := require.New(t)
expected := []byte{
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, 0x05, 0x04, 0x03, 0x02, 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, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02,
0x03, 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, 0x07, 0x00,
0x00, 0x00, 0x00, 0xc8, 0x17, 0xa8, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xfc, 0xed, 0xa8, 0xf9, 0x0f, 0xcb,
0x5d, 0x30, 0x61, 0x4b, 0x99, 0xd7, 0x9f, 0xc4, 0xba, 0xa2, 0x93, 0x07, 0x76, 0x26, 0x01, 0x02,
0x03, 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, 0x07, 0x00,
0x00, 0x00, 0x00, 0xc8, 0x17, 0xa8, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6e, 0xad, 0x69, 0x3c, 0x17, 0xab,
0xb1, 0xbe, 0x42, 0x2b, 0xb5, 0x0b, 0x30, 0xb9, 0x71, 0x1f, 0xf9, 0x8d, 0x66, 0x7e, 0x01, 0x02,
0x03, 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, 0x07, 0x00,
0x00, 0x00, 0x00, 0xc8, 0x17, 0xa8, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf2, 0x42, 0x08, 0x46, 0x87, 0x6e,
0x69, 0xf4, 0x73, 0xdd, 0xa2, 0x56, 0x17, 0x29, 0x67, 0xe9, 0x92, 0xf0, 0xee, 0x31, 0x00, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0x6e, 0x61, 0x6d, 0x65,
0x04, 0x00, 0x73, 0x79, 0x6d, 0x62, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xfc, 0xed, 0xa8, 0xf9, 0x0f, 0xcb, 0x5d, 0x30, 0x61,
0x4b, 0x99, 0xd7, 0x9f, 0xc4, 0xba, 0xa2, 0x93, 0x07, 0x76, 0x26, 0x00, 0x00, 0x00, 0x00,
}
unsignedTx := &CreateAssetTx{
tx := &Tx{Unsigned: utx}
require.NoError(tx.Initialize())
parsed, err := Parse(tx.Bytes())
require.NoError(err)
require.Equal(tx.Bytes(), parsed.Bytes())
require.Equal(tx.ID(), parsed.ID())
got := parsed.Unsigned.(*CreateAssetTx)
require.Equal("Volatility Index", got.Name)
require.Equal("VIX", got.Symbol)
require.Equal(byte(2), got.Denomination)
require.Equal(constants.UnitTestID, got.NetworkID)
require.Len(got.States, 1)
require.EqualValues(0, got.States[0].FxIndex)
require.Len(got.States[0].Outs, 2)
require.Equal(fxBytes(t, utx.States[0].Outs[0]), fxBytes(t, got.States[0].Outs[0]))
require.Equal(fxBytes(t, utx.States[0].Outs[1]), fxBytes(t, got.States[0].Outs[1]))
}
// TestCreateAssetTxNoStatesRoundTrip covers the degenerate shape (no base
// outputs, memo only, single mint state) — a common wallet-built create tx.
func TestCreateAssetTxNoStatesRoundTrip(t *testing.T) {
require := require.New(t)
utx := &CreateAssetTx{
BaseTx: BaseTx{BaseTx: lux.BaseTx{
NetworkID: constants.UnitTestID,
BlockchainID: chainID,
@@ -206,32 +97,20 @@ func TestCreateAssetTxSerializationAgain(t *testing.T) {
},
},
}
tx := &Tx{Unsigned: unsignedTx}
for _, key := range keys[:3] {
addr := key.PublicKey().Address()
unsignedTx.Outs = append(unsignedTx.Outs, &lux.TransferableOutput{
Asset: lux.Asset{ID: assetID},
Out: &secp256k1fx.TransferOutput{
Amt: 20 * constants.KiloLux,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{addr},
},
},
})
}
tx := &Tx{Unsigned: utx}
require.NoError(tx.Initialize())
parser, err := NewParser(
[]fxs.Fx{
&secp256k1fx.Fx{},
},
)
parsed, err := Parse(tx.Bytes())
require.NoError(err)
require.NoError(tx.Initialize(parser.Codec()))
require.Equal(tx.Bytes(), parsed.Bytes())
result := tx.Bytes()
require.Equal(expected, result)
got := parsed.Unsigned.(*CreateAssetTx)
require.Equal("name", got.Name)
require.Equal("symb", got.Symbol)
require.Empty(got.Outs)
require.Len(got.States, 1)
require.Equal(fxBytes(t, utx.States[0].Outs[0]), fxBytes(t, got.States[0].Outs[0]))
}
func TestCreateAssetTxNotState(t *testing.T) {
+11 -18
View File
@@ -1,22 +1,15 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package txs is the X-chain (xvm) transaction package for the
// luxfi/node binary. It owns the parser, signed-tx envelope, and
// per-fx Initial-State / Operation accessors that the X-VM uses to
// build, sign, and parse on-chain transactions.
// Package txs is the X-chain (xvm) transaction package for the luxfi/node
// binary. It owns the native-ZAP struct-is-wire tx codec: the signed-tx
// envelope (wire.SignedTx), the per-type unsigned bodies keyed by a 1-byte
// xkind discriminator, and the per-fx Initial-State / Operation accessors the
// X-VM uses to build, sign, and parse on-chain transactions.
//
// The wire shapes (BaseTx, CreateAssetTx, OperationTx, ImportTx,
// ExportTx, InitialState) are declared by the canonical schema at
// github.com/luxfi/proto/schemas/xvm/txs.zap. Hand-rolled accessor
// files in this package are the current source of truth for offsets;
// the schema-driven *_zap.go output replaces them incrementally.
//
// Regenerate the *_zap.go files after editing the schema:
//
// go generate ./...
//
// See proto/schemas/README.md for the schema-layout convention.
// There is no pcodecs.Manager, no linearcodec, and no reflection-driven slot
// map. Each tx type serializes itself directly onto a github.com/luxfi/zap
// buffer; polymorphic fx primitives (outputs, inputs, operations, credentials)
// name themselves on the wire via their (TypeKind, ShapeKind) envelope and are
// reconstructed by envelope dispatch (fxwire.go).
package txs
//go:generate zapgen ../../../../proto/schemas/xvm/txs.zap
-2
View File
@@ -10,7 +10,6 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/config"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/runtime"
@@ -22,7 +21,6 @@ type Backend struct {
Config *config.Config
Fxs []*fxs.ParsedFx
TypeToFxIndex map[reflect.Type]int
Codec pcodecs.Manager
// Note: FeeAssetID may be different than ctx.UTXOAssetID if this XVM is
// running in a chain.
FeeAssetID ids.ID
-2
View File
@@ -8,7 +8,6 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/state"
"github.com/luxfi/node/vms/xvm/txs"
lux "github.com/luxfi/utxo"
@@ -18,7 +17,6 @@ import (
var _ txs.Visitor = (*Executor)(nil)
type Executor struct {
Codec pcodecs.Manager
State state.Chain // state will be modified
Tx *txs.Tx
Inputs set.Set[ids.ID] // imported inputs
+2 -9
View File
@@ -51,7 +51,6 @@ func TestBaseTxExecutor(t *testing.T) {
[]fxs.Fx{secpFx},
)
require.NoError(err)
codec := parser.Codec()
db := memdb.New()
vdb := versiondb.New(db)
@@ -109,10 +108,9 @@ func TestBaseTxExecutor(t *testing.T) {
},
}},
}}}
require.NoError(baseTx.SignSECP256K1Fx(codec, [][]*secp256k1.PrivateKey{{keys[0]}}))
require.NoError(baseTx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{keys[0]}}))
executor := &Executor{
Codec: codec,
State: state,
Tx: baseTx,
}
@@ -158,7 +156,6 @@ func TestCreateAssetTxExecutor(t *testing.T) {
[]fxs.Fx{secpFx},
)
require.NoError(err)
codec := parser.Codec()
db := memdb.New()
vdb := versiondb.New(db)
@@ -234,10 +231,9 @@ func TestCreateAssetTxExecutor(t *testing.T) {
},
},
}}
require.NoError(createAssetTx.SignSECP256K1Fx(codec, [][]*secp256k1.PrivateKey{{keys[0]}}))
require.NoError(createAssetTx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{keys[0]}}))
executor := &Executor{
Codec: codec,
State: state,
Tx: createAssetTx,
}
@@ -303,7 +299,6 @@ func TestOperationTxExecutor(t *testing.T) {
[]fxs.Fx{secpFx},
)
require.NoError(err)
codec := parser.Codec()
db := memdb.New()
vdb := versiondb.New(db)
@@ -392,7 +387,6 @@ func TestOperationTxExecutor(t *testing.T) {
}},
}}
require.NoError(operationTx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
{keys[0]},
@@ -400,7 +394,6 @@ func TestOperationTxExecutor(t *testing.T) {
))
executor := &Executor{
Codec: codec,
State: state,
Tx: operationTx,
}
+4 -34
View File
@@ -52,7 +52,7 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
typeToFxIndex := make(map[reflect.Type]int)
secpFx := &secp256k1fx.Fx{}
parser, err := txs.NewCustomParser(
_, err := txs.NewCustomParser(
typeToFxIndex,
new(mockable.Clock),
log.NoLog{},
@@ -62,7 +62,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
)
require.NoError(t, err)
codec := parser.Codec()
txID := ids.GenerateTestID()
utxoID := lux.UTXOID{
TxID: txID,
@@ -109,7 +108,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
},
},
TypeToFxIndex: typeToFxIndex,
Codec: codec,
FeeAssetID: ids.GenerateTestID(),
Bootstrapped: true,
}
@@ -160,7 +158,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -186,7 +183,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -217,7 +213,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -241,7 +236,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[1]},
},
@@ -264,7 +258,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -294,7 +287,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -332,7 +324,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{},
))
return tx
@@ -354,7 +345,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -382,7 +372,6 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Unsigned: &baseTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -417,7 +406,7 @@ func TestSemanticVerifierExportTx(t *testing.T) {
typeToFxIndex := make(map[reflect.Type]int)
secpFx := &secp256k1fx.Fx{}
parser, err := txs.NewCustomParser(
_, err := txs.NewCustomParser(
typeToFxIndex,
new(mockable.Clock),
log.NoLog{},
@@ -427,7 +416,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
)
require.NoError(t, err)
codec := parser.Codec()
txID := ids.GenerateTestID()
utxoID := lux.UTXOID{
TxID: txID,
@@ -479,7 +467,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
},
},
TypeToFxIndex: typeToFxIndex,
Codec: codec,
FeeAssetID: ids.GenerateTestID(),
Bootstrapped: true,
}
@@ -530,7 +517,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -556,7 +542,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -587,7 +572,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -611,7 +595,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[1]},
},
@@ -634,7 +617,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -664,7 +646,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -702,7 +683,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{},
))
return tx
@@ -724,7 +704,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -752,7 +731,6 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -858,7 +836,7 @@ func TestSemanticVerifierExportTxDifferentNet(t *testing.T) {
typeToFxIndex := make(map[reflect.Type]int)
secpFx := &secp256k1fx.Fx{}
parser, err := txs.NewCustomParser(
_, err := txs.NewCustomParser(
typeToFxIndex,
new(mockable.Clock),
log.NoLog{},
@@ -868,7 +846,6 @@ func TestSemanticVerifierExportTxDifferentNet(t *testing.T) {
)
require.NoError(t, err)
codec := parser.Codec()
txID := ids.GenerateTestID()
utxoID := lux.UTXOID{
TxID: txID,
@@ -915,7 +892,6 @@ func TestSemanticVerifierExportTxDifferentNet(t *testing.T) {
},
},
TypeToFxIndex: typeToFxIndex,
Codec: codec,
FeeAssetID: ids.GenerateTestID(),
Bootstrapped: true,
}
@@ -954,7 +930,6 @@ func TestSemanticVerifierExportTxDifferentNet(t *testing.T) {
Unsigned: &exportTx,
}
require.NoError(t, tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -978,7 +953,7 @@ func TestSemanticVerifierImportTx(t *testing.T) {
typeToFxIndex := make(map[reflect.Type]int)
fx := &secp256k1fx.Fx{}
parser, err := txs.NewCustomParser(
_, err := txs.NewCustomParser(
typeToFxIndex,
new(mockable.Clock),
log.NoLog{},
@@ -988,7 +963,6 @@ func TestSemanticVerifierImportTx(t *testing.T) {
)
require.NoError(t, err)
codec := parser.Codec()
utxoID := lux.UTXOID{
TxID: ids.GenerateTestID(),
OutputIndex: 2,
@@ -1037,7 +1011,6 @@ func TestSemanticVerifierImportTx(t *testing.T) {
Unsigned: &unsignedImportTx,
}
require.NoError(t, importTx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[0]},
},
@@ -1058,7 +1031,6 @@ func TestSemanticVerifierImportTx(t *testing.T) {
},
},
TypeToFxIndex: typeToFxIndex,
Codec: codec,
FeeAssetID: ids.GenerateTestID(),
Bootstrapped: true,
SharedMemory: &testSharedMemory{sm: m.NewSharedMemory(chainID)},
@@ -1145,7 +1117,6 @@ func TestSemanticVerifierImportTx(t *testing.T) {
Unsigned: &unsignedImportTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
[][]*secp256k1.PrivateKey{
{keys[1]},
},
@@ -1176,7 +1147,6 @@ func TestSemanticVerifierImportTx(t *testing.T) {
Unsigned: &importTx,
}
require.NoError(tx.SignSECP256K1Fx(
codec,
nil,
))
return tx
+2 -2
View File
@@ -128,7 +128,7 @@ func (v *SyntacticVerifier) CreateAssetTx(tx *txs.CreateAssetTx) error {
}
for _, state := range tx.States {
if err := state.Verify(v.Codec, len(v.Fxs)); err != nil {
if err := state.Verify(len(v.Fxs)); err != nil {
return err
}
}
@@ -191,7 +191,7 @@ func (v *SyntacticVerifier) OperationTx(tx *txs.OperationTx) error {
inputs.Add(inputID)
}
}
if !txs.IsSortedAndUniqueOperations(tx.Ops, v.Codec) {
if !txs.IsSortedAndUniqueOperations(tx.Ops) {
return errOperationsNotSortedUnique
}
@@ -40,7 +40,7 @@ func TestSyntacticVerifierBaseTx(t *testing.T) {
ctx := context.Background()
fx := &secp256k1fx.Fx{}
parser, err := txs.NewParser(
_, err := txs.NewParser(
[]fxs.Fx{
fx,
},
@@ -97,7 +97,6 @@ func TestSyntacticVerifierBaseTx(t *testing.T) {
&cred,
}
codec := parser.Codec()
// Override Runtime to match baseTx's NetworkID and BlockchainID
luxRT.NetworkID = constants.UnitTestID
luxRT.ChainID = chainID
@@ -112,7 +111,6 @@ func TestSyntacticVerifierBaseTx(t *testing.T) {
Fx: fx,
},
},
Codec: codec,
FeeAssetID: feeAssetID,
}
@@ -420,7 +418,7 @@ func TestSyntacticVerifierCreateAssetTx(t *testing.T) {
ctx := context.Background()
fx := &secp256k1fx.Fx{}
parser, err := txs.NewParser(
_, err := txs.NewParser(
[]fxs.Fx{
fx,
},
@@ -492,7 +490,6 @@ func TestSyntacticVerifierCreateAssetTx(t *testing.T) {
&cred,
}
codec := parser.Codec()
// Override Runtime to match baseTx's NetworkID and BlockchainID
luxRT.NetworkID = constants.UnitTestID
luxRT.ChainID = chainID
@@ -507,7 +504,6 @@ func TestSyntacticVerifierCreateAssetTx(t *testing.T) {
Fx: fx,
},
},
Codec: codec,
FeeAssetID: feeAssetID,
}
@@ -926,7 +922,7 @@ func TestSyntacticVerifierCreateAssetTx(t *testing.T) {
&fxOutput0,
&fxOutput1,
}
initialState.Sort(codec)
initialState.Sort()
initialState.Outs[0], initialState.Outs[1] = initialState.Outs[1], initialState.Outs[0]
tx := tx
@@ -1040,7 +1036,7 @@ func TestSyntacticVerifierOperationTx(t *testing.T) {
luxRT.NetworkID = constants.UnitTestID
fx := &secp256k1fx.Fx{}
parser, err := txs.NewParser(
_, err := txs.NewParser(
[]fxs.Fx{
fx,
},
@@ -1120,7 +1116,6 @@ func TestSyntacticVerifierOperationTx(t *testing.T) {
&cred,
}
codec := parser.Codec()
backend := &Backend{
Ctx: ctx,
Runtime: luxRT,
@@ -1132,7 +1127,6 @@ func TestSyntacticVerifierOperationTx(t *testing.T) {
Fx: fx,
},
},
Codec: codec,
FeeAssetID: feeAssetID,
}
@@ -1425,7 +1419,7 @@ func TestSyntacticVerifierOperationTx(t *testing.T) {
&op,
&newOp,
}
txs.SortOperations(tx.Ops, codec)
txs.SortOperations(tx.Ops)
return &txs.Tx{
Unsigned: &tx,
Creds: creds,
@@ -1444,7 +1438,7 @@ func TestSyntacticVerifierOperationTx(t *testing.T) {
&op,
&op,
}
txs.SortOperations(tx.Ops, codec)
txs.SortOperations(tx.Ops)
return &txs.Tx{
Unsigned: &tx,
Creds: creds,
@@ -1533,7 +1527,7 @@ func TestSyntacticVerifierImportTx(t *testing.T) {
cChainID := ids.GenerateTestID()
fx := &secp256k1fx.Fx{}
parser, err := txs.NewParser(
_, err := txs.NewParser(
[]fxs.Fx{
fx,
},
@@ -1594,7 +1588,6 @@ func TestSyntacticVerifierImportTx(t *testing.T) {
&cred,
}
codec := parser.Codec()
luxRT := consensustest.Runtime(t, chainID)
// Override Runtime to match baseTx's NetworkID
luxRT.NetworkID = constants.UnitTestID
@@ -1609,7 +1602,6 @@ func TestSyntacticVerifierImportTx(t *testing.T) {
Fx: fx,
},
},
Codec: codec,
FeeAssetID: feeAssetID,
}
@@ -1940,7 +1932,7 @@ func TestSyntacticVerifierExportTx(t *testing.T) {
cChainID := ids.GenerateTestID()
fx := &secp256k1fx.Fx{}
parser, err := txs.NewParser(
_, err := txs.NewParser(
[]fxs.Fx{
fx,
},
@@ -2001,7 +1993,6 @@ func TestSyntacticVerifierExportTx(t *testing.T) {
&cred,
}
codec := parser.Codec()
luxRT := consensustest.Runtime(t, chainID)
// Override Runtime to match baseTx's NetworkID
luxRT.NetworkID = constants.UnitTestID
@@ -2016,7 +2007,6 @@ func TestSyntacticVerifierExportTx(t *testing.T) {
Fx: fx,
},
},
Codec: codec,
FeeAssetID: feeAssetID,
}
+76 -8
View File
@@ -1,14 +1,15 @@
// 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 txs
import (
"github.com/luxfi/ids"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/runtime"
"github.com/luxfi/utxo/wire"
"github.com/luxfi/zap"
)
var (
@@ -17,16 +18,27 @@ var (
)
// ExportTx is a transaction that exports an asset to another blockchain.
//
// Wire (unsigned): the shared { xkind@0, baseTxEnvelope@8 } prefix, then
// DestinationChain@16 (32B) + the exported outputs as a packed (length list
// @48, blob @56) of self-delimiting wire TransferableOut envelopes.
type ExportTx struct {
BaseTx `serialize:"true"`
BaseTx
// Which chain to send the funds to
DestinationChain ids.ID `serialize:"true" json:"destinationChain"`
DestinationChain ids.ID `json:"destinationChain"`
// The outputs this transaction is sending to the other chain
ExportedOuts []*lux.TransferableOutput `serialize:"true" json:"exportedOutputs"`
ExportedOuts []*lux.TransferableOutput `json:"exportedOutputs"`
}
const (
offExportDest = 16 // 32B
offExportOutsLen = 48 // list ptr
offExportOutsBlob = 56 // bytes ptr
sizeExport = 64
)
func (t *ExportTx) InitRuntime(rt *runtime.Runtime) {
for _, out := range t.ExportedOuts {
out.InitRuntime(rt)
@@ -45,7 +57,63 @@ func (t *ExportTx) Visit(v Visitor) error {
}
// InitializeWithRuntime initializes the transaction with Runtime
func (tx *ExportTx) InitializeWithRuntime(rt *runtime.Runtime) error {
// Initialize any context-dependent fields here
func (t *ExportTx) InitializeWithRuntime(rt *runtime.Runtime) error {
t.InitRuntime(rt)
return nil
}
func (t *ExportTx) serialize() ([]byte, error) {
env, err := t.baseTxWire()
if err != nil {
return nil, err
}
outs := make([][]byte, len(t.ExportedOuts))
for i, o := range t.ExportedOuts {
b, err := transferableOutBytes(o)
if err != nil {
return nil, err
}
outs[i] = b
}
b := zap.NewBuilder(zap.HeaderSize + sizeExport + len(env) + 256)
outsLenOff, outsLenCount, outsBlob := writeBlobList(b, outs)
ob := b.StartObject(sizeExport)
ob.SetUint8(offXKind, uint8(xkindExport))
ob.SetBytes(offBaseTx, env)
ob.SetBytesFixed(offExportDest, t.DestinationChain[:])
ob.SetList(offExportOutsLen, outsLenOff, outsLenCount)
ob.SetBytes(offExportOutsBlob, outsBlob)
ob.FinishAsRoot()
return b.Finish(), nil
}
func parseExportTx(unsignedBytes []byte, obj zap.Object) (*ExportTx, error) {
base, err := decodeBaseTxWire(obj)
if err != nil {
return nil, err
}
var destChain ids.ID
copy(destChain[:], obj.BytesFixedSlice(offExportDest, 32))
outBufs, err := readBlobList(obj, offExportOutsLen, offExportOutsBlob)
if err != nil {
return nil, err
}
outs := make([]*lux.TransferableOutput, len(outBufs))
for i, buf := range outBufs {
w, err := wire.WrapTransferableOut(buf)
if err != nil {
return nil, err
}
out, err := outputFromWire(w)
if err != nil {
return nil, err
}
outs[i] = out
}
return &ExportTx{
BaseTx: BaseTx{BaseTx: base, bytes: unsignedBytes},
DestinationChain: destChain,
ExportedOuts: outs,
}, nil
}
+38 -128
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 txs
@@ -10,144 +10,54 @@ import (
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/fxs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
func TestExportTxSerialization(t *testing.T) {
t.Skip("LP-023 BE→LE wire flip rotated all signature bytes; fixture needs re-capture")
func TestExportTxRoundTrip(t *testing.T) {
require := require.New(t)
expected := []byte{
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xee, 0xee,
0xee, 0xee, 0xdd, 0xdd, 0xdd, 0xdd, 0xcc, 0xcc, 0xcc, 0xcc, 0xbb, 0xbb, 0xbb, 0xbb, 0xaa, 0xaa,
0xaa, 0xaa, 0x99, 0x99, 0x99, 0x99, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x0f, 0x2f, 0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee, 0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac,
0xcc, 0xec, 0x0b, 0x2b, 0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea, 0x09, 0x29, 0x49, 0x69, 0x88, 0xa8,
0xc8, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x3f, 0x5f, 0x7f, 0x9e, 0xbe, 0xde, 0xfe, 0x1d, 0x3d,
0x5d, 0x7d, 0x9c, 0xbc, 0xdc, 0xfc, 0x1b, 0x3b, 0x5b, 0x7b, 0x9a, 0xba, 0xda, 0xfa, 0x19, 0x39,
0x59, 0x79, 0x98, 0xb8, 0xd8, 0xf8, 0x05, 0x00, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
0x02, 0x03, 0x1f, 0x8f, 0x9f, 0x0f, 0x1e, 0x8e, 0x9e, 0x0e, 0x2d, 0x7d, 0xad, 0xfd, 0x2c, 0x7c,
0xac, 0xfc, 0x3b, 0x6b, 0xbb, 0xeb, 0x3a, 0x6a, 0xba, 0xea, 0x49, 0x59, 0xc9, 0xd9, 0x48, 0x58,
0xc8, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
destChain := ids.ID{
0x1f, 0x8f, 0x9f, 0x0f, 0x1e, 0x8e, 0x9e, 0x0e,
0x2d, 0x7d, 0xad, 0xfd, 0x2c, 0x7c, 0xac, 0xfc,
0x3b, 0x6b, 0xbb, 0xeb, 0x3a, 0x6a, 0xba, 0xea,
0x49, 0x59, 0xc9, 0xd9, 0x48, 0x58, 0xc8, 0xd8,
}
tx := &Tx{Unsigned: &ExportTx{
BaseTx: BaseTx{BaseTx: lux.BaseTx{
NetworkID: 2,
BlockchainID: ids.ID{
0xff, 0xff, 0xff, 0xff, 0xee, 0xee, 0xee, 0xee,
0xdd, 0xdd, 0xdd, 0xdd, 0xcc, 0xcc, 0xcc, 0xcc,
0xbb, 0xbb, 0xbb, 0xbb, 0xaa, 0xaa, 0xaa, 0xaa,
0x99, 0x99, 0x99, 0x99, 0x88, 0x88, 0x88, 0x88,
},
Ins: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{TxID: ids.ID{
0x0f, 0x2f, 0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee,
0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac, 0xcc, 0xec,
0x0b, 0x2b, 0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea,
0x09, 0x29, 0x49, 0x69, 0x88, 0xa8, 0xc8, 0xe8,
}},
Asset: lux.Asset{ID: ids.ID{
0x1f, 0x3f, 0x5f, 0x7f, 0x9e, 0xbe, 0xde, 0xfe,
0x1d, 0x3d, 0x5d, 0x7d, 0x9c, 0xbc, 0xdc, 0xfc,
0x1b, 0x3b, 0x5b, 0x7b, 0x9a, 0xba, 0xda, 0xfa,
0x19, 0x39, 0x59, 0x79, 0x98, 0xb8, 0xd8, 0xf8,
}},
In: &secp256k1fx.TransferInput{
Amt: 1000,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
utx := &ExportTx{
BaseTx: *sampleBaseTx(),
DestinationChain: destChain,
ExportedOuts: []*lux.TransferableOutput{{
Asset: lux.Asset{ID: assetID},
Out: &secp256k1fx.TransferOutput{
Amt: 1000,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{keys[0].PublicKey().Address()},
},
}},
Memo: []byte{0x00, 0x01, 0x02, 0x03},
},
}},
DestinationChain: ids.ID{
0x1f, 0x8f, 0x9f, 0x0f, 0x1e, 0x8e, 0x9e, 0x0e,
0x2d, 0x7d, 0xad, 0xfd, 0x2c, 0x7c, 0xac, 0xfc,
0x3b, 0x6b, 0xbb, 0xeb, 0x3a, 0x6a, 0xba, 0xea,
0x49, 0x59, 0xc9, 0xd9, 0x48, 0x58, 0xc8, 0xd8,
},
}}
parser, err := NewParser(
[]fxs.Fx{
&secp256k1fx.Fx{},
},
)
require.NoError(err)
require.NoError(tx.Initialize(parser.Codec()))
require.Equal("218sX5RECzRWffe1JKGET72KsUhAGJAuPe8tAe2s8PLCJrAeqZ", tx.ID().String())
result := tx.Bytes()
require.Equal(expected, result)
credBytes := []byte{
// type id
0x00, 0x00, 0x00, 0x09,
// there are two signers (thus two signatures)
0x00, 0x00, 0x00, 0x02,
// 65 bytes
0x61, 0xdd, 0x9b, 0xff, 0xc0, 0x49, 0x95, 0x6e, 0xd7, 0xf8,
0xcd, 0x92, 0xec, 0xda, 0x03, 0x6e, 0xac, 0xb8, 0x16, 0x9e,
0x53, 0x83, 0xc0, 0x3a, 0x2e, 0x88, 0x5b, 0x5f, 0xc6, 0xef,
0x2e, 0xbe, 0x50, 0x59, 0x72, 0x8d, 0x0f, 0xa6, 0x59, 0x66,
0x93, 0x28, 0x88, 0xb4, 0x56, 0x3b, 0x77, 0x7c, 0x59, 0xa5,
0x8f, 0xe0, 0x2a, 0xf3, 0xcc, 0x31, 0x32, 0xef, 0xfe, 0x7d,
0x3d, 0x9f, 0x14, 0x94, 0x01,
// 65 bytes
0x61, 0xdd, 0x9b, 0xff, 0xc0, 0x49, 0x95, 0x6e, 0xd7, 0xf8,
0xcd, 0x92, 0xec, 0xda, 0x03, 0x6e, 0xac, 0xb8, 0x16, 0x9e,
0x53, 0x83, 0xc0, 0x3a, 0x2e, 0x88, 0x5b, 0x5f, 0xc6, 0xef,
0x2e, 0xbe, 0x50, 0x59, 0x72, 0x8d, 0x0f, 0xa6, 0x59, 0x66,
0x93, 0x28, 0x88, 0xb4, 0x56, 0x3b, 0x77, 0x7c, 0x59, 0xa5,
0x8f, 0xe0, 0x2a, 0xf3, 0xcc, 0x31, 0x32, 0xef, 0xfe, 0x7d,
0x3d, 0x9f, 0x14, 0x94, 0x01,
// type id
0x00, 0x00, 0x00, 0x09,
// there are two signers (thus two signatures)
0x00, 0x00, 0x00, 0x02,
// 65 bytes
0x61, 0xdd, 0x9b, 0xff, 0xc0, 0x49, 0x95, 0x6e, 0xd7, 0xf8,
0xcd, 0x92, 0xec, 0xda, 0x03, 0x6e, 0xac, 0xb8, 0x16, 0x9e,
0x53, 0x83, 0xc0, 0x3a, 0x2e, 0x88, 0x5b, 0x5f, 0xc6, 0xef,
0x2e, 0xbe, 0x50, 0x59, 0x72, 0x8d, 0x0f, 0xa6, 0x59, 0x66,
0x93, 0x28, 0x88, 0xb4, 0x56, 0x3b, 0x77, 0x7c, 0x59, 0xa5,
0x8f, 0xe0, 0x2a, 0xf3, 0xcc, 0x31, 0x32, 0xef, 0xfe, 0x7d,
0x3d, 0x9f, 0x14, 0x94, 0x01,
// 65 bytes
0x61, 0xdd, 0x9b, 0xff, 0xc0, 0x49, 0x95, 0x6e, 0xd7, 0xf8,
0xcd, 0x92, 0xec, 0xda, 0x03, 0x6e, 0xac, 0xb8, 0x16, 0x9e,
0x53, 0x83, 0xc0, 0x3a, 0x2e, 0x88, 0x5b, 0x5f, 0xc6, 0xef,
0x2e, 0xbe, 0x50, 0x59, 0x72, 0x8d, 0x0f, 0xa6, 0x59, 0x66,
0x93, 0x28, 0x88, 0xb4, 0x56, 0x3b, 0x77, 0x7c, 0x59, 0xa5,
0x8f, 0xe0, 0x2a, 0xf3, 0xcc, 0x31, 0x32, 0xef, 0xfe, 0x7d,
0x3d, 0x9f, 0x14, 0x94, 0x01,
}
require.NoError(tx.SignSECP256K1Fx(
parser.Codec(),
[][]*secp256k1.PrivateKey{
{keys[0], keys[0]},
{keys[0], keys[0]},
},
))
require.Equal("2XWcfypBSo6rhikT9hYvvk6YwJvoaYKrHrK1DLjGpPPxyFuNtE", tx.ID().String())
// there are two credentials
expected[len(expected)-1] = 0x02
expected = append(expected, credBytes...)
result = tx.Bytes()
require.Equal(expected, result)
tx := &Tx{Unsigned: utx}
require.NoError(tx.Initialize())
parsed, err := Parse(tx.Bytes())
require.NoError(err)
require.Equal(tx.Bytes(), parsed.Bytes())
require.Equal(tx.ID(), parsed.ID())
got := parsed.Unsigned.(*ExportTx)
require.Equal(destChain, got.DestinationChain)
require.Len(got.ExportedOuts, 1)
require.Equal(utx.ExportedOuts[0].AssetID(), got.ExportedOuts[0].AssetID())
require.Equal(fxBytes(t, utx.ExportedOuts[0].Out), fxBytes(t, got.ExportedOuts[0].Out))
require.NoError(tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{keys[0], keys[0]}}))
signed, err := Parse(tx.Bytes())
require.NoError(err)
require.Equal(tx.ID(), signed.ID())
require.Len(signed.Creds, 1)
}
func TestExportTxNotState(t *testing.T) {
+263
View File
@@ -0,0 +1,263 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
// Native-ZAP wire plumbing shared by every X-chain tx type: the (TypeKind,
// ShapeKind) envelope dispatchers that reconstruct polymorphic fx primitives
// (value outputs/inputs, state outputs, operations, credentials) and the
// length-prefixed blob-list codec used for the variable sections. There is no
// pcodecs.Manager and no reflect: an fx primitive names itself on the wire via
// its 2-byte discriminator, and each branch calls exactly one fx-package
// Wrap*. Adding a new (fx, shape) is a new branch — no shared codec to grow.
import (
"fmt"
"github.com/luxfi/ids"
componentslux "github.com/luxfi/node/vms/components/lux"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/fxs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/wire"
"github.com/luxfi/zap"
)
// itemLenStride is the per-element width of the u32 length list that prefixes
// every packed blob list (one length per element, elements concatenated).
const itemLenStride = 4
// childBytes returns the canonical wire envelope of a wire-serializable fx
// value (every production fx primitive exposes Bytes() []byte via its wire.go
// adapter). This is the ONE serialization contract the tx wire composes on.
func childBytes(v any) ([]byte, error) {
ws, ok := v.(interface{ Bytes() []byte })
if !ok {
return nil, fmt.Errorf("xvm txs: %T is not wire-serializable (missing Bytes() []byte)", v)
}
return ws.Bytes(), nil
}
// ---- packed blob list: (u32 length list, concatenated blob) ----
// writeBlobList packs a slice of self-contained buffers as a u32 length list
// plus a concatenated blob. AddUint32 counts elements, so the list length is
// the item count directly.
func writeBlobList(b *zap.Builder, bufs [][]byte) (lenOff, lenCount int, blob []byte) {
if len(bufs) == 0 {
return 0, 0, nil
}
lb := b.StartList(itemLenStride)
for _, buf := range bufs {
lb.AddUint32(uint32(len(buf)))
blob = append(blob, buf...)
}
lenOff, lenCount = lb.Finish()
return lenOff, lenCount, blob
}
// readBlobList slices the blob at blobPtrOff by the u32 lengths at lenPtrOff,
// returning each element's exact bytes (aliasing the parent buffer).
func readBlobList(obj zap.Object, lenPtrOff, blobPtrOff int) ([][]byte, error) {
lengths := obj.ListStride(lenPtrOff, itemLenStride)
n := lengths.Len()
if n == 0 {
return nil, nil
}
blob := obj.Bytes(blobPtrOff)
out := make([][]byte, n)
cursor := 0
for i := 0; i < n; i++ {
size := int(lengths.Uint32(i))
if size < 0 || cursor+size > len(blob) {
return nil, fmt.Errorf("xvm txs: element %d length %d overruns blob (%d)", i, size, len(blob))
}
out[i] = blob[cursor : cursor+size]
cursor += size
}
return out, nil
}
// ---- TransferableOutput / TransferableInput <-> wire envelopes ----
// transferableOutBytes builds a wire TransferableOut envelope (AssetID + inner
// fx Output) from a lux.TransferableOutput.
func transferableOutBytes(o *lux.TransferableOutput) ([]byte, error) {
inner, err := childBytes(o.Out)
if err != nil {
return nil, err
}
return wire.NewTransferableOut(o.Asset.ID, inner), nil
}
// transferableInBytes builds a wire TransferableIn envelope (UTXOID + AssetID +
// inner fx Input) from a lux.TransferableInput.
func transferableInBytes(in *lux.TransferableInput) ([]byte, error) {
inner, err := childBytes(in.In)
if err != nil {
return nil, err
}
return wire.NewTransferableIn(in.UTXOID.TxID, in.UTXOID.OutputIndex, in.Asset.ID, inner), nil
}
// outputFromWire reconstructs a lux.TransferableOutput from a parsed wire
// TransferableOut, dispatching the inner fx Output on its own discriminator.
func outputFromWire(w wire.TransferableOut) (*lux.TransferableOutput, error) {
fxOut, err := componentslux.WrapOutputBytes(w.OutputBytes())
if err != nil {
return nil, err
}
to, ok := fxOut.(lux.TransferableOut)
if !ok {
return nil, fmt.Errorf("xvm txs: decoded output %T is not a lux.TransferableOut", fxOut)
}
return &lux.TransferableOutput{Asset: lux.Asset{ID: w.AssetID()}, Out: to}, nil
}
// inputFromWire reconstructs a lux.TransferableInput from a parsed wire
// TransferableIn, dispatching the inner fx Input on its own discriminator.
func inputFromWire(w wire.TransferableIn) (*lux.TransferableInput, error) {
fxIn, err := componentslux.WrapInputBytes(w.InputBytes())
if err != nil {
return nil, err
}
in, ok := fxIn.(lux.TransferableIn)
if !ok {
return nil, fmt.Errorf("xvm txs: decoded input %T is not a lux.TransferableIn", fxIn)
}
return &lux.TransferableInput{
UTXOID: lux.UTXOID{TxID: w.TxID(), OutputIndex: w.OutputIndex()},
Asset: lux.Asset{ID: w.AssetID()},
In: in,
}, nil
}
// ---- fx state output ([]verify.State in InitialState) ----
// wrapFxState reconstructs a fx state output (the polymorphic verify.State an
// InitialState carries) from its wire envelope. nftfx / propertyfx state
// shapes are dispatched here; every value-transfer fx family (secp256k1,
// mldsa, slhdsa, ed25519, secp256r1, schnorr, bls) is delegated to the shared
// components/lux output dispatcher so this package stays out of their lane.
func wrapFxState(env []byte) (verify.State, error) {
tk, sk, err := wire.PeekDiscriminator(env)
if err != nil {
return nil, err
}
switch tk {
case wire.TypeKindNFT:
switch sk {
case wire.ShapeKindNFTMintOutput:
return nftfx.WrapMintOutput(env)
case wire.ShapeKindNFTTransferOutput:
return nftfx.WrapTransferOutput(env)
}
case wire.TypeKindProperty:
switch sk {
case wire.ShapeKindMintOutput:
return propertyfx.WrapMintOutput(env)
case wire.ShapeKindOwnedOutput:
return propertyfx.WrapOwnedOutput(env)
}
}
return componentslux.WrapOutputBytes(env)
}
// ---- fx operation (fxs.FxOperation in Operation) ----
// wrapFxOperation reconstructs the polymorphic fx operation an Operation
// carries from its wire envelope, dispatched on (TypeKind, ShapeKind).
func wrapFxOperation(env []byte) (fxs.FxOperation, error) {
tk, sk, err := wire.PeekDiscriminator(env)
if err != nil {
return nil, err
}
switch tk {
case wire.TypeKindSecp256k1:
if sk == wire.ShapeKindMintOperation {
return secp256k1fx.WrapMintOperation(env)
}
case wire.TypeKindNFT:
switch sk {
case wire.ShapeKindNFTMintOperation:
return nftfx.WrapMintOperation(env)
case wire.ShapeKindNFTTransferOp:
return nftfx.WrapTransferOperation(env)
}
case wire.TypeKindProperty:
switch sk {
case wire.ShapeKindMintOperation:
return propertyfx.WrapMintOperation(env)
case wire.ShapeKindBurnOperation:
return propertyfx.WrapBurnOperation(env)
}
}
return nil, fmt.Errorf("xvm txs: unknown fx operation (TypeKind=0x%02x, ShapeKind=0x%02x)", tk, sk)
}
// ---- fx credential (verify.Verifiable in FxCredential) ----
// wrapFxCredential reconstructs a fx credential from its wire envelope,
// dispatched on TypeKind (every credential carries ShapeKindCredential).
func wrapFxCredential(env []byte) (verify.Verifiable, error) {
tk, sk, err := wire.PeekDiscriminator(env)
if err != nil {
return nil, err
}
if sk != wire.ShapeKindCredential {
return nil, fmt.Errorf("xvm txs: credential envelope has non-credential shape 0x%02x", sk)
}
switch tk {
case wire.TypeKindSecp256k1:
return secp256k1fx.WrapCredential(env)
case wire.TypeKindNFT:
return nftfx.WrapCredential(env)
case wire.TypeKindProperty:
return propertyfx.WrapCredential(env)
}
return nil, fmt.Errorf("xvm txs: unknown fx credential family (TypeKind=0x%02x)", tk)
}
// ---- UTXOID fixed-stride list (36 bytes: TxID 32B + OutputIndex u32) ----
const utxoIDStride = 36
// writeUTXOIDs packs []*lux.UTXOID as a fixed 36-byte-stride list. AddBytes
// counts bytes, so the caller stores len(ids) as the list count.
func writeUTXOIDs(b *zap.Builder, utxoIDs []*lux.UTXOID) (off, count int) {
if len(utxoIDs) == 0 {
return 0, 0
}
lb := b.StartList(utxoIDStride)
for _, u := range utxoIDs {
var e [utxoIDStride]byte
copy(e[0:], u.TxID[:])
e[32] = byte(u.OutputIndex)
e[33] = byte(u.OutputIndex >> 8)
e[34] = byte(u.OutputIndex >> 16)
e[35] = byte(u.OutputIndex >> 24)
lb.AddBytes(e[:])
}
off, _ = lb.Finish()
return off, len(utxoIDs)
}
// readUTXOIDs reconstructs []*lux.UTXOID from a fixed 36-byte-stride list.
func readUTXOIDs(obj zap.Object, ptrOff int) []*lux.UTXOID {
list := obj.ListStride(ptrOff, utxoIDStride)
n := list.Len()
if n == 0 {
return nil
}
out := make([]*lux.UTXOID, n)
for i := 0; i < n; i++ {
e := list.Object(i, utxoIDStride)
var txID ids.ID
copy(txID[:], e.BytesFixedSlice(0, 32))
out[i] = &lux.UTXOID{TxID: txID, OutputIndex: e.Uint32(32)}
}
return out
}
+76 -8
View File
@@ -1,15 +1,16 @@
// 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 txs
import (
"github.com/luxfi/runtime"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/wire"
"github.com/luxfi/zap"
)
var (
@@ -18,16 +19,27 @@ var (
)
// ImportTx is a transaction that imports an asset from another blockchain.
//
// Wire (unsigned): the shared { xkind@0, baseTxEnvelope@8 } prefix, then
// SourceChain@16 (32B) + the imported inputs as a packed (length list @48,
// blob @56) of self-delimiting wire TransferableIn envelopes.
type ImportTx struct {
BaseTx `serialize:"true"`
BaseTx
// Which chain to consume the funds from
SourceChain ids.ID `serialize:"true" json:"sourceChain"`
SourceChain ids.ID `json:"sourceChain"`
// The inputs to this transaction
ImportedIns []*lux.TransferableInput `serialize:"true" json:"importedInputs"`
ImportedIns []*lux.TransferableInput `json:"importedInputs"`
}
const (
offImportSource = 16 // 32B
offImportInsLen = 48 // list ptr
offImportInsBlob = 56 // bytes ptr
sizeImport = 64
)
// InputUTXOs track which UTXOs this transaction is consuming.
func (t *ImportTx) InputUTXOs() []*lux.UTXOID {
utxos := t.BaseTx.InputUTXOs()
@@ -66,7 +78,63 @@ func (t *ImportTx) Visit(v Visitor) error {
}
// InitializeWithRuntime initializes the transaction with Runtime
func (tx *ImportTx) InitializeWithRuntime(rt *runtime.Runtime) error {
// Initialize any context-dependent fields here
func (t *ImportTx) InitializeWithRuntime(rt *runtime.Runtime) error {
t.InitRuntime(rt)
return nil
}
func (t *ImportTx) serialize() ([]byte, error) {
env, err := t.baseTxWire()
if err != nil {
return nil, err
}
ins := make([][]byte, len(t.ImportedIns))
for i, in := range t.ImportedIns {
b, err := transferableInBytes(in)
if err != nil {
return nil, err
}
ins[i] = b
}
b := zap.NewBuilder(zap.HeaderSize + sizeImport + len(env) + 256)
insLenOff, insLenCount, insBlob := writeBlobList(b, ins)
ob := b.StartObject(sizeImport)
ob.SetUint8(offXKind, uint8(xkindImport))
ob.SetBytes(offBaseTx, env)
ob.SetBytesFixed(offImportSource, t.SourceChain[:])
ob.SetList(offImportInsLen, insLenOff, insLenCount)
ob.SetBytes(offImportInsBlob, insBlob)
ob.FinishAsRoot()
return b.Finish(), nil
}
func parseImportTx(unsignedBytes []byte, obj zap.Object) (*ImportTx, error) {
base, err := decodeBaseTxWire(obj)
if err != nil {
return nil, err
}
var sourceChain ids.ID
copy(sourceChain[:], obj.BytesFixedSlice(offImportSource, 32))
inBufs, err := readBlobList(obj, offImportInsLen, offImportInsBlob)
if err != nil {
return nil, err
}
ins := make([]*lux.TransferableInput, len(inBufs))
for i, buf := range inBufs {
w, err := wire.WrapTransferableIn(buf)
if err != nil {
return nil, err
}
in, err := inputFromWire(w)
if err != nil {
return nil, err
}
ins[i] = in
}
return &ImportTx{
BaseTx: BaseTx{BaseTx: base, bytes: unsignedBytes},
SourceChain: sourceChain,
ImportedIns: ins,
}, nil
}
+48 -128
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 txs
@@ -8,146 +8,66 @@ import (
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/fxs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
func TestImportTxSerialization(t *testing.T) {
t.Skip("LP-023 BE→LE wire flip rotated all signature bytes; fixture needs re-capture")
func TestImportTxRoundTrip(t *testing.T) {
require := require.New(t)
expected := []byte{
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xee, 0xee,
0xee, 0xee, 0xdd, 0xdd, 0xdd, 0xdd, 0xcc, 0xcc, 0xcc, 0xcc, 0xbb, 0xbb, 0xbb, 0xbb, 0xaa, 0xaa,
0xaa, 0xaa, 0x99, 0x99, 0x99, 0x99, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x1f, 0x8f, 0x9f, 0x0f, 0x1e, 0x8e,
0x9e, 0x0e, 0x2d, 0x7d, 0xad, 0xfd, 0x2c, 0x7c, 0xac, 0xfc, 0x3b, 0x6b, 0xbb, 0xeb, 0x3a, 0x6a,
0xba, 0xea, 0x49, 0x59, 0xc9, 0xd9, 0x48, 0x58, 0xc8, 0xd8, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x2f,
0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee, 0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac, 0xcc, 0xec, 0x0b, 0x2b,
0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea, 0x09, 0x29, 0x49, 0x69, 0x88, 0xa8, 0xc8, 0xe8, 0x00, 0x00,
0x00, 0x00, 0x1f, 0x3f, 0x5f, 0x7f, 0x9e, 0xbe, 0xde, 0xfe, 0x1d, 0x3d, 0x5d, 0x7d, 0x9c, 0xbc,
0xdc, 0xfc, 0x1b, 0x3b, 0x5b, 0x7b, 0x9a, 0xba, 0xda, 0xfa, 0x19, 0x39, 0x59, 0x79, 0x98, 0xb8,
0xd8, 0xf8, 0x05, 0x00, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
sourceChain := ids.ID{
0x1f, 0x8f, 0x9f, 0x0f, 0x1e, 0x8e, 0x9e, 0x0e,
0x2d, 0x7d, 0xad, 0xfd, 0x2c, 0x7c, 0xac, 0xfc,
0x3b, 0x6b, 0xbb, 0xeb, 0x3a, 0x6a, 0xba, 0xea,
0x49, 0x59, 0xc9, 0xd9, 0x48, 0x58, 0xc8, 0xd8,
}
tx := &Tx{Unsigned: &ImportTx{
importedIn := &lux.TransferableInput{
UTXOID: lux.UTXOID{TxID: ids.ID{
0x0f, 0x2f, 0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee,
0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac, 0xcc, 0xec,
0x0b, 0x2b, 0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea,
0x09, 0x29, 0x49, 0x69, 0x88, 0xa8, 0xc8, 0xe8,
}, OutputIndex: 5},
Asset: lux.Asset{ID: assetID},
In: &secp256k1fx.TransferInput{
Amt: 1000,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}
utx := &ImportTx{
BaseTx: BaseTx{BaseTx: lux.BaseTx{
NetworkID: 2,
BlockchainID: ids.ID{
0xff, 0xff, 0xff, 0xff, 0xee, 0xee, 0xee, 0xee,
0xdd, 0xdd, 0xdd, 0xdd, 0xcc, 0xcc, 0xcc, 0xcc,
0xbb, 0xbb, 0xbb, 0xbb, 0xaa, 0xaa, 0xaa, 0xaa,
0x99, 0x99, 0x99, 0x99, 0x88, 0x88, 0x88, 0x88,
},
Memo: []byte{0x00, 0x01, 0x02, 0x03},
NetworkID: constants.UnitTestID,
BlockchainID: chainID,
Memo: []byte{0x00, 0x01, 0x02, 0x03},
}},
SourceChain: ids.ID{
0x1f, 0x8f, 0x9f, 0x0f, 0x1e, 0x8e, 0x9e, 0x0e,
0x2d, 0x7d, 0xad, 0xfd, 0x2c, 0x7c, 0xac, 0xfc,
0x3b, 0x6b, 0xbb, 0xeb, 0x3a, 0x6a, 0xba, 0xea,
0x49, 0x59, 0xc9, 0xd9, 0x48, 0x58, 0xc8, 0xd8,
},
ImportedIns: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{TxID: ids.ID{
0x0f, 0x2f, 0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee,
0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac, 0xcc, 0xec,
0x0b, 0x2b, 0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea,
0x09, 0x29, 0x49, 0x69, 0x88, 0xa8, 0xc8, 0xe8,
}},
Asset: lux.Asset{ID: ids.ID{
0x1f, 0x3f, 0x5f, 0x7f, 0x9e, 0xbe, 0xde, 0xfe,
0x1d, 0x3d, 0x5d, 0x7d, 0x9c, 0xbc, 0xdc, 0xfc,
0x1b, 0x3b, 0x5b, 0x7b, 0x9a, 0xba, 0xda, 0xfa,
0x19, 0x39, 0x59, 0x79, 0x98, 0xb8, 0xd8, 0xf8,
}},
In: &secp256k1fx.TransferInput{
Amt: 1000,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
}}
parser, err := NewParser(
[]fxs.Fx{
&secp256k1fx.Fx{},
},
)
require.NoError(err)
require.NoError(tx.Initialize(parser.Codec()))
require.Equal("tSexDDwyJnwDsjkP8Eam2ecKKuvcNZFiHkrXHbhK141DTdZoS", tx.ID().String())
result := tx.Bytes()
require.Equal(expected, result)
credBytes := []byte{
// type id
0x00, 0x00, 0x00, 0x09,
// there are two signers (thus two signatures)
0x00, 0x00, 0x00, 0x02,
// 65 bytes
0x8c, 0xc7, 0xdc, 0x8c, 0x11, 0xd3, 0x75, 0x9e, 0x16, 0xa5,
0x9f, 0xd2, 0x9c, 0x64, 0xd7, 0x1f, 0x9b, 0xad, 0x1a, 0x62,
0x33, 0x98, 0xc7, 0xaf, 0x67, 0x02, 0xc5, 0xe0, 0x75, 0x8e,
0x62, 0xcf, 0x15, 0x6d, 0x99, 0xf5, 0x4e, 0x71, 0xb8, 0xf4,
0x8b, 0x5b, 0xbf, 0x0c, 0x59, 0x62, 0x79, 0x34, 0x97, 0x1a,
0x1f, 0x49, 0x9b, 0x0a, 0x4f, 0xbf, 0x95, 0xfc, 0x31, 0x39,
0x46, 0x4e, 0xa1, 0xaf, 0x00,
// 65 bytes
0x8c, 0xc7, 0xdc, 0x8c, 0x11, 0xd3, 0x75, 0x9e, 0x16, 0xa5,
0x9f, 0xd2, 0x9c, 0x64, 0xd7, 0x1f, 0x9b, 0xad, 0x1a, 0x62,
0x33, 0x98, 0xc7, 0xaf, 0x67, 0x02, 0xc5, 0xe0, 0x75, 0x8e,
0x62, 0xcf, 0x15, 0x6d, 0x99, 0xf5, 0x4e, 0x71, 0xb8, 0xf4,
0x8b, 0x5b, 0xbf, 0x0c, 0x59, 0x62, 0x79, 0x34, 0x97, 0x1a,
0x1f, 0x49, 0x9b, 0x0a, 0x4f, 0xbf, 0x95, 0xfc, 0x31, 0x39,
0x46, 0x4e, 0xa1, 0xaf, 0x00,
// type id
0x00, 0x00, 0x00, 0x09,
// there are two signers (thus two signatures)
0x00, 0x00, 0x00, 0x02,
// 65 bytes
0x8c, 0xc7, 0xdc, 0x8c, 0x11, 0xd3, 0x75, 0x9e, 0x16, 0xa5,
0x9f, 0xd2, 0x9c, 0x64, 0xd7, 0x1f, 0x9b, 0xad, 0x1a, 0x62,
0x33, 0x98, 0xc7, 0xaf, 0x67, 0x02, 0xc5, 0xe0, 0x75, 0x8e,
0x62, 0xcf, 0x15, 0x6d, 0x99, 0xf5, 0x4e, 0x71, 0xb8, 0xf4,
0x8b, 0x5b, 0xbf, 0x0c, 0x59, 0x62, 0x79, 0x34, 0x97, 0x1a,
0x1f, 0x49, 0x9b, 0x0a, 0x4f, 0xbf, 0x95, 0xfc, 0x31, 0x39,
0x46, 0x4e, 0xa1, 0xaf, 0x00,
// 65 bytes
0x8c, 0xc7, 0xdc, 0x8c, 0x11, 0xd3, 0x75, 0x9e, 0x16, 0xa5,
0x9f, 0xd2, 0x9c, 0x64, 0xd7, 0x1f, 0x9b, 0xad, 0x1a, 0x62,
0x33, 0x98, 0xc7, 0xaf, 0x67, 0x02, 0xc5, 0xe0, 0x75, 0x8e,
0x62, 0xcf, 0x15, 0x6d, 0x99, 0xf5, 0x4e, 0x71, 0xb8, 0xf4,
0x8b, 0x5b, 0xbf, 0x0c, 0x59, 0x62, 0x79, 0x34, 0x97, 0x1a,
0x1f, 0x49, 0x9b, 0x0a, 0x4f, 0xbf, 0x95, 0xfc, 0x31, 0x39,
0x46, 0x4e, 0xa1, 0xaf, 0x00,
SourceChain: sourceChain,
ImportedIns: []*lux.TransferableInput{importedIn},
}
require.NoError(tx.SignSECP256K1Fx(
parser.Codec(),
[][]*secp256k1.PrivateKey{
{keys[0], keys[0]},
{keys[0], keys[0]},
},
))
require.Equal("Uv77K7265Um1dTQhZJXfVGGPPMPwtJf8RpQqw4pDg2AZysEzD", tx.ID().String())
// there are two credentials
expected[len(expected)-1] = 0x02
expected = append(expected, credBytes...)
result = tx.Bytes()
require.Equal(expected, result)
tx := &Tx{Unsigned: utx}
require.NoError(tx.Initialize())
parsed, err := Parse(tx.Bytes())
require.NoError(err)
require.Equal(tx.Bytes(), parsed.Bytes())
require.Equal(tx.ID(), parsed.ID())
got := parsed.Unsigned.(*ImportTx)
require.Equal(sourceChain, got.SourceChain)
require.Len(got.ImportedIns, 1)
require.Equal(importedIn.UTXOID, got.ImportedIns[0].UTXOID)
require.Equal(importedIn.AssetID(), got.ImportedIns[0].AssetID())
require.Equal(fxBytes(t, importedIn.In), fxBytes(t, got.ImportedIns[0].In))
require.NoError(tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{keys[0], keys[0]}}))
signed, err := Parse(tx.Bytes())
require.NoError(err)
require.Equal(tx.ID(), signed.ID())
require.Len(signed.Creds, 1)
}
func TestImportTxNotState(t *testing.T) {
+87 -44
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 txs
@@ -11,9 +11,9 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/runtime"
"github.com/luxfi/utils"
"github.com/luxfi/zap"
)
var (
@@ -25,18 +25,80 @@ var (
_ utils.Sortable[*InitialState] = (*InitialState)(nil)
)
// InitialState is the per-fx genesis state a CreateAssetTx installs: an fx
// index plus that fx's initial output set. Outs is a heterogeneous slice of fx
// state outputs, each serialized as its own (TypeKind, ShapeKind, ZAP) wire
// envelope — the FxID is recovered from the envelope's TypeKind, so it is not
// stored on the wire.
//
// Wire: zap object { FxIndex@0 (u32), Outs (length list @4, blob @12) }.
type InitialState struct {
FxIndex uint32 `serialize:"true" json:"fxIndex"`
FxID ids.ID `serialize:"false" json:"fxID"`
Outs []verify.State `serialize:"true" json:"outputs"`
FxIndex uint32 `json:"fxIndex"`
FxID ids.ID `json:"fxID"`
Outs []verify.State `json:"outputs"`
}
func (is *InitialState) InitRuntime(rt *runtime.Runtime) {
// verify.State doesn't have InitRuntime method
// The InitRuntime is handled at a higher level
const (
offISFxIndex = 0 // u32
offISOutsLen = 4 // list ptr
offISOutsBlob = 12 // bytes ptr
sizeIS = 20
)
func (is *InitialState) InitRuntime(*runtime.Runtime) {
// verify.State outputs are fx-agnostic here; runtime binding happens at a
// higher level.
}
func (is *InitialState) Verify(c pcodecs.Manager, numFxs int) error {
// Bytes serializes the InitialState to its self-delimiting native-ZAP wire
// object. Each output is its fx primitive's own wire envelope.
func (is *InitialState) Bytes() ([]byte, error) {
outs := make([][]byte, len(is.Outs))
for i, out := range is.Outs {
b, err := childBytes(out)
if err != nil {
return nil, err
}
outs[i] = b
}
b := zap.NewBuilder(zap.HeaderSize + sizeIS + 256)
outsLenOff, outsLenCount, outsBlob := writeBlobList(b, outs)
ob := b.StartObject(sizeIS)
ob.SetUint32(offISFxIndex, is.FxIndex)
ob.SetList(offISOutsLen, outsLenOff, outsLenCount)
ob.SetBytes(offISOutsBlob, outsBlob)
ob.FinishAsRoot()
return b.Finish(), nil
}
// parseInitialState reconstructs an InitialState from its wire object, wrapping
// each output on its (TypeKind, ShapeKind) discriminator.
func parseInitialState(buf []byte) (*InitialState, error) {
msg, err := zap.Parse(buf)
if err != nil {
return nil, err
}
obj := msg.Root()
outBufs, err := readBlobList(obj, offISOutsLen, offISOutsBlob)
if err != nil {
return nil, err
}
outs := make([]verify.State, len(outBufs))
for i, env := range outBufs {
out, err := wrapFxState(env)
if err != nil {
return nil, err
}
outs[i] = out
}
return &InitialState{
FxIndex: obj.Uint32(offISFxIndex),
Outs: outs,
}, nil
}
func (is *InitialState) Verify(numFxs int) error {
switch {
case is == nil:
return ErrNilInitialState
@@ -52,10 +114,9 @@ func (is *InitialState) Verify(c pcodecs.Manager, numFxs int) error {
return err
}
}
if !isSortedState(is.Outs, c) {
if !isSortedState(is.Outs) {
return ErrOutputsNotSorted
}
return nil
}
@@ -63,43 +124,25 @@ func (is *InitialState) Compare(other *InitialState) int {
return cmp.Compare(is.FxIndex, other.FxIndex)
}
func (is *InitialState) Sort(c pcodecs.Manager) {
sortState(is.Outs, c)
func (is *InitialState) Sort() {
sortState(is.Outs)
}
type innerSortState struct {
vers []verify.State
codec pcodecs.Manager
// stateBytes returns the canonical wire bytes for a state output, used as the
// sort key. Unknown (non-wire-serializable) outputs sort as empty.
func stateBytes(v verify.State) []byte {
b, _ := childBytes(v)
return b
}
func (vers *innerSortState) Less(i, j int) bool {
iVer := vers.vers[i]
jVer := vers.vers[j]
iBytes, err := vers.codec.Marshal(CodecVersion, &iVer)
if err != nil {
return false
}
jBytes, err := vers.codec.Marshal(CodecVersion, &jVer)
if err != nil {
return false
}
return bytes.Compare(iBytes, jBytes) == -1
func sortState(vers []verify.State) {
sort.Slice(vers, func(i, j int) bool {
return bytes.Compare(stateBytes(vers[i]), stateBytes(vers[j])) < 0
})
}
func (vers *innerSortState) Len() int {
return len(vers.vers)
}
func (vers *innerSortState) Swap(i, j int) {
v := vers.vers
v[j], v[i] = v[i], v[j]
}
func sortState(vers []verify.State, c pcodecs.Manager) {
sort.Sort(&innerSortState{vers: vers, codec: c})
}
func isSortedState(vers []verify.State, c pcodecs.Manager) bool {
return sort.IsSorted(&innerSortState{vers: vers, codec: c})
func isSortedState(vers []verify.State) bool {
return sort.SliceIsSorted(vers, func(i, j int) bool {
return bytes.Compare(stateBytes(vers[i]), stateBytes(vers[j])) < 0
})
}
+29 -66
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 txs
@@ -12,29 +12,15 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
var errTest = errors.New("non-nil error")
func TestInitialStateVerifySerialization(t *testing.T) {
func TestInitialStateRoundTrip(t *testing.T) {
require := require.New(t)
c := pcodecs.NewLinearCodec()
require.NoError(c.RegisterType(&secp256k1fx.TransferOutput{}))
m := pcodecs.NewDefaultManager()
require.NoError(m.RegisterCodec(CodecVersion, c))
expected := []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x30,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xd4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x51, 0x02, 0x5c, 0x61, 0xfb, 0xcf, 0xc0, 0x78, 0xf6, 0x93,
0x34, 0xf8, 0x34, 0xbe, 0x6d, 0xd2, 0x6d, 0x55, 0xa9, 0x55, 0xc3, 0x34, 0x41, 0x28, 0xe0, 0x60,
0x12, 0x8e, 0xde, 0x35, 0x23, 0xa2, 0x4a, 0x46, 0x1c, 0x89, 0x43, 0xab, 0x08, 0x59,
}
is := &InitialState{
FxIndex: 0,
Outs: []verify.State{
@@ -43,109 +29,86 @@ func TestInitialStateVerifySerialization(t *testing.T) {
OutputOwners: secp256k1fx.OutputOwners{
Locktime: 54321,
Threshold: 1,
Addrs: []ids.ShortID{
{
0x51, 0x02, 0x5c, 0x61, 0xfb, 0xcf, 0xc0, 0x78,
0xf6, 0x93, 0x34, 0xf8, 0x34, 0xbe, 0x6d, 0xd2,
0x6d, 0x55, 0xa9, 0x55,
},
{
0xc3, 0x34, 0x41, 0x28, 0xe0, 0x60, 0x12, 0x8e,
0xde, 0x35, 0x23, 0xa2, 0x4a, 0x46, 0x1c, 0x89,
0x43, 0xab, 0x08, 0x59,
},
},
Addrs: []ids.ShortID{keys[0].PublicKey().Address()},
},
},
},
}
isBytes, err := m.Marshal(CodecVersion, is)
b, err := is.Bytes()
require.NoError(err)
require.Equal(expected, isBytes)
got, err := parseInitialState(b)
require.NoError(err)
require.EqualValues(0, got.FxIndex)
require.Len(got.Outs, 1)
require.Equal(fxBytes(t, is.Outs[0]), fxBytes(t, got.Outs[0]))
}
func TestInitialStateVerifyNil(t *testing.T) {
require := require.New(t)
c := pcodecs.NewLinearCodec()
m := pcodecs.NewDefaultManager()
require.NoError(m.RegisterCodec(CodecVersion, c))
numFxs := 1
is := (*InitialState)(nil)
err := is.Verify(m, numFxs)
err := is.Verify(numFxs)
require.ErrorIs(err, ErrNilInitialState)
}
func TestInitialStateVerifyUnknownFxID(t *testing.T) {
require := require.New(t)
c := pcodecs.NewLinearCodec()
m := pcodecs.NewDefaultManager()
require.NoError(m.RegisterCodec(CodecVersion, c))
numFxs := 1
is := InitialState{
FxIndex: 1,
}
err := is.Verify(m, numFxs)
err := is.Verify(numFxs)
require.ErrorIs(err, ErrUnknownFx)
}
func TestInitialStateVerifyNilOutput(t *testing.T) {
require := require.New(t)
c := pcodecs.NewLinearCodec()
m := pcodecs.NewDefaultManager()
require.NoError(m.RegisterCodec(CodecVersion, c))
numFxs := 1
is := InitialState{
FxIndex: 0,
Outs: []verify.State{nil},
}
err := is.Verify(m, numFxs)
err := is.Verify(numFxs)
require.ErrorIs(err, ErrNilFxOutput)
}
func TestInitialStateVerifyInvalidOutput(t *testing.T) {
require := require.New(t)
c := pcodecs.NewLinearCodec()
require.NoError(c.RegisterType(&lux.TestState{}))
m := pcodecs.NewDefaultManager()
require.NoError(m.RegisterCodec(CodecVersion, c))
numFxs := 1
is := InitialState{
FxIndex: 0,
Outs: []verify.State{&lux.TestState{Err: errTest}},
}
err := is.Verify(m, numFxs)
err := is.Verify(numFxs)
require.ErrorIs(err, errTest)
}
func TestInitialStateVerifyUnsortedOutputs(t *testing.T) {
require := require.New(t)
c := pcodecs.NewLinearCodec()
require.NoError(c.RegisterType(&lux.TestTransferable{}))
m := pcodecs.NewDefaultManager()
require.NoError(m.RegisterCodec(CodecVersion, c))
numFxs := 1
is := InitialState{
FxIndex: 0,
Outs: []verify.State{
&lux.TestTransferable{Val: 1},
&lux.TestTransferable{Val: 0},
},
outA := &secp256k1fx.TransferOutput{
Amt: 1,
OutputOwners: secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{keys[0].PublicKey().Address()}},
}
err := is.Verify(m, numFxs)
require.ErrorIs(err, ErrOutputsNotSorted)
is.Sort(m)
require.NoError(is.Verify(m, numFxs))
outB := &secp256k1fx.TransferOutput{
Amt: 2,
OutputOwners: secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{keys[0].PublicKey().Address()}},
}
is := InitialState{FxIndex: 0, Outs: []verify.State{outA, outB}}
// Force an unsorted order regardless of the canonical wire byte ordering.
if isSortedState(is.Outs) {
is.Outs[0], is.Outs[1] = is.Outs[1], is.Outs[0]
}
require.ErrorIs(is.Verify(numFxs), ErrOutputsNotSorted)
is.Sort()
require.NoError(is.Verify(numFxs))
}
func TestInitialStateCompare(t *testing.T) {
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import "github.com/luxfi/zap"
// xkind is the 1-byte tx discriminator at object offset 0 of every X-chain
// unsigned-tx buffer. It is the whole dispatch: Parse reads it and returns the
// typed unsigned tx. There is no codec, no version, no slot map.
//
// The values are the registration order of the old linearcodec (BaseTx,
// CreateAssetTx, OperationTx, ImportTx, ExportTx) shifted by one so 0 stays a
// reserved sentinel that never appears on the wire.
type xkind uint8
const (
xkindReserved xkind = iota
xkindBase // 1
xkindCreateAsset // 2
xkindOperation // 3
xkindImport // 4
xkindExport // 5
)
// offXKind is the fixed wire position of the discriminator (object offset 0).
const offXKind = 0
// xkindOf reads the discriminator from a parsed unsigned-tx buffer.
func xkindOf(msg *zap.Message) xkind {
return xkind(msg.Root().Uint8(offXKind))
}
+75 -58
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 txs
@@ -11,10 +11,10 @@ import (
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/utils"
lux "github.com/luxfi/utxo"
"github.com/luxfi/zap"
)
var (
@@ -23,13 +23,26 @@ var (
ErrNotSortedAndUniqueUTXOIDs = errors.New("utxo IDs not sorted and unique")
)
// Operation runs a single fx operation (mint / NFT transfer / property burn)
// over a set of consumed UTXOs of a given asset.
//
// Wire: zap object { Asset@0 (32B), UTXOIDs@32 (36B-stride list), Op@40 (the
// fx operation's own (TypeKind, ShapeKind, ZAP) envelope) }. FxID is recovered
// from the Op envelope's TypeKind, so it is not stored on the wire.
type Operation struct {
lux.Asset `serialize:"true"`
UTXOIDs []*lux.UTXOID `serialize:"true" json:"inputIDs"`
FxID ids.ID `serialize:"false" json:"fxID"`
Op fxs.FxOperation `serialize:"true" json:"operation"`
lux.Asset
UTXOIDs []*lux.UTXOID `json:"inputIDs"`
FxID ids.ID `json:"fxID"`
Op fxs.FxOperation `json:"operation"`
}
const (
offOpAsset = 0 // 32B
offOpUTXOIDs = 32 // list ptr (36B stride)
offOpFxOp = 40 // bytes ptr
sizeOp = 48
)
func (op *Operation) Verify() error {
switch {
case op == nil:
@@ -43,79 +56,83 @@ func (op *Operation) Verify() error {
}
}
type operationAndCodec struct {
op *Operation
codec pcodecs.Manager
// Bytes serializes the Operation to its self-delimiting native-ZAP wire object.
func (op *Operation) Bytes() ([]byte, error) {
fxOp, err := childBytes(op.Op)
if err != nil {
return nil, err
}
b := zap.NewBuilder(zap.HeaderSize + sizeOp + len(fxOp) + 256)
utxoOff, utxoCount := writeUTXOIDs(b, op.UTXOIDs)
ob := b.StartObject(sizeOp)
ob.SetBytesFixed(offOpAsset, op.Asset.ID[:])
ob.SetList(offOpUTXOIDs, utxoOff, utxoCount)
ob.SetBytes(offOpFxOp, fxOp)
ob.FinishAsRoot()
return b.Finish(), nil
}
func (o *operationAndCodec) Compare(other *operationAndCodec) int {
oBytes, err := o.codec.Marshal(CodecVersion, o.op)
// parseOperation reconstructs an Operation from its wire object, wrapping the
// fx operation on its (TypeKind, ShapeKind) discriminator.
func parseOperation(buf []byte) (*Operation, error) {
msg, err := zap.Parse(buf)
if err != nil {
return 0
return nil, err
}
otherBytes, err := o.codec.Marshal(CodecVersion, other.op)
obj := msg.Root()
fxOp, err := wrapFxOperation(obj.Bytes(offOpFxOp))
if err != nil {
return 0
return nil, err
}
return bytes.Compare(oBytes, otherBytes)
var asset ids.ID
copy(asset[:], obj.BytesFixedSlice(offOpAsset, 32))
return &Operation{
Asset: lux.Asset{ID: asset},
UTXOIDs: readUTXOIDs(obj, offOpUTXOIDs),
Op: fxOp,
}, nil
}
func SortOperations(ops []*Operation, c pcodecs.Manager) {
sortableOps := make([]*operationAndCodec, len(ops))
for i, op := range ops {
sortableOps[i] = &operationAndCodec{
op: op,
codec: c,
// opBytes is the canonical wire sort key for an Operation.
func opBytes(op *Operation) []byte {
b, _ := op.Bytes()
return b
}
func SortOperations(ops []*Operation) {
sort.Slice(ops, func(i, j int) bool {
return bytes.Compare(opBytes(ops[i]), opBytes(ops[j])) < 0
})
}
func IsSortedAndUniqueOperations(ops []*Operation) bool {
for i := 0; i < len(ops)-1; i++ {
if bytes.Compare(opBytes(ops[i]), opBytes(ops[i+1])) >= 0 {
return false
}
}
utils.Sort(sortableOps)
for i, sortableOp := range sortableOps {
ops[i] = sortableOp.op
}
}
func IsSortedAndUniqueOperations(ops []*Operation, c pcodecs.Manager) bool {
sortableOps := make([]*operationAndCodec, len(ops))
for i, op := range ops {
sortableOps[i] = &operationAndCodec{
op: op,
codec: c,
}
}
return utils.IsSortedAndUnique(sortableOps)
return true
}
type innerSortOperationsWithSigners struct {
ops []*Operation
signers [][]*secp256k1.PrivateKey
codec pcodecs.Manager
}
func (ops *innerSortOperationsWithSigners) Less(i, j int) bool {
iOp := ops.ops[i]
jOp := ops.ops[j]
iBytes, err := ops.codec.Marshal(CodecVersion, iOp)
if err != nil {
return false
}
jBytes, err := ops.codec.Marshal(CodecVersion, jOp)
if err != nil {
return false
}
return bytes.Compare(iBytes, jBytes) == -1
func (o *innerSortOperationsWithSigners) Less(i, j int) bool {
return bytes.Compare(opBytes(o.ops[i]), opBytes(o.ops[j])) < 0
}
func (ops *innerSortOperationsWithSigners) Len() int {
return len(ops.ops)
func (o *innerSortOperationsWithSigners) Len() int {
return len(o.ops)
}
func (ops *innerSortOperationsWithSigners) Swap(i, j int) {
ops.ops[j], ops.ops[i] = ops.ops[i], ops.ops[j]
ops.signers[j], ops.signers[i] = ops.signers[i], ops.signers[j]
func (o *innerSortOperationsWithSigners) Swap(i, j int) {
o.ops[j], o.ops[i] = o.ops[i], o.ops[j]
o.signers[j], o.signers[i] = o.signers[i], o.signers[j]
}
func SortOperationsWithSigners(ops []*Operation, signers [][]*secp256k1.PrivateKey, codec pcodecs.Manager) {
sort.Sort(&innerSortOperationsWithSigners{ops: ops, signers: signers, codec: codec})
func SortOperationsWithSigners(ops []*Operation, signers [][]*secp256k1.PrivateKey) {
sort.Sort(&innerSortOperationsWithSigners{ops: ops, signers: signers})
}
+27 -44
View File
@@ -1,35 +1,39 @@
// 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 txs
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
)
// testOperable is a minimal fxs.FxOperation: it verifies (via the embedded
// TestState), costs zero (via TestTransferable), yields no outputs, and is
// wire-serializable via a constant fx-operation envelope so the byte-keyed
// operation sort has a stable key.
type testOperable struct {
lux.TestTransferable `serialize:"true"`
lux.TestTransferable
Outputs []verify.State `serialize:"true"`
Outputs []verify.State
}
func (*testOperable) InitRuntime(context.Context) {}
func (*testOperable) InitializeRuntime(*runtime.Runtime) error { return nil }
func (o *testOperable) Outs() []verify.State {
return o.Outputs
}
func (*testOperable) Bytes() []byte {
// A deterministic, non-empty fx-operation envelope stand-in. The operation
// sort key is Asset + UTXOIDs + this blob; distinct UTXOIDs make distinct
// keys.
return []byte{0x01, 0x05}
}
func TestOperationVerifyNil(t *testing.T) {
op := (*Operation)(nil)
err := op.Verify()
@@ -81,48 +85,27 @@ func TestOperationVerify(t *testing.T) {
func TestOperationSorting(t *testing.T) {
require := require.New(t)
c := pcodecs.NewLinearCodec()
require.NoError(c.RegisterType(&testOperable{}))
m := pcodecs.NewDefaultManager()
require.NoError(m.RegisterCodec(CodecVersion, c))
ops := []*Operation{
{
Asset: lux.Asset{ID: ids.Empty},
UTXOIDs: []*lux.UTXOID{
{
TxID: ids.Empty,
OutputIndex: 1,
},
},
Op: &testOperable{},
Asset: lux.Asset{ID: ids.Empty},
UTXOIDs: []*lux.UTXOID{{TxID: ids.Empty, OutputIndex: 1}},
Op: &testOperable{},
},
{
Asset: lux.Asset{ID: ids.Empty},
UTXOIDs: []*lux.UTXOID{
{
TxID: ids.Empty,
OutputIndex: 0,
},
},
Op: &testOperable{},
Asset: lux.Asset{ID: ids.Empty},
UTXOIDs: []*lux.UTXOID{{TxID: ids.Empty, OutputIndex: 0}},
Op: &testOperable{},
},
}
require.False(IsSortedAndUniqueOperations(ops, m))
SortOperations(ops, m)
require.True(IsSortedAndUniqueOperations(ops, m))
require.False(IsSortedAndUniqueOperations(ops))
SortOperations(ops)
require.True(IsSortedAndUniqueOperations(ops))
ops = append(ops, &Operation{
Asset: lux.Asset{ID: ids.Empty},
UTXOIDs: []*lux.UTXOID{
{
TxID: ids.Empty,
OutputIndex: 1,
},
},
Op: &testOperable{},
Asset: lux.Asset{ID: ids.Empty},
UTXOIDs: []*lux.UTXOID{{TxID: ids.Empty, OutputIndex: 1}},
Op: &testOperable{},
})
require.False(IsSortedAndUniqueOperations(ops, m))
require.False(IsSortedAndUniqueOperations(ops))
}
func TestOperationTxNotState(t *testing.T) {
+67 -12
View File
@@ -1,15 +1,15 @@
// 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 txs
import (
"github.com/luxfi/runtime"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/zap"
)
var (
@@ -17,18 +17,25 @@ var (
_ secp256k1fx.UnsignedTx = (*OperationTx)(nil)
)
// OperationTx is a transaction with no credentials.
// OperationTx is a transaction that runs a set of fx operations over existing
// UTXOs (mint / NFT transfer / property burn ...).
//
// Wire (unsigned): the shared { xkind@0, baseTxEnvelope@8 } prefix, then the
// Operations as a packed (length list @16, blob @24) of self-delimiting
// Operation objects.
type OperationTx struct {
BaseTx `serialize:"true"`
BaseTx
Ops []*Operation `serialize:"true" json:"operations"`
Ops []*Operation `json:"operations"`
}
const (
offOpsLen = 16 // list ptr
offOpsBlob = 24 // bytes ptr
sizeOpTx = 32
)
func (t *OperationTx) InitRuntime(rt *runtime.Runtime) {
// FxOperation doesn't have InitRuntime method
// for _, op := range t.Ops {
// op.Op.InitRuntime(rt)
// }
t.BaseTx.InitRuntime(rt)
}
@@ -72,7 +79,55 @@ func (t *OperationTx) Visit(v Visitor) error {
}
// InitializeWithRuntime initializes the transaction with Runtime
func (tx *OperationTx) InitializeWithRuntime(rt *runtime.Runtime) error {
// Initialize any context-dependent fields here
func (t *OperationTx) InitializeWithRuntime(rt *runtime.Runtime) error {
t.InitRuntime(rt)
return nil
}
func (t *OperationTx) serialize() ([]byte, error) {
env, err := t.baseTxWire()
if err != nil {
return nil, err
}
ops := make([][]byte, len(t.Ops))
for i, op := range t.Ops {
b, err := op.Bytes()
if err != nil {
return nil, err
}
ops[i] = b
}
b := zap.NewBuilder(zap.HeaderSize + sizeOpTx + len(env) + 256)
opsLenOff, opsLenCount, opsBlob := writeBlobList(b, ops)
ob := b.StartObject(sizeOpTx)
ob.SetUint8(offXKind, uint8(xkindOperation))
ob.SetBytes(offBaseTx, env)
ob.SetList(offOpsLen, opsLenOff, opsLenCount)
ob.SetBytes(offOpsBlob, opsBlob)
ob.FinishAsRoot()
return b.Finish(), nil
}
func parseOperationTx(unsignedBytes []byte, obj zap.Object) (*OperationTx, error) {
base, err := decodeBaseTxWire(obj)
if err != nil {
return nil, err
}
opBufs, err := readBlobList(obj, offOpsLen, offOpsBlob)
if err != nil {
return nil, err
}
ops := make([]*Operation, len(opBufs))
for i, buf := range opBufs {
op, err := parseOperation(buf)
if err != nil {
return nil, err
}
ops[i] = op
}
return &OperationTx{
BaseTx: BaseTx{BaseTx: base, bytes: unsignedBytes},
Ops: ops,
}, nil
}
+145 -130
View File
@@ -1,47 +1,43 @@
// 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 txs
// Parse dispatch. There is no codec: ParseTx wraps the wire.SignedTx envelope
// zero-copy, reads the 1-byte xkind at object offset 0 of the unsigned body to
// select the concrete tx type, and reconstructs each fx credential by its
// (TypeKind, ShapeKind) wire discriminator — no reflect, no slot map.
import (
"errors"
"fmt"
"reflect"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/timer/mockable"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/wire"
"github.com/luxfi/zap"
)
// CodecVersion is the current default codec version
const CodecVersion = 0
var _ Parser = (*parser)(nil)
var (
_ Parser = (*parser)(nil)
_ secp256k1fx.VM = (*fxVM)(nil)
)
type Parser interface {
Codec() pcodecs.Manager
GenesisCodec() pcodecs.Manager
CodecRegistry() pcodecs.Registry
GenesisCodecRegistry() pcodecs.Registry
ParseTx(bytes []byte) (*Tx, error)
ParseGenesisTx(bytes []byte) (*Tx, error)
}
type parser struct {
cm pcodecs.Manager
gcm pcodecs.Manager
c pcodecs.LinearCodec
gc pcodecs.LinearCodec
fxs []fxs.Fx
}
func NewParser(fxs []fxs.Fx) (Parser, error) {
// Create a basic logger for parsing
return NewCustomParser(
make(map[reflect.Type]int),
&mockable.Clock{},
@@ -53,136 +49,155 @@ func NewParser(fxs []fxs.Fx) (Parser, error) {
func NewCustomParser(
typeToFxIndex map[reflect.Type]int,
clock *mockable.Clock,
log log.Logger,
fxs []fxs.Fx,
logger log.Logger,
fxList []fxs.Fx,
) (Parser, error) {
gc := pcodecs.NewLinearCodec()
c := pcodecs.NewLinearCodec()
gcm := pcodecs.NewMaxInt32Manager()
cm := pcodecs.NewDefaultManager()
err := errors.Join(
c.RegisterType(&BaseTx{}),
c.RegisterType(&CreateAssetTx{}),
c.RegisterType(&OperationTx{}),
c.RegisterType(&ImportTx{}),
c.RegisterType(&ExportTx{}),
cm.RegisterCodec(CodecVersion, c),
gc.RegisterType(&BaseTx{}),
gc.RegisterType(&CreateAssetTx{}),
gc.RegisterType(&OperationTx{}),
gc.RegisterType(&ImportTx{}),
gc.RegisterType(&ExportTx{}),
gcm.RegisterCodec(CodecVersion, gc),
)
if err != nil {
return nil, err
}
vm := &fxVM{
typeToFxIndex: typeToFxIndex,
clock: clock,
log: log,
}
for i, fx := range fxs {
vm.codecRegistry = &codecRegistry{
codecs: []pcodecs.Registry{gc, c},
index: i,
typeToIndex: vm.typeToFxIndex,
}
// Initialize with a proper VM that has a logger
vm := &fxVM{clock: clock, log: logger}
for i, fx := range fxList {
if err := fx.Initialize(vm); err != nil {
return nil, err
}
// utxo v0.3.5+ fx.Initialize is a no-op (ZAP-native; LP-023 /
// utxo FINAL_RIP step 8). Per-fx wire types used to be registered
// inside fx.Initialize via vm.CodecRegistry().RegisterType(...);
// re-register them here through vm.codecRegistry so both the
// zapcodec interface dispatch AND the typeToFxIndex map
// (semantic_verifier.getFx) are populated.
// Populate the fx-usage index (concrete fx type -> fx index) that the
// semantic verifier's getFx and tx_init.InitializeFx consult. This is
// verification policy, distinct from wire decoding (which is envelope
// dispatched); it is a plain map fill, no codec.
switch fx.(type) {
case *secp256k1fx.Fx:
if err := errors.Join(
vm.codecRegistry.RegisterType(&secp256k1fx.TransferInput{}),
vm.codecRegistry.RegisterType(&secp256k1fx.MintOutput{}),
vm.codecRegistry.RegisterType(&secp256k1fx.TransferOutput{}),
vm.codecRegistry.RegisterType(&secp256k1fx.MintOperation{}),
vm.codecRegistry.RegisterType(&secp256k1fx.Credential{}),
); err != nil {
return nil, err
}
registerFxTypes(typeToFxIndex, i,
&secp256k1fx.TransferInput{},
&secp256k1fx.MintOutput{},
&secp256k1fx.TransferOutput{},
&secp256k1fx.MintOperation{},
&secp256k1fx.Credential{},
)
case *nftfx.Fx:
if err := errors.Join(
vm.codecRegistry.RegisterType(&nftfx.MintOutput{}),
vm.codecRegistry.RegisterType(&nftfx.TransferOutput{}),
vm.codecRegistry.RegisterType(&nftfx.MintOperation{}),
vm.codecRegistry.RegisterType(&nftfx.TransferOperation{}),
vm.codecRegistry.RegisterType(&nftfx.Credential{}),
); err != nil {
return nil, err
}
registerFxTypes(typeToFxIndex, i,
&nftfx.MintOutput{},
&nftfx.TransferOutput{},
&nftfx.MintOperation{},
&nftfx.TransferOperation{},
&nftfx.Credential{},
)
case *propertyfx.Fx:
if err := errors.Join(
vm.codecRegistry.RegisterType(&propertyfx.MintOutput{}),
vm.codecRegistry.RegisterType(&propertyfx.OwnedOutput{}),
vm.codecRegistry.RegisterType(&propertyfx.MintOperation{}),
vm.codecRegistry.RegisterType(&propertyfx.BurnOperation{}),
vm.codecRegistry.RegisterType(&propertyfx.Credential{}),
); err != nil {
return nil, err
}
registerFxTypes(typeToFxIndex, i,
&propertyfx.MintOutput{},
&propertyfx.OwnedOutput{},
&propertyfx.MintOperation{},
&propertyfx.BurnOperation{},
&propertyfx.Credential{},
)
}
}
return &parser{
cm: cm,
gcm: gcm,
c: c,
gc: gc,
return &parser{fxs: fxList}, nil
}
func registerFxTypes(m map[reflect.Type]int, index int, vals ...interface{}) {
for _, v := range vals {
m[reflect.TypeOf(v)] = index
}
}
// Parse decodes a signed X-chain tx from its wire bytes. It is the codec-free,
// parser-instance-free entry point used by the block layer and any consumer
// holding raw tx bytes; fx credential dispatch is envelope-based (stateless).
func Parse(signedBytes []byte) (*Tx, error) {
return parseSignedTx(signedBytes)
}
func (*parser) ParseTx(bytes []byte) (*Tx, error) {
return parseSignedTx(bytes)
}
func (*parser) ParseGenesisTx(bytes []byte) (*Tx, error) {
return parseSignedTx(bytes)
}
// parseSignedTx wraps a wire.SignedTx envelope: the leading unsigned body plus
// the packed fx credential list. TxID = hash(signedBytes), byte-preserving.
func parseSignedTx(signedBytes []byte) (*Tx, error) {
st, err := wire.WrapSignedTx(signedBytes)
if err != nil {
return nil, fmt.Errorf("couldn't parse signed tx: %w", err)
}
unsigned, err := parseUnsigned(st.UnsignedBytes())
if err != nil {
return nil, fmt.Errorf("couldn't parse unsigned tx: %w", err)
}
creds, err := parseCreds(st)
if err != nil {
return nil, fmt.Errorf("couldn't parse credentials: %w", err)
}
return &Tx{
Unsigned: unsigned,
Creds: creds,
TxID: hash.ComputeHash256Array(signedBytes),
bytes: signedBytes,
}, nil
}
func (p *parser) Codec() pcodecs.Manager {
return p.cm
}
func (p *parser) GenesisCodec() pcodecs.Manager {
return p.gcm
}
func (p *parser) CodecRegistry() pcodecs.Registry {
return p.c
}
func (p *parser) GenesisCodecRegistry() pcodecs.Registry {
return p.gc
}
func (p *parser) ParseTx(bytes []byte) (*Tx, error) {
return parse(p.cm, bytes)
}
func (p *parser) ParseGenesisTx(bytes []byte) (*Tx, error) {
return parse(p.gcm, bytes)
}
func parse(cm pcodecs.Manager, signedBytes []byte) (*Tx, error) {
tx := &Tx{}
parsedVersion, err := cm.Unmarshal(signedBytes, tx)
// parseUnsigned wraps the leading unsigned body as the typed UnsignedTx by
// dispatching on its xkind discriminator (object offset 0).
func parseUnsigned(unsignedBytes []byte) (UnsignedTx, error) {
msg, err := zap.Parse(unsignedBytes)
if err != nil {
return nil, err
}
if parsedVersion != CodecVersion {
return nil, fmt.Errorf("expected codec version %d but got %d", CodecVersion, parsedVersion)
obj := msg.Root()
switch k := xkindOf(msg); k {
case xkindBase:
return parseBaseTx(unsignedBytes, obj)
case xkindCreateAsset:
return parseCreateAssetTx(unsignedBytes, obj)
case xkindOperation:
return parseOperationTx(unsignedBytes, obj)
case xkindImport:
return parseImportTx(unsignedBytes, obj)
case xkindExport:
return parseExportTx(unsignedBytes, obj)
default:
return nil, fmt.Errorf("xvm txs: unknown tx kind %d", k)
}
}
unsignedBytesLen, err := cm.Size(CodecVersion, &tx.Unsigned)
if err != nil {
return nil, fmt.Errorf("couldn't calculate UnsignedTx marshal length: %w", err)
// parseCreds reconstructs the fx credential list, dispatching each envelope on
// its TypeKind. FxID is recovered later by tx_init.InitializeFx.
func parseCreds(st wire.SignedTx) ([]*fxs.FxCredential, error) {
n := st.CredentialCount()
if n == 0 {
return nil, nil
}
creds := make([]*fxs.FxCredential, 0, n)
blob := st.CredentialBytes()
for i := uint32(0); i < n; i++ {
env, rest, err := wire.NextEnvelope(blob)
if err != nil {
return nil, err
}
cred, err := wrapFxCredential(env)
if err != nil {
return nil, err
}
creds = append(creds, &fxs.FxCredential{Credential: cred})
blob = rest
}
return creds, nil
}
// fxVM is the minimal VM surface an fx needs at Initialize time (clock +
// logger). ZAP-native: fx wire schemas are compile-time static, so there is no
// codec registry to provide.
type fxVM struct {
clock *mockable.Clock
log log.Logger
}
func (vm *fxVM) Clock() *mockable.Clock { return vm.clock }
func (vm *fxVM) Logger() log.Logger { return vm.log }
unsignedBytes := signedBytes[:unsignedBytesLen]
tx.SetBytes(unsignedBytes, signedBytes)
return tx, nil
// ParseUnsignedTx decodes native-ZAP unsigned-tx bytes (as produced by
// UnsignedBytes) back into the concrete UnsignedTx, dispatching on the xkind
// discriminator. Used by the X-Chain genesis wire to re-hydrate each
// GenesisAsset's embedded CreateAssetTx.
func ParseUnsignedTx(unsignedBytes []byte) (UnsignedTx, error) {
return parseUnsigned(unsignedBytes)
}
+89 -76
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 txs
@@ -10,13 +10,14 @@ import (
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/p2p/gossip"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/wire"
)
var _ gossip.Gossipable = (*Tx)(nil)
@@ -39,26 +40,80 @@ type UnsignedTx interface {
// valid if the inputs have the authority to consume the outputs they are
// attempting to consume and the inputs consume sufficient state to produce the
// outputs.
//
// A signed tx is a wire.SignedTx envelope: the unsigned tx bytes (which carry
// the xkind discriminator at offset 0) followed by a packed list of fx
// credential envelopes. There is no codec.
type Tx struct {
Unsigned UnsignedTx `serialize:"true" json:"unsignedTx"`
Creds []*fxs.FxCredential `serialize:"true" json:"credentials"` // The credentials of this transaction
Unsigned UnsignedTx `json:"unsignedTx"`
Creds []*fxs.FxCredential `json:"credentials"` // The credentials of this transaction
TxID ids.ID `json:"id"`
bytes []byte
}
func (t *Tx) Initialize(c pcodecs.Manager) error {
signedBytes, err := c.Marshal(CodecVersion, t)
// UnsignedBytes returns the canonical native-ZAP wire bytes of an unsigned tx,
// serializing (and caching) them on first use. This is the signing target —
// every fx signature is computed over hash(unsignedBytes).
func UnsignedBytes(u UnsignedTx) ([]byte, error) {
if b := u.Bytes(); len(b) > 0 {
return b, nil
}
b, err := serializeUnsigned(u)
if err != nil {
return nil, err
}
u.SetBytes(b)
return b, nil
}
// serializeUnsigned encodes an unsigned tx to its wire bytes, dispatching on
// the concrete type. This is the write-side inverse of parseUnsigned.
func serializeUnsigned(u UnsignedTx) ([]byte, error) {
switch t := u.(type) {
case *BaseTx:
return t.serialize()
case *CreateAssetTx:
return t.serialize()
case *OperationTx:
return t.serialize()
case *ImportTx:
return t.serialize()
case *ExportTx:
return t.serialize()
default:
return nil, fmt.Errorf("xvm txs: cannot serialize unknown unsigned tx %T", u)
}
}
// signedBytesFrom wraps unsigned bytes plus fx credential envelopes into a
// wire.SignedTx buffer.
func signedBytesFrom(unsignedBytes []byte, creds []*fxs.FxCredential) ([]byte, error) {
credEnvelopes := make([][]byte, len(creds))
for i, c := range creds {
b, err := childBytes(c.Credential)
if err != nil {
return nil, fmt.Errorf("credential %d: %w", i, err)
}
credEnvelopes[i] = b
}
return wire.NewSignedTx(wire.SignedTxInput{
UnsignedBytes: unsignedBytes,
Credentials: credEnvelopes,
}), nil
}
// Initialize binds the tx's cached bytes and TxID from its Unsigned tx and
// Creds. Used for txs built fresh in-process (wallet, block builder).
func (t *Tx) Initialize() error {
unsignedBytes, err := UnsignedBytes(t.Unsigned)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
unsignedBytesLen, err := c.Size(CodecVersion, &t.Unsigned)
signedBytes, err := signedBytesFrom(unsignedBytes, t.Creds)
if err != nil {
return fmt.Errorf("couldn't calculate UnsignedTx marshal length: %w", err)
return fmt.Errorf("problem creating transaction: %w", err)
}
unsignedBytes := signedBytes[:unsignedBytesLen]
t.SetBytes(unsignedBytes, signedBytes)
return nil
}
@@ -101,28 +156,26 @@ func (t *Tx) InputIDs() set.Set[ids.ID] {
return t.Unsigned.InputIDs()
}
func (t *Tx) SignSECP256K1Fx(c pcodecs.Manager, signers [][]*secp256k1.PrivateKey) error {
unsignedBytes, err := c.Marshal(CodecVersion, &t.Unsigned)
// sign attaches credentials for the provided signer sets over the unsigned
// bytes, then binds signed bytes = unsigned ‖ creds.
func (t *Tx) sign(signers [][]*secp256k1.PrivateKey, wrap func([][secp256k1.SignatureLen]byte) verify.Verifiable) error {
unsignedBytes, err := UnsignedBytes(t.Unsigned)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
hash := hash.ComputeHash256(unsignedBytes)
h := hash.ComputeHash256(unsignedBytes)
for _, keys := range signers {
cred := &secp256k1fx.Credential{
Sigs: make([][secp256k1.SignatureLen]byte, len(keys)),
}
sigs := make([][secp256k1.SignatureLen]byte, len(keys))
for i, key := range keys {
sig, err := key.SignHash(hash)
sig, err := key.SignHash(h)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
copy(cred.Sigs[i][:], sig)
copy(sigs[i][:], sig)
}
t.Creds = append(t.Creds, &fxs.FxCredential{Credential: cred})
t.Creds = append(t.Creds, &fxs.FxCredential{Credential: wrap(sigs)})
}
signedBytes, err := c.Marshal(CodecVersion, t)
signedBytes, err := signedBytesFrom(unsignedBytes, t.Creds)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
@@ -130,60 +183,20 @@ func (t *Tx) SignSECP256K1Fx(c pcodecs.Manager, signers [][]*secp256k1.PrivateKe
return nil
}
func (t *Tx) SignPropertyFx(c pcodecs.Manager, signers [][]*secp256k1.PrivateKey) error {
unsignedBytes, err := c.Marshal(CodecVersion, &t.Unsigned)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
hash := hash.ComputeHash256(unsignedBytes)
for _, keys := range signers {
cred := &propertyfx.Credential{Credential: secp256k1fx.Credential{
Sigs: make([][secp256k1.SignatureLen]byte, len(keys)),
}}
for i, key := range keys {
sig, err := key.SignHash(hash)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
copy(cred.Sigs[i][:], sig)
}
t.Creds = append(t.Creds, &fxs.FxCredential{Credential: cred})
}
signedBytes, err := c.Marshal(CodecVersion, t)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
t.SetBytes(unsignedBytes, signedBytes)
return nil
func (t *Tx) SignSECP256K1Fx(signers [][]*secp256k1.PrivateKey) error {
return t.sign(signers, func(sigs [][secp256k1.SignatureLen]byte) verify.Verifiable {
return &secp256k1fx.Credential{Sigs: sigs}
})
}
func (t *Tx) SignNFTFx(c pcodecs.Manager, signers [][]*secp256k1.PrivateKey) error {
unsignedBytes, err := c.Marshal(CodecVersion, &t.Unsigned)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
func (t *Tx) SignPropertyFx(signers [][]*secp256k1.PrivateKey) error {
return t.sign(signers, func(sigs [][secp256k1.SignatureLen]byte) verify.Verifiable {
return &propertyfx.Credential{Credential: secp256k1fx.Credential{Sigs: sigs}}
})
}
hash := hash.ComputeHash256(unsignedBytes)
for _, keys := range signers {
cred := &nftfx.Credential{Credential: secp256k1fx.Credential{
Sigs: make([][secp256k1.SignatureLen]byte, len(keys)),
}}
for i, key := range keys {
sig, err := key.SignHash(hash)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
copy(cred.Sigs[i][:], sig)
}
t.Creds = append(t.Creds, &fxs.FxCredential{Credential: cred})
}
signedBytes, err := c.Marshal(CodecVersion, t)
if err != nil {
return fmt.Errorf("problem creating transaction: %w", err)
}
t.SetBytes(unsignedBytes, signedBytes)
return nil
func (t *Tx) SignNFTFx(signers [][]*secp256k1.PrivateKey) error {
return t.sign(signers, func(sigs [][secp256k1.SignatureLen]byte) verify.Verifiable {
return &nftfx.Credential{Credential: secp256k1fx.Credential{Sigs: sigs}}
})
}
+3 -7
View File
@@ -10,7 +10,6 @@ import (
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/math"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/timer/mockable"
lux "github.com/luxfi/utxo"
@@ -93,17 +92,14 @@ type Spender interface {
func NewSpender(
clk *mockable.Clock,
codec pcodecs.Manager,
) Spender {
return &spender{
clock: clk,
codec: codec,
}
}
type spender struct {
clock *mockable.Clock
codec pcodecs.Manager
}
func (s *spender) Spend(
@@ -239,7 +235,7 @@ func (s *spender) SpendNFT(
return nil, nil, errInsufficientFunds
}
txs.SortOperationsWithSigners(ops, keys, s.codec)
txs.SortOperationsWithSigners(ops, keys)
return ops, keys, nil
}
@@ -358,7 +354,7 @@ func (s *spender) Mint(
}
}
txs.SortOperationsWithSigners(ops, keys, s.codec)
txs.SortOperationsWithSigners(ops, keys)
return ops, keys, nil
}
@@ -426,6 +422,6 @@ func (s *spender) MintNFT(
return nil, nil, errAddressesCantMintAsset
}
txs.SortOperationsWithSigners(ops, keys, s.codec)
txs.SortOperationsWithSigners(ops, keys)
return ops, keys, nil
}
+5 -16
View File
@@ -32,7 +32,6 @@ import (
"github.com/luxfi/node/utils/json"
"github.com/luxfi/node/version"
"github.com/luxfi/node/vms/components/index"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/txs/auth"
"github.com/luxfi/node/vms/txs/mempool"
"github.com/luxfi/node/vms/xvm/block"
@@ -333,8 +332,7 @@ func (vm *VM) initialize(
return err
}
codec := vm.parser.Codec()
vm.Spender = utxo.NewSpender(&vm.clock, codec)
vm.Spender = utxo.NewSpender(&vm.clock)
state, err := state.New(
vm.db,
@@ -378,7 +376,6 @@ func (vm *VM) initialize(
Config: &vm.Config,
Fxs: vm.fxs,
TypeToFxIndex: vm.typeToFxIndex,
Codec: vm.parser.Codec(),
FeeAssetID: vm.feeAssetID,
Bootstrapped: false,
SharedMemory: vm.SharedMemory,
@@ -684,11 +681,11 @@ func (vm *VM) issueTxFromRPC(tx *txs.Tx) (ids.ID, error) {
*/
func (vm *VM) initGenesis(genesisBytes []byte) error {
genesisCodec := vm.parser.GenesisCodec()
genesis := Genesis{}
if _, err := genesisCodec.Unmarshal(genesisBytes, &genesis); err != nil {
parsed, err := parseGenesis(genesisBytes)
if err != nil {
return err
}
genesis := *parsed
stateInitialized, err := vm.state.IsInitialized()
if err != nil {
@@ -707,7 +704,7 @@ func (vm *VM) initGenesis(genesisBytes []byte) error {
tx := &txs.Tx{
Unsigned: &genesisTx.CreateAssetTx,
}
if err := tx.Initialize(genesisCodec); err != nil {
if err := tx.Initialize(); err != nil {
return err
}
@@ -1033,14 +1030,6 @@ func (vm *VM) Clock() *mockable.Clock {
return &vm.clock
}
// CodecRegistry returns the codec registry for marshalling/unmarshalling
func (vm *VM) CodecRegistry() pcodecs.Registry {
if vm.parser == nil {
return nil
}
return vm.parser.CodecRegistry()
}
// Logger returns the VM's logger
func (vm *VM) Logger() log.Logger {
return vm.log
-5
View File
@@ -70,7 +70,6 @@ func TestMerkleRootStampedAndVerified(t *testing.T) {
// Build a sibling block identical in every way EXCEPT a deliberately wrong
// root, parse it through the VM, and verify it: the executor recomputes the
// expected root and rejects the mismatch.
cm := env.vm.parser.Codec()
wrongRoot := ids.GenerateTestID()
require.NotEqual(wrongRoot, root)
tampered, err := block.NewStandardBlockWithRoot(
@@ -79,7 +78,6 @@ func TestMerkleRootStampedAndVerified(t *testing.T) {
time.Unix(int64(built.Timestamp().Unix()), 0),
wrongRoot,
built.Txs(),
cm,
)
require.NoError(err)
tamperedParsed, err := env.vm.ParseBlock(context.Background(), tampered.Bytes())
@@ -111,13 +109,11 @@ func TestMerkleRootEmptyRootRejected(t *testing.T) {
built := blkIntf.(*blkexecutor.Block)
// A sibling block identical to the built block but carrying an EMPTY root.
cm := env.vm.parser.Codec()
empty, err := block.NewStandardBlock(
built.Parent(),
built.Height(),
time.Unix(int64(built.Timestamp().Unix()), 0),
built.Txs(),
cm,
)
require.NoError(err)
require.Equal(ids.Empty, empty.MerkleRoot())
@@ -145,7 +141,6 @@ func postBlockState(t *testing.T, env *testEnv, parentID ids.ID, blkTxs []*txs.T
for _, tx := range blkTxs {
executor := &txexecutor.Executor{
Codec: env.vm.parser.Codec(),
State: diff,
Tx: tx,
Inputs: set.NewSet[ids.ID](0),
+3 -7
View File
@@ -106,17 +106,13 @@ func newGenesisBytesTest(t *testing.T) []byte {
func getCreateTxFromGenesisTest(t *testing.T, genesisBytes []byte, assetAlias string) *txs.Tx {
require := require.New(t)
c, err := newGenesisCodec()
require.NoError(err)
genesis := &Genesis{}
_, err = c.Unmarshal(genesisBytes, genesis)
genesis, err := ParseGenesisBytes(genesisBytes)
require.NoError(err)
for _, asset := range genesis.Txs {
if asset.Alias == assetAlias {
tx := &txs.Tx{Unsigned: &asset.CreateAssetTx}
require.NoError(tx.Initialize(c))
require.NoError(tx.Initialize())
return tx
}
}
@@ -405,7 +401,7 @@ func newTx(tb testing.TB, genesisBytes []byte, chainID ids.ID, parser txs.Parser
},
}}
require.NoError(
tx.SignSECP256K1Fx(parser.Codec(), [][]*secp256k1.PrivateKey{{keys[0]}}),
tx.SignSECP256K1Fx([][]*secp256k1.PrivateKey{{keys[0]}}),
)
return tx
}
+4 -5
View File
@@ -9,15 +9,15 @@ import (
"maps"
"net/http"
apitypes "github.com/luxfi/api/types"
"github.com/luxfi/container/linked"
"github.com/luxfi/formatting"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math"
apitypes "github.com/luxfi/api/types"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/node/vms/txs/mempool"
"github.com/luxfi/node/vms/xvm/txs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
@@ -269,7 +269,6 @@ func (w *WalletService) SendMultiple(_ *http.Request, args *SendMultipleArgs, re
}
}
codec := w.vm.parser.Codec()
lux.SortTransferableOutputs(outs)
tx := &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
@@ -279,7 +278,7 @@ func (w *WalletService) SendMultiple(_ *http.Request, args *SendMultipleArgs, re
Ins: ins,
Memo: memoBytes,
}}}
if err := tx.SignSECP256K1Fx(codec, keys); err != nil {
if err := tx.SignSECP256K1Fx(keys); err != nil {
return err
}