Summary
The hardcoded DEFAULT_PUBLISHER_KEY in node/src/lib.rs:44-45 is a publicly known test key (1234567890abcdef × 4). Drainer bots watching Mutinynet for incoming transactions to addresses derived from publicly-grepable keys sweep the publisher wallet within minutes of any topup — minimum-fee, single-input → single-output, rotating recipient addresses.
Forensic evidence
Publisher Taproot address derived from the burned key: tb1pvyhjfd90wmlw2dyjjdg2k0zqfp6mr9sufu4ztjwu78z2t0h5pjgshwz8up
Esplora stats: funded_txo_count: 312, spent_txo_count: 312 (all received → all spent), lifetime ~10.3M sats.
4 historical drain transactions on this address (signet/Mutinynet):
| Drain TX (first 16 chars) |
In |
Out (to drainer) |
Fee |
Drainer address (rotating) |
8c8fe64ddb036981… |
1,000,000 |
999,889 |
111 |
tb1pulayahl2u6mcutpwqw3v70g34mlhamtuetm32wpplclanx95d20slv35wc |
a7211d0d24aa0c6a… |
98,605 |
98,494 |
111 |
tb1pnf9fe29mrdcxulpph9lkyk92fh8rd9jt6jllmc8d75nzux54s9lqkhdga2 |
e9ef0b16797c5b89… |
98,884 |
98,773 |
111 |
tb1ppkal7eaxzepwylueraxrt25gzgdn6xjs04pphgupz7yzekz5scqqt8vtt2 |
6173868903d4592a… |
99,163 |
99,052 |
111 |
tb1plql0j9umkey086c0nk7pawz4vhjj3a4a7sauj2w9crz0me3x29sqasfzxy |
Signature pattern (consistent across all 4):
- Single input from publisher
- Single output to a unique destination (no change back to publisher)
- Fee exactly 111 sats (network-minimum)
- TX size exactly 162 bytes / 444 weight (no witness data — pure key-path spend)
- Triggered within minutes of each top-up arriving in mempool
This is a fully-automated mempool sweeper bot keyed on the publicly known private key.
Root cause
node/src/lib.rs:80-87:
pub static ref PUBLISHER_KEY: String = {
let key = std::env::var("PUBLISHER_KEY")
.unwrap_or_else(|_| DEFAULT_PUBLISHER_KEY.to_string());
if NETWORK_CONFIG.is_mainnet && key == DEFAULT_PUBLISHER_KEY {
panic!("PUBLISHER_KEY env var must be set for mainnet");
}
key
};
The mainnet-only panic guard allowed DEV (and any other non-mainnet deploy) to silently fall back to the burned test key. Any deployment whose secret-management pipeline did not list PUBLISHER_KEY as a required env var would silently inherit the burned key on every non-mainnet network.
Impact
| Scope |
Severity |
| Non-mainnet deploys (DEV, signet) |
Wallet drained within ~3 min of every topup. Mint inscriptions cannot broadcast (publisher empty). API E2E tests dev_skip!-masked failures until #94 made the failure visible. |
| Mainnet deploys |
Not impacted by the silent-fallback path — the mainnet-only panic guard catches an unset PUBLISHER_KEY on mainnet. Code hygiene risk remains regardless. |
| Code hygiene |
Hardcoded test key + network-conditional guard is an audit red flag, even after the fallback is removed. |
Fix plan
- Code (PR #102 — Draft): Remove
DEFAULT_PUBLISHER_KEY entirely. PUBLISHER_KEY env var is REQUIRED on every network (DEV / signet / mainnet). Panic at startup if not set, with a message pointing at openssl rand -hex 32 for local dev.
- Operator action: Provision a freshly-generated 32-byte hex
PUBLISHER_KEY in your deployment's secret manager and wire it through to the node container env. Must happen BEFORE PR #102 merges — otherwise the next deploy panics.
- CI (in PR #102): Inject
PUBLISHER_KEY: 0000…0001 as a 32-byte hex placeholder in the node-tests and coverage job env (clearly NOT a secret; chosen so a future grep for the burned 1234… key catches regressions).
Lessons
- Hardcoded secrets in production code are toxic even when behind a mainnet guard — the guard only catches the configured-network case, not a key intentionally checked into source.
- The previous
dev_skip!() macro pattern in node/tests/api_remote.rs masked the 503 ("publisher empty") errors as test-passes. #94 removes that masking — without it, this issue would still be invisible behind a green CI.
Related
- PR #94 —
test: harden suite — remove dev_skip masking + publisher preflight (already merged)
- PR #102 —
security: require PUBLISHER_KEY env var on every network (no default) (Draft)
- Issue #89 —
Mint state-desync class (related: removed DEV_SKIP_BROADCAST_FAILURE which masked some publisher-empty failures)
Summary
The hardcoded
DEFAULT_PUBLISHER_KEYinnode/src/lib.rs:44-45is a publicly known test key (1234567890abcdef× 4). Drainer bots watching Mutinynet for incoming transactions to addresses derived from publicly-grepable keys sweep the publisher wallet within minutes of any topup — minimum-fee, single-input → single-output, rotating recipient addresses.Forensic evidence
Publisher Taproot address derived from the burned key:
tb1pvyhjfd90wmlw2dyjjdg2k0zqfp6mr9sufu4ztjwu78z2t0h5pjgshwz8upEsplora stats:
funded_txo_count: 312,spent_txo_count: 312(all received → all spent), lifetime ~10.3M sats.4 historical drain transactions on this address (signet/Mutinynet):
8c8fe64ddb036981…tb1pulayahl2u6mcutpwqw3v70g34mlhamtuetm32wpplclanx95d20slv35wca7211d0d24aa0c6a…tb1pnf9fe29mrdcxulpph9lkyk92fh8rd9jt6jllmc8d75nzux54s9lqkhdga2e9ef0b16797c5b89…tb1ppkal7eaxzepwylueraxrt25gzgdn6xjs04pphgupz7yzekz5scqqt8vtt26173868903d4592a…tb1plql0j9umkey086c0nk7pawz4vhjj3a4a7sauj2w9crz0me3x29sqasfzxySignature pattern (consistent across all 4):
This is a fully-automated mempool sweeper bot keyed on the publicly known private key.
Root cause
node/src/lib.rs:80-87:The mainnet-only panic guard allowed DEV (and any other non-mainnet deploy) to silently fall back to the burned test key. Any deployment whose secret-management pipeline did not list
PUBLISHER_KEYas a required env var would silently inherit the burned key on every non-mainnet network.Impact
dev_skip!-masked failures until #94 made the failure visible.PUBLISHER_KEYon mainnet. Code hygiene risk remains regardless.Fix plan
DEFAULT_PUBLISHER_KEYentirely.PUBLISHER_KEYenv var is REQUIRED on every network (DEV / signet / mainnet). Panic at startup if not set, with a message pointing atopenssl rand -hex 32for local dev.PUBLISHER_KEYin your deployment's secret manager and wire it through to the node container env. Must happen BEFORE PR #102 merges — otherwise the next deploy panics.PUBLISHER_KEY: 0000…0001as a 32-byte hex placeholder in thenode-testsandcoveragejob env (clearly NOT a secret; chosen so a future grep for the burned1234…key catches regressions).Lessons
dev_skip!()macro pattern innode/tests/api_remote.rsmasked the 503 ("publisher empty") errors as test-passes. #94 removes that masking — without it, this issue would still be invisible behind a green CI.Related
test: harden suite — remove dev_skip masking + publisher preflight(already merged)security: require PUBLISHER_KEY env var on every network (no default)(Draft)Mint state-desync class(related: removedDEV_SKIP_BROADCAST_FAILUREwhich masked some publisher-empty failures)