Skip to content

Security: BlockIndexAI/digidollar-toolkit

Security

SECURITY.md

Securing a DigiDollar payment installation

How to run this toolkit safely in production. Written for the merchant / node-operator who will actually deploy it. If you follow this document, you can accept DigiDollar and DGB with a blast radius you understand and control.

New to any of this? Start with the README and the operator playbook. This page assumes you have read the "five DigiDollar facts" there — the security model follows directly from them.

DigiDollar is DigiByte's native, exact-USD instrument: 1 DD = $1.00 by definition, denominated in integer US cents on-chain. DGB is the DigiByte coin that pays the network fee. This toolkit lets you accept both non-custodially, from a DigiByte Core node you run — no hosted gateway, no third party ever holding a cent. That sovereignty is the whole point, and it is also the reason security is your job. This guide is how you do that job well.


1. The core reality: your detecting node is a HOT wallet

There is one fact you cannot design around, only mitigate:

DigiByte Core v1 has no working watch-only model for DigiDollar. importdigidollaraddress is a no-op, and watch-only DigiDollar outputs are excluded from balance by design. The DigiDollar value lives in an OP_RETURN, not in the output's satoshi value — so the only way to detect a DigiDollar payment is to run the DigiDollar RPCs (listdigidollarunspent / listdigidollartxs) against a wallet that holds the spending keys.

Therefore: the node that watches for DigiDollar payments is a hot wallet. It has private keys, and it is online. You cannot make it an air-gapped, watch-only deposit box the way you can for BTC/LTC or even for DGB coin (which does support watch-only). This is a protocol constraint of Core v1, not a choice this toolkit made — see operator playbook §0 and the protocol docs in the DigiByte Core repo (DIGIDOLLAR_ARCHITECTURE.md, DIGIDOLLAR_WALLET_INTEGRATION.md).

Everything else in this document is about shrinking that blast radius: keep the hot balance small, sweep the rest to cold storage, lock the node down so nothing but your own scripts can reach it, and make sure a stolen secret can't redirect customer funds. If someone fully compromises the hot node, the most they can take is what is currently sitting in the hot wallet — which is why "keep it low, sweep often" is the first commandment.


2. Harden the wallet and the node

The hot node is your crown jewel. Treat it like one.

Encrypt the wallet, unlock it only in short bursts

# One-time: encrypt the hot DigiDollar wallet with a strong passphrase.
digibyte-cli -rpcwallet=dd_hot encryptwallet "<long random passphrase>"

The wallet must be unlocked only for the two brief operations that need private keys — minting fresh receive addresses, and sweeping to cold — and locked the rest of the time:

# Unlock for 30 seconds, do the thing, then lock immediately — don't wait for timeout.
digibyte-cli -rpcwallet=dd_hot walletpassphrase "<passphrase>" 30
digibyte-cli -rpcwallet=dd_hot getdigidollaraddress "pool-refill"
digibyte-cli -rpcwallet=dd_hot walletlock
  • Never store the passphrase in the same place as the wallet file. Never bake it into digibyte.conf, a script, or a shell history. Prefer a secrets manager or an interactive prompt at unlock time.
  • Detection (listdigidollarunspent, listdigidollartxs, getdigidollarbalance) does not require the wallet to be unlocked — only address generation and spending do. Keep the wallet locked during normal watching.

Bind the RPC to localhost — never expose it

The node's JSON-RPC gives full control of the hot wallet. It must never be reachable from the internet. In digibyte.conf:

server=1
digidollar=1
txindex=1                 # MANDATORY — DigiDollar refuses to start without it
rpcbind=127.0.0.1         # localhost ONLY
rpcallowip=127.0.0.1      # accept RPC only from the loopback interface
rpcauth=...               # strong generated credentials — see below
  • Generate rpcauth with the official rpcauth.py tool; use a long random password. Do not use rpcuser/rpcpassword in cleartext if you can avoid it.
  • Do not set rpcbind to 0.0.0.0 or a LAN address "just to test." If a remote process must reach the node, tunnel it over SSH — do not open the RPC port.
  • Only your own toolkit scripts (walletnotify.sh, blocknotify.sh, refill-dd-pool.mjs, or the pull-model watcher.mjs) should ever call the RPC, and they run on the same box over 127.0.0.1.

Lock down the host

  • Least-privileged OS user. Run digibyted as a dedicated non-root user that owns only the datadir and wallet. Don't run the node as root.
  • Full-disk encryption on the node (LUKS / FileVault / equivalent). The wallet file is a target even at rest.
  • SSH: key-only, no passwords. PasswordAuthentication no, PermitRootLogin no, and a firewall that exposes nothing but the SSH port (and, if you must, only from known IPs). The RPC, ZMQ, and P2P ports do not need to be publicly reachable.
  • Keep the OS and DigiByte Core patched. Track the DigiByte Core repo for security releases.

3. Sweep to cold — keep the hot balance low

The single most effective mitigation for a hot wallet is to not keep much in it. Received DigiDollar is spendable after 1 confirmation, so you can sweep promptly.

Policy: on a schedule (e.g. hourly) or a threshold (e.g. whenever the hot balance exceeds $500), move confirmed DigiDollar and DGB out of dd_hot to an offline / cold descriptor wallet, and record each sweep (BlockIndex logs these to crypto_sweeps). The watcher and walletnotify references leave the sweep as a stub wired to your policy — see watcher-reference §Responsibilities and operator playbook §6.

# Sweep example (unlock briefly, send to your cold DD address, re-lock):
digibyte-cli -rpcwallet=dd_hot walletpassphrase "<passphrase>" 60
digibyte-cli -rpcwallet=dd_hot senddigidollar "<COLD_DD_ADDRESS>" <amount_cents>
digibyte-cli -rpcwallet=dd_hot walletlock
  • Sweep only confirmed, safe:true DigiDollar (listdigidollarunspent reports confirmations and safe). Don't sweep 0-conf funds.
  • The cold destination should be an offline descriptor wallet (or a hardware wallet's DD address) whose keys never touch the hot node.

Back up and restore the descriptor wallet

A hot wallet you can't recover is its own disaster. Descriptor wallets have a clean backup/restore path:

# Back up — export the wallet's descriptors (store the output somewhere safe & encrypted).
digibyte-cli -rpcwallet=dd_hot listdescriptors            # public
digibyte-cli -rpcwallet=dd_hot listdescriptors true       # WITH private keys — treat like a seed

# Restore — recreate the wallet and import, then rescan history.
digibyte-cli createwallet "dd_hot_restore"
digibyte-cli -rpcwallet=dd_hot_restore importdescriptors '[{"desc":"<descriptor with privkey>","active":true,"timestamp":0,"range":[0,1000]}]'
digibyte-cli -rpcwallet=dd_hot_restore rescanblockchain
  • listdescriptors true reveals private keys — its output is seed-equivalent. Encrypt it, store it offline, and never paste it into a chat, a ticket, or a log.
  • After importing, rescanblockchain re-derives your balance from chain history. With txindex=1 (required anyway for DigiDollar) this is straightforward.

4. The two shared secrets — and pool poisoning

This toolkit uses two independent shared secrets between your node and your backend. Understanding why there are two is the most important security idea here.

Secret Direction Guards Config var
Ingest secret node → backend payment reports ({asset, network, address, txid, vout, amount, confirmations}) reach the ingest endpoint INGEST_SECRET
DD refill secret node → backend fresh DigiDollar receive addresses reach the address pool DD_REFILL_SECRET

Both are sent in the x-crypto-ingest-secret header over TLS and verified constant-time before any side effect. See .env.example.

Why the refill secret MUST be its own secret: pool poisoning

Because DigiDollar has no watch-only model, receive addresses can't be derived by your web backend from a cold xpub — they must be minted by the hot node and pushed into an address pool that invoice creation draws from (refill-dd-pool.mjs).

That makes the refill endpoint a uniquely dangerous target:

Pool poisoning: anyone who can POST to your refill endpoint can inject attacker-owned DigiDollar addresses into the pool. Your next invoice hands one of those addresses to a real customer, the customer pays, and the money goes to the attacker — not you. No wallet was hacked; the payment simply never belonged to you.

Defenses:

  1. Use a dedicated DD_REFILL_SECRET, distinct from INGEST_SECRET. The refill script falls back to INGEST_SECRET if you leave it unset — do not rely on that fallback in production. Two secrets means compromising the (more widely wired) ingest path does not hand over the ability to poison the pool.
  2. Audit the pool. Periodically verify that every unused address in the pool is ismine on your hot node — i.e., it was really minted by you and you hold its key. Any pool address that is not ismine is an injected address; freeze invoice creation and rotate the refill secret immediately.
# For each unused pool address, confirm it belongs to your wallet:
digibyte-cli -rpcwallet=dd_hot getaddressinfo "<pool_address>" | jq .ismine
# Expect true for every one. A false is a poisoned entry — investigate now.

Handling both secrets

  • Rotate both secrets periodically, and immediately on any suspicion. Change them on the backend and the node's .env together, then restart the node.
  • Never commit a secret to git (the toolkit's .env.example holds placeholders only), never bake one into an image, never paste one into an issue.
  • Never log a secret. The scripts send it in a header, not in a logged body; keep it that way.
  • Generate them with real entropy: openssl rand -hex 32.

5. Endpoint hardening (ingest + refill)

Your backend exposes two secret-gated endpoints (crypto-payment-ingest and crypto-dd-pool-refill). Harden both:

  • TLS everywhere. Serve ingest and refill over HTTPS only. The secret is a bearer token — never let it cross the wire in cleartext.
  • IP allowlist. These endpoints are only ever called by your node. Restrict them to your node's source IP (at the load balancer, WAF, or edge-function guard). A leaked secret is far less useful to an attacker who also has to originate from your node's address.
  • Timing-safe secret comparison. Compare the presented secret with a constant-time equality check (e.g. crypto.timingSafeEqual), never === on strings — a naive compare leaks the secret one byte at a time under a timing attack. The toolkit's receivers already do this; keep it if you re-implement.
  • Reject nonsense. Reject any reported amount <= 0, unknown asset/network, or malformed address before it touches invoice logic.
  • The node's RPC is not one of these endpoints. It stays on 127.0.0.1 and is never exposed to the internet — not behind a "temporary" tunnel, not on a LAN IP, not ever. Only ingest and refill face outward, and only over TLS + secret + IP allowlist.

6. Payment-integrity invariants (never trust the client)

These are the rules that keep a clever customer — or a replayed request — from crediting themselves for free. They are enforced in the unit-tested @blockindex/crypto-payments library; if you re-implement any of it, keep every one.

  • Never trust a client-reported payment. The browser saying "I paid" means nothing. Credit an order only on a payment your node verified on-chain and reported through the secret-gated ingest path. Everything the client sends is a hint, not a fact.
  • Require N confirmations before crediting. Confirmation policy is USD-tiered: < $100 → 6 confs (~90 s at 15-second blocks), $100–$10k → 20, > $10k → 60 (all env-overridable). Low-confirmation credit invites a reorg to reverse a payment you already fulfilled. Treat 0-conf / RBF-pending as double_spend_risk, never as paid.
  • Idempotency by (asset, network, txid, vout). Key every credit on the exact output. A block re-scan or a retried POST will report the same payment more than once — the credit must apply exactly once no matter how many times it arrives.
  • Integer cents only — never floats. DigiDollar amounts are integer US cents as BigInt. DGB/BTC/LTC amounts are integer base units (satoshis). A sats-vs-cents or float mixup is a 10⁶-scale money bug — silent rounding will over- or under-credit by a factor of a million. No Number math on money, no floating-point, ever.
  • One fresh address per invoice, never reused. Every invoice gets a brand-new receive address (getdigidollaraddress for DD; HD-derived from a cold xpub for BTC/LTC/DGB). Reuse breaks the address→invoice match, leaks customer privacy, and can misattribute a payment. Honor the wallet's gap limit when pre-minting.

7. Operational monitoring

Security failures often show up first as operational weirdness. Watch:

  • Pool depth. If the DigiDollar address pool runs dry, invoice creation starts failing (a 503) and you lose sales. Alert when unused pool depth drops below a threshold, and make sure the refill cron (refill-dd-pool.mjs) is actually running. A pool that grows unexpectedly, or contains non-ismine addresses, is a poisoning signal — see §4.
  • Stuck / underpaid / manual-review invoices. Payments that arrive late, short, or after slot-expiry land in manual_review rather than silently crediting or silently vanishing. Watch that queue — an unattended pile there can mean a detection problem or an attempted underpayment.
  • Hot-wallet balance. Alert when getdigidollarbalance (plus the DGB balance) crosses your sweep threshold, and confirm the sweep actually fired. A hot balance that keeps climbing means your sweep is broken and your blast radius is growing.

8. Security checklist

Copy this into your runbook and tick every box before you take real money.

Node & wallet

  • Hot DigiDollar wallet is encrypted (encryptwallet); passphrase not stored with the wallet.
  • Wallet is unlocked only briefly (address mint / sweep), then walletlock — never left unlocked.
  • RPC bound to 127.0.0.1 (rpcbind/rpcallowip); strong rpcauth; never internet-exposed.
  • txindex=1 and digidollar=1 set; node built from the pinned feature/digidollar-v1 RC.
  • Node runs as a least-privileged OS user (not root); host has full-disk encryption.
  • SSH is key-only (no passwords, no root login); firewall exposes nothing but SSH.

Cold storage

  • Hot balance kept low; confirmed DD/DGB swept to cold on a schedule/threshold (e.g. > $500).
  • Only confirmed, safe:true funds are swept.
  • Descriptor wallet backed up (listdescriptors true, stored offline & encrypted); restore path tested.

Secrets

  • Separate INGEST_SECRET and DD_REFILL_SECRETnot the fallback-shared value.
  • Secrets are long/random (openssl rand -hex 32), never committed, never logged, rotated periodically.

Endpoints

  • Ingest + refill served over TLS, with an IP allowlist for the node's source IP.
  • Secret comparison is constant-time; amount <= 0 and malformed input rejected.

Payment integrity

  • Every credit comes from an on-chain-verified payment — never a client claim.
  • N confirmations required before credit (USD-tiered 6 / 20 / 60); 0-conf/RBF = double-spend risk.
  • Credits are idempotent by (asset, network, txid, vout).
  • Money is integer base units (cents / sats) as BigIntnever floats.
  • One fresh address per invoice; no reuse; gap limit honored.

Pool integrity & monitoring

  • Every unused pool address audited as ismine on the hot node.
  • Alerts on: pool depth, hot-wallet balance, and the manual-review queue.
  • DigiDollar mainnet gated behind a feature flag until BIP9 activation (DigiDollar is now live on mainnet, 2026).

9. Reporting a vulnerability

In this toolkit (@blockindex/crypto-payments, the guides, the demo): please report privately and responsibly.

  • Open a private report or a security advisory via the toolkit's repository, or raise it with the maintainers through DigiByte-Core Discussions.
  • Please do not open a public issue with exploit details for anything that could put merchants' funds at risk before a fix is available. Give us a reasonable window to respond and patch, then coordinate disclosure.
  • Include: affected file/version, a clear reproduction, and the impact (especially anything touching funds, secrets, or the pool).

In the DigiByte Core protocol or DigiDollar consensus code (the node itself, the DigiDollar RPCs, the oracle, activation): report to the DigiByte Core project, not here. Follow its SECURITY.md in the DigiByte-Core/digibyte repository. Consensus- and wallet-level issues are handled upstream under their disclosure process — the deeper protocol security model lives in DIGIDOLLAR_ARCHITECTURE.md and DIGIDOLLAR_ORACLE_ARCHITECTURE.md.


See also

BlockIndex.AI runs exactly this toolkit in production to accept DigiDollar for its $350 lifetime membership at blockindex.ai/founder ("Pay with Crypto" → DigiDollar). The security practices above are the ones that keep that live checkout safe.

There aren't any published security advisories