This is the test to run before trusting a word on the other pages. Two wallets, two nodes, one payment each way, then four viewpoints: the sender, the receiver, the receiver's accountant holding only a view key, and a stranger reading the raw chain file. At the end you try to break it.
Everything below runs in a terminal opened in the vesper folder. On one PC, "Alice" is the normal folder and "Bob" is a second data folder with a second node. On two PCs, Bob runs the same commands on his own machine and points his node at Alice's, see the note at the end of this section.
wallet.bat (creates data\wallets\default.json, shows Alice's tvsp2... address and her recovery code) start_node.bat (node + explorer at http://127.0.0.1:8880, mining to Alice)
python coin.py --data-dir data2 wallet new start_node2.bat (node 2 at http://127.0.0.1:8881, peer of node 1, does not mine)
Bob's commands always carry --node http://127.0.0.1:8881 --data-dir data2. Alice's carry nothing. Keep both node windows open; they are the network. A coin can be spent, or used as a decoy, only once it is 10 blocks old, so a brand-new chain refuses its first payment until 48 such coins exist: about 58 blocks, fifteen to thirty minutes of mining. The wallet tells you plainly when a coin is "not yet 10 blocks old." If your nodes are on testnet-1 instead of a chain of their own, the coins already exist and you skip the wait.
python testdrive.py, which runs exactly these steps on two throwaway nodes with a throwaway network of their own (it says "test-drive" where your nodes say "testnet-1"); its folder and port names are shown below as they are in the setup above. Long keys and identifiers are trimmed; block numbers and balances depend on the run and will differ on yours. Test coins are tVESP and worth nothing.python node.py --host 0.0.0.0 --mine, finds her PC's address with ipconfig, and Bob starts his with python node.py --peer http://ALICES-IP:8880. The nodes talk plain HTTP with no authentication; they now have per-address limits and bans, but they have never met strangers, so keep this to your own network until the public testnet. Do not want to type any of this? testdrive.bat runs the whole test on two throwaway nodes by itself, about five minutes, and deletes its folders afterwards.python coin.py info
Vesper (tVESP) node http://127.0.0.1:8880 v0.9.0 Network : test-drive (test coins: no value, ever) Privacy : receivers HIDDEN, amounts HIDDEN, senders HIDDEN Ring size : every input hides among 48 outputs Spend lock : a new output can be used 10 block(s) after the one that made it (about 5 min); 48 of 57 are old enough now Curve maths : libsodium Proof of work: argon2id, 32 MiB of memory per try Dev fund : off Height : 57 (tip 6c4a15258fa4c1dce3dd...) Difficulty : 3,938 tries per block now, 4,020 next (chain work 2^16.95) Block time : target 30s, recent average 6.8s Supply : 2850 tVESP of 199999.99978 tVESP max, in 57 output(s); 0 spent so far, and nobody can tell which Next reward : 50 tVESP (halving in 1942 blocks) Mempool : 0 waiting transaction(s) Peers : http://127.0.0.1:8881 Miner : RUNNING 361 tries/s 57 block(s) found
Fifty-seven blocks, fifty-seven outputs, all Alice's, and the node that holds them cannot say so. Forty-eight are old enough to spend, so from here on the chain can hide a payment in a full ring; it took 499 seconds of the test drive's fast mining to get there. The 361 tries a second is Argon2id at 32 MiB per try on eight worker processes: slow on purpose. (The 7-second blocks are the test drive's fast setting; the rule targets 30 seconds.)
python coin.py send tvsp2NpgcpxTj8hoxBvexwWHSAsi3gThwRLXJyNk...Uynh8QA 12.5
Sent 12.5 tVESP Fee 0.00001 tVESP (goes to whoever mines the block) txid 1188480b5a815fa769e7c5b1c948aa84e488aa1101d6de3f733c2fc00a085d70 On the chain this is: 1 input(s) -> 2 sealed output(s). No address, no amount, and no spent output appears anywhere: each input points at a ring of 48 outputs and proves it owns one, without saying which. Only the fee is public. One output is the payment, one is your change (even when the change is zero), in random order. Sealing, proving and ring-signing took 0.41s. On the network: your node passed it quietly to ONE peer, which passes it on in turn, and another node, usually several hops away, broadcasts it. The first broadcast does not come from your computer.
The last two lines are phase 4. Alice's node did not shout the payment to everyone; it handed it to one peer, and that peer broadcast it. With two nodes the path is one hop; on a real network it is several, and the node that broadcasts first is not the sender's.
python coin.py --node http://127.0.0.1:8881 --data-dir data2 wallet show
Wallet : default
Address : tvsp2NpgcpxTj8hoxBvexwWHSAsi3gThwRLXJyNkoTWaHL9oKvwKVjnvkYvheTWh787zkhPQpVhkaQ3YC2nwSRyxtymUynh8QA
File : data2\wallets\default.json (encrypted)
Balance : 12.5 tVESP (1 unspent output(s) of yours among 61 on the chain; 61 new one(s) read in 0.0s)
1 of them (12.5 tVESP) arrived in the last 10 blocks and can be spent once they are that old
12.5 tVESP sealed one-time key c6c5550b04dacb... from tx 1188480b5a81...
Sixty-one outputs on the chain, one is his, and his node learned nothing from the question: it handed over the new blocks and Bob did the finding at home. Next time it will hand over only the blocks since this look; the wallet keeps an encrypted notebook of what it has already read. That is his whole 98-character address above, the only place it ever appears.
python coin.py --node http://127.0.0.1:8881 --data-dir data2 wallet export-view python coin.py --node http://127.0.0.1:8881 watch data2\wallets\default.viewkey.json
View key written to data2\wallets\default.viewkey.json
Whoever holds that file can SEE every payment to your address and its amount, forever, and can never spend any of it.
It also carries the spent markers of the 1 output(s) you hold today, so its holder can see when those are spent.
Outputs you receive later show as 'spent status unknown' to them until you export a fresh file.
Watching address tvsp2NpgcpxTj8hoxBvexwWHSAsi3gThwRLXJyNk... with a view key only
Balance : 12.5 tVESP (1 unspent output(s) of yours among 61 on the chain; 61 new one(s) read in 0.0s)
1 of them (12.5 tVESP) arrived in the last 10 blocks and can be spent once they are that old
12.5 tVESP sealed one-time key c6c5550b04dacb... from tx 1188480b5a81...
(A view key opens the sealed amounts of this address. It still cannot spend any of it.)
One block per line, plain text. Search it for both addresses and the amount in every form it could take:
findstr /c:"(30 characters of Alice's address)" data2\chain.jsonl findstr /c:"NpgcpxTj8hoxBvexwWHSAsi3gThwRL" data2\chain.jsonl (30 characters of Bob's address) findstr /c:"1250000000" data2\chain.jsonl (12.5 tVESP in base units) findstr /c:"12.5" data2\chain.jsonl
30 characters of Alice's address found 0 time(s) 30 characters of Bob's address found 0 time(s) 12.5 tVESP in base units found 0 time(s) 12.5 found 0 time(s)
python coin.py tx 1188480b5a815fa7...
txid 1188480b5a815fa7... | transfer | in block 59, 1 confirmation(s) | inputs: 1 | outputs: 2 | fee: 0.00001 tVESP
input 0 : one of a ring of 48 outputs, e.g. 0385d50fd3..., 05b7e0715e..., 070c1f605f...
spent marker (key image) 2604d0abeaec4f8ee0d35836... pseudo commitment 5169bfad7efc6b31...
signature: c + 48 s-values + D (which of the 48 it was: not written anywhere)
output 0 : one-time key 5e33c62aaaec6b... sealed amount
output 1 : one-time key c6c5550b04dacb... sealed amount
one 736-byte proof that none of the 2 sealed amounts is negative
size: 9479 bytes of JSON (add --json to see every field)
The input no longer says which output it spends. It lists 48 existing outputs in sorted order and carries one ring signature proving the signer owns one of them, plus a key image: a one-way marker of the real output that lets every node refuse a second spend without learning which output it marks. The two outputs are sealed, with one 736-byte proof that neither hidden value is negative. Output 1 is the key Bob's wallet recognised in step 3; nothing in this listing says so, and the other output is Alice's change. Which of the 48 was really spent: not knowable from this.
python coin.py --node http://127.0.0.1:8881 --data-dir data2 send tvsp2...(Alice's address) 3
Not yet: Bob's 12.5 is inside the 10-block lock, and the wallet says so. Once it is 10 blocks old:
Sent 3 tVESP Fee 0.00001 tVESP (goes to whoever mines the block) txid 45769f774f8518b29d40ae1621337b64e2baac946685271cdb2485c8c50a1305 On the chain this is: 1 input(s) -> 2 sealed output(s). No address, no amount, and no spent output appears anywhere: each input points at a ring of 48 outputs and proves it owns one, without saying which. Only the fee is public. One output is the payment, one is your change (even when the change is zero), in random order. Sealing, proving and ring-signing took 0.38s. On the network: your node passed it quietly to ONE peer, which passes it on in turn, and another node, usually several hops away, broadcasts it. The first broadcast does not come from your computer.
Bob's node handed the payment to its one peer, Alice's, which broadcast it and mined it into the next block. The test drive checks exactly this: Bob's node passed it on instead of broadcasting it.
Balance : 9.49999 tVESP (1 unspent output(s) of yours among 73 on the chain; 25 new one(s) read in 0.0s)
1 more output(s) were yours and are spent (12.5 tVESP)
1 of them (9.49999 tVESP) arrived in the last 10 blocks and can be spent once they are that old
9.49999 tVESP sealed one-time key 815cb9c8c7f30a... from tx 45769f774f85...
12.5 in, 3 out, the fee gone, 9.49999 of change at a fresh key, locked for 10 blocks like any new coin. The view-key file Bob exported in step 4 carries the key image of the 12.5 output he held then, so his accountant can see that output was spent. The change arrived after the export, so the accountant sees its amount but not whether it has since been spent, until Bob exports a fresh file. That is the honest limit of a view key once senders are hidden.
python coin.py --data-dir data2 validate
Re-checking every block in data2 from the genesis block (every proof of work, signature and sum)... Chain OK: 70 blocks (height 69), 71 transactions, 73 outputs, 2 spends, chain work 2^17.46, supply 3450 tVESP - checked in 2.0s
Every ring signature, every key image, every sealed-amount sum and every range proof, re-verified from genesis by a computer that never saw an amount or a sender.
| Person | Knows | Does not know |
|---|---|---|
| Alice (sender) | She paid Bob 12.5; the one-time key and sealed amount she made | Bob's balance, his other payments, who else pays him, and now also when he spends what she gave him |
| Bob (receiver) | He received 12.5 at that one-time key | Which of the 48 ring members Alice really spent; her address, balance or other payments |
| Bob's accountant (view key) | Every payment to Bob with amounts, and spends of the outputs the key file was exported with | Anything about anyone else; cannot spend |
| A stranger with the chain file | Block 59 has a payment with 1 input (one of 48 outputs) and 2 sealed outputs; the fee | Who paid, who was paid, how much, which of the 48 was spent |
| The miner | Same as the stranger, plus that the fee is now theirs | Same as the stranger |
| The node Bob asked | Bob's IP address asked for the blocks since his last look and for whole rings when he paid; and when he paid, it received the payment from his node to pass on, so with two nodes it knows where it came from | Which outputs are Bob's; he scanned at home. On a real network, whether the node that handed it a payment was the sender or just another relay |
| Every other node | The payment, once a relay broadcast it | Which node started it: an attacker running a tenth of the nodes guesses right about a tenth of the time |
Each of these should fail. If one doesn't, you have found something worth the bounty, and the page you are reading is wrong.
Stop node 2. Open data2\chain.jsonl in Notepad, change one character inside any commitment, range_proof or signature, save, then run python coin.py --data-dir data2 validate.
Expected, from the run: "INVALID: block 59 of the saved chain breaks a rule: txid does not match the transaction contents." Start node 2 again and it logs that block 59 was damaged, keeps the good blocks before it, sets the file aside as chain.jsonl.damaged-<time> and fetches the rest from node 1.
As Bob: send vespALICE 50 when he holds 9.49999, still inside the lock.
Expected, from the run: "insufficient funds: have 0 tVESP, need 50.00001 tVESP (amount + fee). Another 9.49999 tVESP is yours but cannot be spent yet: it is less than 10 blocks old, or already in a payment that is waiting to be mined. Try again in a few minutes." Refused before it leaves the wallet.
Send a payment as Bob, and before it is mined send another from the same wallet.
Expected: the wallet refuses to reuse a coin that is already in a waiting payment. Build two payments from one coin by hand and the node refuses the second with "key image already used: that output was spent before (double spend)." The node never learned which output; the marker alone was enough.
Take the JSON of a confirmed transaction from coin.py tx TXID and post it to a node again.
Expected, from the run: "transaction is already in the chain."
Change a character in the middle of Bob's address and send 1 VESP to it.
Expected, from the run: "'tvsp2CbacAEbNVawwNbzK3dDwXZWrXY79xEhmqCZXbbCtUcP...' is not a valid tVESP address (they start with 'tvsp' and are long: two keys plus a checksum)." The checksum catches it. (A valid address that is simply the wrong person's is not caught by anything; copy-paste, never retype.)
Copy data2\wallets\default.viewkey.json over a spare wallet file and try to send from it.
Expected, from the run: "that is a view-key file: it can watch, not spend."
In the explorer, open block 59 and look at the payment's input. Try to find which output it spent.
Expected: you see 48 candidate outputs and a key image, and no way to pick the real one. Then try the statistical version: decoys are drawn from a measured spending pattern, and an attacker who knows that pattern still names the real coin 2.1% of the time where 2.1% would be fair (python leaktest.py prints the number). This is the known weakness of rings, it is the same one Monero carries, and it is why stage two of phase 3 exists.
Each session is one idea, one thing to run, and one question to keep asking until you can answer it without the page.
Run the learning chain's explorer and watch hashes chain together; change one byte in its chain file and watch validation fail.
Why is it cheap to check a block and expensive to make one, and why does that protect history?
Run python demo.py and read step 5: a signature that verifies against a key nobody can connect to a person.
What exactly does a signature prove, and what does it not prove?
Steps 1 to 4 of demo.py, then section 4 of the whitepaper, then steps 3 and 5 of this test.
Why can Bob find his payment and Eve cannot, when both have the same chain?
Step 6 of this test, then section 5 of the whitepaper: commitments, the balance equation, range proofs.
If nobody can see the numbers, how does the chain know no coins were created?
Step 6 of demo.py, the "spend the same output twice" and "follow the money" exercises, then section 6 of the whitepaper.
How can a node refuse a double spend without knowing which output was spent, and why is a crowd of 48 not enough?
Node 2's log, and the two-PC setup. Run python leaktest.py and read the two tables it prints. Then section 7 and the threat model.
Who sees your IP address when you pay, how often can a spy running a tenth of the nodes still guess it, and what does hiding it cost in time?