279 lines
6.3 KiB
Go
279 lines
6.3 KiB
Go
// Package bip44 provides BIP44 address derivation using MPC public keys.
|
|
package bip44
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
"golang.org/x/crypto/ripemd160"
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
type CoinType uint32
|
|
|
|
const (
|
|
CoinTypeBitcoin CoinType = 0
|
|
CoinTypeEthereum CoinType = 60
|
|
CoinTypeCosmos CoinType = 118
|
|
CoinTypeSonr CoinType = 703
|
|
)
|
|
|
|
type ChainConfig struct {
|
|
CoinType CoinType
|
|
Prefix string
|
|
ChainID string
|
|
}
|
|
|
|
var DefaultChains = map[string]ChainConfig{
|
|
"bitcoin": {
|
|
CoinType: CoinTypeBitcoin,
|
|
Prefix: "bc1",
|
|
ChainID: "bitcoin-mainnet",
|
|
},
|
|
"ethereum": {
|
|
CoinType: CoinTypeEthereum,
|
|
Prefix: "0x",
|
|
ChainID: "1",
|
|
},
|
|
"sonr": {
|
|
CoinType: CoinTypeSonr,
|
|
Prefix: "snr",
|
|
ChainID: "sonr-testnet-1",
|
|
},
|
|
"cosmos": {
|
|
CoinType: CoinTypeCosmos,
|
|
Prefix: "cosmos",
|
|
ChainID: "cosmoshub-4",
|
|
},
|
|
}
|
|
|
|
func DeriveAddress(pubKeyBytes []byte, chain string) (string, error) {
|
|
config, ok := DefaultChains[chain]
|
|
if !ok {
|
|
return "", fmt.Errorf("bip44: unknown chain: %s", chain)
|
|
}
|
|
return DeriveAddressWithConfig(pubKeyBytes, config)
|
|
}
|
|
|
|
func DeriveAddressWithConfig(pubKeyBytes []byte, config ChainConfig) (string, error) {
|
|
if len(pubKeyBytes) == 0 {
|
|
return "", fmt.Errorf("bip44: public key cannot be empty")
|
|
}
|
|
|
|
switch config.CoinType {
|
|
case CoinTypeBitcoin:
|
|
return deriveBitcoinAddress(pubKeyBytes, config.Prefix)
|
|
case CoinTypeEthereum:
|
|
return deriveEthereumAddress(pubKeyBytes)
|
|
case CoinTypeCosmos, CoinTypeSonr:
|
|
return deriveCosmosAddress(pubKeyBytes, config.Prefix)
|
|
default:
|
|
return "", fmt.Errorf("bip44: unsupported coin type: %d", config.CoinType)
|
|
}
|
|
}
|
|
|
|
// deriveBitcoinAddress creates P2WPKH (native SegWit) address using SHA256+RIPEMD160
|
|
func deriveBitcoinAddress(pubKeyBytes []byte, prefix string) (string, error) {
|
|
compressed := ensureCompressed(pubKeyBytes)
|
|
if compressed == nil {
|
|
return "", fmt.Errorf("bip44: invalid public key length: %d", len(pubKeyBytes))
|
|
}
|
|
|
|
sha256Hash := sha256.Sum256(compressed)
|
|
ripemd := ripemd160.New()
|
|
ripemd.Write(sha256Hash[:])
|
|
pubKeyHash := ripemd.Sum(nil)
|
|
|
|
return bech32Encode(prefix, pubKeyHash)
|
|
}
|
|
|
|
// deriveEthereumAddress creates address using Keccak256 of uncompressed pubkey
|
|
func deriveEthereumAddress(pubKeyBytes []byte) (string, error) {
|
|
var keyData []byte
|
|
|
|
switch len(pubKeyBytes) {
|
|
case 65:
|
|
keyData = pubKeyBytes[1:] // strip 0x04 prefix
|
|
case 64:
|
|
keyData = pubKeyBytes
|
|
default:
|
|
return "", fmt.Errorf("bip44: ethereum requires uncompressed key (64 or 65 bytes), got %d", len(pubKeyBytes))
|
|
}
|
|
|
|
hash := sha3.NewLegacyKeccak256()
|
|
hash.Write(keyData)
|
|
hashBytes := hash.Sum(nil)
|
|
|
|
return "0x" + hex.EncodeToString(hashBytes[12:]), nil
|
|
}
|
|
|
|
// deriveCosmosAddress creates bech32 address using SHA256+RIPEMD160
|
|
func deriveCosmosAddress(pubKeyBytes []byte, prefix string) (string, error) {
|
|
compressed := ensureCompressed(pubKeyBytes)
|
|
if compressed == nil {
|
|
return "", fmt.Errorf("bip44: invalid public key length: %d", len(pubKeyBytes))
|
|
}
|
|
|
|
sha256Hash := sha256.Sum256(compressed)
|
|
ripemd := ripemd160.New()
|
|
ripemd.Write(sha256Hash[:])
|
|
addressBytes := ripemd.Sum(nil)
|
|
|
|
return bech32Encode(prefix, addressBytes)
|
|
}
|
|
|
|
func ensureCompressed(pubKeyBytes []byte) []byte {
|
|
switch len(pubKeyBytes) {
|
|
case 33:
|
|
return pubKeyBytes
|
|
case 65:
|
|
return compressPublicKey(pubKeyBytes)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func compressPublicKey(uncompressed []byte) []byte {
|
|
if len(uncompressed) != 65 {
|
|
return uncompressed
|
|
}
|
|
|
|
x := uncompressed[1:33]
|
|
y := uncompressed[33:65]
|
|
|
|
compressed := make([]byte, 33)
|
|
if y[31]&1 == 0 {
|
|
compressed[0] = 0x02
|
|
} else {
|
|
compressed[0] = 0x03
|
|
}
|
|
copy(compressed[1:], x)
|
|
|
|
return compressed
|
|
}
|
|
|
|
func bech32Encode(hrp string, data []byte) (string, error) {
|
|
converted, err := convertBits(data, 8, 5, true)
|
|
if err != nil {
|
|
return "", fmt.Errorf("bip44: convert bits: %w", err)
|
|
}
|
|
return encode(hrp, converted)
|
|
}
|
|
|
|
func convertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) {
|
|
acc := uint32(0)
|
|
bits := uint8(0)
|
|
maxv := uint32(1<<toBits) - 1
|
|
result := make([]byte, 0, len(data)*int(fromBits)/int(toBits)+1)
|
|
|
|
for _, b := range data {
|
|
acc = (acc << fromBits) | uint32(b)
|
|
bits += fromBits
|
|
for bits >= toBits {
|
|
bits -= toBits
|
|
result = append(result, byte((acc>>bits)&maxv))
|
|
}
|
|
}
|
|
|
|
if pad {
|
|
if bits > 0 {
|
|
result = append(result, byte((acc<<(toBits-bits))&maxv))
|
|
}
|
|
} else if bits >= fromBits {
|
|
return nil, fmt.Errorf("illegal zero padding")
|
|
} else if ((acc << (toBits - bits)) & maxv) != 0 {
|
|
return nil, fmt.Errorf("non-zero padding")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
|
|
func encode(hrp string, data []byte) (string, error) {
|
|
checksum := createChecksum(hrp, data)
|
|
combined := append(data, checksum...)
|
|
|
|
result := make([]byte, len(hrp)+1+len(combined))
|
|
copy(result, hrp)
|
|
result[len(hrp)] = '1'
|
|
|
|
for i, b := range combined {
|
|
result[len(hrp)+1+i] = charset[b]
|
|
}
|
|
|
|
return string(result), nil
|
|
}
|
|
|
|
func createChecksum(hrp string, data []byte) []byte {
|
|
values := append(hrpExpand(hrp), data...)
|
|
values = append(values, []byte{0, 0, 0, 0, 0, 0}...)
|
|
polymod := polymod(values) ^ 1
|
|
|
|
checksum := make([]byte, 6)
|
|
for i := 0; i < 6; i++ {
|
|
checksum[i] = byte((polymod >> (5 * (5 - i))) & 31)
|
|
}
|
|
return checksum
|
|
}
|
|
|
|
func hrpExpand(hrp string) []byte {
|
|
result := make([]byte, len(hrp)*2+1)
|
|
for i, c := range hrp {
|
|
result[i] = byte(c >> 5)
|
|
result[i+len(hrp)+1] = byte(c & 31)
|
|
}
|
|
result[len(hrp)] = 0
|
|
return result
|
|
}
|
|
|
|
func polymod(values []byte) uint32 {
|
|
gen := []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
|
|
chk := uint32(1)
|
|
for _, v := range values {
|
|
top := chk >> 25
|
|
chk = (chk&0x1ffffff)<<5 ^ uint32(v)
|
|
for i := 0; i < 5; i++ {
|
|
if (top>>i)&1 == 1 {
|
|
chk ^= gen[i]
|
|
}
|
|
}
|
|
}
|
|
return chk
|
|
}
|
|
|
|
type Account struct {
|
|
Address string
|
|
ChainID string
|
|
CoinType CoinType
|
|
AccountIndex uint32
|
|
AddressIndex uint32
|
|
}
|
|
|
|
func DeriveAccounts(pubKeyBytes []byte, chains []string) ([]Account, error) {
|
|
accounts := make([]Account, 0, len(chains))
|
|
|
|
for _, chain := range chains {
|
|
config, ok := DefaultChains[chain]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
addr, err := DeriveAddressWithConfig(pubKeyBytes, config)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
accounts = append(accounts, Account{
|
|
Address: addr,
|
|
ChainID: config.ChainID,
|
|
CoinType: config.CoinType,
|
|
AccountIndex: 0,
|
|
AddressIndex: 0,
|
|
})
|
|
}
|
|
|
|
return accounts, nil
|
|
}
|