Documentation
The PowMachina wallet
Status: implemented · the code is in internal/wallet, internal/walletrpc,
cmd/powmachina-wallet-cli, cmd/powmachina-wallet-rpc
1. The architecture — like Monero’s
[powmachinad] the P2P network, consensus. NO keys.
↑ RPC: getinfo, getrawblocks, getrawmempool, sendrawtransaction
[powmachina-wallet-cli] a person, interactively
[powmachina-wallet-rpc] an exchange / a pool / a shop, Bitcoin-style JSON-RPC
↑
[the exchange's adapter]
- The wallet scans the blocks itself (
getrawblocks, 100 blocks per request) and looks for its coins locally. The node never learns which addresses belong to whom; the wallet can also be pointed at somebody else’s node. - The blocks are parsed by the same code as in the node (
chain.DeserializeBlock); the client verifies the block ID and the Merkle root against the header. - The process that talks to the internet (the node) has nothing to steal. The container with the keys (wallet-rpc) has no public port.
2. The keys
| what | how |
|---|---|
| the backup | BIP39, 24 words (256 bits of randomness). The word list is verified by the SHA-256 from BIP39, the implementation by Trezor’s vectors |
| the tree | BIP32, account m/44'/9797'/0', receiving addresses …/0/i, change …/1/i |
| the lookahead | 1,000 receiving, 100 change — a restore also finds addresses handed out before the backup |
| the address | bech32m powm1… / tpowm1… / rpowm1… |
| a private key (export) | bech32m powmsec1… / tpowmsec1… / rpowmsec1… — a checksum, not confusable with an address |
| imported keys | stored in the file, not covered by the 24 words |
A BIP39 passphrase is not supported (we do not write the NFKD normalization ourselves).
3. The wallet.dat file
One binary file, encrypted in full — without the password neither the addresses, nor the number of keys, nor the history are visible (Bitcoin Core only encrypts the private keys). The file name is arbitrary.
magic 8 B "POWMWLT\0"
version 2 B 1
network 4 B the network magic (PWM1 / testnet / regtest)
kdf 1 B 1 = Argon2id (RFC 9106)
memory 4 B KiB (262,144 = 256 MiB by default)
time 4 B (3 by default)
threads 1 B (4 by default)
salt 16 B
aead 1 B 1 = XChaCha20-Poly1305 (RFC 8439, a 24 B nonce)
nonce 24 B
length 4 B
sealed … the contents + a 16 B tag
- Little-endian. The whole header is AAD — changing the parameters, the network or the version breaks the tag.
- The contents are not parsed until the tag matches. Argon2 parameters outside the bounds (8 MiB – 4 GiB, ≤ 32 passes) are rejected before the computation — somebody else’s file cannot eat memory.
- The contents (inside the encryption): JSON with a version — the BIP39 entropy, the address counters, imported keys, labels, and a cache: the height, the last 501 blocks (for reorgs), the coins, the history.
- Writing: a temporary file →
fsync→rename→fsyncof the directory. A crash never leaves a half-written file. file.lock+ an exclusive lock (flockon Linux and macOS,LockFileExon Windows): a second process (wallet-cli while wallet-rpc is running, or two copies of the desktop wallet) cannot open the file. The lock belongs to the open handle, so a crash leaves nothing to clean up.- The file’s key is held in memory; every write gets a fresh random nonce. Changing the password = a new salt.
4. Synchronization
- From the wallet’s birth (a new one: the current tip; a restored one: the given height or 1).
- Every block has to build on the previous one. When it does not, the wallet finds the common ancestor among the last 501 blocks and goes back to it; a deeper reorg = a rescan from birth.
- Coins spent deeper than 500 blocks are forgotten from the file.
- The coin model is like BTC’s, not like Monero’s. No “main address with subaddresses”: all the HD addresses are equal, every deposit is a separate coin (a UTXO). Only what is genuinely being spent is locked — a block reward for 100 blocks, a deposit until its 1st confirmation, the coins of our own unconfirmed transaction until it confirms. The rest of the wallet keeps sending.
- Our own unconfirmed change can be spent immediately (bitcoind: “trusted”). The node chains
unconfirmed transactions up to 25 (
chain.MaxChain), so a hot wallet with one big coin does not have to wait for a block between withdrawals. The wallet does prefer confirmed coins, so that the chains stay short. - Our own sent transactions: the inputs are reserved until a block confirms them. When a transaction disappears from the node’s mempool (a restart, say), the wallet resends it after 2 minutes; when the node rejects it (the inputs were spent elsewhere), it drops it and releases the coins — along with our transactions that spent its unconfirmed change. After a reorg a confirmed transaction goes back among the unconfirmed ones.
5. powmachina-wallet-cli
powmachina-wallet-cli # asks for a name, offers to create one
powmachina-wallet-cli --generate-new-wallet mine # the password twice, 24 words
powmachina-wallet-cli --generate-new-wallet mine --restore-from-seed [--restore-height N]
powmachina-wallet-cli --wallet-file mine [--command balance]
The commands: address [new [label]], balance, transfer <address> <amount> […] (it shows the
fee and asks for confirmation), show_transfers, refresh, status, seed,
export_key <address>, import_key <key> [label], rescan [height], set_label, password,
save, exit. seed and export_key ask for the password again.
The environment: POWM_RPC_TOKEN (the node’s token), POWM_WALLET_PASSWORD (for scripts;
otherwise it asks).
6. powmachina-wallet-rpc (an exchange)
POWM_WALLET_PASSWORD_DEPOSIT=… POWM_WALLET_PASSWORD_HOT=… POWM_WALLET_RPC_TOKEN=… POWM_RPC_TOKEN=… \
powmachina-wallet-rpc --wallet-file /data/deposit --wallet-file /data/hot \
--change-address hot=powm1… \
--daemon-address powmachinad:7697 --rpc-bind 0.0.0.0:7695
-
Several wallets as in bitcoind’s multiwallet:
/wallet/<name>(the name = the file’s name),listwallets. A wallet answers on/only when a single one is loaded (otherwise −19). The password isPOWM_WALLET_PASSWORD_<NAME>, otherwise the sharedPOWM_WALLET_PASSWORD. -
walletpassphrase password seconds/walletlock— without unlocking, nothing is signed (withdrawals,signrawtransactionwithwallet,sweepall,dumpprivkey,importprivkey): −13. The RPC token alone is not enough for a withdrawal. The first verification costs an Argon2id (~1 s), the rest are instant.--no-passphrase-requiredis for development only. -
--change-address name=address— a fixed change address (the counterpart of HOT_MAIN_WALLET), so the change is not scattered across new addresses; it has to belong to that wallet.getrawchangeaddressreturns it. For a one-off elsewhere:fundrawtransaction … {"changeAddress": …}(sweeping deposits: change to the hot wallet). -
Authentication:
Authorization: Bearer <token>or HTTP Basic (any username, the password = the token) — Bitcoin-style clients withrpcuser/rpcpasswordwork without changes. -
JSON-RPC 1.0 and 2.0, positional and named parameters, batches. A response has both
resultanderror. -
The amounts: POWM with 8 decimal places, numbers on the wire as in bitcoind — but the server reads and writes them as text, never through a float. The input may also be a string; an exponent is rejected.
-
Error codes as in bitcoind: −5 an invalid address/key, −6 insufficient funds, −8 an invalid parameter, −28 the node is unavailable, −32601 an unknown method.
| the wallet’s methods | |
|---|---|
getnewaddress [label] |
a new deposit address |
getbalance [*] [minconf], getbalances, getunconfirmedbalance |
|
listunspent [minconf] [maxconf] [addresses] |
|
sendtoaddress address amount, sendmany "" {address: amount} |
there is no subtractfeefromamount |
gettransaction txid, listtransactions [label] [count] [skip] |
the categories receive, send, generate, immature; change is not shown |
listsinceblock [blockhash] [target_confirmations] |
an unknown block = from the beginning (so nothing is missed after a reorg); removed is still always empty |
getreceivedbyaddress address [minconf] |
|
listreceivedbyaddress [minconf] [include_empty] |
the wallet’s own addresses with label, amount received and txids; with include_empty every address, paid or not — this is how a UI lists them |
validateaddress, getaddressinfo, setlabel, getaddressesbylabel |
|
dumpprivkey address, importprivkey key [label] [rescan], rescanblockchain [from] |
|
getwalletinfo, listwallets |
|
walletpassphrase, walletlock, walletpassphrasechange |
|
getrawchangeaddress |
|
createrawtransaction, fundrawtransaction {changeAddress, changePosition, feeRate}, signrawtransactionwithwallet, decoderawtransaction, sendrawtransaction |
the deposit-sweeping path from chains/btc; sendrawtransaction reserves the coins in the wallet right away |
sweepall address [minconf] [maxinputs] [estimate] |
the whole confirmed balance to an address, in transactions of ≤ 500 inputs, the fee taken from the output — sweeping deposits in one call |
Forwarded to the node (one endpoint, as with bitcoind): getinfo, getblockcount,
getbestblockhash, getblockhash, getblock, getrawtransaction, getrawmempool, gettxout,
getlanestats, getfinality.
Sweeping deposits: many coins → one output has zero storage mass, you pay only for the bytes
(~107 B per input; 100 deposits ≈ 0.0001 POWM at the minimum rate). Careful: getblock has the
PowMachina node’s format, not bitcoind’s (verbose with vout.scriptPubKey) — an adapter that scans
blocks itself has to be checked for that; the recommended path is listsinceblock.
On regtest the node has generatetoaddress nblocks address — for testing an integration.
7. What is not there yet
- a watch-only wallet (an xpub) and separated signing (a cold wallet signing offline)
removedinlistsinceblock,abandontransaction,bumpfee,lockunspent- an orphan pool in the node: a child that reaches a neighbour before its parent is discarded (the wallet resends it after 2 min)
- a web wallet (the plan: the same package compiled to WebAssembly)