Skip to content

hmdsefi/staking

Repository files navigation

Staking

Staking is a staking backend for ETH that uses the Lido protocol on Ethereum. The system is split into five HTTP services plus one Temporal worker:

  • ledger — append-only money movements and materialized balances
  • protocols — Lido calldata builder, chain reader, and reconciliation endpoint
  • rewards — MasterChef-style reward index
  • pool — request orchestration and Temporal starter
  • relayer — transaction signing, broadcast, and status tracking
  • pool worker — Temporal workflows and schedules

Architecture

User-facing traffic is expected to come through your own API/BFF layer. Staking services only trust internal callers via X-Internal-Key.

graph TD
    bff["Your API / BFF"]
    pool["pool"]
    ledger["ledger"]
    rewards["rewards"]
    protocols["protocols"]
    relayer["relayer"]
    worker["pool worker"]
    temporal["Temporal"]
    eth["Ethereum RPC + Lido"]

    bff --> pool
    pool --> ledger
    pool --> rewards
    pool --> protocols
    pool --> relayer
    worker --> temporal
    temporal --> worker
    protocols --> ledger
    protocols --> eth
    relayer --> eth
Loading

See docs/architecture.md for the full flow and concurrency model.

Databases

Create one Postgres database per stateful service:

CREATE DATABASE ledger;
CREATE DATABASE protocols;
CREATE DATABASE rewards;
CREATE DATABASE pool;
CREATE DATABASE relayer;

Migrations

From the repo root:

make migrate

Roll back:

make migrate-down

Temporal

Start Temporal locally:

make temporal

Stop it:

make temporal-down

Required Services

Before running Staking, make sure you have:

  • Go 1.25+
  • PostgreSQL 15+
  • an Ethereum RPC URL for the target network
  • a relayer hot wallet private key in relayer/.env

Redis is not required anymore.

Environment Files

Each service loads .env from its own directory.

ledger/.env

DATABASE_URL=postgres://postgres:postgres@localhost:5432/ledger?sslmode=disable
HTTP_PORT=8101
INTERNAL_API_KEY=dev-internal-key-change-me

protocols/.env

DATABASE_URL=postgres://postgres:postgres@localhost:5432/protocols?sslmode=disable
HTTP_PORT=8102
INTERNAL_API_KEY=dev-internal-key-change-me
LEDGER_BASE_URL=http://127.0.0.1:8101
HTTP_CLIENT_TIMEOUT_SEC=30
ETH_RPC_URL=https://your-rpc
CUSTODY_ETH_ADDRESS=0xyour-custody-address
RELAYER_OPERATOR_ADDRESS=0xyour-relayer-signer
LIDO_STETH_ADDRESS=0x...
LIDO_WITHDRAWAL_QUEUE_ADDRESS=0x...
POOL_ID=eth_native

rewards/.env

DATABASE_URL=postgres://postgres:postgres@localhost:5432/rewards?sslmode=disable
HTTP_PORT=8103
INTERNAL_API_KEY=dev-internal-key-change-me

pool/.env

DATABASE_URL=postgres://postgres:postgres@localhost:5432/pool?sslmode=disable
HTTP_PORT=8104
INTERNAL_API_KEY=dev-internal-key-change-me
TEMPORAL_HOST_PORT=localhost:7233
LEDGER_BASE_URL=http://127.0.0.1:8101
REWARDS_BASE_URL=http://127.0.0.1:8103
PROTOCOLS_BASE_URL=http://127.0.0.1:8102
RELAYER_BASE_URL=http://127.0.0.1:8082
HTTP_CLIENT_TIMEOUT_SEC=30
STAKE_BATCH_THRESHOLD_WEI=1000000000000000000

relayer/.env

DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=relayer
HTTP_PORT=8082
RPC_URL=https://your-rpc
CHAIN_ID=1
SIGNER_KEYS=hex-private-key-without-0x
CONFIRMATIONS=1
GAS_LIMIT=300000
RELAYER_HTTP_API_KEY=
FALLBACK_GAS_TIP_CAP_WEI=1000000000

Run Order

From separate terminals:

go run ./ledger/cmd
go run ./protocols/cmd
go run ./rewards/cmd
go run ./pool/cmd
go run ./relayer/cmd

Then start the Temporal worker:

go run ./pool/cmd/worker

Health Checks

  • http://127.0.0.1:8101/status/ledger
  • http://127.0.0.1:8102/status/protocols
  • http://127.0.0.1:8103/status/rewards
  • http://127.0.0.1:8104/status/pool
  • http://127.0.0.1:8082/healthz

Primary Flows

Stake

  1. pool creates a pool_request and reserves balance in ledger.
  2. StakeBatchWorkflow claims pending stake requests under a DB transaction using FOR UPDATE SKIP LOCKED.
  3. protocols builds one batched Lido submit transaction.
  4. relayer signs and broadcasts it.
  5. pool confirms stake in ledger and updates rewards stake deltas.

Unstake

  1. pool creates an unstake request, updates ledger, and decreases the user reward position.
  2. UnstakeWorkflow asks protocols to build the approve/request calldata.
  3. relayer sends the transactions using stable message_ids derived from the Temporal workflow ID.
  4. protocols reads the withdrawal NFT id from the tx receipt and later polls claimability.
  5. Once claimable, protocols builds the claim tx, relayer sends it, and pool finalizes the ledger state.

Rewards

  1. RewardDistributionWorkflow asks protocols for the current getTotalPooledEther value.
  2. pool compares it with the last stored snapshot in protocol_metrics.
  3. The delta becomes new_rewards_wei and is sent to rewards.Accrue.
  4. rewards advances acc_reward_per_share using row locking and immutable event logs.

Reconciliation

ReconciliationWorkflow calls protocols which compares:

  • on-chain custody ETH balance
  • ledger total staked sum
  • Lido pooled ether snapshot

and writes the audit result to protocols.reconciliation_runs.

More Docs

  • docs/architecture.md — detailed service roles, workflows, race-control, reward index math
  • docs/schema.md — database schema reference
  • docs/example-walkthrough.md — concrete end-to-end example with table snapshots

License

This repository uses a production-restricted license in LICENSE.

  • Non-production use (development, testing, research) is allowed.
  • Production use is allowed only if you open-source the full deployed work under AGPL-3.0-or-later, or obtain prior written commercial/enterprise permission from the copyright holder.
  • Unauthorized enterprise production use is subject to retroactive fees and penalties as defined in LICENSE.

About

Staking backend for Ethereum using Lido, built as microservices with Temporal workflows for stake/unstake orchestration, rewards accounting, reconciliation, and secure transaction relaying.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors