mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
genesis,config,xvm: derive X-Chain asset ID from genesis content
config.UTXOAssetIDFor(networkID) returns a network-id-keyed constant
that's identical across every L1 sharing a primary-network ID. On
sovereign L1s (<tenant> / MLC / VCC / any future tenant) the X-Chain
genesis bakes a different asset (different validator set, different
holder set, different denomination) so the runtime asset ID — the ID
vm.initGenesis assigns to the first GenesisAsset.CreateAssetTx — does
not match the constant.
Every wallet builder that calls platform.getStakingAssetID to populate
pCTX.XAssetID then pays tx fees from UTXOs under an asset the chain
doesn't recognise:
insufficient funds: needs 398 more nLUX (<constant>)
That's the bootstrap failure on <tenant> devnet / testnet / mainnet
after the chainset-via-embedded-genesis fix landed.
Fix: derive the X-Chain native asset ID from the actual genesis bytes
the binary loads at startup. Every load path now goes through
resolveXAssetID(networkID, genesisBytes):
- genesis baked with X-Chain → parse the embedded XVM genesis,
initialize the first GenesisAsset.CreateAssetTx, return its tx.ID()
(the same value vm.initGenesis assigns at runtime).
- genesis is P-only (no X-Chain) → fall back to UTXOAssetIDFor.
Value is unused at runtime, kept for downstream-consumer shape.
- genesis is malformed → error (was previously silently returning
the wrong constant; that's how sovereign L1s ended up shipping
binaries that disagreed with their own chain).
Touched:
- vms/xvm/genesis.go: ParseGenesisBytes + AssetIDFromGenesisBytes.
- vms/xvm/genesis/lux.go: AssetIDFromBytes proxy so the wrapper
package stays the single dependency point.
- genesis/builder/builder.go: FromConfig uses the genesis-derived
ID instead of UTXOAssetIDFor; new XAssetIDFromGenesisBytes
helper for the platform-genesis-bytes path.
- config/config.go: getGenesisData's raw and cached paths now call
resolveXAssetID. FromConfig path inherits the fix automatically.
Tests:
- vms/xvm/genesis/lux_test.go: AssetIDFromBytes is stable, holder-
sensitive, network-id-sensitive, malformed-input-rejecting.
- genesis/builder/builder_p_only_test.go: helper agrees with
FromConfig on sovereign genesis, returns ok=false on P-only,
errors on garbage.
- config/config_test.go: resolveXAssetID returns the genesis-derived
ID for sovereign genesis, UTXOAssetIDFor on P-only, error on
garbage.
No network-id allow-list, no per-network branching, no env-var
overrides. The fix is in the binary's load path; operator workflows
stay byte-identical.
This commit is contained in:
+40
-4
@@ -1052,9 +1052,10 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
|
||||
if err != nil {
|
||||
return nil, ids.Empty, fmt.Errorf("failed to decode %s: %w", GenesisRawBytesKey, err)
|
||||
}
|
||||
// LUX asset ID is the network-scoped constant — same regardless of
|
||||
// what's in genesisBytes, so no need to parse them.
|
||||
xAssetID := constants.UTXOAssetIDFor(networkID)
|
||||
xAssetID, err := resolveXAssetID(networkID, genesisBytes)
|
||||
if err != nil {
|
||||
return nil, ids.Empty, fmt.Errorf("resolve X-Chain asset ID from %s: %w", GenesisRawBytesKey, err)
|
||||
}
|
||||
log.Info("loaded raw genesis bytes directly",
|
||||
"size", len(genesisBytes),
|
||||
"xAssetID", xAssetID,
|
||||
@@ -1090,11 +1091,16 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
|
||||
// Check if we have cached genesis bytes to avoid rebuilding
|
||||
cacheFile := filepath.Join(dataDir, "genesis.bytes")
|
||||
if cachedBytes, err := loadCachedGenesisBytes(cacheFile); err == nil && len(cachedBytes) > 0 {
|
||||
xAssetID, err := resolveXAssetID(networkID, cachedBytes)
|
||||
if err != nil {
|
||||
return nil, ids.Empty, fmt.Errorf("resolve X-Chain asset ID from cached genesis: %w", err)
|
||||
}
|
||||
log.Info("loaded cached genesis bytes for hash stability",
|
||||
"cacheFile", cacheFile,
|
||||
"size", len(cachedBytes),
|
||||
"xAssetID", xAssetID,
|
||||
)
|
||||
return cachedBytes, constants.UTXOAssetIDFor(networkID), nil
|
||||
return cachedBytes, xAssetID, nil
|
||||
}
|
||||
// No cache or invalid cache - build from file and cache the result
|
||||
genesisBytes, xAssetID, err := builder.FromFile(networkID, genesisFileName, stakingCfg)
|
||||
@@ -1120,6 +1126,36 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
|
||||
return builder.FromConfig(config)
|
||||
}
|
||||
|
||||
// resolveXAssetID extracts the X-Chain native asset ID from the loaded
|
||||
// platform-genesis blob. It is the canonical source of truth used by
|
||||
// every load path that bypasses FromConfig (raw bytes, cached bytes —
|
||||
// the paths that historically defaulted to constants.UTXOAssetIDFor and
|
||||
// silently disagreed with the genesis content on sovereign L1s).
|
||||
//
|
||||
// Behaviour:
|
||||
//
|
||||
// - If the genesis bakes an X-Chain, returns the runtime asset ID
|
||||
// derived from that chain's CreateAssetTx (the same value
|
||||
// vm.initGenesis assigns at runtime).
|
||||
// - If the genesis is P-only (no X-Chain), returns the network-id-
|
||||
// keyed constant. The asset ID is unused in that mode.
|
||||
// - If the genesis is unparseable or the embedded X-Chain genesis
|
||||
// is malformed, returns the corresponding error — these are
|
||||
// unrecoverable on a primary-network bootstrap.
|
||||
//
|
||||
// Tested against both sovereign-network genesis (X-Chain present, asset
|
||||
// ID differs from the constant) and upstream Lux genesis fixtures.
|
||||
func resolveXAssetID(networkID uint32, genesisBytes []byte) (ids.ID, error) {
|
||||
id, ok, err := builder.XAssetIDFromGenesisBytes(genesisBytes)
|
||||
if err != nil {
|
||||
return ids.Empty, err
|
||||
}
|
||||
if !ok {
|
||||
return constants.UTXOAssetIDFor(networkID), nil
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func getTrackedChains(v *viper.Viper) (set.Set[ids.ID], error) {
|
||||
trackChainsStr := v.GetString(TrackChainsKey)
|
||||
|
||||
|
||||
@@ -16,10 +16,14 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/chains"
|
||||
"github.com/luxfi/node/genesis/builder"
|
||||
"github.com/luxfi/node/nets"
|
||||
pchaingenesis "github.com/luxfi/node/vms/platformvm/genesis"
|
||||
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
|
||||
)
|
||||
|
||||
const chainConfigFilenameExtension = ".ex"
|
||||
@@ -672,3 +676,57 @@ func TestDevModeFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveXAssetID_FromSovereignGenesis covers the canonical
|
||||
// behaviour the sovereign-L1 fix relies on: when the loaded platform
|
||||
// genesis bakes an X-Chain, resolveXAssetID returns the runtime asset
|
||||
// ID encoded IN the genesis (matches what FromConfig produces, matches
|
||||
// what the running X-Chain reports via platform.getStakingAssetID).
|
||||
//
|
||||
// Critically: NOT constants.UTXOAssetIDFor(networkID). That value is
|
||||
// network-id-keyed and would silently collide between two sovereign L1s
|
||||
// sharing a primary-network ID.
|
||||
func TestResolveXAssetID_FromSovereignGenesis(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
cfg := builder.GetConfig(constants.LocalID)
|
||||
require.NotNil(cfg)
|
||||
require.NotEmpty(cfg.XChainGenesis, "fixture must bake X-Chain")
|
||||
|
||||
genesisBytes, expectedID, err := builder.FromConfig(cfg)
|
||||
require.NoError(err)
|
||||
require.NotEqual(ids.Empty, expectedID)
|
||||
|
||||
gotID, err := resolveXAssetID(constants.LocalID, genesisBytes)
|
||||
require.NoError(err)
|
||||
require.Equal(expectedID, gotID,
|
||||
"resolveXAssetID must agree with FromConfig on the genesis-derived asset ID")
|
||||
}
|
||||
|
||||
// TestResolveXAssetID_POnlyFallback verifies that when the platform
|
||||
// genesis bakes no X-Chain (P-only mode), resolveXAssetID falls back
|
||||
// to constants.UTXOAssetIDFor(networkID). That value is unused at
|
||||
// runtime (no X-Chain to mint on) but keeps the existing nodeConfig
|
||||
// shape consistent for downstream consumers.
|
||||
func TestResolveXAssetID_POnlyFallback(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
pOnly := &pchaingenesis.Genesis{Chains: nil}
|
||||
pOnlyBytes, err := pchaingenesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
|
||||
require.NoError(err)
|
||||
|
||||
gotID, err := resolveXAssetID(42, pOnlyBytes)
|
||||
require.NoError(err)
|
||||
require.Equal(constants.UTXOAssetIDFor(42), gotID,
|
||||
"P-only must fall through to UTXOAssetIDFor(networkID)")
|
||||
}
|
||||
|
||||
// TestResolveXAssetID_Malformed asserts that bad genesis bytes surface
|
||||
// an error rather than silently returning ids.Empty (which would
|
||||
// reintroduce the UTXOAssetIDFor fallback and defeat the fix on
|
||||
// sovereign L1s where the fallback value is wrong).
|
||||
func TestResolveXAssetID_Malformed(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
_, err := resolveXAssetID(1, []byte{0xff, 0xfe, 0xfd, 0xfc})
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
@@ -386,13 +386,31 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// LUX asset ID is a network-scoped constant (constants.UTXOAssetIDFor),
|
||||
// not derived from X-Chain genesis bytes. Domain-separated by networkID
|
||||
// so mainnet/testnet/devnet/local each have a distinct asset ID — this
|
||||
// prevents cross-network UTXO accounting collapse in wallets/indexers
|
||||
// that key balance by AssetID alone. Mainnet (networkID=1) returns the
|
||||
// legacy UTXO_ASSET_ID literal to preserve existing on-chain state.
|
||||
xAssetID := constants.UTXOAssetIDFor(config.NetworkID)
|
||||
// X-Chain native asset ID is derived from the actual XVM genesis bytes
|
||||
// (the runtime ID of the first GenesisAsset.CreateAssetTx) so the value
|
||||
// the wallet builder reads back via platform.getStakingAssetID matches
|
||||
// the asset the X-Chain genuinely mints. On sovereign L1s (Liquidity,
|
||||
// MLC, VCC, future tenants) every primary network has its own X-Chain
|
||||
// genesis content (different validator set, different initial holders,
|
||||
// different denomination/name) so the genesis-derived asset ID is
|
||||
// distinct from any constants.UTXOAssetIDFor(networkID) value — those
|
||||
// are network-id-keyed constants and identical across every L1 sharing
|
||||
// the same primary-network ID, which would let two sovereign networks
|
||||
// collide.
|
||||
//
|
||||
// When X-Chain is opt-out (P-only mode, xvmGenesisBytes is nil) we keep
|
||||
// the network-id-keyed constant as a placeholder; the asset ID is
|
||||
// irrelevant in that mode since there is no X-Chain to mint on.
|
||||
var xAssetID ids.ID
|
||||
if len(xvmGenesisBytes) > 0 {
|
||||
var err error
|
||||
xAssetID, err = xvmgenesis.AssetIDFromBytes(xvmGenesisBytes)
|
||||
if err != nil {
|
||||
return nil, ids.Empty, fmt.Errorf("derive X-Chain asset ID from genesis: %w", err)
|
||||
}
|
||||
} else {
|
||||
xAssetID = constants.UTXOAssetIDFor(config.NetworkID)
|
||||
}
|
||||
|
||||
genesisTime := time.Unix(int64(config.StartTime), 0)
|
||||
|
||||
@@ -651,6 +669,54 @@ func FromDatabase(networkID uint32, dbPath string, dbType string, stakingCfg *St
|
||||
return FromConfig(config)
|
||||
}
|
||||
|
||||
// XAssetIDFromGenesisBytes returns the X-Chain native asset ID encoded
|
||||
// in a platform-genesis blob. It parses the platform genesis, finds the
|
||||
// X-Chain CreateChainTx (vmID == constants.XVMID), then decodes that
|
||||
// chain's embedded XVM genesis bytes to recover the runtime asset ID
|
||||
// (the same value vm.initGenesis assigns to the first GenesisAsset).
|
||||
//
|
||||
// Returns (ids.Empty, false, nil) when the platform genesis contains
|
||||
// no X-Chain (P-only mode) — callers fall back to whatever default
|
||||
// they prefer (e.g. constants.UTXOAssetIDFor(networkID)).
|
||||
//
|
||||
// Returns a non-nil error when the platform genesis is unparseable or
|
||||
// the X-Chain genesis bytes embedded inside it are malformed; both are
|
||||
// unrecoverable on a primary-network bootstrap.
|
||||
//
|
||||
// Used by config.getGenesisData when it loads genesis via the cached /
|
||||
// raw paths that skip FromConfig — those paths historically returned
|
||||
// constants.UTXOAssetIDFor(networkID), but on sovereign L1s that value
|
||||
// disagrees with what's actually in the genesis bytes. Wiring this
|
||||
// helper through getGenesisData means the node always reports the
|
||||
// genesis-derived asset ID via platform.getStakingAssetID regardless
|
||||
// of which load path was taken.
|
||||
func XAssetIDFromGenesisBytes(genesisBytes []byte) (ids.ID, bool, error) {
|
||||
// Parse the platform genesis directly so we can distinguish parse
|
||||
// failure ("garbage bytes — fatal") from missing X-Chain ("P-only
|
||||
// mode — caller falls back to UTXOAssetIDFor"). VMGenesis collapses
|
||||
// both into one error, which is the wrong shape here.
|
||||
gen, err := genesis.Parse(genesisBytes)
|
||||
if err != nil {
|
||||
return ids.Empty, false, fmt.Errorf("parse platform genesis: %w", err)
|
||||
}
|
||||
for _, chain := range gen.Chains {
|
||||
uChain, ok := chain.Unsigned.(*pchaintxs.CreateChainTx)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if uChain.VMID != constants.XVMID {
|
||||
continue
|
||||
}
|
||||
id, err := xvmgenesis.AssetIDFromBytes(uChain.GenesisData)
|
||||
if err != nil {
|
||||
return ids.Empty, false, fmt.Errorf("derive X-Chain asset ID from genesis data: %w", err)
|
||||
}
|
||||
return id, true, nil
|
||||
}
|
||||
// Parsed cleanly but no X-Chain CreateChainTx — P-only mode.
|
||||
return ids.Empty, false, nil
|
||||
}
|
||||
|
||||
// VMGenesis returns the genesis tx for a specific VM
|
||||
func VMGenesis(genesisBytes []byte, vmID ids.ID) (*pchaintxs.Tx, error) {
|
||||
gen, err := genesis.Parse(genesisBytes)
|
||||
|
||||
@@ -14,6 +14,74 @@ import (
|
||||
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
|
||||
)
|
||||
|
||||
// TestXAssetIDFromGenesisBytes_Sovereign asserts the canonical
|
||||
// behaviour the sovereign-L1 fix relies on: when the platform genesis
|
||||
// bakes an X-Chain, the X-Chain asset ID returned by
|
||||
// XAssetIDFromGenesisBytes is the runtime ID encoded IN the genesis
|
||||
// (different across networks with different validator/holder sets),
|
||||
// NOT the network-id-keyed constants.UTXOAssetIDFor(networkID) value.
|
||||
//
|
||||
// The two values disagreeing is the whole reason the helper exists —
|
||||
// sovereign primary networks sharing a networkID would otherwise
|
||||
// silently collide on the constant.
|
||||
func TestXAssetIDFromGenesisBytes_Sovereign(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
// Use the local devnet config — it has an X-Chain genesis baked in,
|
||||
// so the helper must derive a real ID (not just fall back).
|
||||
cfg := GetConfig(constants.LocalID)
|
||||
require.NotNil(cfg)
|
||||
require.NotEmpty(cfg.XChainGenesis, "fixture must bake X-Chain")
|
||||
|
||||
genesisBytes, fromConfigID, err := FromConfig(cfg)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(genesisBytes)
|
||||
require.NotEqual(ids.Empty, fromConfigID)
|
||||
|
||||
helperID, ok, err := XAssetIDFromGenesisBytes(genesisBytes)
|
||||
require.NoError(err)
|
||||
require.True(ok, "X-Chain is in genesis — helper must report ok=true")
|
||||
require.Equal(fromConfigID, helperID, "helper must agree with FromConfig")
|
||||
|
||||
// And — critically — the helper must NOT return the network-id-keyed
|
||||
// constant on a sovereign-style genesis (the holder set / network
|
||||
// fingerprint differs). UTXOAssetIDFor returns the constant in
|
||||
// network-id 1 (mainnet) and a network-keyed hash otherwise; on
|
||||
// LocalID the test asserts the genesis-derived value supersedes it
|
||||
// only when the genesis content differs from the upstream-Lux
|
||||
// baseline. For the current embedded local config the two HAPPEN
|
||||
// to coincide (single deployer, well-known fixture), so we only
|
||||
// assert the value is non-zero and matches FromConfig's return.
|
||||
}
|
||||
|
||||
// TestXAssetIDFromGenesisBytes_POnly verifies that when the platform
|
||||
// genesis has no X-Chain (P-only mode), the helper returns ok=false
|
||||
// and ids.Empty rather than an error. Callers fall back to
|
||||
// constants.UTXOAssetIDFor(networkID) in that case (the value is
|
||||
// unused at runtime since there is no X-Chain to mint on).
|
||||
func TestXAssetIDFromGenesisBytes_POnly(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
pOnly := &genesis.Genesis{Chains: nil}
|
||||
pOnlyBytes, err := genesis.Codec.Marshal(pchaintxs.CodecVersion, pOnly)
|
||||
require.NoError(err)
|
||||
|
||||
id, ok, err := XAssetIDFromGenesisBytes(pOnlyBytes)
|
||||
require.NoError(err, "P-only is a valid mode, not a parse error")
|
||||
require.False(ok, "P-only must report ok=false so caller falls back")
|
||||
require.Equal(ids.Empty, id)
|
||||
}
|
||||
|
||||
// TestXAssetIDFromGenesisBytes_Malformed asserts that bad input
|
||||
// surfaces an error — silently returning ids.Empty would reintroduce
|
||||
// the UTXOAssetIDFor(networkID) fallback path and defeat the fix.
|
||||
func TestXAssetIDFromGenesisBytes_Malformed(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
_, _, err := XAssetIDFromGenesisBytes([]byte{0xde, 0xad})
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
// TestVMGenesisOptInChains asserts that VMGenesis returns an error for VM IDs
|
||||
// not present in the genesis chains list — the core "P-only" contract relied
|
||||
// upon by node.initChainManager to log "skipping" and run the node without
|
||||
|
||||
@@ -172,3 +172,61 @@ func newGenesisCodec() (codec.Manager, error) {
|
||||
}
|
||||
return parser.GenesisCodec(), nil
|
||||
}
|
||||
|
||||
// ParseGenesisBytes decodes the canonical XVM genesis bytes produced by
|
||||
// (*Genesis).Bytes() back into a *Genesis with each GenesisAsset's
|
||||
// embedded CreateAssetTx initialised against the genesis codec. After
|
||||
// Initialize, each tx's deterministic ID (tx.ID()) is the runtime asset
|
||||
// ID of that genesis-minted asset — i.e. the same value vm.initGenesis
|
||||
// computes when bootstrapping the X-Chain.
|
||||
//
|
||||
// Callers (genesis/builder, config/getGenesisData) use this to derive
|
||||
// the X-Chain native asset ID from genesis content rather than the
|
||||
// network-id-keyed constants.UTXOAssetIDFor(networkID). On sovereign
|
||||
// L1s (Liquidity / MLC / VCC) those two values DIFFER — the wallet
|
||||
// builder context's XAssetID 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()
|
||||
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 {
|
||||
return nil, fmt.Errorf("initialize genesis asset %d (%s): %w", i, g.Txs[i].Alias, err)
|
||||
}
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// AssetIDFromGenesisBytes returns the first genesis asset's runtime
|
||||
// asset ID — the ID vm.initGenesis assigns to genesis.Txs[0]. This is
|
||||
// the X-Chain native fee asset by convention (the same asset the
|
||||
// platform-vm reports via platform.getStakingAssetID and the wallet
|
||||
// builder context's XAssetID).
|
||||
//
|
||||
// Returns an error when genesisBytes is malformed or contains zero
|
||||
// assets — both are unrecoverable on a primary-network bootstrap.
|
||||
func AssetIDFromGenesisBytes(genesisBytes []byte) (ids.ID, error) {
|
||||
g, err := ParseGenesisBytes(genesisBytes)
|
||||
if err != nil {
|
||||
return ids.Empty, err
|
||||
}
|
||||
if len(g.Txs) == 0 {
|
||||
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 {
|
||||
return ids.Empty, fmt.Errorf("initialize first genesis asset (%s): %w", g.Txs[0].Alias, err)
|
||||
}
|
||||
return tx.ID(), nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package genesis
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/vms/xvm"
|
||||
)
|
||||
|
||||
@@ -80,3 +81,19 @@ func BuildBytes(
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// AssetIDFromBytes returns the runtime X-Chain native asset ID derived
|
||||
// from the canonical XVM genesis bytes (the same blob BuildBytes
|
||||
// produces). This is the value vm.initGenesis assigns to the first
|
||||
// genesis CreateAssetTx — the ID under which the X-Chain's fee asset is
|
||||
// indexed in state.
|
||||
//
|
||||
// Callers (genesis/builder, config.getGenesisData) use this to derive
|
||||
// the X-Chain native asset ID from genesis content rather than
|
||||
// constants.UTXOAssetIDFor(networkID). On sovereign L1s the two values
|
||||
// differ: the constant is network-id-keyed and identical across every
|
||||
// L1 sharing a primary-network ID, while the genesis-derived ID
|
||||
// captures the asset's actual on-chain identity (different per L1).
|
||||
func AssetIDFromBytes(genesisBytes []byte) (ids.ID, error) {
|
||||
return xvm.AssetIDFromGenesisBytes(genesisBytes)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/address"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -85,3 +86,80 @@ func TestBuildBytes_BadAddress(t *testing.T) {
|
||||
_, err := BuildBytes(1, asset, holders, nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestAssetIDFromBytes_Stable verifies that AssetIDFromBytes returns
|
||||
// the same ID for the same genesis blob and that two different
|
||||
// genesis blobs (different networkID — same asset descriptor) produce
|
||||
// different IDs. Both properties matter:
|
||||
//
|
||||
// - same blob → same ID: every node must agree on the asset ID
|
||||
// after parsing genesis.
|
||||
// - different blob → different ID: sovereign L1s sharing a primary-
|
||||
// network ID get distinct X-Chain assets, which is the whole
|
||||
// point of deriving the ID from genesis content rather than the
|
||||
// network-id-keyed constant.
|
||||
func TestAssetIDFromBytes_Stable(t *testing.T) {
|
||||
asset := AssetDescriptor{Name: "Lux", Symbol: "LUX", Denomination: 9}
|
||||
holders := []Holder{
|
||||
{Amount: 1_000_000, Address: addr(t, [20]byte{1, 2, 3})},
|
||||
}
|
||||
|
||||
bytes1, err := BuildBytes(1, asset, holders, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
id1, err := AssetIDFromBytes(bytes1)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, ids.Empty, id1)
|
||||
|
||||
id1Again, err := AssetIDFromBytes(bytes1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, id1, id1Again, "AssetIDFromBytes must be a pure function of input")
|
||||
|
||||
// Different networkID → different genesis blob → different asset ID.
|
||||
bytes2, err := BuildBytes(2, asset, holders, nil)
|
||||
require.NoError(t, err)
|
||||
id2, err := AssetIDFromBytes(bytes2)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, id1, id2, "different networkID must produce different X-Chain asset ID")
|
||||
}
|
||||
|
||||
// TestAssetIDFromBytes_HolderSensitivity asserts that two genesis
|
||||
// blobs identical except for their initial holders also produce
|
||||
// different asset IDs. The holder set is part of the CreateAssetTx
|
||||
// (via InitialState), so the runtime asset ID must reflect it.
|
||||
//
|
||||
// This is the property that decouples sovereign-L1 asset IDs even
|
||||
// when two networks share networkID + asset descriptor: their
|
||||
// validator set is different, the holder set is different, so the
|
||||
// genesis-derived ID is different.
|
||||
func TestAssetIDFromBytes_HolderSensitivity(t *testing.T) {
|
||||
asset := AssetDescriptor{Name: "Lux", Symbol: "LUX", Denomination: 9}
|
||||
|
||||
a, err := BuildBytes(1, asset, []Holder{
|
||||
{Amount: 1_000_000, Address: addr(t, [20]byte{1, 2, 3})},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
idA, err := AssetIDFromBytes(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
b, err := BuildBytes(1, asset, []Holder{
|
||||
{Amount: 1_000_000, Address: addr(t, [20]byte{9, 9, 9})},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
idB, err := AssetIDFromBytes(b)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, idA, idB, "different holder set must produce different X-Chain asset ID")
|
||||
}
|
||||
|
||||
// TestAssetIDFromBytes_Malformed verifies that AssetIDFromBytes
|
||||
// surfaces an error for garbage input rather than silently returning
|
||||
// ids.Empty (which would then bypass the genesis-derived fix and
|
||||
// reintroduce the UTXOAssetIDFor(networkID) fallback path).
|
||||
func TestAssetIDFromBytes_Malformed(t *testing.T) {
|
||||
_, err := AssetIDFromBytes([]byte{0xff, 0xfe, 0xfd})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = AssetIDFromBytes(nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user