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>
100 lines
3.6 KiB
Go
100 lines
3.6 KiB
Go
package wallet
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// testMnemonic is the canonical all-zero-entropy BIP39 test vector. The derived
|
|
// TRON addresses logged by TestAccountXpubDeriveConsistency must match
|
|
// iancoleman.io/bip39 (Coin = TRX, BIP44) — that manual comparison is Phase A.4
|
|
// of the crypto-tx-engine plan (guards against a derivation mismatch that would
|
|
// silently send funds to addresses we don't control).
|
|
const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
|
|
// TestAccountXpubDeriveConsistency proves the watcher's watch-only path
|
|
// (xpub -> address) yields exactly the same address as the offline private path
|
|
// (seed -> privkey -> pubkey -> address) for each index. That equality is what
|
|
// guarantees every receiving address the watcher hands out is spendable by the
|
|
// key we hold in cold storage.
|
|
func TestAccountXpubDeriveConsistency(t *testing.T) {
|
|
xpub, err := AccountXpubFromMnemonic(testMnemonic, "", 0)
|
|
if err != nil {
|
|
t.Fatalf("account xpub: %v", err)
|
|
}
|
|
t.Logf("account xpub (m/44'/195'/0'): %s", xpub)
|
|
|
|
acct, err := AccountKeyFromMnemonic(testMnemonic, "", 0)
|
|
if err != nil {
|
|
t.Fatalf("account key: %v", err)
|
|
}
|
|
|
|
for i := uint32(0); i < 5; i++ {
|
|
viaXpub, err := AddressFromAccountXpub(xpub, 0, i) // what the watcher does
|
|
if err != nil {
|
|
t.Fatalf("via xpub [%d]: %v", i, err)
|
|
}
|
|
|
|
ck, err := acct.Derive(0)
|
|
if err != nil {
|
|
t.Fatalf("derive change: %v", err)
|
|
}
|
|
ak, err := ck.Derive(i)
|
|
if err != nil {
|
|
t.Fatalf("derive index %d: %v", i, err)
|
|
}
|
|
pub, err := ak.ECPubKey()
|
|
if err != nil {
|
|
t.Fatalf("ec pubkey: %v", err)
|
|
}
|
|
viaPriv := PubKeyToTronAddress(pub.SerializeUncompressed())
|
|
|
|
if viaXpub != viaPriv {
|
|
t.Fatalf("index %d: xpub-derived %q != priv-derived %q", i, viaXpub, viaPriv)
|
|
}
|
|
if !strings.HasPrefix(viaXpub, "T") || len(viaXpub) != 34 {
|
|
t.Fatalf("index %d: not a valid TRON address: %q", i, viaXpub)
|
|
}
|
|
t.Logf("m/44'/195'/0'/0/%d -> %s", i, viaXpub)
|
|
}
|
|
}
|
|
|
|
// TestKnownVector locks the derivation to a golden result (filled from the run of
|
|
// TestAccountXpubDeriveConsistency, then confirmed against iancoleman.io — A.4).
|
|
// If this ever changes, the derivation implementation regressed.
|
|
func TestKnownVector(t *testing.T) {
|
|
if len(goldenAddrs) == 0 {
|
|
t.Skip("golden vector not yet baked — run TestAccountXpubDeriveConsistency, confirm vs Ian Coleman, then fill goldenAddrs")
|
|
}
|
|
xpub, err := AccountXpubFromMnemonic(testMnemonic, "", 0)
|
|
if err != nil {
|
|
t.Fatalf("xpub: %v", err)
|
|
}
|
|
if goldenXpub != "" && xpub != goldenXpub {
|
|
t.Fatalf("account xpub changed:\n got %s\n want %s", xpub, goldenXpub)
|
|
}
|
|
for i, want := range goldenAddrs {
|
|
got, err := AddressFromAccountXpub(xpub, 0, uint32(i))
|
|
if err != nil {
|
|
t.Fatalf("addr[%d]: %v", i, err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("addr[%d]: got %s want %s", i, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Golden vector for the "abandon…about" test mnemonic, Coin=TRX, m/44'/195'/0'.
|
|
// Locks the derivation against regression. ⚠️ MUST be confirmed once against
|
|
// iancoleman.io/bip39 (Phase A.4) — self-consistency (TestAccountXpubDeriveConsistency)
|
|
// proves the xpub and private paths agree, but only an independent tool proves
|
|
// both aren't wrong the same way. If Ian Coleman disagrees, the impl has a bug.
|
|
var (
|
|
goldenXpub = "xpub6D1AabNHCupeiLM65ZR9UStMhJ1vCpyV4XbZdyhMZBiJXALQtmn9p42VTQckoHVn8WNqS7dqnJokZHAHcHGoaQgmv8D45oNUKx6DZMNZBCd"
|
|
goldenAddrs = []string{
|
|
"TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", // m/44'/195'/0'/0/0
|
|
"TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK", // m/44'/195'/0'/0/1
|
|
"TYJPRrdB5APNeRs4R7fYZSwW3TcrTKw2gx", // m/44'/195'/0'/0/2
|
|
}
|
|
)
|