Files
bank/bench_test.go
zeekayandHanzo Dev abfcb41413 the ledger reserves before the chain moves, and no key is a sibling
A send broadcast first and checked the balance afterwards, so a customer
holding one micro-LUX could ask for any amount, watch it settle on chain,
and then be told "insufficient balance" for money already gone. Gas
funding counted the transfer's own value as a shortfall and covered twice
it out of the treasury, so the bank financed it and kept no record. Order
is now reserve, broadcast, settle — with release returning the hold when
the chain refuses — and fund covers gas alone.

Every step of a bank key's path is hardened. CKDpriv for an unhardened
index is k_i = IL + k_par (mod n) with IL from the parent's public key
and chain code, so one leaked customer key plus an xpub solved for the
parent and from there every sibling — the treasury among them at index
zero. The treasury now lives on its own branch and shares no number space
with the accounts it funds.

Also: one key has one nonce sequence, so treasury spending serializes;
toMinor errors instead of wrapping Int64; scale divides for tokens with
fewer decimals than the ledger, where Exp returned 1 in silence; each
Earn verb scales by the token that denominates its own amount, which
differ by 1e10 for 8dp BTC against an 18dp synthetic; the dial backoff is
keyed by endpoint; newTx returns a reloaded record so amending it is not
read as a move out of nothing; and settle makes one transition rather
than two, since nothing reads "processing" and it cost a durable write on
the hottest path.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-25 12:34:26 -07:00

141 lines
4.0 KiB
Go

package bank
import (
"crypto/ecdsa"
"math/big"
"testing"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/tests"
)
// -----------------------------------------------------------------------------
// Benchmarks. These measure the paths a customer actually waits on — the
// dashboard's first call, a payment, a balance read — against the real ledger
// on real SQLite, not against mocks. A number here is a number a user feels.
//
// They deliberately do not benchmark the chain: a broadcast is dominated by
// block time, which says nothing about this code.
// -----------------------------------------------------------------------------
// benchApp is newBankApp with a seeded principal, built once per benchmark.
func benchApp(b *testing.B) (*tests.TestApp, *core.Record) {
b.Helper()
app := newBankApp(b)
seedPrincipal(b, app)
acct := primaryAccount(app, principalID(b, app))
if acct == nil {
b.Fatal("no account")
}
return app, acct
}
// BenchmarkBalances is the read behind every balance widget and the first half
// of the dashboard's opening call.
func BenchmarkBalances(b *testing.B) {
app, acct := benchApp(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if len(viewBalances(app, acct.Id)) == 0 {
b.Fatal("no balances")
}
}
}
// BenchmarkEarnSummary folds the account's vault positions into one figure. It
// runs on every dashboard load, so it sits behind the same wait.
func BenchmarkEarnSummary(b *testing.B) {
app, acct := benchApp(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
viewEarnSummary(app, acct.Id)
}
}
// BenchmarkSettle is the whole money path end to end: create a pending debit
// (which validates the balance, checks the limits and holds the funds), then
// drive it through processing to completed (which releases the hold and moves
// the money). Every hook that guards a payment runs here.
func BenchmarkSettle(b *testing.B) {
app, acct := benchApp(b)
if err := setBalance(app, acct.Id, "USD", 1_000_000_000); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
tx, err := newTx(app, map[string]any{
"account": acct.Id, "type": "payment", "direction": "debit",
"amount": 100, "currency": "USD", "status": "pending",
"reference": "bench",
})
if err != nil {
b.Fatal(err)
}
if err := settle(app, tx); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkRelease is settle's counterpart: the refusal path, where a movement
// the chain or the rails rejected must give the held funds back. It runs on
// every failed send, so it is as hot as the success path in a bad hour.
func BenchmarkRelease(b *testing.B) {
app, acct := benchApp(b)
if err := setBalance(app, acct.Id, "USD", 1_000_000_000); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
tx, err := newTx(app, map[string]any{
"account": acct.Id, "type": "payment", "direction": "debit",
"amount": 100, "currency": "USD", "status": "pending",
"reference": "bench",
})
if err != nil {
b.Fatal(err)
}
if err := release(app, tx); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkScale is the unit conversion every on-chain amount passes through,
// in both directions. It is pure arithmetic on the hot path of every balance
// read, so it wants to stay allocation-light.
func BenchmarkScale(b *testing.B) {
c := &evmChain{}
wei := new(big.Int).SetUint64(1e18)
b.Run("toWei", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
c.toWei(1_000000, 18)
}
})
b.Run("toMinor", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := c.toMinor(wei, 18); err != nil {
b.Fatal(err)
}
}
})
}
// BenchmarkDerive is one account's signing key. It is BIP-32 over five hardened
// steps, so it is deliberately expensive — the cache in front of it is what
// keeps it off the request path, and this measures what a cache miss costs.
func BenchmarkDerive(b *testing.B) {
c := offlineChain(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.mu.Lock()
c.keys = map[string]*ecdsa.PrivateKey{}
c.mu.Unlock()
if _, err := c.key("1"); err != nil {
b.Fatal(err)
}
}
}