Skip to content

Commit 659e286

Browse files
authored
Merge pull request #28 from icon-project/feat/derive-stacks-address
feat(c32): derive stacks address
2 parents 90d83da + e2de07a commit 659e286

File tree

4 files changed

+255
-34
lines changed

4 files changed

+255
-34
lines changed

pkg/c32/c32.go

+111-34
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package c32
22

33
import (
4-
"encoding/base32"
4+
"bytes"
5+
"crypto/sha256"
56
"errors"
67
"fmt"
78
"math/big"
@@ -12,9 +13,35 @@ import (
1213

1314
var crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
1415

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

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

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))
94+
func DecodeWithChecksum(c32addr string) (byte, []byte, error) {
95+
if len(c32addr) < 2 {
96+
return 0, nil, errors.New("address too short")
97+
}
98+
if c32addr[0] != 'S' {
99+
return 0, nil, errors.New("address must start with 'S'")
70100
}
71101

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])
102+
// Extract and decode the version character
103+
versionChar := c32addr[1]
104+
version := byte(strings.IndexRune(crockfordAlphabet, rune(versionChar)))
105+
if version == 255 { // strings.IndexRune returns -1 if not found, which becomes 255 as byte
106+
return 0, nil, errors.New("invalid version character")
80107
}
81108

82-
hashBytes, err := C32Decode(address[1:])
109+
// Decode the remaining C32 string
110+
c32str := c32addr[2:]
111+
data, err := C32Decode(c32str)
83112
if err != nil {
84-
return nil, fmt.Errorf("invalid address hash: %v", err)
113+
return 0, nil, err
114+
}
115+
116+
// Expected length: data (20 bytes) + checksum (4 bytes)
117+
expectedLength := stacks.AddressHashLength + 4
118+
if len(data) != expectedLength {
119+
return 0, nil, fmt.Errorf("invalid decoded length: expected %d, got %d", expectedLength, len(data))
120+
}
121+
122+
payload := data[:stacks.AddressHashLength]
123+
checksum := data[stacks.AddressHashLength:]
124+
125+
// Recompute checksum
126+
versionedData := append([]byte{version}, payload...)
127+
computedChecksum := sha256.Sum256(versionedData)
128+
computedChecksum = sha256.Sum256(computedChecksum[:])
129+
computedChecksumBytes := computedChecksum[:4]
130+
131+
// Compare checksums
132+
if !bytes.Equal(checksum, computedChecksumBytes) {
133+
return 0, nil, errors.New("checksum mismatch")
85134
}
86135

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

91-
return result, nil
152+
// Append checksum
153+
fullData := append(data, checksumBytes...)
154+
155+
// Encode to c32
156+
c32str := C32Encode(fullData)
157+
158+
// Add prefix 'S'
159+
return "S" + string(crockfordAlphabet[version]) + c32str, nil
160+
}
161+
162+
func SerializeAddress(version stacks.AddressVersion, hash160 []byte) (string, error) {
163+
return EncodeWithChecksum(byte(version), hash160)
92164
}
93165

94-
func DeserializeAddress(data []byte) (string, int, error) {
95-
if len(data) < 1+stacks.AddressHashLength {
96-
return "", 0, errors.New("insufficient data for address")
166+
func DeserializeAddress(address string) (stacks.AddressVersion, []byte, error) {
167+
version, payload, err := DecodeWithChecksum(address)
168+
if err != nil {
169+
return 0, nil, err
97170
}
98171

99-
version := stacks.AddressVersion(data[0])
100-
var prefix string
172+
var addrVersion stacks.AddressVersion
101173
switch version {
102-
case stacks.AddressVersionMainnetSingleSig:
103-
prefix = "S"
104-
case stacks.AddressVersionTestnetSingleSig:
105-
prefix = "T"
174+
case byte(stacks.AddressVersionMainnetSingleSig):
175+
addrVersion = stacks.AddressVersionMainnetSingleSig
176+
case byte(stacks.AddressVersionTestnetSingleSig):
177+
addrVersion = stacks.AddressVersionTestnetSingleSig
106178
default:
107-
return "", 0, fmt.Errorf("invalid address version: %d", version)
179+
return 0, nil, fmt.Errorf("unknown address version: %d", version)
108180
}
109181

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

113-
return address, 1 + stacks.AddressHashLength + 5, nil
185+
func reverseString(s string) string {
186+
runes := []rune(s)
187+
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
188+
runes[i], runes[j] = runes[j], runes[i]
189+
}
190+
return string(runes)
114191
}

pkg/c32/c32_test.go

+27
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,28 @@ 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+
version, decodedHash, err := DeserializeAddress(address)
104+
if err != nil {
105+
t.Fatalf("Failed to deserialize address: %v", err)
106+
}
107+
108+
if version != stacks.AddressVersionMainnetSingleSig {
109+
t.Errorf("Expected version %d, got %d", stacks.AddressVersionMainnetSingleSig, version)
110+
}
111+
112+
if string(decodedHash) != string(hash160) {
113+
t.Errorf("Decoded hash160 does not match original")
114+
}
115+
}

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)