Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(c32): derive stacks address #28

Merged
merged 2 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 111 additions & 34 deletions pkg/c32/c32.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package c32

import (
"encoding/base32"
"bytes"
"crypto/sha256"
"errors"
"fmt"
"math/big"
Expand All @@ -12,9 +13,35 @@ import (

var crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

func C32Encode(input []byte) string {
encoder := base32.NewEncoding(crockfordAlphabet).WithPadding(base32.NoPadding)
return encoder.EncodeToString(input)
func C32Encode(data []byte) string {
// Convert bytes to big.Int
bi := new(big.Int).SetBytes(data)

// Convert big.Int to base32 string
var encoded strings.Builder
for bi.Cmp(big.NewInt(0)) > 0 {
mod := new(big.Int)
bi.DivMod(bi, big.NewInt(32), mod)
encoded.WriteByte(crockfordAlphabet[mod.Int64()])
}

// Reverse the string
encodedStr := reverseString(encoded.String())

// Handle leading zeros
leadingZeros := 0
for _, b := range data {
if b == 0 {
leadingZeros++
} else {
break
}
}
for i := 0; i < leadingZeros; i++ {
encodedStr = "0" + encodedStr
}

return encodedStr
}

func C32Decode(input string) ([]byte, error) {
Expand Down Expand Up @@ -64,51 +91,101 @@ func DecodeC32Address(address string) (version byte, hash160 [20]byte, err error
return version, hash160, nil
}

func SerializeAddress(address string) ([]byte, error) {
if len(address) != 1+20*2 { // 'S' + version + 40 hex chars
return nil, fmt.Errorf("invalid address length: %d", len(address))
func DecodeWithChecksum(c32addr string) (byte, []byte, error) {
if len(c32addr) < 2 {
return 0, nil, errors.New("address too short")
}
if c32addr[0] != 'S' {
return 0, nil, errors.New("address must start with 'S'")
}

var version byte
switch address[0] {
case 'S':
version = byte(stacks.AddressVersionMainnetSingleSig)
case 'T':
version = byte(stacks.AddressVersionTestnetSingleSig)
default:
return nil, fmt.Errorf("invalid address version: %c", address[0])
// Extract and decode the version character
versionChar := c32addr[1]
version := byte(strings.IndexRune(crockfordAlphabet, rune(versionChar)))
if version == 255 { // strings.IndexRune returns -1 if not found, which becomes 255 as byte
return 0, nil, errors.New("invalid version character")
}

hashBytes, err := C32Decode(address[1:])
// Decode the remaining C32 string
c32str := c32addr[2:]
data, err := C32Decode(c32str)
if err != nil {
return nil, fmt.Errorf("invalid address hash: %v", err)
return 0, nil, err
}

// Expected length: data (20 bytes) + checksum (4 bytes)
expectedLength := stacks.AddressHashLength + 4
if len(data) != expectedLength {
return 0, nil, fmt.Errorf("invalid decoded length: expected %d, got %d", expectedLength, len(data))
}

payload := data[:stacks.AddressHashLength]
checksum := data[stacks.AddressHashLength:]

// Recompute checksum
versionedData := append([]byte{version}, payload...)
computedChecksum := sha256.Sum256(versionedData)
computedChecksum = sha256.Sum256(computedChecksum[:])
computedChecksumBytes := computedChecksum[:4]

// Compare checksums
if !bytes.Equal(checksum, computedChecksumBytes) {
return 0, nil, errors.New("checksum mismatch")
}

result := make([]byte, 1+len(hashBytes))
result[0] = version
copy(result[1:], hashBytes)
return version, payload, nil
}

func EncodeWithChecksum(version byte, data []byte) (string, error) {
if len(data) != stacks.AddressHashLength {
return "", errors.New("data must be 20 bytes for P2PKH")
}

// Version byte + data
versionedData := append([]byte{version}, data...)

// Compute checksum: double SHA256, first 4 bytes
checksum := sha256.Sum256(versionedData)
checksum = sha256.Sum256(checksum[:])
checksumBytes := checksum[:4]

return result, nil
// Append checksum
fullData := append(data, checksumBytes...)

// Encode to c32
c32str := C32Encode(fullData)

// Add prefix 'S'
return "S" + string(crockfordAlphabet[version]) + c32str, nil
}

func SerializeAddress(version stacks.AddressVersion, hash160 []byte) (string, error) {
return EncodeWithChecksum(byte(version), hash160)
}

func DeserializeAddress(data []byte) (string, int, error) {
if len(data) < 1+stacks.AddressHashLength {
return "", 0, errors.New("insufficient data for address")
func DeserializeAddress(address string) (stacks.AddressVersion, []byte, error) {
version, payload, err := DecodeWithChecksum(address)
if err != nil {
return 0, nil, err
}

version := stacks.AddressVersion(data[0])
var prefix string
var addrVersion stacks.AddressVersion
switch version {
case stacks.AddressVersionMainnetSingleSig:
prefix = "S"
case stacks.AddressVersionTestnetSingleSig:
prefix = "T"
case byte(stacks.AddressVersionMainnetSingleSig):
addrVersion = stacks.AddressVersionMainnetSingleSig
case byte(stacks.AddressVersionTestnetSingleSig):
addrVersion = stacks.AddressVersionTestnetSingleSig
default:
return "", 0, fmt.Errorf("invalid address version: %d", version)
return 0, nil, fmt.Errorf("unknown address version: %d", version)
}

c32hash := C32Encode(data[1 : 1+stacks.AddressHashLength+5])
address := fmt.Sprintf("%s%s", prefix, c32hash)
return addrVersion, payload, nil
}

return address, 1 + stacks.AddressHashLength + 5, nil
func reverseString(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
27 changes: 27 additions & 0 deletions pkg/c32/c32_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/hex"
"reflect"
"testing"

"github.com/icon-project/stacks-go-sdk/pkg/stacks"
)

func TestC32Decode(t *testing.T) {
Expand Down Expand Up @@ -86,3 +88,28 @@ func TestDecodeC32Address(t *testing.T) {
})
}
}

func TestC32CheckEncodeDecode(t *testing.T) {
hash160, err := hex.DecodeString("1a2b3c4d5e6f7081920a1b2c3d4e5f60718293a4")
if err != nil {
t.Fatalf("Failed to decode hash160: %v", err)
}

address, err := SerializeAddress(stacks.AddressVersionMainnetSingleSig, hash160)
if err != nil {
t.Fatalf("Failed to serialize address: %v", err)
}

version, decodedHash, err := DeserializeAddress(address)
if err != nil {
t.Fatalf("Failed to deserialize address: %v", err)
}

if version != stacks.AddressVersionMainnetSingleSig {
t.Errorf("Expected version %d, got %d", stacks.AddressVersionMainnetSingleSig, version)
}

if string(decodedHash) != string(hash160) {
t.Errorf("Decoded hash160 does not match original")
}
}
49 changes: 49 additions & 0 deletions pkg/crypto/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/icon-project/stacks-go-sdk/pkg/c32"
"github.com/icon-project/stacks-go-sdk/pkg/stacks"
)

Expand Down Expand Up @@ -65,6 +66,54 @@ func GetPublicKeyFromPrivate(privateKey []byte) []byte {
return pubKey.SerializeCompressed()
}

func GetAddressFromPrivateKey(privateKey []byte, network stacks.ChainID) (string, error) {
if len(privateKey) != 33 {
return "", errors.New("private key must be 33 bytes")
}

// Derive public key
privKey, pubKey := btcec.PrivKeyFromBytes(privateKey)
if privKey == nil || pubKey == nil {
return "", errors.New("invalid private key")
}

compressedPubKey := pubKey.SerializeCompressed()
address, err := CalculateStacksAddress(compressedPubKey, network)
if err != nil {
return "", err
}

return address, nil
}

func CalculateStacksAddress(pubKey []byte, network stacks.ChainID) (string, error) {
if len(pubKey) != 33 {
return "", errors.New("public key must be 33 bytes (compressed)")
}

// Perform SHA-256 hashing
pubKeyHash := Hash160(pubKey)

// Determine version based on network
var version stacks.AddressVersion
switch network {
case stacks.ChainIDMainnet:
version = stacks.AddressVersionMainnetSingleSig
case stacks.ChainIDTestnet:
version = stacks.AddressVersionTestnetSingleSig
default:
return "", fmt.Errorf("unsupported network: %d", network)
}

// Encode to c32check address
address, err := c32.SerializeAddress(version, pubKeyHash)
if err != nil {
return "", fmt.Errorf("failed to serialize address: %w", err)
}

return address, nil
}

func VerifySignature(messageHash string, signature MessageSignature, publicKey []byte) (bool, error) {
messageHashBytes, err := hex.DecodeString(messageHash)
if err != nil {
Expand Down
68 changes: 68 additions & 0 deletions pkg/crypto/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,71 @@ func TestSignWithKey2(t *testing.T) {
t.Fatalf("SignWithKey signature mismatch. Got %s, want %s", signature.Data, expectedSignature)
}
}

func TestCalculateStacksAddress(t *testing.T) {
tests := []struct {
name string
publicKeyHex string
network stacks.ChainID
expectedAddress string
}{
{
name: "Testnet Address",
publicKeyHex: "0332fc778e5beb5f944c75b2b63c21dd12c40bdcdf99ba0663168ae0b2be880aef",
network: stacks.ChainIDTestnet,
expectedAddress: "ST15C893XJFJ6FSKM020P9JQDB5T7X6MQTXMBPAVH",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
publicKeyBytes, err := hex.DecodeString(tt.publicKeyHex)
if err != nil {
t.Fatalf("Failed to decode public key hex: %v", err)
}

address, err := CalculateStacksAddress(publicKeyBytes, tt.network)
if err != nil {
t.Fatalf("CalculateStacksAddress failed: %v", err)
}

if address != tt.expectedAddress {
t.Errorf("CalculateStacksAddress mismatch.\nGot: %s\nWant: %s", address, tt.expectedAddress)
}
})
}
}

func TestGetAddressFromPrivateKey(t *testing.T) {
tests := []struct {
name string
privateKeyHex string
network stacks.ChainID
expectedAddress string
}{
{
name: "Mainnet Address from Private Key",
privateKeyHex: "c1d5bb638aa70862621667f9997711fce692cad782694103f8d9561f62e9f19701",
network: stacks.ChainIDTestnet,
expectedAddress: "ST15C893XJFJ6FSKM020P9JQDB5T7X6MQTXMBPAVH",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
privateKeyBytes, err := hex.DecodeString(tt.privateKeyHex)
if err != nil {
t.Fatalf("Failed to decode private key hex: %v", err)
}

address, err := GetAddressFromPrivateKey(privateKeyBytes, tt.network)
if err != nil {
t.Fatalf("GetAddressFromPrivateKey failed: %v", err)
}

if address != tt.expectedAddress {
t.Errorf("GetAddressFromPrivateKey mismatch.\nGot: %s\nWant: %s", address, tt.expectedAddress)
}
})
}
}
Loading