Skip to content

Commit 10cad8d

Browse files
committed
feat(c32): derive stacks address
1 parent f1dd8fa commit 10cad8d

File tree

4 files changed

+252
-35
lines changed

4 files changed

+252
-35
lines changed

pkg/c32/c32.go

+106-35
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package c32
22

33
import (
4-
"encoding/base32"
4+
"crypto/sha256"
55
"errors"
66
"fmt"
77
"math/big"
@@ -12,9 +12,35 @@ import (
1212

1313
var crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
1414

15-
func C32Encode(input []byte) string {
16-
encoder := base32.NewEncoding(crockfordAlphabet).WithPadding(base32.NoPadding)
17-
return encoder.EncodeToString(input)
15+
func C32Encode(data []byte) string {
16+
// Convert bytes to big.Int
17+
bi := new(big.Int).SetBytes(data)
18+
19+
// Convert big.Int to base32 string
20+
var encoded strings.Builder
21+
for bi.Cmp(big.NewInt(0)) > 0 {
22+
mod := new(big.Int)
23+
bi.DivMod(bi, big.NewInt(32), mod)
24+
encoded.WriteByte(crockfordAlphabet[mod.Int64()])
25+
}
26+
27+
// Reverse the string
28+
encodedStr := reverseString(encoded.String())
29+
30+
// Handle leading zeros
31+
leadingZeros := 0
32+
for _, b := range data {
33+
if b == 0 {
34+
leadingZeros++
35+
} else {
36+
break
37+
}
38+
}
39+
for i := 0; i < leadingZeros; i++ {
40+
encodedStr = "0" + encodedStr
41+
}
42+
43+
return encodedStr
1844
}
1945

2046
func C32Decode(input string) ([]byte, error) {
@@ -64,51 +90,96 @@ func DecodeC32Address(address string) (version byte, hash160 [20]byte, err error
6490
return version, hash160, nil
6591
}
6692

67-
func SerializeAddress(address string) ([]byte, error) {
68-
if len(address) != 1+20*2 { // 'S' + version + 40 hex chars
69-
return nil, fmt.Errorf("invalid address length: %d", len(address))
93+
func DecodeWithChecksum(c32addr string) (byte, []byte, error) {
94+
if len(c32addr) < 1 {
95+
return 0, nil, errors.New("address too short")
7096
}
71-
72-
var version byte
73-
switch address[0] {
74-
case 'S':
75-
version = byte(stacks.AddressVersionMainnetSingleSig)
76-
case 'T':
77-
version = byte(stacks.AddressVersionTestnetSingleSig)
78-
default:
79-
return nil, fmt.Errorf("invalid address version: %c", address[0])
97+
if c32addr[0] != 'S' {
98+
return 0, nil, errors.New("address must start with 'S'")
8099
}
81100

82-
hashBytes, err := C32Decode(address[1:])
101+
c32str := c32addr[1:]
102+
103+
data, err := C32Decode(c32str)
83104
if err != nil {
84-
return nil, fmt.Errorf("invalid address hash: %v", err)
105+
return 0, nil, err
106+
}
107+
108+
if len(data) != 1+stacks.AddressHashLength+4 {
109+
return 0, nil, fmt.Errorf("invalid decoded length: expected %d, got %d", 1+stacks.AddressHashLength+4, len(data))
110+
}
111+
112+
version := data[0]
113+
payload := data[1 : 1+stacks.AddressHashLength]
114+
checksum := data[1+stacks.AddressHashLength:]
115+
116+
// Recompute checksum
117+
versionedData := append([]byte{version}, payload...)
118+
computedChecksum := sha256.Sum256(versionedData)
119+
computedChecksum = sha256.Sum256(computedChecksum[:])
120+
computedChecksum = sha256.Sum256(computedChecksum[:])
121+
computedChecksumBytes := computedChecksum[:4]
122+
123+
// Compare checksums
124+
for i := 0; i < 4; i++ {
125+
if checksum[i] != computedChecksumBytes[i] {
126+
return 0, nil, errors.New("checksum mismatch")
127+
}
85128
}
86129

87-
result := make([]byte, 1+len(hashBytes))
88-
result[0] = version
89-
copy(result[1:], hashBytes)
130+
return version, payload, nil
131+
}
132+
133+
func EncodeWithChecksum(version byte, data []byte) (string, error) {
134+
if len(data) != stacks.AddressHashLength {
135+
return "", errors.New("data must be 20 bytes for P2PKH")
136+
}
137+
138+
// Version byte + data
139+
versionedData := append([]byte{version}, data...)
140+
141+
// Compute checksum: double SHA256, first 4 bytes
142+
checksum := sha256.Sum256(versionedData)
143+
checksum = sha256.Sum256(checksum[:])
144+
checksumBytes := checksum[:4]
90145

91-
return result, nil
146+
// Append checksum
147+
fullData := append(data, checksumBytes...)
148+
149+
// Encode to c32
150+
c32str := C32Encode(fullData)
151+
152+
// Add prefix 'S'
153+
return "S" + string(crockfordAlphabet[version]) + c32str, nil
154+
}
155+
156+
func SerializeAddress(version stacks.AddressVersion, hash160 []byte) (string, error) {
157+
return EncodeWithChecksum(byte(version), hash160)
92158
}
93159

94-
func DeserializeAddress(data []byte) (string, int, error) {
95-
if len(data) < 1+stacks.AddressHashLength {
96-
return "", 0, errors.New("insufficient data for address")
160+
func DeserializeAddress(address string) (stacks.AddressVersion, []byte, error) {
161+
version, payload, err := DecodeWithChecksum(address)
162+
if err != nil {
163+
return 0, nil, err
97164
}
98165

99-
version := stacks.AddressVersion(data[0])
100-
var prefix string
166+
var addrVersion stacks.AddressVersion
101167
switch version {
102-
case stacks.AddressVersionMainnetSingleSig:
103-
prefix = "S"
104-
case stacks.AddressVersionTestnetSingleSig:
105-
prefix = "T"
168+
case byte(stacks.AddressVersionMainnetSingleSig):
169+
addrVersion = stacks.AddressVersionMainnetSingleSig
170+
case byte(stacks.AddressVersionTestnetSingleSig):
171+
addrVersion = stacks.AddressVersionTestnetSingleSig
106172
default:
107-
return "", 0, fmt.Errorf("invalid address version: %d", version)
173+
return 0, nil, fmt.Errorf("unknown address version: %d", version)
108174
}
109175

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

113-
return address, 1 + stacks.AddressHashLength + 5, nil
179+
func reverseString(s string) string {
180+
runes := []rune(s)
181+
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
182+
runes[i], runes[j] = runes[j], runes[i]
183+
}
184+
return string(runes)
114185
}

pkg/c32/c32_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"encoding/hex"
55
"reflect"
66
"testing"
7+
8+
"github.com/icon-project/stacks-go-sdk/pkg/stacks"
79
)
810

911
func TestC32Decode(t *testing.T) {
@@ -86,3 +88,30 @@ func TestDecodeC32Address(t *testing.T) {
8688
})
8789
}
8890
}
91+
92+
func TestC32CheckEncodeDecode(t *testing.T) {
93+
hash160, err := hex.DecodeString("1a2b3c4d5e6f7081920a1b2c3d4e5f60718293a4")
94+
if err != nil {
95+
t.Fatalf("Failed to decode hash160: %v", err)
96+
}
97+
98+
address, err := SerializeAddress(stacks.AddressVersionMainnetSingleSig, hash160)
99+
if err != nil {
100+
t.Fatalf("Failed to serialize address: %v", err)
101+
}
102+
103+
t.Logf("Encoded Address: %s", address)
104+
105+
version, decodedHash, err := DeserializeAddress(address)
106+
if err != nil {
107+
t.Fatalf("Failed to deserialize address: %v", err)
108+
}
109+
110+
if version != stacks.AddressVersionMainnetSingleSig {
111+
t.Errorf("Expected version %d, got %d", stacks.AddressVersionMainnetSingleSig, version)
112+
}
113+
114+
if string(decodedHash) != string(hash160) {
115+
t.Errorf("Decoded hash160 does not match original")
116+
}
117+
}

pkg/crypto/signature.go

+49
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/btcsuite/btcd/btcec/v2"
1515
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
16+
"github.com/icon-project/stacks-go-sdk/pkg/c32"
1617
"github.com/icon-project/stacks-go-sdk/pkg/stacks"
1718
)
1819

@@ -65,6 +66,54 @@ func GetPublicKeyFromPrivate(privateKey []byte) []byte {
6566
return pubKey.SerializeCompressed()
6667
}
6768

69+
func GetAddressFromPrivateKey(privateKey []byte, network stacks.ChainID) (string, error) {
70+
if len(privateKey) != 33 {
71+
return "", errors.New("private key must be 33 bytes")
72+
}
73+
74+
// Derive public key
75+
privKey, pubKey := btcec.PrivKeyFromBytes(privateKey)
76+
if privKey == nil || pubKey == nil {
77+
return "", errors.New("invalid private key")
78+
}
79+
80+
compressedPubKey := pubKey.SerializeCompressed()
81+
address, err := CalculateStacksAddress(compressedPubKey, network)
82+
if err != nil {
83+
return "", err
84+
}
85+
86+
return address, nil
87+
}
88+
89+
func CalculateStacksAddress(pubKey []byte, network stacks.ChainID) (string, error) {
90+
if len(pubKey) != 33 {
91+
return "", errors.New("public key must be 33 bytes (compressed)")
92+
}
93+
94+
// Perform SHA-256 hashing
95+
pubKeyHash := Hash160(pubKey)
96+
97+
// Determine version based on network
98+
var version stacks.AddressVersion
99+
switch network {
100+
case stacks.ChainIDMainnet:
101+
version = stacks.AddressVersionMainnetSingleSig
102+
case stacks.ChainIDTestnet:
103+
version = stacks.AddressVersionTestnetSingleSig
104+
default:
105+
return "", fmt.Errorf("unsupported network: %d", network)
106+
}
107+
108+
// Encode to c32check address
109+
address, err := c32.SerializeAddress(version, pubKeyHash)
110+
if err != nil {
111+
return "", fmt.Errorf("failed to serialize address: %w", err)
112+
}
113+
114+
return address, nil
115+
}
116+
68117
func VerifySignature(messageHash string, signature MessageSignature, publicKey []byte) (bool, error) {
69118
messageHashBytes, err := hex.DecodeString(messageHash)
70119
if err != nil {

pkg/crypto/signature_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,71 @@ func TestSignWithKey2(t *testing.T) {
137137
t.Fatalf("SignWithKey signature mismatch. Got %s, want %s", signature.Data, expectedSignature)
138138
}
139139
}
140+
141+
func TestCalculateStacksAddress(t *testing.T) {
142+
tests := []struct {
143+
name string
144+
publicKeyHex string
145+
network stacks.ChainID
146+
expectedAddress string
147+
}{
148+
{
149+
name: "Testnet Address",
150+
publicKeyHex: "0332fc778e5beb5f944c75b2b63c21dd12c40bdcdf99ba0663168ae0b2be880aef",
151+
network: stacks.ChainIDTestnet,
152+
expectedAddress: "ST15C893XJFJ6FSKM020P9JQDB5T7X6MQTXMBPAVH",
153+
},
154+
}
155+
156+
for _, tt := range tests {
157+
t.Run(tt.name, func(t *testing.T) {
158+
publicKeyBytes, err := hex.DecodeString(tt.publicKeyHex)
159+
if err != nil {
160+
t.Fatalf("Failed to decode public key hex: %v", err)
161+
}
162+
163+
address, err := CalculateStacksAddress(publicKeyBytes, tt.network)
164+
if err != nil {
165+
t.Fatalf("CalculateStacksAddress failed: %v", err)
166+
}
167+
168+
if address != tt.expectedAddress {
169+
t.Errorf("CalculateStacksAddress mismatch.\nGot: %s\nWant: %s", address, tt.expectedAddress)
170+
}
171+
})
172+
}
173+
}
174+
175+
func TestGetAddressFromPrivateKey(t *testing.T) {
176+
tests := []struct {
177+
name string
178+
privateKeyHex string
179+
network stacks.ChainID
180+
expectedAddress string
181+
}{
182+
{
183+
name: "Mainnet Address from Private Key",
184+
privateKeyHex: "c1d5bb638aa70862621667f9997711fce692cad782694103f8d9561f62e9f19701",
185+
network: stacks.ChainIDTestnet,
186+
expectedAddress: "ST15C893XJFJ6FSKM020P9JQDB5T7X6MQTXMBPAVH",
187+
},
188+
}
189+
190+
for _, tt := range tests {
191+
t.Run(tt.name, func(t *testing.T) {
192+
privateKeyBytes, err := hex.DecodeString(tt.privateKeyHex)
193+
if err != nil {
194+
t.Fatalf("Failed to decode private key hex: %v", err)
195+
}
196+
197+
address, err := GetAddressFromPrivateKey(privateKeyBytes, tt.network)
198+
if err != nil {
199+
t.Fatalf("GetAddressFromPrivateKey failed: %v", err)
200+
}
201+
202+
if address != tt.expectedAddress {
203+
t.Errorf("GetAddressFromPrivateKey mismatch.\nGot: %s\nWant: %s", address, tt.expectedAddress)
204+
}
205+
})
206+
}
207+
}

0 commit comments

Comments
 (0)