4393edf1d7
新建独立 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>
79 lines
3.0 KiB
Go
79 lines
3.0 KiB
Go
// 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)
|
|
}
|