feat(pay): pangolin-pay 钱包派生模块(#34/34A · 加密货币交易引擎 Phase B.2)
新建独立 Go module pay/(pangolin-pay:独立 VPS 跑,与控制面分开、隔离 crypto 依赖)。 wallet 包: - AddressFromAccountXpub(xpub, change, index):watch-only 从账户 xpub 派生 TRON 地址 (Keccak-256 legacy → 后20字节 → 0x41 → base58check),watcher 用,不碰私钥。 - seed.go(离线专用):助记词→account xpub / 地址私钥(hex),给 Phase A 导 xpub、Phase D 归集签名。 - 测试:①派生一致性——xpub 路径与私钥路径逐个相等(证明每个收款地址对得上签名私钥); ②金标准向量锁定实现防回归。⚠️ 金标准需按 A.4 用 Ian Coleman 交叉核对一次。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user