Files
zeekay 2faf6ed0df The wire is ZAP: no codec, no marshal, no parallel wallet
github.com/luxfi/codec is gone from this module — no import, no call, no
require. Nothing here marshals or unmarshals a transaction any more.

WHY IT BROKE. codec deleted its jsonrpc subpackage (1b67d6a, "delete dead
subpackages") while nine files imported it. It looked dead because the
PUBLISHED [email protected] still ships it, so only a workspace build — which
resolves codec from the local checkout — ever saw the break.

THE WIRE. A tx already carries its own ZAP encoding, so the codec was
re-deriving what the tx could hand over:
  txs.Codec.Marshal(txs.CodecVersion, &tx.Unsigned)  ->  tx.Unsigned.Bytes()
                                                          txs.UnsignedBytes(tx.Unsigned)
  txs.Codec.Marshal(txs.CodecVersion, tx)            ->  tx.Initialize()
  txs.Codec.Unmarshal(txBytes, &tx)                  ->  txs.Parse(txBytes)
  codec.Unmarshal(utxoBytes, &utxo)                  ->  utxo.ParseUTXO(...)
These are the same calls the node makes in txs.Tx.Sign. AddAllUTXOs no longer
takes a codec.Manager, and SortTransferableOutputs / state.Sort /
SortOperations no longer get one — upstream dropped the parameter.

PARALLEL IMPLEMENTATIONS, DELETED. wallet/chain/{p,x} carried a second copy of
the builder and signer beside the subpackages that already held them —
p/signer_visitor.go and p/signer/visitor.go were both 399 lines of the same
thing, and each defect had to be fixed twice. The flat files are gone; the
package keeps the facade it already had in wallet_exports.go, which re-exports
the subpackage. -4787 lines.

wallet/chain/c is deleted. Its whole reason to exist was a codec.Manager
scaffold for C-chain atomic-tx wire that nothing runs: NewWallet was passed
nil for it, and the builder was commented out. The two symbols anything used
move to where they are read — Account, a balance and a nonce off an EVM
client, and EVMKeychain, the geth-address half of a keychain. Wallet no longer
advertises a C() that only ever returned nil.

ALIGNED. XAssetID -> UTXOAssetID, matching runtime.Runtime and netrunner, which
already expected it. Keychain GetEth -> GetByEVM, EthAddrs -> EVMAddrs. warp
UnsignedMessage and Message collapsed into one flat Message, so a signature is
an Envelope rather than a second message type. netrunner's HealthResponse is
read as a field: the getter form is protobuf, behind //go:build grpc, and the
default build is ZAP-native.

VERIFIED on a machine with the native libraries: go build ./... exits 0 with no
codec require in go.mod, and go test ./... is 23 ok. Three packages still fail
to build their tests — profiles, wallet/keychain, wallet/primary — and all
three fail identically on the tree before this change: Consensus,
VerifyHybridSecp256k1MLDSA and isXChainNotEnabled are referenced by tests and
defined nowhere. evm failed there too and now passes.
2026-08-28 22:25:12 -07:00

301 lines
8.5 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package signer
import (
"context"
"errors"
"fmt"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/keychain"
"github.com/luxfi/proto/x/fxs"
"github.com/luxfi/proto/x/txs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/vm/components/verify"
)
var (
_ txs.Visitor = (*visitor)(nil)
ErrUnknownInputType = errors.New("unknown input type")
ErrUnknownOpType = errors.New("unknown operation type")
ErrInvalidNumUTXOsInOp = errors.New("invalid number of UTXOs in operation")
ErrUnknownCredentialType = errors.New("unknown credential type")
ErrUnknownOutputType = errors.New("unknown output type")
ErrInvalidUTXOSigIndex = errors.New("invalid UTXO signature index")
emptySig [secp256k1.SignatureLen]byte
)
// visitor handles signing transactions for the signer
type visitor struct {
kc keychain.Keychain
backend Backend
ctx context.Context
tx *txs.Tx
}
func (s *visitor) BaseTx(tx *txs.BaseTx) error {
txCreds, txSigners, err := s.getSigners(s.ctx, tx.BlockchainID, tx.Ins)
if err != nil {
return err
}
return sign(s.tx, txCreds, txSigners)
}
func (s *visitor) CreateAssetTx(tx *txs.CreateAssetTx) error {
txCreds, txSigners, err := s.getSigners(s.ctx, tx.BlockchainID, tx.Ins)
if err != nil {
return err
}
return sign(s.tx, txCreds, txSigners)
}
func (s *visitor) OperationTx(tx *txs.OperationTx) error {
txCreds, txSigners, err := s.getSigners(s.ctx, tx.BlockchainID, tx.Ins)
if err != nil {
return err
}
txOpsCreds, txOpsSigners, err := s.getOpsSigners(s.ctx, tx.BlockchainID, tx.Ops)
if err != nil {
return err
}
txCreds = append(txCreds, txOpsCreds...)
txSigners = append(txSigners, txOpsSigners...)
return sign(s.tx, txCreds, txSigners)
}
func (s *visitor) ImportTx(tx *txs.ImportTx) error {
txCreds, txSigners, err := s.getSigners(s.ctx, tx.BlockchainID, tx.Ins)
if err != nil {
return err
}
txImportCreds, txImportSigners, err := s.getSigners(s.ctx, tx.SourceChain, tx.ImportedIns)
if err != nil {
return err
}
txCreds = append(txCreds, txImportCreds...)
txSigners = append(txSigners, txImportSigners...)
return sign(s.tx, txCreds, txSigners)
}
func (s *visitor) ExportTx(tx *txs.ExportTx) error {
txCreds, txSigners, err := s.getSigners(s.ctx, tx.BlockchainID, tx.Ins)
if err != nil {
return err
}
return sign(s.tx, txCreds, txSigners)
}
func (s *visitor) getSigners(ctx context.Context, sourceChainID ids.ID, ins []*lux.TransferableInput) ([]verify.Verifiable, [][]keychain.Signer, error) {
txCreds := make([]verify.Verifiable, len(ins))
txSigners := make([][]keychain.Signer, len(ins))
for credIndex, transferInput := range ins {
txCreds[credIndex] = &secp256k1fx.Credential{}
input, ok := transferInput.In.(*secp256k1fx.TransferInput)
if !ok {
return nil, nil, ErrUnknownInputType
}
inputSigners := make([]keychain.Signer, len(input.SigIndices))
txSigners[credIndex] = inputSigners
utxoID := transferInput.InputID()
utxo, err := s.backend.GetUTXO(ctx, sourceChainID, utxoID)
if err == database.ErrNotFound {
// If we don't have access to the UTXO, then we can't sign this
// transaction. However, we can attempt to partially sign it.
continue
}
if err != nil {
return nil, nil, err
}
out, ok := utxo.Out.(*secp256k1fx.TransferOutput)
if !ok {
return nil, nil, ErrUnknownOutputType
}
for sigIndex, addrIndex := range input.SigIndices {
if addrIndex >= uint32(len(out.Addrs)) {
return nil, nil, ErrInvalidUTXOSigIndex
}
addr := out.Addrs[addrIndex]
key, ok := s.kc.Get(addr)
if !ok {
// If we don't have access to the key, then we can't sign this
// transaction. However, we can attempt to partially sign it.
continue
}
inputSigners[sigIndex] = key
}
}
return txCreds, txSigners, nil
}
func (s *visitor) getOpsSigners(ctx context.Context, sourceChainID ids.ID, ops []*txs.Operation) ([]verify.Verifiable, [][]keychain.Signer, error) {
txCreds := make([]verify.Verifiable, len(ops))
txSigners := make([][]keychain.Signer, len(ops))
for credIndex, op := range ops {
var input *secp256k1fx.Input
switch op := op.Op.(type) {
case *secp256k1fx.MintOperation:
txCreds[credIndex] = &secp256k1fx.Credential{}
input = &op.MintInput
case *nftfx.MintOperation:
txCreds[credIndex] = &nftfx.Credential{}
input = &op.MintInput
case *nftfx.TransferOperation:
txCreds[credIndex] = &nftfx.Credential{}
input = &op.Input
case *propertyfx.MintOperation:
txCreds[credIndex] = &propertyfx.Credential{}
input = &op.MintInput
case *propertyfx.BurnOperation:
txCreds[credIndex] = &propertyfx.Credential{}
input = &op.Input
default:
return nil, nil, ErrUnknownOpType
}
inputSigners := make([]keychain.Signer, len(input.SigIndices))
txSigners[credIndex] = inputSigners
if len(op.UTXOIDs) != 1 {
return nil, nil, ErrInvalidNumUTXOsInOp
}
utxoID := op.UTXOIDs[0].InputID()
utxo, err := s.backend.GetUTXO(ctx, sourceChainID, utxoID)
if err == database.ErrNotFound {
// If we don't have access to the UTXO, then we can't sign this
// transaction. However, we can attempt to partially sign it.
continue
}
if err != nil {
return nil, nil, err
}
var addrs []ids.ShortID
switch out := utxo.Out.(type) {
case *secp256k1fx.MintOutput:
addrs = out.Addrs
case *nftfx.MintOutput:
addrs = out.Addrs
case *nftfx.TransferOutput:
addrs = out.Addrs
case *propertyfx.MintOutput:
addrs = out.Addrs
case *propertyfx.OwnedOutput:
addrs = out.Addrs
default:
return nil, nil, ErrUnknownOutputType
}
for sigIndex, addrIndex := range input.SigIndices {
if addrIndex >= uint32(len(addrs)) {
return nil, nil, ErrInvalidUTXOSigIndex
}
addr := addrs[addrIndex]
key, ok := s.kc.Get(addr)
if !ok {
// If we don't have access to the key, then we can't sign this
// transaction. However, we can attempt to partially sign it.
continue
}
inputSigners[sigIndex] = key
}
}
return txCreds, txSigners, nil
}
func sign(tx *txs.Tx, creds []verify.Verifiable, txSigners [][]keychain.Signer) error {
// The tx carries its own ZAP encoding, so the unsigned buffer comes from
// the tx itself rather than from a codec and a version.
unsignedBytes, err := txs.UnsignedBytes(tx.Unsigned)
if err != nil {
return fmt.Errorf("couldn't encode unsigned tx: %w", err)
}
if expectedLen := len(txSigners); expectedLen != len(tx.Creds) {
tx.Creds = make([]*fxs.FxCredential, expectedLen)
}
sigCache := make(map[ids.ShortID][secp256k1.SignatureLen]byte)
for credIndex, inputSigners := range txSigners {
fxCred := tx.Creds[credIndex]
if fxCred == nil {
fxCred = &fxs.FxCredential{}
tx.Creds[credIndex] = fxCred
}
credIntf := fxCred.Credential
if credIntf == nil {
credIntf = creds[credIndex]
fxCred.Credential = credIntf
}
var cred *secp256k1fx.Credential
switch credImpl := credIntf.(type) {
case *secp256k1fx.Credential:
fxCred.FxID = secp256k1fx.ID
cred = credImpl
case *nftfx.Credential:
fxCred.FxID = nftfx.ID
cred = &credImpl.Credential
case *propertyfx.Credential:
fxCred.FxID = propertyfx.ID
cred = &credImpl.Credential
default:
return ErrUnknownCredentialType
}
if expectedLen := len(inputSigners); expectedLen != len(cred.Sigs) {
cred.Sigs = make([][secp256k1.SignatureLen]byte, expectedLen)
}
for sigIndex, signer := range inputSigners {
if signer == nil {
// If we don't have access to the key, then we can't sign this
// transaction. However, we can attempt to partially sign it.
continue
}
addr := signer.Address()
if sig := cred.Sigs[sigIndex]; sig != emptySig {
// If this signature has already been populated, we can just
// copy the needed signature for the future.
sigCache[addr] = sig
continue
}
if sig, exists := sigCache[addr]; exists {
// If this key has already produced a signature, we can just
// copy the previous signature.
cred.Sigs[sigIndex] = sig
continue
}
sig, err := signer.Sign(unsignedBytes)
if err != nil {
return fmt.Errorf("problem signing tx: %w", err)
}
copy(cred.Sigs[sigIndex][:], sig)
sigCache[addr] = cred.Sigs[sigIndex]
}
}
// Initialize re-derives the unsigned buffer, appends the credentials just
// populated, and binds the tx ID from the result.
if err := tx.Initialize(); err != nil {
return err
}
return nil
}