10 KiB
10 KiB
wallet-daemon.md
Role: Local wallet service (keys, signing, RPC) for the 🤖 AITBC 🤑 stack
Audience: Windsurf (programming assistant) + developers
Stage: Bootable without blockchain; later pluggable to chain node
1) What this daemon is (and isn’t)
Goals
- Generate/import encrypted wallets (seed or raw keys).
- Derive accounts/addresses (HD, multiple curves).
- Hold keys locally and sign messages/transactions/receipts.
- Expose a minimal RPC for client/coordinator/miner.
- Provide a mock “ledger view” (balance cache + test transfers) until the chain is ready.
Non-Goals (for now)
- No P2P networking.
- No full node / block validation.
- No remote key export (never leaves box unencrypted).
2) Architecture (minimal, extensible)
- Process: Python FastAPI app (
uvicorn) - RPC: HTTP+JSON (REST) and JSON-RPC (both; same server)
- KeyStore: on-disk, encrypted with Argon2id + XChaCha20-Poly1305
- Curves:
ed25519(default),secp256k1(optional flag per-account) - HD Derivation: BIP-39 seed → SLIP-10 (ed25519), BIP-32 (secp256k1)
- Coin type (provisional):
AITBC = 12345(placeholder; replace once registered) - Auth: Local-only by default (bind
127.0.0.1), optional token header for remote. - Events: Webhooks + local FIFO/Unix-socket stream for “signed” notifications.
- Mock Ledger (stage-1): sqlite table for balances & transfers; sync adapter later.
Directory layout
wallet-daemon/
├─ app/ # FastAPI service
│ ├─ main.py # entrypoint
│ ├─ api_rest.py # REST routes
│ ├─ api_jsonrpc.py # JSON-RPC methods
│ ├─ crypto/ # key, derivation, sign, addr
│ ├─ keystore/ # encrypted store backend
│ ├─ models/ # pydantic schemas
│ ├─ ledger_mock/ # sqlite-backed balances
│ └─ settings.py # config
├─ data/
│ ├─ keystore/ # *.kdb (per wallet)
│ └─ ledger.db
├─ tests/
└─ run.sh
3) Security model (Windsurf implementation notes)
- Passwords: Argon2id (tuned to machine; env overrides)
ARGON_TIME=4,ARGON_MEMORY=256MB,ARGON_PARALLELISM=2(defaults)
- Encryption: libsodium
crypto_aead_xchacha20poly1305_ietf - At-Rest Format (per wallet file)
magic=v1-kdb salt=32B argon_params={t,m,p} nonce=24B ciphertext=… # includes seed or master private key; plus metadata JSON - In-memory: zeroize sensitive bytes after use; use
memoryview/ctypesscrubbing where possible. - API hardening:
- Bind to
127.0.0.1by default. - Optional
X-Auth-Token: <TOKEN>for REST/JSON-RPC. - Rate limits on sign endpoints.
- Bind to
4) Key & address strategy
-
Wallet types
- HD (preferred): BIP-39 mnemonic (12/24 words) → master seed.
- Single-key: import raw private key (ed25519/secp256k1).
-
Derivation paths
- ed25519 (SLIP-10):
m / 44' / 12345' / account' / change / index - secp256k1 (BIP-44):
m / 44' / 12345' / account' / change / index
- ed25519 (SLIP-10):
-
Address format (temporary)
- ed25519:
base32(bech32_hrp="ait")ofblake2b-20(pubkey) - secp256k1: same hash → bech32; flag curve in metadata
- ed25519:
Replace HRP and hash rules if the canonical AITBC chain spec differs.
5) REST API (for Windsurf to scaffold)
Headers
- Optional:
X-Auth-Token: <token> Content-Type: application/json
5.1 Wallet lifecycle
POST /v1/wallet/create- body:
{ "name": "main", "password": "…", "mnemonic_len": 24 } - returns:
{ "wallet_id": "…" }
- body:
POST /v1/wallet/import-mnemonic{ "name":"…","password":"…","mnemonic":"…","passphrase":"" }
POST /v1/wallet/import-key{ "name":"…","password":"…","curve":"ed25519|secp256k1","private_key_hex":"…" }
POST /v1/wallet/unlock{ "wallet_id":"…","password":"…" }→ unlocks into memory for N seconds (config TTL)
POST /v1/wallet/lock→ no bodyGET /v1/wallets→ list minimal metadata (never secrets)
5.2 Accounts & addresses
POST /v1/account/derive{ "wallet_id":"…","curve":"ed25519","path":"m/44'/12345'/0'/0/0" }
GET /v1/accounts?wallet_id=…GET /v1/address/{account_id}→{ "address":"ait1…", "curve":"ed25519" }
5.3 Signing
POST /v1/sign/message{ "account_id":"…","message_base64":"…" }→{ "signature_base64":"…" }
POST /v1/sign/tx{ "account_id":"…","tx_bytes_base64":"…","type":"aitbc_v0" }→ signature + (optionally) signed blob
POST /v1/sign/receipt{ "account_id":"…","payload":"…"}- Used by coordinator/miner to sign job receipts & payouts.
5.4 Mock ledger (stage-1 only)
GET /v1/ledger/balance/{address}POST /v1/ledger/transfer{ "from":"…","to":"…","amount":"123.456","memo":"test" }
GET /v1/ledger/tx/{txid}
5.5 Webhooks
POST /v1/webhooks{ "url":"https://…/callback", "events":["signed","transfer"] }
Error model
{ "error": { "code": "WALLET_LOCKED|NOT_FOUND|BAD_PASSWORD|RATE_LIMIT", "detail": "…" } }
6) JSON-RPC mirror (method → params)
wallet_create(name, password, mnemonic_len)wallet_import_mnemonic(name, password, mnemonic, passphrase)wallet_import_key(name, password, curve, private_key_hex)wallet_unlock(wallet_id, password)/wallet_lock()account_derive(wallet_id, curve, path)sign_message(account_id, message_base64)sign_tx(account_id, tx_bytes_base64, type)ledger_getBalance(address)/ledger_transfer(from, to, amount, memo)
Same auth header; endpoint /rpc.
7) Data schemas (Pydantic hints)
class WalletMeta(BaseModel):
wallet_id: str
name: str
curves: list[str] # ["ed25519", "secp256k1"]
created_at: datetime
class AccountMeta(BaseModel):
account_id: str
wallet_id: str
curve: Literal["ed25519","secp256k1"]
path: str # HD path or "imported"
address: str # bech32 ait1...
class SignedResult(BaseModel):
signature_base64: str
public_key_hex: str
algo: str # e.g., ed25519
8) Configuration (ENV)
WALLET_BIND=127.0.0.1
WALLET_PORT=8555
WALLET_TOKEN= # optional
KEYSTORE_DIR=./data/keystore
LEDGER_DB=./data/ledger.db
UNLOCK_TTL_SEC=120
ARGON_TIME=4
ARGON_MEMORY_MB=256
ARGON_PARALLELISM=2
9) Minimal boot script (dev)
# run.sh
export WALLET_BIND=127.0.0.1
export WALLET_PORT=8555
export KEYSTORE_DIR=./data/keystore
mkdir -p "$KEYSTORE_DIR" data
exec uvicorn app.main:app --host ${WALLET_BIND} --port ${WALLET_PORT}
10) Systemd unit (prod, template)
[Unit]
Description=AITBC Wallet Daemon
After=network.target
[Service]
User=root
WorkingDirectory=/opt/aitbc/wallet-daemon
Environment=WALLET_BIND=127.0.0.1
Environment=WALLET_PORT=8555
Environment=KEYSTORE_DIR=/opt/aitbc/wallet-daemon/data/keystore
ExecStart=/usr/bin/uvicorn app.main:app --host 127.0.0.1 --port 8555
Restart=always
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
[Install]
WantedBy=multi-user.target
11) Curl smoke tests
# 1) Create + unlock
curl -s localhost:8555/v1/wallet/create -X POST \
-H 'Content-Type: application/json' \
-d '{"name":"main","password":"pw","mnemonic_len":24}'
curl -s localhost:8555/v1/wallets
# 2) Derive first account
curl -s localhost:8555/v1/account/derive -X POST \
-H 'Content-Type: application/json' \
-d '{"wallet_id":"W1","curve":"ed25519","path":"m/44'/12345'/0'/0/0"}'
# 3) Sign message
curl -s localhost:8555/v1/sign/message -X POST \
-H 'Content-Type: application/json' \
-d '{"account_id":"A1","message_base64":"aGVsbG8="}'
12) Coordinator/miner integration (stage-1)
Coordinator needs:
GET /v1/address/{account_id}for payout address lookup.POST /v1/sign/receiptto sign{job_id, miner_id, result_hash, ts}.- Optional webhook on
signedto chain/queue the payout request.
Miner needs:
- Local message signing for proof-of-work-done receipts.
- Optional “ephemeral sub-accounts” derived at
change=1for per-job audit.
13) Migration path to real chain
- Introduce ChainAdapter interface:
get_balance(address),broadcast_tx(signed_tx),fetch_utxos|nonce,estimate_fee.
- Implement
MockAdapter(current), thenAitbcNodeAdapter(RPC to real node). - Swap via
WALLET_CHAIN=mock|aitbc.
14) Windsurf tasks checklist
- Create repo structure (see Directory layout).
- Add dependencies:
fastapi,uvicorn,pydantic,argon2-cffi,pynacl(orlibsodiumbindings),bech32,sqlalchemy(for mock ledger),aiosqlite. - Implement keystore (encrypt/decrypt, file IO, metadata).
- Implement HD derivation (SLIP-10 for ed25519, BIP-32 for secp256k1).
- Implement address helper (hash → bech32 with
ait). - Implement REST routes, then JSON-RPC shim.
- Implement mock ledger with sqlite + simple transfers.
- Add webhook delivery (async task + retry).
- Add rate limits on signing; add unlock TTL.
- Write unit tests for keystore, derivation, signing.
- Provide
run.sh+ systemd unit. - Add curl examples to
README.
15) Open questions (defaults proposed)
- Keep both curves or start ed25519-only? → Start ed25519-only, gate secp256k1 by flag.
- HRP
aitand hashblake2b-20acceptable as interim? → Yes (interim). - Store per-account tags (e.g., “payout”, “ops”)? → Yes, simple string map.
16) Threat model notes (MVP)
- Local compromise = keys at risk ⇒ encourage passphrase on mnemonic + short unlock TTL.
- Add IPC allowlist if binding to non-localhost.
- Consider YubiKey/PKCS#11 module later (signing via hardware).
17) Logging
- Default: INFO without sensitive fields.
- Redact: passwords, mnemonics, private keys, nonces.
- Correlate by
req_idheader (generate if missing).
18) License / Compliance
- Keep crypto libs permissive (MIT/ISC/BSD).
- Record algorithm choices & versions in
/aboutendpoint for audits.
Proceed to generate the scaffold (FastAPI app + keystore + ed25519 HD + REST) now? y / n