Skip to content

Commit ffd2170

Browse files
committed
feat: updated the hash process when calculating first memo data hash
1 parent ce51441 commit ffd2170

4 files changed

Lines changed: 155 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
XRP Ledger blockchain indexer built in Go. Collects blocks and transactions from XRP and stores them in PostgreSQL. Built on the `verifier-indexer-framework` which provides the indexing loop, database management, and configuration loading — this repo implements the XRP-specific `BlockchainClient` interface.
8+
9+
## Build & Run Commands
10+
11+
```bash
12+
# Build
13+
go build ./cmd/indexer
14+
15+
# Run locally (two terminals):
16+
# Terminal 1: start PostgreSQL
17+
cd tests/ && docker compose up postgresdb
18+
# Terminal 2: run the indexer
19+
go run ./cmd/indexer --config tests/config_test.toml
20+
21+
# Test (requires running xrpld — see below)
22+
go test ./...
23+
24+
# Run a single test
25+
go test ./internal/xrp/ -run TestFirstMemoDataHash
26+
# Run a single subtest
27+
go test ./internal/xrp/ -run TestXrpTestSuite/TestGetBlockResult
28+
29+
# Lint (uses golangci-lint v2+)
30+
golangci-lint run --timeout 5m0s
31+
32+
# Format
33+
go fmt ./...
34+
```
35+
36+
## Test Infrastructure
37+
38+
There are two types of tests:
39+
- **Unit tests** (e.g., `TestFirstMemoDataHash` in `memo_test.go`): Run without external dependencies, use `package xrp` to test unexported functions.
40+
- **Integration tests** (e.g., `XrpTestSuite`): Require a running xrpld standalone node. The test setup creates wallets, funds accounts, and submits transactions against the local xrpld.
41+
42+
```bash
43+
# Start xrpld standalone node (from repo root)
44+
docker compose up -d
45+
# Tests connect to xrpld RPC at http://localhost:5005
46+
# Override with XRP_RPC_URL env var
47+
```
48+
49+
Test suites use `testify/suite`. Integration tests are in `xrp_test.go` (package `xrp_test`), unit tests are in `memo_test.go` (package `xrp`).
50+
51+
## Architecture
52+
53+
**Entry point**: `cmd/indexer/main.go` — minimal bootstrap that calls the framework's `Run()` with XRP-specific types. The framework handles config loading, CLI arg parsing (`--config`, default `config.toml`), logger setup, database init, and the indexing loop.
54+
55+
**Core implementation**: `internal/xrp/` (only 3 source files):
56+
- `xrp.go``xrpClient` implementing `indexer.BlockchainClient` interface (GetLatestBlockInfo, GetBlockTimestamp, GetBlockResult, GetServerInfo). Handles JSON-RPC communication with XRP nodes, transaction parsing, and field extraction.
57+
- `entities.go` — GORM models for `Block` and `Transaction` tables. Implements `database.Deletable` for history cleanup.
58+
59+
**Transaction field extraction** (in `GetBlockResult`):
60+
- `PaymentReference`: From first memo's MemoData if exactly 64 hex chars (payment transactions only)
61+
- `FirstMemoDataHash`: Keccak256 hash of hex-decoded first memo MemoData
62+
- `IsNativePayment`: True if Amount is a digit string (drops) or has `currency: "XRP"`
63+
- `SourceAddressesRoot`: Merkle root of Keccak256-hashed accounts with decreased balance
64+
- `SourceAddress`, `Sequence`, `TicketSequence`, `DestinationTag`: Passed through from XRP transaction JSON
65+
66+
**Key domain details**:
67+
- XRP timestamps use a custom epoch offset: `XRPTimeToUTD = 946684800` (seconds from Unix epoch to 2000-01-01)
68+
- XRP MemoData is hex-encoded — decode before hashing
69+
- Only `validated: true` ledgers are processed
70+
- The XRPL testnet is periodically reset; old blocks become unavailable
71+
72+
## Configuration
73+
74+
TOML-based config (see `config.example.toml`). The framework loads `config.toml` by default, overridable with `--config` flag or `CONFIG_FILE` env var. The `[blockchain].url` field can be overridden with the `XRP_URL` env var.
75+
76+
Key `[indexer]` settings: `start_block_number` and `end_block_number` control the block range. With `history_drop` enabled (seconds), `start_block_number` is ignored and the indexer starts from the history window.
77+
78+
## Local Framework Development
79+
80+
To develop against a local fork of `verifier-indexer-framework`, add a `replace` directive to `go.mod` (outside the `require` block):
81+
82+
```
83+
replace github.com/flare-foundation/verifier-indexer-framework => ../verifier-indexer-framework
84+
```
85+
86+
Keep the original `require` line with version intact. Remove the `replace` before committing.
87+
88+
## Linting
89+
90+
Strict `golangci-lint` config in `.golangci.yml` with ~28 linters enabled including `exhaustive` (switch completeness), `nolintlint` (requires specific linter in nolint directives), and `bodyclose`.

internal/xrp/memo_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package xrp
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestFirstMemoDataHash(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
memoData string
13+
expected string
14+
}{
15+
{
16+
name: "long memo data",
17+
memoData: "5468697320746F6B656E2064726F70206973206120726573756C74206F6620796F75722041534320686F6C64696E67732070726F706F7274696F6E61746520746F20746F74616C20737570706C792C2063616C63756C617465642061732070726F6365656473206F6620343525206F6620616E6E75616C2031322C3030302C3030302041534320737570706C7920696E63726561736520646973747269627574656420746F20616C6C2041534320686F6C646572732077686572652044726F70733E46656573206F6E20616E20686F75726C792C206461696C792C207765656B6C792C206F72206D6F6E74686C792062617369732E20466F72206D6F726520696E666F726D6174696F6E2076697369742068747470733A2F2F7777772E617363656E73696F6E696E6465782E636F6D2F",
18+
expected: "fe42159c2a27cf1451f890cec1aa0a07c8465da151794ec6a5a1294a261655d3",
19+
},
20+
{
21+
name: "short memo data",
22+
memoData: "3639303835373338",
23+
expected: "e8a4c3409f0f9db91fb53de91eb47e02e58f8f1062b7f1930a4defbf6d66e017",
24+
},
25+
{
26+
name: "binary-like memo data",
27+
memoData: "46425052664100020000000000000000000000000000000000000000004D5760",
28+
expected: "7513e735152cafd288c9247151d760f1ac038d318d6edadbe12c8cd0025739a6",
29+
},
30+
{
31+
name: "no memos",
32+
memoData: "",
33+
expected: "",
34+
},
35+
}
36+
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
var tx xrpTransaction
40+
if tt.memoData != "" {
41+
tx.Memos = []map[string]map[string]string{
42+
{"Memo": {"MemoData": tt.memoData}},
43+
}
44+
}
45+
result := firstMemoDataHash(tx)
46+
require.Equal(t, tt.expected, result)
47+
})
48+
}
49+
}

internal/xrp/xrp.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package xrp
33
import (
44
"bytes"
55
"context"
6+
"encoding/hex"
67
"encoding/json"
78
"fmt"
89
"io"
@@ -303,7 +304,11 @@ func firstMemoDataHash(tx xrpTransaction) string {
303304
if len(tx.Memos) > 0 {
304305
if memo, ok := tx.Memos[0]["Memo"]; ok {
305306
if memoData, ok := memo["MemoData"]; ok && memoData != "" {
306-
hash := crypto.Keccak256Hash([]byte(memoData))
307+
decoded, err := hex.DecodeString(memoData)
308+
if err != nil {
309+
return ""
310+
}
311+
hash := crypto.Keccak256Hash(decoded)
307312
return strings.ToLower(hash.Hex()[2:])
308313
}
309314
}

tests/config_test.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@ password = "password"
44
db_name = "flare_xrp_indexer"
55
port = 5432
66
drop_table_at_start = false
7-
history_drop = 3600
7+
history_drop = 13600
88
history_drop_frequency = 600
99

1010
[indexer]
1111
confirmations = 1
1212
max_block_range = 100
13-
max_concurrency = 10
14-
start_block_number = 700000
13+
max_concurrency = 1
14+
start_block_number = 100000000
15+
# start_block_number = 10172243
16+
# end_block_number = 10172442
1517

1618
[blockchain]
17-
url = "https://s.altnet.rippletest.net:51234"
19+
url = "https://s1.ripple.com:51234"
20+
# url = "https://xrplcluster.com"
21+
# Testnet
22+
# url = "https://s.altnet.rippletest.net:51234"
23+
# url = "https://testnet.xrpl-labs.com"
1824

1925
[timeout]
2026
request_timeout_millis = 3000

0 commit comments

Comments
 (0)