Skip to content

Commit 69934f5

Browse files
committed
Harden settlement indexing and test database isolation
1 parent 02e488c commit 69934f5

5 files changed

Lines changed: 200 additions & 21 deletions

File tree

controller/st_controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,9 @@ func (self *CoreStClient) stEventDecoders() []stEventDecoder {
15621562
stEvent("EmissionDeferred", v.UnpackEmissionDeferredEvent, func(e *stabi.STSettlementVaultEmissionDeferred) map[string]any {
15631563
return map[string]any{"e": e.Epoch.String(), "no_id": e.NoId.String()}
15641564
}),
1565+
stEvent("EmissionDustDeferred", v.UnpackEmissionDustDeferredEvent, func(e *stabi.STSettlementVaultEmissionDustDeferred) map[string]any {
1566+
return map[string]any{"e": e.Epoch.String(), "no_id": e.NoId.String(), "pool_hotkey": fmt.Sprintf("0x%x", e.PoolHotkey), "observed_alpha_rao": e.ObservedAlphaRao.String(), "tao_equivalent_rao": e.TaoEquivalentRao.String(), "minimum_transfer_tao_rao": fmt.Sprintf("%d", e.MinimumTransferTaoRao)}
1567+
}),
15651568
stEvent("EntitlementFinalized", v.UnpackEntitlementFinalizedEvent, func(e *stabi.STSettlementVaultEntitlementFinalized) map[string]any {
15661569
return map[string]any{"e": e.Epoch.String(), "no_id": e.NoId.String(), "payout_root": fmt.Sprintf("0x%x", e.PayoutRoot), "artifact_hash": fmt.Sprintf("0x%x", e.ArtifactHash), "pool_total": e.Total.String(), "expiry_block": e.ExpiryBlock}
15671570
}),
@@ -1571,6 +1574,12 @@ func (self *CoreStClient) stEventDecoders() []stEventDecoder {
15711574
stEvent("MinerClaimed", v.UnpackClaimedEvent, func(e *stabi.STSettlementVaultClaimed) map[string]any {
15721575
return map[string]any{"e": e.Epoch.String(), "no_id": e.NoId.String(), "coldkey": fmt.Sprintf("0x%x", e.Coldkey), "share_bps": e.ShareBps.String(), "amount": e.Amount.String(), "caller": e.Relayer.Hex()}
15731576
}),
1577+
stEvent("ClaimPaymentDeferred", v.UnpackClaimPaymentDeferredEvent, func(e *stabi.STSettlementVaultClaimPaymentDeferred) map[string]any {
1578+
return map[string]any{"coldkey": fmt.Sprintf("0x%x", e.Coldkey), "credit_alpha_rao": e.CreditAlphaRao.String(), "tao_equivalent_rao": e.TaoEquivalentRao.String(), "minimum_transfer_tao_rao": fmt.Sprintf("%d", e.MinimumTransferTaoRao), "reason": fmt.Sprintf("%d", e.Reason)}
1579+
}),
1580+
stEvent("ClaimPaid", v.UnpackClaimPaidEvent, func(e *stabi.STSettlementVaultClaimPaid) map[string]any {
1581+
return map[string]any{"coldkey": fmt.Sprintf("0x%x", e.Coldkey), "amount": e.Amount.String(), "caller": e.Relayer.Hex()}
1582+
}),
15741583
stEvent("BuybackReserved", r.UnpackReservePrincipalAddedEvent, func(e *stabi.STReserveSinkReservePrincipalAdded) map[string]any {
15751584
return map[string]any{"e": e.Epoch.String(), "no_id": e.NoId.String(), "amount": e.Amount.String(), "buyback_total": e.TotalPrincipal.String(), "live_stake": e.LiveStake.String()}
15761585
}),
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package controller
2+
3+
import (
4+
"math/big"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/core/types"
10+
"github.com/urfoundation/sn/stabi"
11+
)
12+
13+
func settlementVaultEventLog(t *testing.T, name string, indexed []common.Hash, values ...any) *types.Log {
14+
t.Helper()
15+
parsed, err := stabi.STSettlementVaultMetaData.ParseABI()
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
event, ok := parsed.Events[name]
20+
if !ok {
21+
t.Fatalf("settlement-vault ABI has no %s event", name)
22+
}
23+
data, err := event.Inputs.NonIndexed().Pack(values...)
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
return &types.Log{Topics: append([]common.Hash{event.ID}, indexed...), Data: data}
28+
}
29+
30+
func decodeReleaseEvent(t *testing.T, log *types.Log) (string, map[string]any) {
31+
t.Helper()
32+
client := &CoreStClient{
33+
coordinator: stabi.NewSTCoordinator(),
34+
vault: stabi.NewSTSettlementVault(),
35+
reserve: stabi.NewSTReserveSink(),
36+
}
37+
for _, decoder := range client.stEventDecoders() {
38+
if kind, data, ok := decoder(log); ok {
39+
return kind, data
40+
}
41+
}
42+
t.Fatal("release event was not decoded")
43+
return "", nil
44+
}
45+
46+
func TestStEventDecoderMirrorsEmissionDustDeferral(t *testing.T) {
47+
pool := common.HexToHash("0x1234")
48+
log := settlementVaultEventLog(
49+
t,
50+
"EmissionDustDeferred",
51+
[]common.Hash{common.BigToHash(big.NewInt(7)), common.BigToHash(big.NewInt(3)), pool},
52+
big.NewInt(175_960_612),
53+
big.NewInt(99_999),
54+
uint64(100_000),
55+
)
56+
kind, data := decodeReleaseEvent(t, log)
57+
want := map[string]any{
58+
"e": "7", "no_id": "3", "pool_hotkey": pool.Hex(),
59+
"observed_alpha_rao": "175960612", "tao_equivalent_rao": "99999", "minimum_transfer_tao_rao": "100000",
60+
}
61+
if kind != "EmissionDustDeferred" || !reflect.DeepEqual(data, want) {
62+
t.Fatalf("decoded dust deferral = %s %#v, want %#v", kind, data, want)
63+
}
64+
}
65+
66+
func TestStEventDecoderMirrorsDeferredClaimCredit(t *testing.T) {
67+
coldkey := common.HexToHash("0xabcd")
68+
log := settlementVaultEventLog(
69+
t,
70+
"ClaimPaymentDeferred",
71+
[]common.Hash{coldkey},
72+
big.NewInt(175_960_612),
73+
big.NewInt(99_999),
74+
uint64(100_000),
75+
uint8(1),
76+
)
77+
kind, data := decodeReleaseEvent(t, log)
78+
want := map[string]any{
79+
"coldkey": coldkey.Hex(), "credit_alpha_rao": "175960612", "tao_equivalent_rao": "99999",
80+
"minimum_transfer_tao_rao": "100000", "reason": "1",
81+
}
82+
if kind != "ClaimPaymentDeferred" || !reflect.DeepEqual(data, want) {
83+
t.Fatalf("decoded claim deferral = %s %#v, want %#v", kind, data, want)
84+
}
85+
}
86+
87+
func TestStEventDecoderDistinguishesActualClaimPayment(t *testing.T) {
88+
coldkey := common.HexToHash("0xabcd")
89+
relayer := common.HexToAddress("0x0000000000000000000000000000000000000521")
90+
log := settlementVaultEventLog(
91+
t,
92+
"ClaimPaid",
93+
[]common.Hash{coldkey, common.BytesToHash(relayer.Bytes())},
94+
big.NewInt(351_921_226),
95+
)
96+
kind, data := decodeReleaseEvent(t, log)
97+
want := map[string]any{"coldkey": coldkey.Hex(), "amount": "351921226", "caller": relayer.Hex()}
98+
if kind != "ClaimPaid" || !reflect.DeepEqual(data, want) {
99+
t.Fatalf("decoded claim payment = %s %#v, want %#v", kind, data, want)
100+
}
101+
}

db.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@ type safePgPool struct {
107107
// maintenance pool uses pg_maintenance.yml (direct Postgres, bypassing
108108
// PgBouncer) and db_maintenance.yml (its own max_connections) — and falls back
109109
// to the defaults (pg.yml / db.yml) when the pool-specific resource is absent
110-
// or does not define the size. That fallback keeps tests working (they only
111-
// redirect pg.yml) and tolerates an empty db_maintenance.yml. The main pool's
112-
// names ARE the defaults, so its resolution is unchanged.
110+
// or does not define the size. The test harness redirects both pool resources
111+
// when a production-shaped profile supplies pg_maintenance.yml, and the
112+
// fallback still tolerates that resource being absent. The main pool's names
113+
// ARE the defaults, so its resolution is unchanged.
113114
func (self *safePgPool) resolveResources() (vaultKeys *SimpleResource, configKeys *SimpleResource) {
114115
vaultKeys = Vault.RequireSimpleResource(DefaultPgVaultResourceName)
115116
if self.vaultResourceName != DefaultPgVaultResourceName {

test_util.go

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,41 @@ func runTestMain(setup func() func(), run func() int) int {
294294
return run()
295295
}
296296

297+
func testPgResourceForDatabase(pg map[string]any, database string) []byte {
298+
return []byte(fmt.Sprintf(
299+
`
300+
authority: "%s"
301+
user: "%s"
302+
password: "%s"
303+
db: "%s"`,
304+
pg["authority"],
305+
pg["user"],
306+
pg["password"],
307+
database,
308+
))
309+
}
310+
311+
// Redirect both application and direct-maintenance pools to one ephemeral
312+
// test database. Production-shaped profiles define pg_maintenance.yml; if it
313+
// remained pointed at the persistent database, migrations would run there
314+
// while the test itself saw an empty temporary schema.
315+
func pushTestPgResources(pg, maintenancePg map[string]any, database string) func() {
316+
popPg := Vault.PushSimpleResource(
317+
DefaultPgVaultResourceName,
318+
testPgResourceForDatabase(pg, database),
319+
)
320+
popMaintenance := Vault.PushSimpleResource(
321+
MaintenancePgVaultResourceName,
322+
testPgResourceForDatabase(maintenancePg, database),
323+
)
324+
PgReset()
325+
return func() {
326+
popMaintenance()
327+
popPg()
328+
PgReset()
329+
}
330+
}
331+
297332
// in each test file, `func TestMain(m *testing.M) {(&server.TestEnv{}).TestMain(m)}`
298333
// https://pkg.go.dev/testing
299334
func (self *TestEnv) TestMain(m *testing.M) {
@@ -476,7 +511,11 @@ func (self *TestEnv) setup() func() {
476511

477512
ctx := context.Background()
478513

479-
pg := Vault.RequireSimpleResource("pg.yml").Parse()
514+
pg := Vault.RequireSimpleResource(DefaultPgVaultResourceName).Parse()
515+
maintenancePg := pg
516+
if resource, resourceErr := Vault.SimpleResource(MaintenancePgVaultResourceName); resourceErr == nil {
517+
maintenancePg = resource.Parse()
518+
}
480519
redisResource := Vault.RequireSimpleResource("redis.yml")
481520
redisAuthority := redisResource.RequireString("authority")
482521
redisPassword := redisResource.RequireString("password")
@@ -534,21 +573,7 @@ func (self *TestEnv) setup() func() {
534573
Raise(err)
535574
}, OptReadWrite())
536575

537-
popPg := Vault.PushSimpleResource(
538-
"pg.yml",
539-
[]byte(fmt.Sprintf(
540-
`
541-
authority: "%s"
542-
user: "%s"
543-
password: "%s"
544-
db: "%s"`,
545-
pg["authority"],
546-
pg["user"],
547-
pg["password"],
548-
testPgDbName,
549-
)),
550-
)
551-
PgReset()
576+
popPgResources := pushTestPgResources(pg, maintenancePg, testPgDbName)
552577

553578
popRedis := Vault.PushSimpleResource(
554579
"redis.yml",
@@ -606,8 +631,7 @@ cluster: %t`,
606631
popRedis()
607632
RedisReset()
608633

609-
popPg()
610-
PgReset()
634+
popPgResources()
611635

612636
Db(ctx, func(conn PgConn) {
613637
_, err := conn.Exec(

test_util_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,50 @@ func TestParseTestPgDbNameValidatesFullIdentifier(t *testing.T) {
7474
}
7575
}
7676

77+
func TestPgResourcesRedirectMaintenancePoolAndRestore(t *testing.T) {
78+
popBasePg := Vault.PushSimpleResource(DefaultPgVaultResourceName, []byte(`
79+
authority: "app.example:5432"
80+
user: "app"
81+
password: "app-secret"
82+
db: "app-db"
83+
`))
84+
defer popBasePg()
85+
popBaseMaintenance := Vault.PushSimpleResource(MaintenancePgVaultResourceName, []byte(`
86+
authority: "direct.example:5432"
87+
user: "maintenance"
88+
password: "maintenance-secret"
89+
db: "maintenance-db"
90+
`))
91+
defer popBaseMaintenance()
92+
93+
app := Vault.RequireSimpleResource(DefaultPgVaultResourceName).Parse()
94+
maintenance := Vault.RequireSimpleResource(MaintenancePgVaultResourceName).Parse()
95+
popTest := pushTestPgResources(app, maintenance, "test_exact")
96+
97+
testApp := Vault.RequireSimpleResource(DefaultPgVaultResourceName)
98+
testMaintenance := Vault.RequireSimpleResource(MaintenancePgVaultResourceName)
99+
if got := testApp.RequireString("db"); got != "test_exact" {
100+
t.Fatalf("test application database = %q, want test_exact", got)
101+
}
102+
if got := testMaintenance.RequireString("db"); got != "test_exact" {
103+
t.Fatalf("test maintenance database = %q, want test_exact", got)
104+
}
105+
if got := testMaintenance.RequireString("authority"); got != "direct.example:5432" {
106+
t.Fatalf("test maintenance authority = %q, want direct authority", got)
107+
}
108+
if got := testMaintenance.RequireString("user"); got != "maintenance" {
109+
t.Fatalf("test maintenance user = %q, want maintenance credentials", got)
110+
}
111+
112+
popTest()
113+
if got := Vault.RequireSimpleResource(DefaultPgVaultResourceName).RequireString("db"); got != "app-db" {
114+
t.Fatalf("restored application database = %q, want app-db", got)
115+
}
116+
if got := Vault.RequireSimpleResource(MaintenancePgVaultResourceName).RequireString("db"); got != "maintenance-db" {
117+
t.Fatalf("restored maintenance database = %q, want maintenance-db", got)
118+
}
119+
}
120+
77121
// TestRunRetriesUntilPass checks every failure mode is retried: attempt 1
78122
// panics, 2 calls t.Fail, 3 fails an assertion (assert.Equal -> FailNow ->
79123
// runtime.Goexit), and 4 passes. Each failure is recorded only on the retryTB

0 commit comments

Comments
 (0)