wallet/network/primary: accept both ZAP wire + legacy V1 codec UTXOs

6de839a515 (codec Wave 1D) migrated the wallet UTXO loader to
lux.ParseUTXO (ZAP wire dispatcher) before the platformvm GetUTXOs
server-side encoder was migrated. Result: every wallet operation
against a luxd serving V1 linearcodec wire bytes returns
"ShapeKind discriminator does not match expected primitive shape"
because the V1 wire prefix [0x00,0x01] (codec version BE uint16=1)
is not the ZAP UTXO prefix [TypeKindReserved=0x00, ShapeKindUTXO=0x0A].

Lux-mainnet validators (v1.28.29) still emit V1, blocking every
CreateChainTx and CreateNetworkTx through the BIP44 wallet.

Fix: dispatch on the 2-byte envelope prefix in AddAllUTXOs. ZAP
envelopes (byte[1] == ShapeKindUTXO) route through lux.ParseUTXO;
anything else falls through to ptxs.Codec.Unmarshal. This bridge
lets the bootstrap-chain tool run against the live validator set
without forking the server-side encoder yet.

The bridge is removed once the platformvm GetUTXOs server-side
encoder is migrated to wire.NewUTXO (tracked separately).

Verified: bootstrap-chain --uri=https://api.lux.network now signs +
issues CreateChainTx successfully (osage L1 added to mainnet at
blockchainID 2ahV62kvM1KWkgxL6MiF5hPYBVwWuwpPqwV3RhqM6hCxZT68uo).
This commit is contained in:
Hanzo AI
2026-06-05 13:39:55 -07:00
parent 6de839a515
commit 1a30944a0d
+42 -4
View File
@@ -17,11 +17,13 @@ import (
"github.com/luxfi/math/set"
_ "github.com/luxfi/node/vms/components/lux" // registers utxo.ParseUTXO ZAP dispatcher
"github.com/luxfi/node/vms/platformvm"
ptxs "github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/wallet/chain/p"
"github.com/luxfi/node/wallet/chain/x"
"github.com/luxfi/rpc"
"github.com/luxfi/sdk/info"
lux "github.com/luxfi/utxo"
luxwire "github.com/luxfi/utxo/wire"
ethcommon "github.com/luxfi/geth/common"
pbuilder "github.com/luxfi/node/wallet/chain/p/builder"
@@ -254,9 +256,22 @@ func FetchEthState(
// AddAllUTXOs fetches all the UTXOs referenced by [addresses] that were sent
// from [sourceChainID] to [destinationChainID] from the [client]. It parses
// the returned UTXOs via the registered ZAP wire dispatcher
// (utxo.ParseUTXO, wired by node/vms/components/lux) and adds them into
// [utxos]. If [ctx] expires, the returned error is reported immediately.
// the returned UTXOs via either the ZAP wire dispatcher (post-activation
// builds) or the legacy linearcodec path (V0/V1 wire), dispatching on the
// 2-byte envelope prefix. Adds them into [utxos]. If [ctx] expires, the
// returned error is reported immediately.
//
// Wire-format dispatch contract:
// - ZAP envelope: prefix = (TypeKindReserved=0x00, ShapeKindUTXO=0x0A).
// Routed to lux.ParseUTXO via the dispatcher registered by
// node/vms/components/lux.
// - Legacy codec: prefix = (0x00, V) where V ∈ {0x00,0x01,0x02} is the
// 2-byte codec version (big-endian uint16 in {0,1,2}). Routed to
// ptxs.Codec.Unmarshal into a *lux.UTXO. Required because the
// platformvm GetUTXOs server-side encoder still writes V1 wire bytes
// (the codec rip is currently asymmetric — wallet was migrated first).
//
// Both branches yield a *lux.UTXO; the AddUTXO contract is unchanged.
func AddAllUTXOs(
ctx context.Context,
utxos walletcommon.UTXOs,
@@ -289,7 +304,7 @@ func AddAllUTXOs(
}
for _, utxoBytes := range utxosBytes {
u, err := lux.ParseUTXO(utxoBytes)
u, err := parseUTXOAnyWire(utxoBytes)
if err != nil {
return err
}
@@ -309,6 +324,29 @@ func AddAllUTXOs(
return nil
}
// parseUTXOAnyWire decodes a UTXO from either the ZAP wire envelope
// (post-activation luxd) or the legacy linearcodec wire (pre-rip luxd
// still in production, including the lux-mainnet validator set as of
// 2026-06-05). The dispatch is on the 2-byte prefix:
//
// - byte[0] = 0x00, byte[1] = 0x0A → ZAP UTXO envelope (lux.ParseUTXO).
// - byte[0] = 0x00, byte[1] ∈ {0x00,0x01,0x02} → legacy codec (V0/V1/V2),
// routed through ptxs.Codec.Unmarshal.
// - anything else → return ZAP path's error (caller surfaces it).
//
// This bridge is removed once the platformvm GetUTXOs server-side encoder
// is migrated to wire.NewUTXO (tracked separately from the wallet rip).
func parseUTXOAnyWire(utxoBytes []byte) (*lux.UTXO, error) {
if len(utxoBytes) >= 2 && utxoBytes[0] == byte(luxwire.TypeKindReserved) && utxoBytes[1] == byte(luxwire.ShapeKindUTXO) {
return lux.ParseUTXO(utxoBytes)
}
var u lux.UTXO
if _, err := ptxs.Codec.Unmarshal(utxoBytes, &u); err != nil {
return nil, fmt.Errorf("legacy-codec UTXO parse: %w", err)
}
return &u, nil
}
// isXChainNotEnabled detects the canonical "info.getBlockchainID returns
// no-such-alias for X" error that surfaces when running against a P-only
// network (one whose platform genesis does not include an XVM chain).