mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.
81 files migrated. LLM.md captures the rule + v1->v2 delta table.
Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
switch to string-form Duration or carry an explicit option. v2 root does not
re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
surface this.
All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go (DA blob/cert storage as JSON)
- vms/platformvm/airdrop (airdrop claims as JSON in db)
- vms/chainadapter/appchain (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go (KMS HTTP client — external technically, leave)
- utils/{bimap,ips} (small marshaler shims — low priority)
411 lines
11 KiB
Go
411 lines
11 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package xvm
|
|
|
|
import (
|
|
"context"
|
|
"github.com/go-json-experiment/json"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/address"
|
|
"github.com/luxfi/vm"
|
|
"github.com/luxfi/runtime"
|
|
"github.com/luxfi/consensus/core/choices"
|
|
validators "github.com/luxfi/validators"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/vm/chains/atomic"
|
|
"github.com/luxfi/node/upgrade"
|
|
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/node/vms/xvm/txs"
|
|
"github.com/luxfi/node/vms/xvm/txs/txstest"
|
|
"github.com/luxfi/utxo/nftfx"
|
|
"github.com/luxfi/utxo/propertyfx"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
)
|
|
|
|
// Test keys for use in tests
|
|
var keys = secp256k1.TestKeys()
|
|
|
|
// All upgrades are active at genesis under activate-all-implicitly.
|
|
var activeUpgrade = upgrade.Default
|
|
|
|
// Test constants
|
|
const (
|
|
// Increased from 50000 to 10*constants.Lux to match larger genesis allocation
|
|
// This ensures tests have enough funds for sequential transactions
|
|
startBalance uint64 = 10 * constants.Lux // 10 LUX = 10,000,000,000 nanoLux
|
|
testTxFee uint64 = 1000
|
|
)
|
|
|
|
var assetID = ids.GenerateTestID()
|
|
|
|
// envConfig configures the test environment
|
|
type envConfig struct {
|
|
fork upgrade.Config
|
|
notLinearized bool
|
|
additionalFxs []interface{}
|
|
indexTransactions bool // Enable transaction indexing
|
|
}
|
|
|
|
// testEnv is the test environment
|
|
type testEnv struct {
|
|
vm *VM
|
|
consensusRuntime *runtime.Runtime
|
|
genesisBytes []byte
|
|
genesisTx *txs.Tx
|
|
testLock *sync.Mutex
|
|
txBuilder *txstest.Builder
|
|
sharedMemory *atomic.Memory
|
|
}
|
|
|
|
// newGenesisBytesTest creates test genesis bytes
|
|
func newGenesisBytesTest(t *testing.T) []byte {
|
|
require := require.New(t)
|
|
|
|
// Format address properly as Bech32
|
|
addr, err := address.FormatBech32(constants.GetHRP(constants.UnitTestID), keys[0].PublicKey().Address().Bytes())
|
|
require.NoError(err)
|
|
|
|
// Create a simple genesis with one asset (LUX)
|
|
// Increased from 100 LUX to 1000 LUX to ensure sufficient funds for multiple transactions in tests
|
|
genesisData := map[string]GenesisAssetDefinition{
|
|
"LUX": {
|
|
Name: "Lux",
|
|
Symbol: "LUX",
|
|
Denomination: 9,
|
|
InitialState: AssetInitialState{
|
|
FixedCap: []GenesisHolder{
|
|
{
|
|
Amount: 1000 * constants.Lux,
|
|
Address: addr,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
genesis, err := NewGenesis(constants.UnitTestID, genesisData)
|
|
require.NoError(err)
|
|
|
|
genesisBytes, err := genesis.Bytes()
|
|
require.NoError(err)
|
|
|
|
return genesisBytes
|
|
}
|
|
|
|
// getCreateTxFromGenesisTest extracts a create asset tx from genesis
|
|
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)
|
|
require.NoError(err)
|
|
|
|
for _, asset := range genesis.Txs {
|
|
if asset.Alias == assetAlias {
|
|
tx := &txs.Tx{Unsigned: &asset.CreateAssetTx}
|
|
require.NoError(tx.Initialize(c))
|
|
return tx
|
|
}
|
|
}
|
|
|
|
require.FailNow("asset not found in genesis", assetAlias)
|
|
return nil
|
|
}
|
|
|
|
// mockValidatorState is a simple validator state for tests
|
|
type mockValidatorState struct {
|
|
chainID ids.ID
|
|
}
|
|
|
|
var _ validators.State = (*mockValidatorState)(nil)
|
|
|
|
func (m *mockValidatorState) GetChainID(ids.ID) (ids.ID, error) {
|
|
return m.chainID, nil
|
|
}
|
|
|
|
func (m *mockValidatorState) GetNetworkID(ids.ID) (ids.ID, error) {
|
|
return m.chainID, nil
|
|
}
|
|
|
|
func (m *mockValidatorState) GetMinimumHeight(context.Context) (uint64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (m *mockValidatorState) GetCurrentHeight(context.Context) (uint64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (m *mockValidatorState) GetValidatorSet(ctx context.Context, height uint64, netID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
|
// Return a simple validator set with the test node
|
|
nodeID := ids.GenerateTestNodeID()
|
|
return map[ids.NodeID]*validators.GetValidatorOutput{
|
|
nodeID: {
|
|
NodeID: nodeID,
|
|
Weight: 1000,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockValidatorState) GetCurrentValidators(ctx context.Context, height uint64, netID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
|
return m.GetValidatorSet(ctx, height, netID)
|
|
}
|
|
|
|
func (m *mockValidatorState) GetWarpValidatorSet(ctx context.Context, height uint64, netID ids.ID) (*validators.WarpSet, error) {
|
|
return &validators.WarpSet{
|
|
Height: height,
|
|
Validators: make(map[ids.NodeID]*validators.WarpValidator),
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockValidatorState) GetWarpValidatorSets(ctx context.Context, heights []uint64, netIDs []ids.ID) (map[ids.ID]map[uint64]*validators.WarpSet, error) {
|
|
result := make(map[ids.ID]map[uint64]*validators.WarpSet)
|
|
for _, netID := range netIDs {
|
|
result[netID] = make(map[uint64]*validators.WarpSet)
|
|
for _, height := range heights {
|
|
result[netID][height] = &validators.WarpSet{
|
|
Height: height,
|
|
Validators: make(map[ids.NodeID]*validators.WarpValidator),
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// testSharedMemory wraps atomic.SharedMemory to match VM's SharedMemory interface
|
|
type testSharedMemory struct {
|
|
mem atomic.SharedMemory
|
|
}
|
|
|
|
func (t *testSharedMemory) Get(peerChainID ids.ID, keys [][]byte) ([][]byte, error) {
|
|
return t.mem.Get(peerChainID, keys)
|
|
}
|
|
|
|
func (t *testSharedMemory) Apply(requests map[ids.ID]interface{}, _ ...interface{}) error {
|
|
// Convert interface{} map to *atomic.Requests map
|
|
atomicRequests := make(map[ids.ID]*atomic.Requests)
|
|
for chainID, req := range requests {
|
|
if atomicReq, ok := req.(*atomic.Requests); ok {
|
|
atomicRequests[chainID] = atomicReq
|
|
}
|
|
}
|
|
return t.mem.Apply(atomicRequests)
|
|
}
|
|
|
|
// setup creates a test environment
|
|
func setup(t testing.TB, config *envConfig) *testEnv {
|
|
require := require.New(t)
|
|
|
|
if config == nil {
|
|
config = &envConfig{
|
|
fork: upgrade.Default,
|
|
}
|
|
}
|
|
|
|
chainID := ids.GenerateTestID()
|
|
rt := &runtime.Runtime{
|
|
NetworkID: constants.UnitTestID,
|
|
ChainID: chainID,
|
|
XChainID: ids.GenerateTestID(),
|
|
CChainID: ids.GenerateTestID(),
|
|
NodeID: ids.GenerateTestNodeID(),
|
|
ValidatorState: &mockValidatorState{chainID: chainID},
|
|
}
|
|
|
|
baseDB := memdb.New()
|
|
sharedMemory := atomic.NewMemory(memdb.New())
|
|
|
|
vmImpl := &VM{}
|
|
genesisBytes := newGenesisBytesTest(t.(*testing.T))
|
|
// Create shared memory wrapper that matches VM's interface
|
|
atomicMem := sharedMemory.NewSharedMemory(rt.ChainID)
|
|
vmImpl.SharedMemory = &testSharedMemory{mem: atomicMem}
|
|
|
|
testLock := &sync.Mutex{}
|
|
testLock.Lock()
|
|
|
|
// Create a mock Sender
|
|
appSender := &noOpSender{}
|
|
|
|
// ALWAYS include secp256k1fx first (required for genesis parsing)
|
|
// Then add additional Fxs if provided, or default to nftfx and propertyfx
|
|
fxs := []interface{}{
|
|
&vm.Fx{
|
|
ID: secp256k1fx.ID,
|
|
Fx: &secp256k1fx.Fx{},
|
|
},
|
|
}
|
|
|
|
if len(config.additionalFxs) == 0 {
|
|
// No additional Fxs specified - add default nftfx and propertyfx
|
|
fxs = append(fxs,
|
|
&vm.Fx{
|
|
ID: nftfx.ID,
|
|
Fx: &nftfx.Fx{},
|
|
},
|
|
&vm.Fx{
|
|
ID: propertyfx.ID,
|
|
Fx: &propertyfx.Fx{},
|
|
},
|
|
)
|
|
} else {
|
|
// Additional Fxs specified - append them after secp256k1fx
|
|
fxs = append(fxs, config.additionalFxs...)
|
|
}
|
|
|
|
// Create config for VM with optional indexing
|
|
vmConfig := DefaultConfig
|
|
if config.indexTransactions {
|
|
vmConfig.IndexTransactions = true
|
|
}
|
|
configBytes, err := json.Marshal(vmConfig)
|
|
require.NoError(err)
|
|
|
|
toEngine := make(chan vm.Message, 1)
|
|
require.NoError(vmImpl.Initialize(
|
|
context.Background(),
|
|
vm.Init{
|
|
Runtime: rt,
|
|
DB: baseDB,
|
|
Genesis: genesisBytes,
|
|
Upgrade: nil,
|
|
Config: configBytes,
|
|
ToEngine: toEngine,
|
|
Fx: fxs,
|
|
Sender: appSender,
|
|
},
|
|
))
|
|
|
|
// Get the genesis transaction
|
|
genesisTx := getCreateTxFromGenesisTest(t.(*testing.T), genesisBytes, "LUX")
|
|
|
|
// Create transaction builder with SharedMemory
|
|
atomicMemForBuilder := sharedMemory.NewSharedMemory(rt.ChainID)
|
|
txBuilder := txstest.New(
|
|
context.Background(),
|
|
&vmImpl.Config,
|
|
vmImpl.feeAssetID,
|
|
vmImpl.state,
|
|
atomicMemForBuilder,
|
|
)
|
|
|
|
// Set the context IDs from the Runtime
|
|
txBuilder.SetContextIDs(rt.NetworkID, rt.ChainID)
|
|
|
|
env := &testEnv{
|
|
vm: vmImpl,
|
|
consensusRuntime: rt,
|
|
genesisBytes: genesisBytes,
|
|
genesisTx: genesisTx,
|
|
testLock: testLock,
|
|
txBuilder: txBuilder,
|
|
sharedMemory: sharedMemory,
|
|
}
|
|
|
|
// Register cleanup to prevent goroutine leaks
|
|
// This ensures PushGossip and PullGossip goroutines are properly terminated
|
|
t.Cleanup(func() {
|
|
// Shutdown the VM to cancel onShutdownCtx and stop gossip goroutines
|
|
_ = vmImpl.Shutdown()
|
|
})
|
|
|
|
// Linearize the DAG to initialize the network
|
|
// This simulates what happens during normal VM bootstrap
|
|
if !config.notLinearized {
|
|
// Use the genesis transaction ID as the stop vertex
|
|
stopVertexID := genesisTx.ID()
|
|
toEngineChan := make(chan vm.Message, 1)
|
|
require.NoError(vmImpl.Linearize(context.Background(), stopVertexID, toEngineChan))
|
|
|
|
// Mark the backend as bootstrapped so tests can issue transactions
|
|
vmImpl.txBackend.Bootstrapped = true
|
|
}
|
|
|
|
// Lock the VM so tests can unlock it when ready
|
|
vmImpl.Lock.Lock()
|
|
|
|
return env
|
|
}
|
|
|
|
// issueAndAccept issues and accepts a transaction
|
|
func issueAndAccept(require *require.Assertions, vm *VM, tx *txs.Tx) {
|
|
// Issue the transaction to the network
|
|
require.NoError(vm.network.IssueTxFromRPC(tx))
|
|
|
|
// Build a block containing the transaction
|
|
blkIntf, err := vm.BuildBlock(context.Background())
|
|
require.NoError(err)
|
|
|
|
// Verify the block
|
|
require.NoError(blkIntf.Verify(context.Background()))
|
|
|
|
// Accept the block
|
|
require.NoError(blkIntf.Accept(context.Background()))
|
|
|
|
// Update the preferred block (normally done by consensus engine)
|
|
require.NoError(vm.SetPreference(context.Background(), blkIntf.ID()))
|
|
|
|
// Commit the versiondb so indexed data is visible
|
|
require.NoError(vm.db.Commit())
|
|
|
|
// Verify the block status is accepted
|
|
require.Equal(uint8(choices.Accepted), uint8(blkIntf.Status()))
|
|
}
|
|
|
|
// newTx creates a simple test transaction
|
|
func newTx(tb testing.TB, genesisBytes []byte, chainID ids.ID, parser txs.Parser, assetName string) *txs.Tx {
|
|
require := require.New(tb)
|
|
|
|
createTx := getCreateTxFromGenesisTest(tb.(*testing.T), genesisBytes, assetName)
|
|
// Genesis creates 1000 LUX for keys[0]
|
|
// This tx spends the entire UTXO and creates a change output back to keys[0]
|
|
// Must account for transaction fee (testTxFee = 1000 nanoLux)
|
|
inputAmt := uint64(1000 * constants.Lux)
|
|
outputAmt := inputAmt - testTxFee // Deduct fee from output
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.BaseTx{
|
|
BaseTx: lux.BaseTx{
|
|
NetworkID: constants.UnitTestID,
|
|
BlockchainID: chainID,
|
|
Ins: []*lux.TransferableInput{{
|
|
UTXOID: lux.UTXOID{
|
|
TxID: createTx.ID(),
|
|
OutputIndex: 0, // First output (fixed cap holder output)
|
|
},
|
|
Asset: lux.Asset{ID: createTx.ID()},
|
|
In: &secp256k1fx.TransferInput{
|
|
Amt: inputAmt, // Must match UTXO amount
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: []uint32{0},
|
|
},
|
|
},
|
|
}},
|
|
Outs: []*lux.TransferableOutput{{
|
|
Asset: lux.Asset{ID: createTx.ID()},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: outputAmt, // Output amount after fee deduction
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{keys[0].PublicKey().Address()},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
}}
|
|
require.NoError(
|
|
tx.SignSECP256K1Fx(parser.Codec(), [][]*secp256k1.PrivateKey{{keys[0]}}),
|
|
)
|
|
return tx
|
|
}
|