Skip to content

Commit b48146f

Browse files
authored
Merge pull request #20 from wcatz/feat/full-chain-sync
feat(chainsync): full chainsyc history
2 parents a304493 + a09846a commit b48146f

20 files changed

+2165
-881
lines changed

CLAUDE.md

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,72 @@
11
# goduckbot - Claude Code Context
22

3-
Cardano stake pool notification bot with chain sync via adder. Calculates leader schedules using CPRAOS and posts to Telegram.
3+
Cardano stake pool notification bot with chain sync and built-in CPRAOS leaderlog calculation. Supports two modes: **lite** (adder tail + Koios fallback) and **full** (historical Shelley-to-tip sync via gouroboros NtN).
44

55
## Architecture
66

77
| File | Purpose |
88
|------|---------|
9-
| `main.go` | Core: config, chain sync pipeline, block notifications, leaderlog orchestration |
10-
| `leaderlog.go` | CPRAOS leader schedule calculation, VRF key parsing, epoch/slot math |
11-
| `nonce.go` | Nonce evolution tracker (VRF accumulation per block) |
12-
| `db.go` | PostgreSQL layer (blocks, epoch nonces, leader schedules) |
9+
| `main.go` | Core: config, chain sync pipeline, block notifications, leaderlog orchestration, mode/social toggles |
10+
| `leaderlog.go` | CPRAOS leader schedule calculation, VRF key parsing, epoch/slot math (`SlotToEpoch`, `GetEpochStartSlot`) |
11+
| `nonce.go` | Nonce evolution tracker (VRF accumulation per block, genesis-seeded or zero-seeded) |
12+
| `store.go` | `Store` interface + SQLite implementation (`SqliteStore` via `modernc.org/sqlite`, pure Go, no CGO) |
13+
| `db.go` | PostgreSQL implementation (`PgStore` via `pgx/v5`) of the `Store` interface |
14+
| `sync.go` | Historical chain syncer using gouroboros NtN ChainSync protocol |
15+
16+
## Modes
17+
18+
### Lite Mode (default)
19+
- Uses blinklabs-io/adder to tail chain from tip
20+
- Nonce tracker zero-seeded, relies on Koios for epoch nonces when local data unavailable
21+
- No historical chain sync — starts tracking from first block seen after launch
22+
- Config: `mode: "lite"`
23+
24+
### Full Mode
25+
- Historical sync from Shelley genesis using gouroboros NtN ChainSync
26+
- Nonce tracker seeded with Shelley genesis hash (`1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81`)
27+
- Skips Byron era (no VRF data), starts from last Byron block intersect point
28+
- Once caught up (within 120 slots of tip), transitions to adder live tail
29+
- Builds complete local nonce history — enables retroactive leaderlog and missed block detection
30+
- Config: `mode: "full"`
31+
32+
### Intersect Points (Shelley start)
33+
| Network | Last Byron Slot | Block Hash |
34+
|---------|----------------|------------|
35+
| Mainnet | 4,492,799 | `f8084c61b6a238acec985b59310b6ecec49c0ab8352249afd7268da5cff2a457` |
36+
| Preprod | 1,598,399 | `7e16781b40ebf8b6da18f7b5e8ade855d6738095ef2f1c58c77e88b6e45997a4` |
37+
| Preview | Origin (no Byron) | N/A |
38+
39+
## Store Interface
40+
41+
Abstract database layer supporting SQLite (default) and PostgreSQL:
42+
43+
```go
44+
type Store interface {
45+
InsertBlock(ctx, slot, epoch, blockHash, vrfOutput, nonceValue) error
46+
UpsertEvolvingNonce(ctx, epoch, nonce, blockCount) error
47+
SetCandidateNonce(ctx, epoch, nonce) error
48+
SetFinalNonce(ctx, epoch, nonce, source) error
49+
GetFinalNonce(ctx, epoch) ([]byte, error)
50+
GetEvolvingNonce(ctx, epoch) ([]byte, int, error)
51+
InsertLeaderSchedule(ctx, schedule) error
52+
IsSchedulePosted(ctx, epoch) bool
53+
MarkSchedulePosted(ctx, epoch) error
54+
GetLastSyncedSlot(ctx) (uint64, error)
55+
Close() error
56+
}
57+
```
58+
59+
- **SQLite** (`SqliteStore`): Default for Docker/standalone. Uses WAL mode, single-writer, `modernc.org/sqlite` (pure Go, CGO_ENABLED=0 compatible).
60+
- **PostgreSQL** (`PgStore`): For K8s deployments with CNPG. Uses `pgx/v5` connection pool.
61+
62+
All INSERT operations use `ON CONFLICT` (upsert) for idempotency on restarts.
1363

1464
## Features
1565
- Real-time block notifications via Telegram/Twitter (with duck images)
66+
- Social network toggles: `telegram.enabled`, `twitter.enabled` in config
1667
- Chain sync using blinklabs-io/adder with auto-reconnect and host failover
1768
- Built-in CPRAOS leaderlog calculation (replaces cncli sidecar)
18-
- VRF nonce evolution tracked per block in PostgreSQL
69+
- VRF nonce evolution tracked per block in SQLite or PostgreSQL
1970
- Koios API integration for stake data and nonce fallback
2071
- WebSocket broadcast for block events
2172
- Telegram message chunking for messages >4096 chars
@@ -34,21 +85,30 @@ Is leader = leaderValue < threshold
3485
```
3586

3687
### Nonce Evolution
37-
Per block: `nonceValue = BLAKE2b-256(0x4E || vrfOutput)`, then `eta_v = BLAKE2b-256(eta_v || nonceValue)`. Candidate nonce freezes at 70% epoch progress (stability window). Koios used as fallback when local nonce unavailable.
88+
Per block: `nonceValue = BLAKE2b-256(0x4E || vrfOutput)`, then `eta_v = BLAKE2b-256(eta_v || nonceValue)`. Rolling eta_v accumulates across epoch boundaries (no reset). Candidate nonce freezes at 70% epoch progress (stability window). Koios used as fallback when local nonce unavailable.
3889

3990
### Trigger Flow
4091
1. Every block: extract VRF output from header, update evolving nonce
4192
2. At 70% epoch progress: freeze candidate nonce
4293
3. After freeze: calculate next epoch schedule (mutex-guarded, one goroutine per epoch)
43-
4. Post schedule to Telegram, store in PostgreSQL
94+
4. Post schedule to Telegram, store in database
4495

4596
### Race Condition Prevention
4697
`checkLeaderlogTrigger` fires on every block after 70% — uses `leaderlogMu` mutex + `leaderlogCalcing` map to ensure only one goroutine runs per epoch. Map entry is cleaned up after goroutine completes.
4798

4899
### VRF Extraction by Era
100+
101+
Two extraction paths depending on sync mode:
102+
103+
**Live tail (adder)**`extractVrfOutput()` in `main.go`:
104+
- Must extract from `event.Event` payload BEFORE JSON marshal (`Block` field is `json:"-"`)
49105
- Conway/Babbage: `header.Body.VrfResult.Output` (combined, 64 bytes)
50106
- Shelley/Allegra/Mary/Alonzo: `header.Body.NonceVrf.Output` (separate)
51-
- Must extract from `event.Event` payload BEFORE JSON marshal (`Block` field is `json:"-"`)
107+
108+
**Historical sync (gouroboros NtN)**`extractVrfFromHeader()` in `sync.go`:
109+
- Receives `ledger.BlockHeader` directly, type-asserts to era-specific header
110+
- Same VRF field access pattern as adder path
111+
- Skips Byron blocks (no VRF data)
52112

53113
### Network Constants
54114

@@ -66,6 +126,11 @@ Constants defined in `leaderlog.go`: `MainnetNetworkMagic`, `PreprodNetworkMagic
66126
- **Preprod**: Genesis 2022-06-01T00:00:00Z, Byron slots at 20s (4 epochs), Shelley at 1s
67127
- **Preview**: Genesis 2022-11-01T00:00:00Z, all slots at 1s (no Byron era)
68128

129+
### Slot/Epoch Math
130+
- `GetEpochStartSlot(epoch, networkMagic)` — first slot of an epoch, accounts for Byron offset
131+
- `SlotToEpoch(slot, networkMagic)` — inverse, determines epoch from slot number
132+
- `GetEpochLength(networkMagic)` — returns epoch length for network
133+
69134
### Data Sources
70135

71136
| Data | Source | Notes |
@@ -75,16 +140,16 @@ Constants defined in `leaderlog.go`: `MainnetNetworkMagic`, `PreprodNetworkMagic
75140
| Pool stake | Koios `GetPoolInfo` | `ActiveStake` is `decimal.Decimal`, use `.IntPart()` |
76141
| Total stake | Koios `GetEpochInfo` | `ActiveStake` is `decimal.Decimal`, use `.IntPart()` |
77142

78-
### Database Schema (auto-created by `InitDB`)
143+
### Database Schema (auto-created by Store constructors)
79144
- `blocks` — per-block VRF data (slot, epoch, block_hash, vrf_output, nonce_value)
80145
- `epoch_nonces` — evolving/candidate/final nonces per epoch with source tracking
81-
- `leader_schedules` — calculated schedules with slots JSONB, posted flag
82-
83-
All INSERT operations use `ON CONFLICT` (upsert) for idempotency on restarts.
146+
- `leader_schedules` — calculated schedules with slots JSON, posted flag
84147

85148
## Config
86149

87150
```yaml
151+
mode: "lite" # "lite" or "full"
152+
88153
poolId: "POOL_ID_HEX"
89154
ticker: "TICKER"
90155
poolName: "Pool Name"
@@ -94,32 +159,43 @@ nodeAddress:
94159
networkMagic: 764824073
95160

96161
telegram:
162+
enabled: true # toggle Telegram notifications
97163
channel: "CHANNEL_ID"
164+
# token via TELEGRAM_TOKEN env var
165+
98166
twitter:
99-
consumer_key: "..." # etc.
167+
enabled: false # toggle Twitter notifications
168+
# credentials via env vars
100169

101170
leaderlog:
102171
enabled: true
103172
vrfKeyPath: "/keys/vrf.skey"
104173
timezone: "America/New_York"
105174

106175
database:
176+
driver: "sqlite" # "sqlite" (default) or "postgres"
177+
path: "./goduckbot.db" # SQLite file path
178+
# PostgreSQL settings (driver: postgres)
107179
host: "postgres-host"
108180
port: 5432
109181
name: "goduckbot"
110182
user: "goduckbot"
111-
password: "" # Prefer GODUCKBOT_DB_PASSWORD env var
183+
password: "" # Prefer GODUCKBOT_DB_PASSWORD env var
112184
```
113185
114186
**Note:** Database password uses `net/url.URL` for URL-safe encoding in connection strings. Passwords with special characters (`@`, `:`, `/`) are handled correctly.
115187

116-
## Helm Chart (v0.2.0)
188+
## Helm Chart (v0.3.0)
117189

118-
Key values for leaderlog:
190+
Key values:
191+
- `config.mode` — "lite" (default) or "full"
192+
- `config.telegram.enabled` / `config.twitter.enabled` — social network toggles (env vars conditionally injected)
119193
- `config.leaderlog.enabled` — enables VRF tracking and schedule calculation
120-
- `config.database.*` — PostgreSQL connection (password via SOPS secret)
194+
- `config.database.driver` — "sqlite" (default) or "postgres"
195+
- `config.database.path` — SQLite file path (default `/data/goduckbot.db`)
196+
- `persistence.enabled` — creates PVC for SQLite data when `database.driver=sqlite`
121197
- `vrfKey.secretName` — K8s secret containing vrf.skey (auto-mounted when `leaderlog.enabled` or `vrfKey.enabled`)
122-
- Helm `required` validations enforce `vrfKey.secretName` and `database.password` when leaderlog is enabled
198+
- DB password secret only required when `leaderlog.enabled` AND `database.driver=postgres`
123199

124200
## Dockerfile
125201

@@ -135,16 +211,30 @@ CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o goduckbot .
135211
docker buildx build --platform linux/amd64,linux/arm64 -t wcatz/goduckbot:latest --push .
136212
137213
# Helm package + push
138-
helm package helm-chart/ --version 0.2.0
139-
helm push goduckbot-0.2.0.tgz oci://ghcr.io/wcatz/helm-charts
214+
helm package helm-chart/ --version 0.3.0
215+
helm push goduckbot-0.3.0.tgz oci://ghcr.io/wcatz/helm-charts
140216
141217
# Deploy via helmfile (from infra repo)
142218
helmfile -e apps -l app=duckbot apply
143219
```
144220

221+
## Tests
222+
223+
```bash
224+
go test ./... -v # 16 tests: store, nonce, leaderlog
225+
go vet ./...
226+
helm lint helm-chart/
227+
```
228+
229+
Test files:
230+
- `store_test.go` — SQLite Store operations (in-memory `:memory:` DB)
231+
- `nonce_test.go` — VRF nonce hashing, nonce evolution, genesis seed
232+
- `leaderlog_test.go` — SlotToEpoch (all networks), round-trip, formatNumber
233+
145234
## Key Dependencies
146-
- `blinklabs-io/adder` v0.37.0 — chain sync (must match gouroboros version)
147-
- `blinklabs-io/gouroboros` v0.153.0 — VRF, block headers, ledger types
235+
- `blinklabs-io/adder` v0.37.0 — live chain tail (must match gouroboros version)
236+
- `blinklabs-io/gouroboros` v0.153.0 — VRF, block headers, ledger types, NtN ChainSync
237+
- `modernc.org/sqlite` — pure Go SQLite (no CGO required)
148238
- `jackc/pgx/v5` — PostgreSQL driver
149239
- `cardano-community/koios-go-client/v3` — Koios API
150240
- `golang.org/x/crypto` — blake2b hashing

README.md

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# duckBot
22

3-
A Cardano stake pool operator's companion. Real-time block notifications, built-in CPRAOS leader schedule calculation, and nonce evolution tracking — all from a single Go binary syncing directly off your local node.
3+
A Cardano stake pool operator's companion. Real-time block notifications, built-in CPRAOS leader schedule calculation, and full chain nonce history — all from a single Go binary syncing directly off your local node.
44

55
```text
66
Quack!(attention)
@@ -24,29 +24,39 @@ Lifetime Blocks: 1,337
2424

2525
**Leader Schedule Calculation** — Built-in CPRAOS implementation calculates your upcoming slot assignments without needing cncli as a sidecar. Validated against cncli on both preview and mainnet. Posts the full schedule to Telegram with local timezone support and message chunking for large schedules.
2626

27-
**Nonce Evolution** — Tracks the evolving nonce per-block by accumulating VRF outputs through BLAKE2b-256 hashing. Freezes the candidate nonce at the stability window (70% epoch progress), then triggers leader schedule calculation for the next epoch. Falls back to Koios when local data isn't available (e.g., after a restart mid-epoch).
27+
**Full Chain Nonce History** — In full mode, syncs every block from Shelley genesis using gouroboros NtN ChainSync, building a complete local nonce evolution history. Enables retroactive leaderlog calculation and missed block detection. In lite mode, tails the chain from tip and falls back to Koios when needed.
28+
29+
**Nonce Evolution** — Tracks the evolving nonce per-block by accumulating VRF outputs through BLAKE2b-256 hashing. Freezes the candidate nonce at the stability window (70% epoch progress), then triggers leader schedule calculation for the next epoch.
2830

2931
**WebSocket Feed** — Broadcasts all block events over WebSocket for custom integrations and dashboards.
3032

3133
**Node Failover** — Supports multiple node addresses with exponential backoff retry. If the primary goes down, duckBot rolls over to the backup.
3234

35+
## Operating Modes
36+
37+
| Mode | Description |
38+
| ---- | ----------- |
39+
| **lite** (default) | Tails chain from tip via adder. Uses Koios API for epoch nonces. No historical data. |
40+
| **full** | Historical sync from Shelley genesis via gouroboros NtN. Builds complete local nonce history. Transitions to live tail once caught up. |
41+
3342
## Architecture
3443

35-
| File | What It Does |
36-
| -------------- | ----------------------------------------------------------------------- |
37-
| `main.go` | Config, chain sync (adder), block notifications, leaderlog, WebSocket |
38-
| `leaderlog.go` | CPRAOS schedule math, VRF key parsing, epoch/slot calculations |
39-
| `nonce.go` | Nonce evolution: VRF accumulation, candidate freeze, final derivation |
40-
| `db.go` | PostgreSQL: blocks, epoch nonces, leader schedules (upserts) |
44+
| File | What It Does |
45+
| -------------- | --------------------------------------------------------------------------- |
46+
| `main.go` | Config, chain sync (adder), block notifications, leaderlog, mode/social toggles |
47+
| `leaderlog.go` | CPRAOS schedule math, VRF key parsing, epoch/slot calculations |
48+
| `nonce.go` | Nonce evolution: VRF accumulation, candidate freeze, genesis-seeded or zero-seeded |
49+
| `store.go` | Store interface + SQLite implementation (default, pure Go, no CGO) |
50+
| `db.go` | PostgreSQL implementation of the Store interface |
51+
| `sync.go` | Historical chain syncer using gouroboros NtN ChainSync protocol |
4152

4253
## Quick Start
4354

4455
### Prerequisites
4556

4657
- Go 1.24+
47-
- Access to a Cardano node (N2N protocol)
58+
- Access to a Cardano node (N2N protocol, TCP port 3001)
4859
- Telegram bot token and channel
49-
- PostgreSQL (if using leaderlog)
5060
- Your pool's `vrf.skey` (if using leaderlog)
5161

5262
### Build
@@ -64,6 +74,8 @@ cp config.yaml.example config.yaml
6474
```
6575

6676
```yaml
77+
mode: "lite" # "lite" or "full"
78+
6779
poolId: "YOUR_POOL_ID_HEX"
6880
ticker: "DUCK"
6981
poolName: "DuckPool"
@@ -73,14 +85,21 @@ nodeAddress:
7385
networkMagic: 764824073 # mainnet=764824073, preprod=1, preview=2
7486

7587
telegram:
88+
enabled: true
7689
channel: "-100XXXXXXXXXX"
7790

91+
twitter:
92+
enabled: false # set true + provide env vars to enable
93+
7894
leaderlog:
7995
enabled: true
8096
vrfKeyPath: "/keys/vrf.skey"
8197
timezone: "America/New_York"
8298

8399
database:
100+
driver: "sqlite" # "sqlite" (default) or "postgres"
101+
path: "./goduckbot.db" # SQLite file path
102+
# PostgreSQL settings (driver: postgres)
84103
host: "localhost"
85104
port: 5432
86105
name: "goduckbot"
@@ -116,23 +135,28 @@ docker buildx build --platform linux/amd64,linux/arm64 \
116135
-t wcatz/goduckbot:latest --push .
117136
```
118137

138+
SQLite is the default database — no external database required. Data persists in `./goduckbot.db` (or mount a volume to `/data` for containers).
139+
119140
## Helm Chart
120141

121-
duckBot ships with a Helm chart for Kubernetes deployments.
142+
duckBot ships with a Helm chart (v0.3.0) for Kubernetes deployments.
122143

123144
```bash
124145
# Package and push
125-
helm package helm-chart/ --version 0.2.0
126-
helm push goduckbot-0.2.0.tgz oci://ghcr.io/wcatz/helm-charts
146+
helm package helm-chart/ --version 0.3.0
147+
helm push goduckbot-0.3.0.tgz oci://ghcr.io/wcatz/helm-charts
127148
128149
# Deploy via helmfile
129150
helmfile -e apps -l app=duckbot apply
130151
```
131152

132-
Key chart values for leaderlog:
153+
Key chart values:
133154

155+
- `config.mode` — "lite" (default) or "full"
156+
- `config.telegram.enabled` / `config.twitter.enabled` — social network toggles
134157
- `config.leaderlog.enabled` — enable VRF tracking and schedule calculation
135-
- `config.database.*` — PostgreSQL connection settings
158+
- `config.database.driver` — "sqlite" (default) or "postgres"
159+
- `persistence.enabled` — PVC for SQLite data (when using sqlite driver)
136160
- `vrfKey.secretName` — K8s secret containing your `vrf.skey`
137161

138162
## How CPRAOS Works (the short version)
@@ -157,16 +181,29 @@ Where `f` = active slot coefficient (0.05), `sigma` = pool's relative stake, and
157181
| Preprod | `1` | 432,000 slots | Byron era offset from epoch 4 |
158182
| Preview | `2` | 86,400 slots | No Byron era |
159183

184+
## Tests
185+
186+
```bash
187+
go test ./... -v # 16 tests covering store, nonce, and leaderlog
188+
go vet ./...
189+
helm lint helm-chart/
190+
```
191+
160192
## Dependencies
161193

162194
Built on the shoulders of:
163195

164-
- [blinklabs-io/adder](https://github.com/blinklabs-io/adder) — Chain sync pipeline
165-
- [blinklabs-io/gouroboros](https://github.com/blinklabs-io/gouroboros) — Ouroboros protocol, VRF, ledger types
196+
- [blinklabs-io/adder](https://github.com/blinklabs-io/adder) — Live chain tail pipeline
197+
- [blinklabs-io/gouroboros](https://github.com/blinklabs-io/gouroboros) — Ouroboros protocol, VRF, ledger types, NtN ChainSync
198+
- [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) — Pure Go SQLite (no CGO required)
166199
- [cardano-community/koios-go-client](https://github.com/cardano-community/koios-go-client) — Koios API for stake data and nonce fallback
167200
- [jackc/pgx](https://github.com/jackc/pgx) — PostgreSQL driver
168201
- [random-d.uk](https://random-d.uk) — The ducks
169202

203+
## Credits
204+
205+
Built by [wcatz](https://github.com/wcatz)
206+
170207
## License
171208

172209
MIT

0 commit comments

Comments
 (0)