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 → fsync of the directory. A crash never leaves a half-written file.
  • file.lock + an exclusive lock (flock on Linux and macOS, LockFileEx on 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 is POWM_WALLET_PASSWORD_<NAME>, otherwise the shared POWM_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-required is 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. getrawchangeaddress returns 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 with rpcuser/rpcpassword work without changes.

  • JSON-RPC 1.0 and 2.0, positional and named parameters, batches. A response has both result and error.

  • 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)
  • removed in listsinceblock, 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)