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

340 lines
10 KiB
Go

// Copyright (C) 2025, Lux Industries Inc All rights reserved.
// See the file LICENSE for licensing terms.
// Package multisig provides multi-signature transaction support.
package multisig
import (
"context"
"fmt"
"github.com/luxfi/sdk/platformvm"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/sdk/network"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/vm/components/verify"
"github.com/luxfi/ids"
"github.com/luxfi/proto/p/txs"
)
type TxKind int64
var ErrUndefinedTx = fmt.Errorf("tx is undefined")
const (
Undefined TxKind = iota
PChainRemoveChainValidatorTx
PChainAddChainValidatorTx
PChainCreateChainTx
PChainTransformChainTx
PChainAddPermissionlessValidatorTx
PChainTransferChainOwnershipTx
)
type Multisig struct {
PChainTx *txs.Tx
controlKeys []ids.ShortID
threshold uint32
}
func New(pChainTx *txs.Tx) *Multisig {
ms := Multisig{
PChainTx: pChainTx,
}
return &ms
}
func (ms *Multisig) String() string {
if ms.PChainTx != nil {
return ms.PChainTx.ID().String()
}
return ""
}
func (ms *Multisig) Undefined() bool {
return ms.PChainTx == nil
}
func (ms *Multisig) ToBytes() ([]byte, error) {
if ms.Undefined() {
return nil, ErrUndefinedTx
}
// The signed tx already holds its ZAP wire bytes.
return ms.PChainTx.Bytes(), nil
}
func (ms *Multisig) FromBytes(txBytes []byte) error {
// Parse binds the tx from its own wire bytes, cached bytes and ID
// included, so there is nothing left to initialize.
tx, err := txs.Parse(txBytes)
if err != nil {
return fmt.Errorf("error parsing signed tx: %w", err)
}
ms.PChainTx = tx
return nil
}
func (ms *Multisig) IsReadyToCommit() (bool, error) {
if ms.Undefined() {
return false, ErrUndefinedTx
}
unsignedTx := ms.PChainTx.Unsigned
switch unsignedTx.(type) {
case *txs.CreateChainTx:
return true, nil
default:
}
_, remainingSigners, err := ms.GetRemainingAuthSigners()
if err != nil {
return false, err
}
return len(remainingSigners) == 0, nil
}
// GetRemainingAuthSigners gets chain auth addresses that have not signed a given tx
// - get the string slice of auth signers for the tx (GetAuthSigners)
// - verifies that all creds in tx.Creds, except the last one, are fully signed
// (a cred is fully signed if all the signatures in cred.Sigs are non-empty)
// - computes remaining signers by iterating the last cred in tx.Creds, associated to chain auth signing
// - for each sig in cred.Sig: if sig is empty, then add the associated auth signer address (obtained from
// authSigners by using the index) to the remaining signers list
//
// if the tx is fully signed, returns empty slice
func (ms *Multisig) GetRemainingAuthSigners() ([]ids.ShortID, []ids.ShortID, error) {
if ms.Undefined() {
return nil, nil, ErrUndefinedTx
}
authSigners, err := ms.GetAuthSigners()
if err != nil {
return nil, nil, err
}
emptySig := [secp256k1.SignatureLen]byte{}
numCreds := len(ms.PChainTx.Creds)
// we should have at least 1 cred for output owners and 1 cred for chain auth
if numCreds < 2 {
return nil, nil, fmt.Errorf("expected tx.Creds of len 2, got %d. doesn't seem to be a multisig tx with chain auth requirements", numCreds)
}
// signatures for output owners should be filled (all creds except last one)
for credIndex := range ms.PChainTx.Creds[:numCreds-1] {
cred, ok := ms.PChainTx.Creds[credIndex].(*secp256k1fx.Credential)
if !ok {
return nil, nil, fmt.Errorf("expected cred to be of type *secp256k1fx.Credential, got %T", ms.PChainTx.Creds[credIndex])
}
for i, sig := range cred.Sigs {
if sig == emptySig {
return nil, nil, fmt.Errorf("expected funding sig %d of cred %d to be filled", i, credIndex)
}
}
}
// signatures for chain auth (last cred)
cred, ok := ms.PChainTx.Creds[numCreds-1].(*secp256k1fx.Credential)
if !ok {
return nil, nil, fmt.Errorf("expected cred to be of type *secp256k1fx.Credential, got %T", ms.PChainTx.Creds[1])
}
if len(cred.Sigs) != len(authSigners) {
return nil, nil, fmt.Errorf("expected number of cred's signatures %d to equal number of auth signers %d",
len(cred.Sigs),
len(authSigners),
)
}
remainingSigners := []ids.ShortID{}
for i, sig := range cred.Sigs {
if sig == emptySig {
remainingSigners = append(remainingSigners, authSigners[i])
}
}
return authSigners, remainingSigners, nil
}
// GetAuthSigners gets all chain auth addresses that are required to sign a given tx
// - get chain control keys as string slice using P-Chain API (GetOwners)
// - get chain auth indices from the tx, field tx.UnsignedTx.ChainAuth
// - creates the string slice of required chain auth addresses by applying
// the indices to the control keys slice
func (ms *Multisig) GetAuthSigners() ([]ids.ShortID, error) {
if ms.Undefined() {
return nil, ErrUndefinedTx
}
controlKeys, _, err := ms.GetNetOwners()
if err != nil {
return nil, err
}
unsignedTx := ms.PChainTx.Unsigned
var netAuth verify.Verifiable
switch unsignedTx := unsignedTx.(type) {
case *txs.RemoveChainValidatorTx:
netAuth = unsignedTx.ChainAuth
case *txs.AddChainValidatorTx:
netAuth = unsignedTx.ChainAuth
case *txs.CreateChainTx:
netAuth = unsignedTx.ChainAuth
case *txs.TransformChainTx:
netAuth = unsignedTx.ChainAuth
case *txs.TransferChainOwnershipTx:
netAuth = unsignedTx.ChainAuth
default:
return nil, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx)
}
netInput, ok := netAuth.(*secp256k1fx.Input)
if !ok {
return nil, fmt.Errorf("expected netAuth of type *secp256k1fx.Input, got %T", netAuth)
}
authSigners := []ids.ShortID{}
for _, sigIndex := range netInput.SigIndices {
if sigIndex >= uint32(len(controlKeys)) {
return nil, fmt.Errorf("signer index %d exceeds number of control keys", sigIndex)
}
authSigners = append(authSigners, controlKeys[sigIndex])
}
return authSigners, nil
}
func (*Multisig) GetSpendSigners() ([]ids.ShortID, error) {
return nil, fmt.Errorf("not implemented yet")
}
func (ms *Multisig) GetTxKind() (TxKind, error) {
if ms.Undefined() {
return Undefined, ErrUndefinedTx
}
unsignedTx := ms.PChainTx.Unsigned
switch unsignedTx := unsignedTx.(type) {
case *txs.RemoveChainValidatorTx:
return PChainRemoveChainValidatorTx, nil
case *txs.AddChainValidatorTx:
return PChainAddChainValidatorTx, nil
case *txs.CreateChainTx:
return PChainCreateChainTx, nil
case *txs.TransformChainTx:
return PChainTransformChainTx, nil
case *txs.AddPermissionlessValidatorTx:
return PChainAddPermissionlessValidatorTx, nil
case *txs.TransferChainOwnershipTx:
return PChainTransferChainOwnershipTx, nil
default:
return Undefined, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx)
}
}
// GetNetworkID gets network id associated to tx.
func (ms *Multisig) GetNetworkID() (uint32, error) {
if ms.Undefined() {
return 0, ErrUndefinedTx
}
unsignedTx := ms.PChainTx.Unsigned
var networkID uint32
switch unsignedTx := unsignedTx.(type) {
case *txs.RemoveChainValidatorTx:
networkID = unsignedTx.NetworkID
case *txs.AddChainValidatorTx:
networkID = unsignedTx.NetworkID
case *txs.CreateChainTx:
networkID = unsignedTx.NetworkID
case *txs.TransformChainTx:
networkID = unsignedTx.NetworkID
case *txs.AddPermissionlessValidatorTx:
networkID = unsignedTx.NetworkID
case *txs.TransferChainOwnershipTx:
networkID = unsignedTx.NetworkID
default:
return 0, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx)
}
return networkID, nil
}
// GetNetwork gets network model associated to tx.
func (ms *Multisig) GetNetwork() (network.LegacyNetwork, error) {
if ms.Undefined() {
return network.UndefinedNetwork, ErrUndefinedTx
}
networkID, err := ms.GetNetworkID()
if err != nil {
return network.UndefinedNetwork, err
}
newNetwork := network.NetworkFromNetworkID(networkID)
if newNetwork.Kind == network.Undefined {
return network.UndefinedNetwork, fmt.Errorf("undefined network model for tx")
}
return newNetwork, nil
}
// GetBlockchainID is deprecated - the transaction types in luxfi/node/vms/platformvm/txs
// do not have BlockchainID fields. These transactions work with net IDs.
// Use GetChainID() instead for net-related operations.
//
// TODO: Remove this function or implement it correctly if blockchain IDs are needed
// func (ms *Multisig) GetBlockchainID() (ids.ID, error) {
// return ids.Empty, fmt.Errorf("GetBlockchainID is not implemented - transaction types do not have BlockchainID fields")
// }
// GetChainID gets net id associated to tx
func (ms *Multisig) GetChainID() (ids.ID, error) {
if ms.Undefined() {
return ids.Empty, ErrUndefinedTx
}
unsignedTx := ms.PChainTx.Unsigned
var chainID ids.ID
switch unsignedTx := unsignedTx.(type) {
case *txs.RemoveChainValidatorTx:
chainID = unsignedTx.Chain
case *txs.AddChainValidatorTx:
chainID = unsignedTx.Chain
case *txs.CreateChainTx:
chainID = unsignedTx.ValidateNetworkID
case *txs.TransformChainTx:
chainID = unsignedTx.Chain
case *txs.AddPermissionlessValidatorTx:
chainID = unsignedTx.Chain
case *txs.TransferChainOwnershipTx:
chainID = unsignedTx.Chain
default:
return ids.Empty, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx)
}
return chainID, nil
}
func (ms *Multisig) GetNetOwners() ([]ids.ShortID, uint32, error) {
if ms.Undefined() {
return nil, 0, ErrUndefinedTx
}
if ms.controlKeys == nil {
chainID, err := ms.GetChainID()
if err != nil {
return nil, 0, err
}
network, err := ms.GetNetwork()
if err != nil {
return nil, 0, err
}
controlKeys, threshold, err := GetOwners(network, chainID)
if err != nil {
return nil, 0, err
}
ms.controlKeys = controlKeys
ms.threshold = threshold
}
return ms.controlKeys, ms.threshold, nil
}
func GetOwners(network network.LegacyNetwork, chainID ids.ID) ([]ids.ShortID, uint32, error) {
pClient := platformvm.NewClient(network.Endpoint)
ctx := context.Background()
netResponse, err := pClient.GetNet(ctx, chainID)
if err != nil {
return nil, 0, fmt.Errorf("net tx %s query error: %w", chainID, err)
}
controlKeys := netResponse.ControlKeys
threshold := netResponse.Threshold
return controlKeys, threshold, nil
}
func (ms *Multisig) GetWrappedPChainTx() (*txs.Tx, error) {
if ms.Undefined() {
return nil, ErrUndefinedTx
}
return ms.PChainTx, nil
}