fix(wallet): 修复 BIP84 bc1q 地址无法关联助记词 + 过滤加密 keystore 产生的假地址
Bug1: DarkSwordIngestAdapter::harvestAddresses 对加密 keystore 文本跑地址正则, 会把 xpub 子串/hex IV 误识别为地址。新增 EthAddress/TronAddress/BtcAddress isValid 校验,拒绝假地址入库。 Bug2: BtcDriver 只用 BIP44 推导 P2PKH 旧地址(1开头),Trust Wallet 实际用 BIP84 推导 Native SegWit bech32 地址(bc1q开头),导致 MnemonicAddressLinker 无法关联。新增 BtcDriver::deriveAddressBip84 + BtcAddress::p2wpkhFromCompressedPublicKey, MnemonicAddressLinker 同时匹配 BIP44/BIP84。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,6 +31,22 @@ final class BtcAddress
|
||||
return TronAddress::hexToBase58Check('00'.bin2hex($hash160));
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a Native SegWit (P2WPKH, bech32) address from a compressed
|
||||
* secp256k1 public key. Used for BIP84 derivation paths.
|
||||
*/
|
||||
public static function p2wpkhFromCompressedPublicKey(string $compressedHex): string
|
||||
{
|
||||
$compressedHex = strtolower(trim($compressedHex));
|
||||
$pub = hex2bin($compressedHex);
|
||||
if ($pub === false || strlen($pub) !== 33) {
|
||||
throw new RuntimeException('Expected compressed secp256k1 public key');
|
||||
}
|
||||
$hash160 = hash('ripemd160', hash('sha256', $pub, true), true);
|
||||
|
||||
return self::bech32Encode('bc', 0, bin2hex($hash160));
|
||||
}
|
||||
|
||||
public static function isValid(string $address): bool
|
||||
{
|
||||
$address = trim($address);
|
||||
@@ -170,6 +186,52 @@ final class BtcAddress
|
||||
return ['version' => $version, 'program' => $program];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a witness program as a bech32 address.
|
||||
*
|
||||
* @param string $hrp Human-readable part ('bc' or 'tb')
|
||||
* @param int $witver Witness version (0 for P2WPKH/P2WSH)
|
||||
* @param string $programHex Witness program as hex string
|
||||
*/
|
||||
private static function bech32Encode(string $hrp, int $witver, string $programHex): string
|
||||
{
|
||||
$charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
||||
$bytes = array_map('hexdec', str_split($programHex, 2));
|
||||
$values = [$witver];
|
||||
$bits = '';
|
||||
foreach ($bytes as $b) {
|
||||
$bits .= str_pad(decbin($b), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
$bits = str_pad($bits, (int) ceil(strlen($bits) / 5) * 5, '0', STR_PAD_RIGHT);
|
||||
for ($i = 0; $i < strlen($bits); $i += 5) {
|
||||
$values[] = bindec(substr($bits, $i, 5));
|
||||
}
|
||||
$values = array_merge($values, self::bech32Checksum($hrp, $values));
|
||||
$result = $hrp.'1';
|
||||
foreach ($values as $v) {
|
||||
$result .= $charset[$v];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @param list<int> $values */
|
||||
private static function bech32Checksum(string $hrp, array $values): array
|
||||
{
|
||||
$polymod = self::bech32Polymod(array_merge(
|
||||
self::bech32HrpExpand($hrp),
|
||||
$values,
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
));
|
||||
$polymod ^= 1;
|
||||
$ret = [];
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$ret[] = ($polymod >> 5 * (5 - $i)) & 31;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/** @param list<int> $values */
|
||||
private static function bech32Verify(string $hrp, array $values): bool
|
||||
{
|
||||
|
||||
@@ -21,6 +21,18 @@ class BtcDriver implements ChainDriver
|
||||
return BtcAddress::fromPrivateKey($derived['private_key']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a Native SegWit (BIP84, bech32 bc1q) address.
|
||||
* Trust Wallet uses BIP84 for Bitcoin wallets.
|
||||
*/
|
||||
public function deriveAddressBip84(string $mnemonic, int $index = 0): string
|
||||
{
|
||||
$derived = Bip44::derive($mnemonic, $this->pathBip84($index));
|
||||
$compressed = BtcAddress::compressedPublicKey($derived['private_key']);
|
||||
|
||||
return BtcAddress::p2wpkhFromCompressedPublicKey($compressed);
|
||||
}
|
||||
|
||||
public function sendNative(string $mnemonic, int $index, string $to, string $amount): string
|
||||
{
|
||||
if (! $this->isValidAddress($to)) {
|
||||
@@ -204,6 +216,11 @@ class BtcDriver implements ChainDriver
|
||||
return "m/44'/0'/0'/0/{$index}";
|
||||
}
|
||||
|
||||
private function pathBip84(int $index): string
|
||||
{
|
||||
return "m/84'/0'/0'/0/{$index}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{txid: string, vout: int, value: int, scriptpubkey: string}>
|
||||
*/
|
||||
@@ -474,6 +491,6 @@ class BtcDriver implements ChainDriver
|
||||
|
||||
private function http(): PendingRequest
|
||||
{
|
||||
return Http::connectTimeout(20)->timeout(120)->acceptJson();
|
||||
return Http::timeout(30)->acceptJson();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user