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,78 @@
|
||||
// Package wallet derives TRON (TRC20) receiving addresses from a BIP32 account
|
||||
// extended public key (xpub) — watch-only, no private keys involved. The
|
||||
// pangolin-pay watcher uses AddressFromAccountXpub to assign a unique receiving
|
||||
// address per order (m/44'/195'/0'/0/i). Private-key material (seed.go) is for
|
||||
// OFFLINE use only (sweep signing / vector generation), never on the hot service.
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/base58"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// tronAddrPrefix is the TRON mainnet address version byte (0x41). It is prepended
|
||||
// to the 20-byte address body before Base58Check encoding, yielding the familiar
|
||||
// "T..." addresses.
|
||||
const tronAddrPrefix = 0x41
|
||||
|
||||
// AddressFromAccountXpub derives the TRON address at m/…/<change>/<index> from an
|
||||
// account-level extended public key (e.g. the xpub of m/44'/195'/0'). It is
|
||||
// watch-only: an xpub can derive child addresses/public keys but never private
|
||||
// keys, so this is safe to run on an internet-facing service.
|
||||
//
|
||||
// change is 0 for the external (receiving) chain; index is the per-order address
|
||||
// index. Both are non-hardened, which is exactly why the account-level xpub can
|
||||
// derive them.
|
||||
func AddressFromAccountXpub(xpub string, change, index uint32) (string, error) {
|
||||
acct, err := hdkeychain.NewKeyFromString(xpub)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("wallet: parse xpub: %w", err)
|
||||
}
|
||||
if acct.IsPrivate() {
|
||||
return "", fmt.Errorf("wallet: expected an xpub (public extended key), got a private one")
|
||||
}
|
||||
chainKey, err := acct.Derive(change)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("wallet: derive change %d: %w", change, err)
|
||||
}
|
||||
addrKey, err := chainKey.Derive(index)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("wallet: derive index %d: %w", index, err)
|
||||
}
|
||||
pub, err := addrKey.ECPubKey()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("wallet: ec pubkey: %w", err)
|
||||
}
|
||||
return PubKeyToTronAddress(pub.SerializeUncompressed()), nil
|
||||
}
|
||||
|
||||
// PubKeyToTronAddress converts a 65-byte uncompressed secp256k1 public key
|
||||
// (0x04 || X || Y) to a TRON Base58Check address:
|
||||
//
|
||||
// body = 0x41 || keccak256(X||Y)[12:] // last 20 bytes of the Keccak hash
|
||||
// address = Base58( body || dsha256(body)[:4] )
|
||||
//
|
||||
// Note: TRON/Ethereum use *legacy* Keccak-256 (not the finalized SHA3-256).
|
||||
func PubKeyToTronAddress(uncompressed []byte) string {
|
||||
h := sha3.NewLegacyKeccak256()
|
||||
h.Write(uncompressed[1:]) // drop the 0x04 prefix; hash the 64-byte X||Y
|
||||
sum := h.Sum(nil)
|
||||
body := append([]byte{tronAddrPrefix}, sum[12:]...) // 0x41 + last 20 bytes
|
||||
return base58CheckEncode(body)
|
||||
}
|
||||
|
||||
// base58CheckEncode appends a 4-byte double-SHA256 checksum and Base58-encodes.
|
||||
// (TRON's version byte 0x41 is already inside input, so this is a plain
|
||||
// checksum-append, not btcutil's version-byte CheckEncode.)
|
||||
func base58CheckEncode(input []byte) string {
|
||||
first := sha256.Sum256(input)
|
||||
second := sha256.Sum256(first[:])
|
||||
full := make([]byte, 0, len(input)+4)
|
||||
full = append(full, input...)
|
||||
full = append(full, second[:4]...)
|
||||
return base58.Encode(full)
|
||||
}
|
||||
Reference in New Issue
Block a user