Skip to content

Commit 815a8b7

Browse files
Merge pull request #182 from XRPL-Commons/thomas/fix/binary_codec
fix(binary-codec): ensure 100% compatibility with xrpl.js reference implementation
2 parents ca24f8b + 1fe19ad commit 815a8b7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+12929
-1463
lines changed

address-codec/codec.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,18 @@ func EncodeClassicAddressFromPublicKeyHex(pubkeyhex string) (string, error) {
8888

8989
// DecodeClassicAddressToAccountID returns the prefix and accountID byte slice from a classic address.
9090
func DecodeClassicAddressToAccountID(cAddress string) (typePrefix, accountID []byte, err error) {
91-
if len(DecodeBase58(cAddress)) != 25 {
91+
// Use Base58CheckDecode to validate checksum
92+
decoded, err := Base58CheckDecode(cAddress)
93+
if err != nil {
94+
return nil, nil, ErrInvalidClassicAddress
95+
}
96+
97+
// Expected length is 21 bytes (1 prefix + 20 accountID) after removing 4-byte checksum
98+
if len(decoded) != 21 {
9299
return nil, nil, ErrInvalidClassicAddress
93100
}
94101

95-
return DecodeBase58(cAddress)[:1], DecodeBase58(cAddress)[1:21], nil
102+
return decoded[:1], decoded[1:21], nil
96103
}
97104

98105
// EncodeAccountIDToClassicAddress returns the classic address encoding of the accountId.

address-codec/compat_test.go

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
package addresscodec
2+
3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"os"
7+
"strings"
8+
"testing"
9+
10+
"github.com/Peersyst/xrpl-go/pkg/crypto"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// Fixtures represents the structure of address-fixtures.json
15+
type Fixtures struct {
16+
EncodeDecodeAccountID []EncodeDecodeTest `json:"encodeDecodeAccountID"`
17+
EncodeDecodeNodePublic []EncodeDecodeTest `json:"encodeDecodeNodePublic"`
18+
EncodeDecodeAccountPublic []EncodeDecodeTest `json:"encodeDecodeAccountPublic"`
19+
Seeds []SeedTest `json:"seeds"`
20+
ValidClassicAddresses []string `json:"validClassicAddresses"`
21+
InvalidClassicAddresses []string `json:"invalidClassicAddresses"`
22+
XAddresses []XAddressTest `json:"xAddresses"`
23+
InvalidXAddresses []InvalidXAddress `json:"invalidXAddresses"`
24+
CodecTests []CodecTest `json:"codecTests"`
25+
}
26+
27+
type EncodeDecodeTest struct {
28+
Hex string `json:"hex"`
29+
Base58 string `json:"base58"`
30+
}
31+
32+
type SeedTest struct {
33+
Hex string `json:"hex"`
34+
Base58 string `json:"base58"`
35+
Type string `json:"type"`
36+
}
37+
38+
type XAddressTest struct {
39+
ClassicAddress string `json:"classicAddress"`
40+
Tag *int64 `json:"tag"` // pointer to handle null
41+
MainnetAddress string `json:"mainnetAddress"`
42+
TestnetAddress string `json:"testnetAddress"`
43+
}
44+
45+
type InvalidXAddress struct {
46+
Address string `json:"address"`
47+
Error string `json:"error"`
48+
}
49+
50+
type CodecTest struct {
51+
Input string `json:"input"`
52+
Version int `json:"version"`
53+
ExpectedLength int `json:"expectedLength"`
54+
Encoded string `json:"encoded"`
55+
}
56+
57+
func loadFixtures(t *testing.T) *Fixtures {
58+
data, err := os.ReadFile("testdata/fixtures/address-fixtures.json")
59+
require.NoError(t, err, "Failed to read fixtures file")
60+
61+
var fixtures Fixtures
62+
err = json.Unmarshal(data, &fixtures)
63+
require.NoError(t, err, "Failed to parse fixtures JSON")
64+
65+
return &fixtures
66+
}
67+
68+
// TestCompat_EncodeDecodeAccountID tests encoding and decoding of AccountIDs
69+
// Reference: xrpl.js/packages/ripple-address-codec/test/xrp-codec.test.ts
70+
func TestCompat_EncodeDecodeAccountID(t *testing.T) {
71+
fixtures := loadFixtures(t)
72+
73+
for _, tc := range fixtures.EncodeDecodeAccountID {
74+
t.Run(tc.Base58, func(t *testing.T) {
75+
// Test encoding
76+
hexBytes, err := hex.DecodeString(tc.Hex)
77+
require.NoError(t, err)
78+
79+
encoded, err := EncodeAccountIDToClassicAddress(hexBytes)
80+
require.NoError(t, err)
81+
require.Equal(t, tc.Base58, encoded, "Encoding mismatch for hex: %s", tc.Hex)
82+
83+
// Test decoding
84+
_, decoded, err := DecodeClassicAddressToAccountID(tc.Base58)
85+
require.NoError(t, err)
86+
require.Equal(t, strings.ToUpper(tc.Hex), strings.ToUpper(hex.EncodeToString(decoded)), "Decoding mismatch for base58: %s", tc.Base58)
87+
})
88+
}
89+
}
90+
91+
// TestCompat_EncodeDecodeNodePublic tests encoding and decoding of NodePublic keys
92+
// Reference: xrpl.js/packages/ripple-address-codec/test/xrp-codec.test.ts
93+
func TestCompat_EncodeDecodeNodePublic(t *testing.T) {
94+
fixtures := loadFixtures(t)
95+
96+
for _, tc := range fixtures.EncodeDecodeNodePublic {
97+
t.Run(tc.Base58, func(t *testing.T) {
98+
// Test encoding
99+
hexBytes, err := hex.DecodeString(tc.Hex)
100+
require.NoError(t, err)
101+
102+
encoded, err := EncodeNodePublicKey(hexBytes)
103+
require.NoError(t, err)
104+
require.Equal(t, tc.Base58, encoded, "Encoding mismatch for hex: %s", tc.Hex)
105+
106+
// Test decoding
107+
decoded, err := DecodeNodePublicKey(tc.Base58)
108+
require.NoError(t, err)
109+
require.Equal(t, strings.ToUpper(tc.Hex), strings.ToUpper(hex.EncodeToString(decoded)), "Decoding mismatch for base58: %s", tc.Base58)
110+
})
111+
}
112+
}
113+
114+
// TestCompat_EncodeDecodeAccountPublic tests encoding and decoding of AccountPublic keys
115+
// Reference: xrpl.js/packages/ripple-address-codec/test/xrp-codec.test.ts
116+
func TestCompat_EncodeDecodeAccountPublic(t *testing.T) {
117+
fixtures := loadFixtures(t)
118+
119+
for _, tc := range fixtures.EncodeDecodeAccountPublic {
120+
t.Run(tc.Base58, func(t *testing.T) {
121+
// Test encoding
122+
hexBytes, err := hex.DecodeString(tc.Hex)
123+
require.NoError(t, err)
124+
125+
encoded, err := EncodeAccountPublicKey(hexBytes)
126+
require.NoError(t, err)
127+
require.Equal(t, tc.Base58, encoded, "Encoding mismatch for hex: %s", tc.Hex)
128+
129+
// Test decoding
130+
decoded, err := DecodeAccountPublicKey(tc.Base58)
131+
require.NoError(t, err)
132+
require.Equal(t, strings.ToUpper(tc.Hex), strings.ToUpper(hex.EncodeToString(decoded)), "Decoding mismatch for base58: %s", tc.Base58)
133+
})
134+
}
135+
}
136+
137+
// TestCompat_EncodeSeed tests seed encoding
138+
// Reference: xrpl.js/packages/ripple-address-codec/test/xrp-codec.test.ts
139+
func TestCompat_EncodeSeed(t *testing.T) {
140+
fixtures := loadFixtures(t)
141+
142+
for _, tc := range fixtures.Seeds {
143+
t.Run(tc.Base58, func(t *testing.T) {
144+
hexBytes, err := hex.DecodeString(tc.Hex)
145+
require.NoError(t, err)
146+
147+
var encoded string
148+
if tc.Type == "ed25519" {
149+
encoded, err = EncodeSeed(hexBytes, crypto.ED25519())
150+
} else {
151+
encoded, err = EncodeSeed(hexBytes, crypto.SECP256K1())
152+
}
153+
require.NoError(t, err)
154+
require.Equal(t, tc.Base58, encoded, "Seed encoding mismatch for hex: %s, type: %s", tc.Hex, tc.Type)
155+
})
156+
}
157+
}
158+
159+
// TestCompat_DecodeSeed tests seed decoding
160+
// Reference: xrpl.js/packages/ripple-address-codec/test/xrp-codec.test.ts
161+
func TestCompat_DecodeSeed(t *testing.T) {
162+
fixtures := loadFixtures(t)
163+
164+
for _, tc := range fixtures.Seeds {
165+
t.Run(tc.Base58, func(t *testing.T) {
166+
decoded, cryptoType, err := DecodeSeed(tc.Base58)
167+
require.NoError(t, err)
168+
require.Equal(t, strings.ToUpper(tc.Hex), strings.ToUpper(hex.EncodeToString(decoded)), "Seed decoding mismatch for base58: %s", tc.Base58)
169+
170+
// Check type by comparing with known implementations
171+
if tc.Type == "ed25519" {
172+
_, ok := cryptoType.(crypto.ED25519CryptoAlgorithm)
173+
require.True(t, ok, "Expected ed25519 type for base58: %s", tc.Base58)
174+
} else {
175+
_, ok := cryptoType.(crypto.SECP256K1CryptoAlgorithm)
176+
require.True(t, ok, "Expected secp256k1 type for base58: %s", tc.Base58)
177+
}
178+
})
179+
}
180+
}
181+
182+
// TestCompat_IsValidClassicAddress tests classic address validation
183+
// Reference: xrpl.js/packages/ripple-address-codec/test/xrp-codec.test.ts
184+
func TestCompat_IsValidClassicAddress(t *testing.T) {
185+
fixtures := loadFixtures(t)
186+
187+
for _, addr := range fixtures.ValidClassicAddresses {
188+
t.Run("valid_"+addr, func(t *testing.T) {
189+
require.True(t, IsValidClassicAddress(addr), "Expected %s to be valid", addr)
190+
})
191+
}
192+
193+
for _, addr := range fixtures.InvalidClassicAddresses {
194+
name := "invalid_" + addr
195+
if addr == "" {
196+
name = "invalid_empty"
197+
}
198+
t.Run(name, func(t *testing.T) {
199+
require.False(t, IsValidClassicAddress(addr), "Expected %s to be invalid", addr)
200+
})
201+
}
202+
}
203+
204+
// TestCompat_XAddressMainnet tests X-address encoding/decoding for mainnet
205+
// Reference: xrpl.js/packages/ripple-address-codec/test/index.test.ts
206+
func TestCompat_XAddressMainnet(t *testing.T) {
207+
fixtures := loadFixtures(t)
208+
209+
for _, tc := range fixtures.XAddresses {
210+
testName := tc.ClassicAddress
211+
if tc.Tag != nil {
212+
testName += "_tag_" + string(rune(*tc.Tag))
213+
} else {
214+
testName += "_no_tag"
215+
}
216+
t.Run("mainnet_"+testName, func(t *testing.T) {
217+
var tag uint32
218+
tagFlag := false
219+
if tc.Tag != nil {
220+
tag = uint32(*tc.Tag)
221+
tagFlag = true
222+
}
223+
224+
// Test classic -> X-address conversion
225+
xAddr, err := ClassicAddressToXAddress(tc.ClassicAddress, tag, tagFlag, false)
226+
require.NoError(t, err)
227+
require.Equal(t, tc.MainnetAddress, xAddr, "Classic to X-address conversion failed for %s", tc.ClassicAddress)
228+
229+
// Test X-address -> classic conversion
230+
classicAddr, decodedTag, isTestnet, err := XAddressToClassicAddress(tc.MainnetAddress)
231+
require.NoError(t, err)
232+
require.Equal(t, tc.ClassicAddress, classicAddr, "X-address to classic conversion failed for %s", tc.MainnetAddress)
233+
require.False(t, isTestnet, "Expected mainnet address")
234+
235+
if tc.Tag != nil {
236+
require.Equal(t, uint32(*tc.Tag), decodedTag, "Tag mismatch for %s", tc.MainnetAddress)
237+
}
238+
239+
// Test IsValidXAddress
240+
require.True(t, IsValidXAddress(tc.MainnetAddress), "Expected %s to be a valid X-address", tc.MainnetAddress)
241+
})
242+
}
243+
}
244+
245+
// TestCompat_XAddressTestnet tests X-address encoding/decoding for testnet
246+
// Reference: xrpl.js/packages/ripple-address-codec/test/index.test.ts
247+
func TestCompat_XAddressTestnet(t *testing.T) {
248+
fixtures := loadFixtures(t)
249+
250+
for _, tc := range fixtures.XAddresses {
251+
testName := tc.ClassicAddress
252+
if tc.Tag != nil {
253+
testName += "_tag"
254+
} else {
255+
testName += "_no_tag"
256+
}
257+
t.Run("testnet_"+testName, func(t *testing.T) {
258+
var tag uint32
259+
tagFlag := false
260+
if tc.Tag != nil {
261+
tag = uint32(*tc.Tag)
262+
tagFlag = true
263+
}
264+
265+
// Test classic -> X-address conversion
266+
xAddr, err := ClassicAddressToXAddress(tc.ClassicAddress, tag, tagFlag, true)
267+
require.NoError(t, err)
268+
require.Equal(t, tc.TestnetAddress, xAddr, "Classic to X-address conversion failed for %s", tc.ClassicAddress)
269+
270+
// Test X-address -> classic conversion
271+
classicAddr, decodedTag, isTestnet, err := XAddressToClassicAddress(tc.TestnetAddress)
272+
require.NoError(t, err)
273+
require.Equal(t, tc.ClassicAddress, classicAddr, "X-address to classic conversion failed for %s", tc.TestnetAddress)
274+
require.True(t, isTestnet, "Expected testnet address")
275+
276+
if tc.Tag != nil {
277+
require.Equal(t, uint32(*tc.Tag), decodedTag, "Tag mismatch for %s", tc.TestnetAddress)
278+
}
279+
280+
// Test IsValidXAddress
281+
require.True(t, IsValidXAddress(tc.TestnetAddress), "Expected %s to be a valid X-address", tc.TestnetAddress)
282+
})
283+
}
284+
}
285+
286+
// TestCompat_InvalidXAddresses tests that invalid X-addresses are properly rejected
287+
// Reference: xrpl.js/packages/ripple-address-codec/test/index.test.ts
288+
func TestCompat_InvalidXAddresses(t *testing.T) {
289+
fixtures := loadFixtures(t)
290+
291+
for _, tc := range fixtures.InvalidXAddresses {
292+
t.Run(tc.Address[:20], func(t *testing.T) {
293+
require.False(t, IsValidXAddress(tc.Address), "Expected %s to be invalid", tc.Address)
294+
295+
_, _, _, err := XAddressToClassicAddress(tc.Address)
296+
require.Error(t, err, "Expected error for invalid X-address: %s", tc.Address)
297+
})
298+
}
299+
}

address-codec/errors.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ var (
1313
// ErrInvalidSeed indicates an invalid seed; could not determine encoding algorithm.
1414
ErrInvalidSeed = errors.New("invalid seed; could not determine encoding algorithm")
1515
// ErrInvalidXAddress indicates an invalid x-address.
16-
ErrInvalidXAddress = errors.New("invalid x-address")
16+
ErrInvalidXAddress = errors.New("Invalid X-address: bad prefix")
17+
// ErrUnsupportedXAddress indicates an unsupported x-address (e.g., 64-bit tag).
18+
ErrUnsupportedXAddress = errors.New("Unsupported X-address")
1719
// ErrInvalidTag indicates an invalid tag.
18-
ErrInvalidTag = errors.New("invalid tag")
20+
ErrInvalidTag = errors.New("Invalid tag")
1921
// ErrInvalidAccountID indicates an invalid account ID.
2022
ErrInvalidAccountID = errors.New("invalid account ID")
2123
// ErrInvalidAddressFormat indicates a general invalid XRPL address format.

0 commit comments

Comments
 (0)