We call it duckBot. There is no better name, and the day is coming soon when it will be unleashed.
Cardano stake pool notification bot. Single Go binary, no CGO, talks directly to cardano-node via gOuroboros.
CPraos leader schedule calculation, epoch nonce evolution, block notifications (Telegram, Twitter/X), stake queries, and full history.
- A reachable cardano-node (NtN port 3001)
- Telegram bot token from @BotFather
- Telegram group/channel ID (add @RawDataBot to your group, grab the chat ID from the JSON response, then remove it)
git clone https://github.com/wcatz/goduckbot.git && cd goduckbot
cp .env.example .env
cp config.yaml.example config.yamlEdit .env:
TELEGRAM_TOKEN=123456789:ABCdef_your_token_here
GODUCKBOT_VERSION=latest
Edit config.yaml:
mode: "lite"
poolId: "YOUR_POOL_ID_HEX"
poolName: "My Pool"
nodeAddress:
host1: "your-node:3001"
networkMagic: 764824073
telegram:
enabled: true
channel: "-100XXXXXXXXXX"
allowedUsers:
- 123456789
allowedGroups:
- -100XXXXXXXXXX
duck:
media: "both"docker compose up -d
docker compose logs -fAdds full chain sync, self-computed nonces, and local leader schedule calculation. Requires a VRF key and a database (SQLite by default, PostgreSQL optional).
mode: "full"
nodeAddress:
host1: "your-node:3001" # NtN (chain sync)
ntcHost: "your-node:30000" # NtC (stake queries, tip) — or UNIX socket path
leaderlog:
enabled: true
vrfKeyValue: "5840..." # cborHex from vrf.skey
timezone: "America/New_York"
timeFormat: "12h"The vrfKeyValue is the cborHex field from your vrf.skey file (5840 + 128 hex chars = 32-byte private scalar + 32-byte public key). SQLite is used by default with no extra config. For PostgreSQL:
database:
driver: "postgres"
host: "postgres-host"
port: 5432
name: "goduckbot"
user: "goduckbot"Set GODUCKBOT_DB_PASSWORD in .env.
| Mode | Chain Sync | Nonces | Startup |
|---|---|---|---|
| lite | Tail from tip | Koios API | Immediate |
| full | Historical NtN ChainSync from Shelley genesis, then live tail | Self-computed from VRF outputs via TICKN rule | Full sync required |
Per slot in target epoch:
1. vrfInput = BLAKE2b-256(slot[8B BE] || epochNonce[32B])
2. _, output = vrf.Prove(vrfSkey[:32], vrfInput)
3. leaderValue = BLAKE2b-256(0x4C || output)
4. threshold = 2^256 * (1 - (1-f)^σ) // f=0.05, σ=poolStake/totalStake
5. isLeader = leaderValue < threshold
The 0x4C ("L") domain separator is the CPraos distinction from TPraos (which uses a 512-bit comparison space).
Full mode streams every block from Shelley genesis, computing nonces per era:
| Era | VRF Nonce Value |
|---|---|
| Shelley–Alonzo (TPraos) | BLAKE2b-256(vrfOutput) |
| Babbage+ (CPraos) | BLAKE2b-256(BLAKE2b-256(0x4E || vrfOutput)) |
Per-block evolution: η_v = BLAKE2b-256(η_v \|\| nonceValue)
At the stability window: freeze candidate nonce η_c from the current η_v.
Epoch transition — TICKN rule for epoch E's nonce:
epochNonce(E) = BLAKE2b-256(η_c(E-1) || η_ph(E-2))
Where:
η_c(E-1)= candidate nonce frozen at the stability window of epoch E-1η_ph(E-2)=prevHashof the last block in epoch E-2 (= blockHash of the second-to-last block of epoch E-2)
The η_ph component lags by one epoch — at the E-1 → E transition, the labNonce (last anchor block nonce) from epoch E-2 is used, while the current epoch's labNonce is saved for the next transition.
Epoch 259's nonce is hardcoded because it is the only mainnet epoch where the extra_entropy protocol parameter was set. Since duckBot computes nonces purely from VRF outputs in block headers, it cannot account for extra_entropy — so the known result is used directly.
The next epoch's nonce becomes deterministic after the stability window. duckBot triggers leader schedule calculation automatically at this point.
| Era | Margin (k/f) |
Freeze Point | Slots Before Epoch End |
|---|---|---|---|
| Shelley–Babbage | 3k/f = 129,600 |
302,400 (70%) | 129,600 (~1.5 days) |
| Conway | 4k/f = 172,800 |
259,200 (60%) | 172,800 (~2 days) |
sync.go (NtN ChainSync) → blockCh [10,000 buf] → batch writer → InsertBlockBatch (CopyFrom) → ProcessBatch (nonce)
- Pipeline limit: 100 concurrent RequestNext messages
- Keepalive disabled during sync (prevents ChainSync stall with gouroboros muxer)
- Unlimited retry with capped backoff (5s–30s) on node timeout
- 100-block overlap window on reconnect prevents block gaps from muxer buffer loss
ON CONFLICT (slot) DO NOTHINGdeduplicates across retries
| Era | Header Field | Output Size |
|---|---|---|
| Shelley, Allegra, Mary, Alonzo | header.Body.NonceVrf.Output |
64 bytes |
| Babbage, Conway | header.Body.VrfResult.Output |
64 bytes |
| Byron | N/A | Skipped |
| Snapshot | Taken At | Used For |
|---|---|---|
| Mark | End of epoch N-1 | Next epoch schedule |
| Set | End of epoch N-2 | Current epoch |
| Go | End of epoch N-3 | Previous epoch |
Queried via NtC QueryPoolStakeSnapshots (primary) with Koios API fallback.
On startup (full mode), validates chain data against cardano-node:
- Layer 1: Block count in
blockstable vsepoch_nonces.block_count - Layer 2:
FindIntersectlast 50 blocks against node
If blocks are orphaned (CNPG failover): truncate all tables, full resync.
If nonce is stale but blocks valid: recompute from blocks table.
| Command | Description |
|---|---|
/menu |
Inline keyboard with all commands (primary group chat interface) |
/help |
Available commands |
/status |
DB sync status |
/tip |
Current chain tip |
/epoch |
Epoch progress and time remaining |
/leaderlog [next|current|<epoch>] |
Leader schedule |
/nextblock |
Next scheduled slot and time |
/nonce [next|current] |
Epoch nonce |
/validate <hash> |
Check block in local DB |
/stake |
Pool and network stake |
/blocks [epoch] |
Pool block count |
/ping |
Node connectivity |
/duck [gif|img] |
Random duck media |
/version |
Bot version |
Commands with subcommands show inline keyboard buttons when called without arguments.
| Network | Magic | Epoch Length | Active Slot Coeff |
|---|---|---|---|
| Mainnet | 764824073 |
432,000 slots (5 days) | 5% |
| Preprod | 1 |
432,000 slots (5 days) | 5% |
| Preview | 2 |
86,400 slots (1 day) | 5% |
| Sancho | 4 |
86,400 slots (1 day) | 5% |
| Variable | Required | Purpose |
|---|---|---|
TELEGRAM_TOKEN |
Yes | Telegram bot API token |
GODUCKBOT_DB_PASSWORD |
No (postgres only) | PostgreSQL password |
TWITTER_API_KEY |
No | Twitter/X API key |
TWITTER_API_KEY_SECRET |
No | Twitter/X API secret |
TWITTER_ACCESS_TOKEN |
No | Twitter/X access token |
TWITTER_ACCESS_TOKEN_SECRET |
No | Twitter/X access token secret |
goduckbot # Start daemon (default)
goduckbot version # Version, commit SHA, build date
goduckbot nonce <epoch> # Show epoch nonce (DB → computed → Koios fallback)
goduckbot leaderlog <epoch> # Leader schedule for a single epoch
goduckbot leaderlog <N>-<M> # Leader schedule for epoch range (max 10)
goduckbot history [--force] [--from N] # Build full historical leaderlogCLI subcommands require config.yaml in the working directory. The leaderlog and history commands require a VRF key and database.
CGO_ENABLED=0 go build -o goduckbot .docker pull wcatz/goduckbot:latestMultiarch images (amd64, arm64) published to Docker Hub.
helm install goduckbot oci://ghcr.io/wcatz/helm-charts/goduckbot| File | Purpose |
|---|---|
main.go |
Config, live tail pipeline, block notifications, Telegram/Twitter |
sync.go |
Historical chain syncer (NtN ChainSync) |
nonce.go |
Nonce evolution, backfill, integrity check, TICKN |
leaderlog.go |
CPraos schedule, VRF key parsing, slot/epoch math |
cli.go |
CLI subcommands (version, leaderlog, nonce, history) |
commands.go |
Telegram bot command handlers |
integrity.go |
Startup DB validation (FindIntersect + nonce repair) |
localquery.go |
NtC local state queries (QueryTip, QueryPoolStakeSnapshots) |
store.go |
Store interface + SQLite implementation |
db.go |
PostgreSQL implementation (pgx CopyFrom) |
securekey.go |
mmap/mlock/mprotect for VRF key material |
| Library | Purpose |
|---|---|
| blinklabs-io/gouroboros | VRF, NtN ChainSync, ledger types |
| cardano-community/koios-go-client | Koios API (stake data, nonce fallback) |
| jackc/pgx | PostgreSQL with COPY protocol |
| modernc.org/sqlite | Pure Go SQLite |
| golang.org/x/crypto | BLAKE2b-256 |
