xvm: drop reflect from fx dispatch — closed-sum type switch + dense tag array

The fx set is a CLOSED sum type (secp256k1fx | nftfx | propertyfx), fixed at
compile time — no hot-loading, no genesis-configured fx. So the runtime
'which fx owns this value' dispatch needs no reflection: it is a total match on
the variant tag. Replaced reflect.TypeOf(val) + map[reflect.Type]int with:
  - fxKindOf(val): a Go type switch over the closed set of fx primitive types
    (compiler-checked exhaustive; lowers to a jump on the interface type tag),
    returning the value's wire.TypeKind — the same family tag the wire envelope
    already carries.
  - FxIndex: a dense [16]int array indexed by that TypeKind (one bounds-checked
    load; -1 = unregistered), filled by the SAME fx.(type) switch NewCustomParser
    already ran — no separate reflect registration.
getFx (semantic verifier + tx_init) is now fxKindOf → array index: zero reflect,
zero map-hash, compile-time-checked. Deleted registerFxTypes + all
map[reflect.Type]int fields/params (parser, block/parser, vm, backend). Node-only
(uses already-imported utxo fx types + wire.TypeKind); no dep cascade.

Full xvm suite green (11 pkgs — secp/nft/property verify dispatch exercised).
This removes the LAST reflection from the X-chain tx/verify path.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-14 10:02:52 -07:00
co-authored by Hanzo Dev
parent 3f65eb486d
commit 4d712c3798
11 changed files with 139 additions and 75 deletions
+2 -1
View File
@@ -70,7 +70,8 @@
"v1.36.5",
"v1.36.6",
"v1.36.7",
"v1.36.8"
"v1.36.8",
"v1.36.9"
],
"41": [
"v1.13.2"
+1 -1
View File
@@ -77,7 +77,7 @@ var (
const (
defaultMajor = 1
defaultMinor = 36
defaultPatch = 8
defaultPatch = 9
)
func init() {
+2 -4
View File
@@ -4,8 +4,6 @@
package block
import (
"reflect"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/node/vms/xvm/txs"
@@ -36,12 +34,12 @@ func NewParser(fxs []fxs.Fx) (Parser, error) {
}
func NewCustomParser(
typeToFxIndex map[reflect.Type]int,
fxIndex *txs.FxIndex,
clock *mockable.Clock,
log log.Logger,
fxs []fxs.Fx,
) (Parser, error) {
p, err := txs.NewCustomParser(typeToFxIndex, clock, log, fxs)
p, err := txs.NewCustomParser(fxIndex, clock, log, fxs)
if err != nil {
return nil, err
}
+3 -3
View File
@@ -130,7 +130,7 @@ func (s *Service) GetBlock(_ *http.Request, args *apitypes.GetBlockArgs, reply *
for _, tx := range block.Txs() {
err := tx.Unsigned.Visit(&txInit{
tx: tx,
typeToFxIndex: s.vm.typeToFxIndex,
fxIndex: s.vm.fxIndex,
fxs: s.vm.fxs,
})
if err != nil {
@@ -187,7 +187,7 @@ func (s *Service) GetBlockByHeight(_ *http.Request, args *apitypes.GetBlockByHei
for _, tx := range block.Txs() {
err := tx.Unsigned.Visit(&txInit{
tx: tx,
typeToFxIndex: s.vm.typeToFxIndex,
fxIndex: s.vm.fxIndex,
fxs: s.vm.fxs,
})
if err != nil {
@@ -408,7 +408,7 @@ func (s *Service) GetTx(_ *http.Request, args *apitypes.GetTxArgs, reply *apityp
if args.Encoding == formatting.JSON {
err = tx.Unsigned.Visit(&txInit{
tx: tx,
typeToFxIndex: s.vm.typeToFxIndex,
fxIndex: s.vm.fxIndex,
fxs: s.vm.fxs,
})
result = tx
+5 -8
View File
@@ -4,8 +4,6 @@
package xvm
import (
"reflect"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/node/vms/xvm/txs"
)
@@ -14,15 +12,14 @@ var _ txs.Visitor = (*txInit)(nil)
// txInit initializes FxID where required
type txInit struct {
tx *txs.Tx
typeToFxIndex map[reflect.Type]int
fxs []*fxs.ParsedFx
tx *txs.Tx
fxIndex *txs.FxIndex
fxs []*fxs.ParsedFx
}
func (t *txInit) getFx(val interface{}) (int, error) {
valType := reflect.TypeOf(val)
fx, exists := t.typeToFxIndex[valType]
if !exists {
fx, ok := t.fxIndex.GetFx(val)
if !ok {
return 0, errUnknownFx
}
return fx, nil
+2 -2
View File
@@ -5,13 +5,13 @@ package executor
import (
"context"
"reflect"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/xvm/config"
"github.com/luxfi/node/vms/xvm/fxs"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/runtime"
)
@@ -20,7 +20,7 @@ type Backend struct {
Runtime *runtime.Runtime
Config *config.Config
Fxs []*fxs.ParsedFx
TypeToFxIndex map[reflect.Type]int
FxIndex *txs.FxIndex
// Note: FeeAssetID may be different than ctx.UTXOAssetID if this XVM is
// running in a chain.
FeeAssetID ids.ID
+2 -4
View File
@@ -6,7 +6,6 @@ package executor
import (
"errors"
"fmt"
"reflect"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
@@ -246,9 +245,8 @@ func (v *SemanticVerifier) verifyFxUsage(
}
func (v *SemanticVerifier) getFx(val interface{}) (int, error) {
valType := reflect.TypeOf(val)
fx, exists := v.TypeToFxIndex[valType]
if !exists {
fx, ok := v.FxIndex.GetFx(val)
if !ok {
return 0, errUnknownFx
}
return fx, nil
+12 -13
View File
@@ -5,7 +5,6 @@ package executor
import (
"context"
"reflect"
"testing"
"github.com/luxfi/mock/gomock"
@@ -50,10 +49,10 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
ctx := context.Background()
cChainID := ids.GenerateTestID()
typeToFxIndex := make(map[reflect.Type]int)
fxIndex := txs.NewFxIndex()
secpFx := &secp256k1fx.Fx{}
_, err := txs.NewCustomParser(
typeToFxIndex,
fxIndex,
new(mockable.Clock),
log.NoLog{},
[]fxs.Fx{
@@ -107,7 +106,7 @@ func TestSemanticVerifierBaseTx(t *testing.T) {
Fx: secpFx,
},
},
TypeToFxIndex: typeToFxIndex,
FxIndex: fxIndex,
FeeAssetID: ids.GenerateTestID(),
Bootstrapped: true,
}
@@ -404,10 +403,10 @@ func TestSemanticVerifierExportTx(t *testing.T) {
cChainID := ids.GenerateTestID()
chainID := ids.GenerateTestID()
typeToFxIndex := make(map[reflect.Type]int)
fxIndex := txs.NewFxIndex()
secpFx := &secp256k1fx.Fx{}
_, err := txs.NewCustomParser(
typeToFxIndex,
fxIndex,
new(mockable.Clock),
log.NoLog{},
[]fxs.Fx{
@@ -466,7 +465,7 @@ func TestSemanticVerifierExportTx(t *testing.T) {
Fx: secpFx,
},
},
TypeToFxIndex: typeToFxIndex,
FxIndex: fxIndex,
FeeAssetID: ids.GenerateTestID(),
Bootstrapped: true,
}
@@ -834,10 +833,10 @@ func TestSemanticVerifierExportTxDifferentNet(t *testing.T) {
},
}
typeToFxIndex := make(map[reflect.Type]int)
fxIndex := txs.NewFxIndex()
secpFx := &secp256k1fx.Fx{}
_, err := txs.NewCustomParser(
typeToFxIndex,
fxIndex,
new(mockable.Clock),
log.NoLog{},
[]fxs.Fx{
@@ -891,7 +890,7 @@ func TestSemanticVerifierExportTxDifferentNet(t *testing.T) {
Fx: secpFx,
},
},
TypeToFxIndex: typeToFxIndex,
FxIndex: fxIndex,
FeeAssetID: ids.GenerateTestID(),
Bootstrapped: true,
}
@@ -951,10 +950,10 @@ func TestSemanticVerifierImportTx(t *testing.T) {
ctx := context.Background() // Use standard context for Backend
m := atomic.NewMemory(prefixdb.New([]byte{0}, memdb.New()))
typeToFxIndex := make(map[reflect.Type]int)
fxIndex := txs.NewFxIndex()
fx := &secp256k1fx.Fx{}
_, err := txs.NewCustomParser(
typeToFxIndex,
fxIndex,
new(mockable.Clock),
log.NoLog{},
[]fxs.Fx{
@@ -1030,7 +1029,7 @@ func TestSemanticVerifierImportTx(t *testing.T) {
Fx: fx,
},
},
TypeToFxIndex: typeToFxIndex,
FxIndex: fxIndex,
FeeAssetID: ids.GenerateTestID(),
Bootstrapped: true,
SharedMemory: &testSharedMemory{sm: m.NewSharedMemory(chainID)},
+97
View File
@@ -0,0 +1,97 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/wire"
)
// fx dispatch — reflection-free. The set of fx primitives is a CLOSED sum type
// (a finite coproduct of statically-known variants: secp256k1fx | nftfx |
// propertyfx). Dispatch is therefore a total match on the variant tag — Go's
// type switch, which the compiler lowers to a jump on the interface type tag,
// exactly as an ADT `case` compiles. The tag then indexes a dense array. No
// reflect.TypeOf, no map[reflect.Type]int: the mechanism is compile-time-typed,
// the lookup is a single indexed load.
// numTypeKinds bounds the dense fx-family enum (wire.TypeKind values 0x00..0x09,
// rounded up). FxIndex indexes by that byte directly.
const numTypeKinds = 16
// FxIndex maps a wire.TypeKind (fx family) to its position in a parser's fx
// list. Array-backed: Get is one bounds-checked load, no hashing. A slot of -1
// means no fx of that family is registered.
type FxIndex struct {
byKind [numTypeKinds]int
}
// NewFxIndex returns an FxIndex with every family unregistered (-1).
func NewFxIndex() *FxIndex {
fi := &FxIndex{}
for i := range fi.byKind {
fi.byKind[i] = -1
}
return fi
}
// Set records that the fx family `kind` lives at list position `idx`.
func (fi *FxIndex) Set(kind wire.TypeKind, idx int) {
if int(kind) < numTypeKinds {
fi.byKind[kind] = idx
}
}
// Get returns the fx-list index for `kind`, or (0, false) when unregistered.
func (fi *FxIndex) Get(kind wire.TypeKind) (int, bool) {
if int(kind) >= numTypeKinds {
return 0, false
}
idx := fi.byKind[kind]
if idx < 0 {
return 0, false
}
return idx, true
}
// GetFx maps a parsed fx value to its fx-list index — the reflection-free
// replacement for `reflect.TypeOf(val)` + `map[reflect.Type]int`. It reads the
// value's family via the compile-time-checked fxKindOf match, then indexes.
func (fi *FxIndex) GetFx(val interface{}) (int, bool) {
kind, ok := fxKindOf(val)
if !ok {
return 0, false
}
return fi.Get(kind)
}
// fxKindOf returns the fx family that owns a parsed fx primitive value — a
// total match over the closed set of fx output/input/credential/operation
// types. Adding a new (fx, shape) is a new case here; the compiler checks the
// types are real. No reflection.
func fxKindOf(val interface{}) (wire.TypeKind, bool) {
switch val.(type) {
case *secp256k1fx.TransferInput,
*secp256k1fx.TransferOutput,
*secp256k1fx.MintOutput,
*secp256k1fx.MintOperation,
*secp256k1fx.Credential:
return wire.TypeKindSecp256k1, true
case *nftfx.MintOutput,
*nftfx.TransferOutput,
*nftfx.MintOperation,
*nftfx.TransferOperation,
*nftfx.Credential:
return wire.TypeKindNFT, true
case *propertyfx.MintOutput,
*propertyfx.OwnedOutput,
*propertyfx.MintOperation,
*propertyfx.BurnOperation,
*propertyfx.Credential:
return wire.TypeKindProperty, true
}
return 0, false
}
+9 -34
View File
@@ -10,7 +10,6 @@ package txs
import (
"fmt"
"reflect"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/log"
@@ -39,7 +38,7 @@ type parser struct {
func NewParser(fxs []fxs.Fx) (Parser, error) {
return NewCustomParser(
make(map[reflect.Type]int),
NewFxIndex(),
&mockable.Clock{},
log.Noop(),
fxs,
@@ -47,7 +46,7 @@ func NewParser(fxs []fxs.Fx) (Parser, error) {
}
func NewCustomParser(
typeToFxIndex map[reflect.Type]int,
fxIndex *FxIndex,
clock *mockable.Clock,
logger log.Logger,
fxList []fxs.Fx,
@@ -57,46 +56,22 @@ func NewCustomParser(
if err := fx.Initialize(vm); err != nil {
return nil, err
}
// 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.
// Record the fx family -> list-position mapping the semantic verifier's
// getFx and tx_init consult. Keyed by wire.TypeKind (the family tag),
// filled by the SAME closed-set match used everywhere else — no
// reflect.TypeOf, no map[reflect.Type]int.
switch fx.(type) {
case *secp256k1fx.Fx:
registerFxTypes(typeToFxIndex, i,
&secp256k1fx.TransferInput{},
&secp256k1fx.MintOutput{},
&secp256k1fx.TransferOutput{},
&secp256k1fx.MintOperation{},
&secp256k1fx.Credential{},
)
fxIndex.Set(wire.TypeKindSecp256k1, i)
case *nftfx.Fx:
registerFxTypes(typeToFxIndex, i,
&nftfx.MintOutput{},
&nftfx.TransferOutput{},
&nftfx.MintOperation{},
&nftfx.TransferOperation{},
&nftfx.Credential{},
)
fxIndex.Set(wire.TypeKindNFT, i)
case *propertyfx.Fx:
registerFxTypes(typeToFxIndex, i,
&propertyfx.MintOutput{},
&propertyfx.OwnedOutput{},
&propertyfx.MintOperation{},
&propertyfx.BurnOperation{},
&propertyfx.Credential{},
)
fxIndex.Set(wire.TypeKindProperty, i)
}
}
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).
+4 -5
View File
@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net/http"
"reflect"
"sync"
"time"
@@ -147,7 +146,7 @@ type VM struct {
baseDB database.Database
db *versiondb.Database
typeToFxIndex map[reflect.Type]int
fxIndex *txs.FxIndex
fxs []*extensions.ParsedFx
walletService WalletService
@@ -321,9 +320,9 @@ func (vm *VM) initialize(
}
}
vm.typeToFxIndex = map[reflect.Type]int{}
vm.fxIndex = txs.NewFxIndex()
vm.parser, err = block.NewCustomParser(
vm.typeToFxIndex,
vm.fxIndex,
&vm.clock,
vm.log,
typedFxs,
@@ -375,7 +374,7 @@ func (vm *VM) initialize(
Runtime: vm.consensusRuntime,
Config: &vm.Config,
Fxs: vm.fxs,
TypeToFxIndex: vm.typeToFxIndex,
FxIndex: vm.fxIndex,
FeeAssetID: vm.feeAssetID,
Bootstrapped: false,
SharedMemory: vm.SharedMemory,