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