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>
This commit is contained in:
wangjia
2026-07-09 04:12:04 +08:00
parent 4bb92209ca
commit 7cf87a764f
11 changed files with 742 additions and 4 deletions
+39
View File
@@ -0,0 +1,39 @@
package wallet
import (
"bytes"
"crypto/sha256"
"fmt"
"github.com/btcsuite/btcd/btcutil/base58"
)
// DecodeTronAddress decodes a base58check TRON address ("T...") to its 21-byte
// payload (0x41 || 20-byte body), validating the checksum. The 20-byte body
// (payload[1:]) is what TRON ABI parameters use (left-padded to 32 bytes).
func DecodeTronAddress(addr string) ([]byte, error) {
raw := base58.Decode(addr)
if len(raw) != 25 { // 21 payload + 4 checksum
return nil, fmt.Errorf("wallet: bad TRON address length %d", len(raw))
}
payload, sum := raw[:21], raw[21:]
h1 := sha256.Sum256(payload)
h2 := sha256.Sum256(h1[:])
if !bytes.Equal(h2[:4], sum) {
return nil, fmt.Errorf("wallet: bad TRON address checksum")
}
if payload[0] != tronAddrPrefix {
return nil, fmt.Errorf("wallet: bad TRON address prefix 0x%02x", payload[0])
}
return payload, nil
}
// TronAddressBodyHex returns the 20-byte address body as hex (no 0x41 prefix) —
// used to build/verify ABI-encoded transfer recipients.
func TronAddressBodyHex(addr string) (string, error) {
payload, err := DecodeTronAddress(addr)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", payload[1:]), nil
}
+39
View File
@@ -0,0 +1,39 @@
package wallet
import "testing"
func TestDecodeTronAddressRoundtrip(t *testing.T) {
// Golden addresses from the test mnemonic (see derive_test.go).
for _, addr := range goldenAddrs {
payload, err := DecodeTronAddress(addr)
if err != nil {
t.Fatalf("decode %s: %v", addr, err)
}
if len(payload) != 21 || payload[0] != 0x41 {
t.Fatalf("bad payload for %s: %x", addr, payload)
}
// Re-encode the payload and expect the same address back.
if got := base58CheckEncode(payload); got != addr {
t.Fatalf("roundtrip: %s -> %s", addr, got)
}
}
}
func TestDecodeTronAddressRejectsBad(t *testing.T) {
if _, err := DecodeTronAddress("TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdX"); err == nil {
t.Fatal("expected checksum failure on tampered address")
}
if _, err := DecodeTronAddress("not-an-address"); err == nil {
t.Fatal("expected failure on garbage")
}
}
func TestTronAddressBodyHex(t *testing.T) {
body, err := TronAddressBodyHex(goldenAddrs[0])
if err != nil {
t.Fatalf("body hex: %v", err)
}
if len(body) != 40 { // 20 bytes -> 40 hex chars
t.Fatalf("body hex len %d, want 40 (%s)", len(body), body)
}
}
+23
View File
@@ -84,3 +84,26 @@ func PrivKeyHexFromMnemonic(mnemonic, passphrase string, account, change, index
}
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
}