Files
pangolin/pay/internal/wallet/seed.go
wangjia 7cf87a764f feat(pay): Phase D 归集 —— 气隙签名 sweep(#34/34A)
动钱代码,按"联网建/广播 + 离线签"气隙流程,离线签名独立验签防篡改:
- wallet: DecodeTronAddress/TronAddressBodyHex(base58check 解码校验)、AddressFromMnemonic(离线校验用)。
- tron/sign: TxID(sha256 raw_data)、SignRawData(secp256k1 → R||S||recid 65B)、RecoverAddressBody。
- tron/abi: ABIEncodeTransferParams(transfer(address,uint256) 参数)、keccak 地址体。
- tron/tx(联网): BuildTransfer(triggersmartcontract 建未签名)、TRC20Balance、Broadcast。
- cmd/sweep: plan/build/sign/broadcast 四段;sign 对每笔独立验:①重算 txid 防篡改 ②收款人+额 ABI
  内嵌 ③USDT 合约内嵌 ④派生地址==owner,任一不符拒签;助记词只经 PAY_SWEEP_MNEMONIC(离线机,不入 arg)。
- 测试:签名可恢复到正确地址、ABI 编码、地址解码 roundtrip 全绿。
- 联网上链部分标注需 Phase E 真链验证;README 补 sweep 用法 + 验签说明 + gas 提醒。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 04:12:04 +08:00

110 lines
3.8 KiB
Go

package wallet
import (
"fmt"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
bip39 "github.com/tyler-smith/go-bip39"
)
// ⚠️ OFFLINE ONLY. Everything in this file touches the BIP39 seed / private keys.
// It exists for (a) generating the account xpub to hand to the watcher, and
// (b) deriving per-address private keys for offline sweep signing (Phase D).
// It must NEVER be linked into or run on the internet-facing pangolin-pay
// watcher — the hot service only ever handles the account xpub (see derive.go).
const (
purposeBIP44 = 44
coinTypeTRON = 195
// hardenedOffset marks a derivation index as hardened (requires the private
// key). BIP44's first three levels (purpose'/coin'/account') are hardened.
hardenedOffset = hdkeychain.HardenedKeyStart // 0x80000000
)
// AccountKeyFromMnemonic derives the account-level extended *private* key at
// m/44'/195'/<account>' from a BIP39 mnemonic (+ optional passphrase).
// OFFLINE ONLY.
func AccountKeyFromMnemonic(mnemonic, passphrase string, account uint32) (*hdkeychain.ExtendedKey, error) {
if !bip39.IsMnemonicValid(mnemonic) {
return nil, fmt.Errorf("wallet: invalid BIP39 mnemonic (checksum/wordlist)")
}
seed := bip39.NewSeed(mnemonic, passphrase)
master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
return nil, fmt.Errorf("wallet: master key: %w", err)
}
for _, step := range []uint32{
hardenedOffset + purposeBIP44,
hardenedOffset + coinTypeTRON,
hardenedOffset + account,
} {
master, err = master.Derive(step)
if err != nil {
return nil, fmt.Errorf("wallet: derive account path: %w", err)
}
}
return master, nil
}
// AccountXpubFromMnemonic returns the account-level xpub string to hand to the
// watcher. OFFLINE ONLY — run this once on the air-gapped machine, copy only the
// returned xpub to the hot service.
func AccountXpubFromMnemonic(mnemonic, passphrase string, account uint32) (string, error) {
k, err := AccountKeyFromMnemonic(mnemonic, passphrase, account)
if err != nil {
return "", err
}
pub, err := k.Neuter() // strip the private key -> xpub
if err != nil {
return "", fmt.Errorf("wallet: neuter: %w", err)
}
return pub.String(), nil
}
// PrivKeyHexFromMnemonic derives the raw secp256k1 private key (hex) for the
// address at m/44'/195'/<account>'/<change>/<index>, for offline sweep signing.
// OFFLINE ONLY.
func PrivKeyHexFromMnemonic(mnemonic, passphrase string, account, change, index uint32) (string, error) {
acct, err := AccountKeyFromMnemonic(mnemonic, passphrase, account)
if err != nil {
return "", err
}
chainKey, err := acct.Derive(change)
if err != nil {
return "", fmt.Errorf("wallet: derive change: %w", err)
}
addrKey, err := chainKey.Derive(index)
if err != nil {
return "", fmt.Errorf("wallet: derive index: %w", err)
}
priv, err := addrKey.ECPrivKey()
if err != nil {
return "", fmt.Errorf("wallet: ec privkey: %w", err)
}
return fmt.Sprintf("%x", priv.Serialize()), nil
}
// AddressFromMnemonic derives the TRON address at m/44'/195'/<account>'/<change>/<index>
// straight from the mnemonic. OFFLINE ONLY — used by the sweep signer to verify
// that a derived key matches the address it is about to sign for.
func AddressFromMnemonic(mnemonic, passphrase string, account, change, index uint32) (string, error) {
acct, err := AccountKeyFromMnemonic(mnemonic, passphrase, account)
if err != nil {
return "", err
}
chainKey, err := acct.Derive(change)
if err != nil {
return "", fmt.Errorf("wallet: derive change: %w", err)
}
addrKey, err := chainKey.Derive(index)
if err != nil {
return "", fmt.Errorf("wallet: derive index: %w", err)
}
pub, err := addrKey.ECPubKey()
if err != nil {
return "", fmt.Errorf("wallet: ec pubkey: %w", err)
}
return PubKeyToTronAddress(pub.SerializeUncompressed()), nil
}