├── .eslintrc.js ├── .gitignore ├── README.md ├── ch1 ├── 1.1 │ ├── README.md │ ├── client.js │ ├── demo.js │ ├── solutions │ │ ├── client.js │ │ └── demo.js │ └── test │ │ └── test.js ├── 1.2 │ ├── README.md │ ├── client.js │ ├── demo.js │ ├── paypal.js │ ├── solutions │ │ ├── client.js │ │ ├── demo.js │ │ └── paypal.js │ └── test │ │ └── test.js ├── 1.3 │ ├── README.md │ ├── client.js │ ├── demo.js │ ├── paypal.js │ ├── solutions │ │ ├── client.js │ │ ├── demo.js │ │ └── paypal.js │ └── test │ │ ├── .test.js.swp │ │ └── test.js ├── 1.4 │ └── README.md └── 1.5 │ ├── README.md │ ├── client.js │ ├── demo.js │ ├── paypal.js │ ├── solutions │ ├── client.js │ ├── demo.js │ └── paypal.js │ └── test │ └── test.js ├── ch2 ├── 2.1 │ ├── README.md │ ├── solutions │ │ └── spenderNode.js │ ├── spenderNode.js │ └── test │ │ └── invalidWithHonestNodesTest.js ├── 2.2 │ ├── README.md │ ├── doubleSpendTest.js │ └── solutions │ │ └── doubleSpendTest.js ├── 2.3 │ ├── README.md │ ├── doubleSpendTest.js │ ├── faultTolerant.js │ ├── faultTolerantTest.js │ ├── networkSimFaultTolerant.js │ ├── solutions │ │ ├── doubleSpendTest.js │ │ ├── faultTolerant.js │ │ ├── faultTolerantTest.js │ │ ├── networkSimFaultTolerant.js │ │ └── timestampTest.js │ └── timestampTest.js ├── 2.4 │ ├── Authority.js │ ├── PoAClient.js │ ├── PoATest.js │ ├── README.md │ ├── doubleSpendTest.js │ ├── networkSimPoA.js │ ├── orderNonceTest.js │ └── solutions │ │ ├── Authority.js │ │ ├── PoAClient.js │ │ ├── PoATest.js │ │ ├── doubleSpendTest.js │ │ ├── networkSimPoA.js │ │ └── orderNonceTest.js ├── README.md ├── netTest.js ├── networkSim.js └── nodeAgent.js ├── ch3 ├── .networksim.js.swp ├── 3.1 │ ├── PoWClient.js │ ├── README.md │ ├── solutions │ │ └── PoWClient.js │ └── test │ │ ├── PoWAllMinerTest.js │ │ ├── PoWMiner.js │ │ ├── PoWSingleMinerTest.js │ │ └── test.js ├── 3.2 │ ├── PoWClient.js │ ├── PoWMiner.js │ ├── README.md │ ├── solutions │ │ ├── PoWClient.js │ │ └── PoWMiner.js │ └── test │ │ ├── PoWAllMinerTest.js │ │ ├── PoWClient.js │ │ └── PoWSingleMinerTest.js ├── 3.3 │ ├── MerkleTree.js │ ├── README.md │ ├── solutions │ │ ├── merkleTree.js │ │ └── verifyProof.js │ ├── test │ │ ├── test.js │ │ └── testUtil.js │ └── verifyProof.js ├── 3.4 │ └── README.md ├── 3.5 │ ├── PoWClient.js │ ├── PoWMiner.js │ ├── README.md │ ├── SelfishMiner.js │ ├── solutions │ │ └── SelfishMiner.js │ └── test │ │ └── SelfishTest.js ├── networksim.js └── nodeAgent.js ├── chromebook.md ├── package-lock.json └── package.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb', 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly', 13 | }, 14 | parserOptions: { 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | ecmaVersion: 2018, 19 | sourceType: 'module', 20 | }, 21 | plugins: [ 22 | 'react', 23 | ], 24 | rules: { 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /ch1/1.1/client.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | 3 | // The client that end-users will use to interact with our central payment processor 4 | class Client { 5 | // The constructor will initialize a public/private key pair for the user 6 | // - the public key is like an username or address that people can send stuff to 7 | // - the private key is like a password or key that allows someone to access the stuff in the account and send transactions/messages from that account 8 | constructor() { 9 | // TODO 10 | // create a new Ethereum-identity with EthCrypto.createIdentity() 11 | // - should create a Javascript object with a privateKey, publicKey and address 12 | this.wallet = 'EthCrypto identity object'; 13 | } 14 | 15 | // Creates a keccak256/SHA3 hash of some data 16 | hash(data) { 17 | // TODO 18 | return 'hash of data'; 19 | } 20 | 21 | // Signs a hash of data with the client's private key 22 | sign(data) { 23 | // TODO 24 | return 'signed hash'; 25 | } 26 | 27 | // Verifies that a messageHash is signed by a certain address 28 | verify(signature, messageHash, address) { 29 | // TODO 30 | return 'boolean'; 31 | } 32 | } 33 | 34 | module.exports = Client; 35 | -------------------------------------------------------------------------------- /ch1/1.1/demo.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./client.js'); 3 | 4 | console.log('/////////////////////////////////////'); 5 | console.log('// Hashing and Public/Private Keys //'); 6 | console.log('/////////////////////////////////////'); 7 | 8 | // Hashing A Message 9 | console.log("\nLet's hash a message!"); 10 | const message = 'Hello World'; 11 | console.log('The message is: ', message); 12 | // const messageHash = TODO 13 | // console.log('The hash of that message is: ', messageHash) 14 | 15 | // Creating Public/Private Keys 16 | // console.log('\nCreating public/private key pairs to sign and verify messages.') 17 | 18 | // Init Alice 19 | // const alice = TODO 20 | // console.log('Init Alice\'s Client\n', alice) 21 | 22 | // Init Bob 23 | // const bob = TODO 24 | // console.log('Init Bob\'s Client\n', bob) 25 | 26 | // Notes 27 | // console.log('Notice that on their own Alice, Bob, and Carol just have keys. In order to have accounts that can hold tokens they need to connect to a network. The Paypal network is one such network, but Bitcoin and Ethereum are also networks. The state of the network is what determines account balances, so how the network operates is very important to users.') 28 | // console.log('Btw, you might notice that the public key is different than the address. This is because Ethereum addresses are generated from public, but are not exactly the same thing. Here\'s more info on that process: https://ethereum.stackexchange.com/questions/3542/how-are-ethereum-addresses-generated/3619#3619') 29 | 30 | // Signing Messages 31 | // console.log('\nSigning Messages') 32 | // const messageFromAlice = 'My name is Alice' 33 | // console.log('Alice\'s message: ', messageFromAlice) 34 | // const hashedMessageFromAlice = TODO 35 | // console.log('Now Alice has hashed her message:', hashedMessageFromAlice) 36 | // const signedMessageFromAlice = TODO 37 | // console.log('Alice\'s message signature: ', signedMessageFromAlice) 38 | 39 | // Verifying Messages 40 | // console.log('\nLet\'s help Bob verify Alice\'s message') 41 | // console.log('To do this we need to verify the message signature and the message hash to see if they return Alice\'s address') 42 | // const isMessageFromAliceAuthentic = TODO 43 | // console.log('Is the message authentic?') 44 | // console.log(isMessageFromAliceAuthentic) 45 | 46 | // Note 47 | // console.log('\nWhile this may seem like a silly example, message signing and verification allows us to securely connect to websites, download files from servers, and run any public blockchain network!\n') 48 | -------------------------------------------------------------------------------- /ch1/1.1/solutions/client.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | 3 | // The client that end-users will use to interact with our central payment processor 4 | class Client { 5 | // The constructor will initialize a public/private key pair for the user 6 | // - the public key is like an username or address that people can send stuff to 7 | // - the private key is like a password or key that allows someone to access the stuff in the account and send transactions/messages from that account 8 | constructor() { 9 | this.wallet = EthCrypto.createIdentity(); 10 | } 11 | 12 | // Creates a keccak256/SHA3 hash of some data 13 | hash(data) { 14 | return EthCrypto.hash.keccak256(data); 15 | } 16 | 17 | // Signs a hash of data with the client's private key 18 | sign(message) { 19 | const messageHash = this.hash(message); 20 | return EthCrypto.sign(this.wallet.privateKey, messageHash); 21 | } 22 | 23 | // Verifies that a messageHash is signed by a certain address 24 | verify(signature, messageHash, address) { 25 | const signer = EthCrypto.recover(signature, messageHash); 26 | return signer === address; 27 | } 28 | } 29 | 30 | module.exports = Client; 31 | -------------------------------------------------------------------------------- /ch1/1.1/solutions/demo.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./client.js'); 3 | 4 | console.log('/////////////////////////////////////'); 5 | console.log('// Hashing and Public/Private Keys //'); 6 | console.log('/////////////////////////////////////'); 7 | 8 | // Hashing A Message 9 | console.log("\nLet's hash a message!"); 10 | const message = 'Hello World'; 11 | console.log('The message is: ', message); 12 | const messageHash = EthCrypto.hash.keccak256(message); 13 | console.log('The hash of that message is: ', messageHash); 14 | 15 | // Creating Public/Private Keys 16 | console.log('\nCreating public/private key pairs to sign and verify messages.'); 17 | 18 | // Init Alice 19 | const alice = new Client(); 20 | console.log("Init Alice's Client\n", alice); 21 | 22 | // Init Bob 23 | const bob = new Client(); 24 | console.log("Init Bob's Client\n", bob); 25 | 26 | // Note 27 | console.log( 28 | 'Notice that on their own Alice, Bob, and Carol just have keys. In order to have accounts that can hold tokens they need to connect to a network. The Paypal network is one such network, but Bitcoin and Ethereum are also networks. The state of the network is what determines account balances, so how the network operates is very important to users.', 29 | ); 30 | console.log( 31 | "Btw, you might notice that the public key is different than the address. This is because Ethereum addresses are generated from public, but are not exactly the same thing. Here's more info on that process: https://ethereum.stackexchange.com/questions/3542/how-are-ethereum-addresses-generated/3619#3619", 32 | ); 33 | 34 | // Signing Messages 35 | console.log('\nSigning Messages'); 36 | const messageFromAlice = 'My name is Alice'; 37 | console.log("Alice's message: ", messageFromAlice); 38 | const hashedMessageFromAlice = alice.hash(messageFromAlice); 39 | console.log('Now Alice has hashed her message:', hashedMessageFromAlice); 40 | const signedMessageFromAlice = alice.sign(messageFromAlice); 41 | console.log("Alice's message signature: ", signedMessageFromAlice); 42 | 43 | // Verifying Messages 44 | console.log("\nLet's help Bob verify Alice's message"); 45 | console.log( 46 | "To do this we need to verify the message signature and the message hash to see if they return Alice's address", 47 | ); 48 | const isMessageFromAliceAuthentic = bob.verify( 49 | signedMessageFromAlice, 50 | hashedMessageFromAlice, 51 | alice.wallet.address, 52 | ); 53 | console.log('Is the message authentic?'); 54 | console.log(isMessageFromAliceAuthentic); 55 | 56 | // Note 57 | console.log( 58 | '\nWhile this may seem like a silly example, message signing and verification allows us to securely connect to websites, download files from servers, and run any public blockchain network!\n', 59 | ); 60 | -------------------------------------------------------------------------------- /ch1/1.1/test/test.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const assert = require('assert'); 3 | const Client = require('../client.js'); 4 | 5 | // Test The Client 6 | describe('Client Tests', () => { 7 | // Testing hash() function to hash random data 8 | describe('Hash', () => { 9 | const data = Math.random(); 10 | const client = new Client(); 11 | const output = client.hash(data); 12 | it('should return the hash of data', () => { 13 | assert.equal(EthCrypto.hash.keccak256(data), output); 14 | }); 15 | }); 16 | 17 | // Testing constructor() to initialize this.wallet with EthCrypto.createIdentity() 18 | describe('Wallet', () => { 19 | const client = new Client(); 20 | const { wallet } = client; 21 | it('should set this.wallet', () => { 22 | assert(wallet); 23 | }); 24 | it('should set this.wallet using createIdentity()', () => { 25 | assert(wallet.address && wallet.publicKey && wallet.privateKey); 26 | }); 27 | }); 28 | 29 | // Testing sign() function to use this.wallet to sign random data 30 | describe('Digital Signatures', () => { 31 | const client = new Client(); 32 | const message = Math.random(); 33 | const signature = EthCrypto.sign( 34 | client.wallet.privateKey, 35 | EthCrypto.hash.keccak256(message), 36 | ); 37 | it('should set successfully sign messages', () => { 38 | assert.equal(client.sign(message), signature); 39 | }); 40 | }); 41 | 42 | // Testing verify() to check signatures from randomly initialized wallets 43 | describe('Verify Signatures', () => { 44 | let Alice; 45 | let Bob; 46 | let Kevin; 47 | let message; 48 | let signature; 49 | beforeEach(() => { 50 | message = Math.random(); 51 | Alice = new Client(); 52 | Bob = new Client(); 53 | Kevin = new Client(); 54 | signature = Alice.sign(message); 55 | }); 56 | it('should be considered valid', () => { 57 | assert( 58 | Kevin.verify( 59 | signature, 60 | EthCrypto.hash.keccak256(message), 61 | Alice.wallet.address, 62 | ), 63 | ); 64 | }); 65 | it('should be considered invalid', () => { 66 | assert( 67 | !Kevin.verify( 68 | signature, 69 | EthCrypto.hash.keccak256(message), 70 | Bob.wallet.address, 71 | ), 72 | ); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /ch1/1.2/client.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | 3 | // The client that end-users will use to interact with our central payment processor 4 | class Client { 5 | // Initializes a public/private key pair for the user 6 | constructor() { 7 | this.wallet = EthCrypto.createIdentity(); 8 | } 9 | 10 | // Creates a keccak256/SHA3 hash of some data 11 | toHash(data) { 12 | const dataStr = JSON.stringify(data); 13 | return EthCrypto.hash.keccak256(dataStr); 14 | } 15 | 16 | // Signs a hash of data with the client's private key 17 | sign(message) { 18 | const messageHash = this.toHash(message); 19 | return EthCrypto.sign(this.wallet.privateKey, messageHash); 20 | } 21 | 22 | // Verifies that a messageHash is signed by a certain address 23 | verify(signature, messageHash, address) { 24 | const signer = EthCrypto.recover(signature, messageHash); 25 | return signer === address; 26 | } 27 | 28 | // Buys tokens from Paypal 29 | buy(amount) { 30 | // Let the user know that they just exchanged off-network goods for network tokens 31 | console.log('AN INTERESTING MESSAGE'); 32 | } 33 | 34 | // Generates new transactions 35 | generateTx(to, amount, type) { 36 | // TODO: 37 | // create an unsigned transaction 38 | // create a signature of the transaction 39 | // return a Javascript object with the unsigned transaction and transaction signature 40 | } 41 | } 42 | 43 | module.exports = Client; 44 | -------------------------------------------------------------------------------- /ch1/1.2/demo.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./Client.js'); 3 | const Paypal = require('./Paypal.js'); 4 | 5 | console.log('/////////////////////////////////////'); 6 | console.log('// Hashing and Public/Private Keys //'); 7 | console.log('/////////////////////////////////////'); 8 | 9 | // Hashing A Message 10 | console.log("\nLet's hash a message!"); 11 | const message = 'Hello World'; 12 | console.log('The message is: ', message); 13 | const messageHash = EthCrypto.hash.keccak256(message); 14 | console.log('The hash of that message is: ', messageHash); 15 | 16 | // Creating Public/Private Keys 17 | console.log('\nCreating public/private key pairs to sign and verify messages.'); 18 | 19 | // Init Alice 20 | const alice = new Client(); 21 | console.log("Init Alice's Client\n", alice); 22 | 23 | // Init Bob 24 | const bob = new Client(); 25 | console.log("Init Bob's Client\n", bob); 26 | 27 | // Init Carol 28 | const carol = new Client(); 29 | console.log("Init Carol's Client\n", carol); 30 | 31 | // Note 32 | console.log( 33 | 'Notice that on their own Alice, Bob, and Carol just have keys. In order to have accounts that can hold tokens they need to connect to a network. The Paypal network is one such network, but Bitcoin and Ethereum are also networks. The state of the network is what determines account balances, so how the network operates is very important to users.', 34 | ); 35 | console.log( 36 | "Btw, you might notice that the public key is different than the address. This is because Ethereum addresses are generated from public, but are not exactly the same thing. Here's more info on that process: https://ethereum.stackexchange.com/questions/3542/how-are-ethereum-addresses-generated/3619#3619", 37 | ); 38 | 39 | // Signing Messages 40 | console.log('\nSigning Messages'); 41 | const messageFromAlice = 'My name is Alice'; 42 | console.log("Alice's message: ", messageFromAlice); 43 | const hashedMessageFromAlice = alice.hash(messageFromAlice); 44 | console.log('Now Alice has hashed her message:', hashedMessageFromAlice); 45 | const signedMessageFromAlice = alice.sign(messageFromAlice); 46 | console.log("Alice's message signature: ", signedMessageFromAlice); 47 | 48 | // Verifying Messages 49 | console.log("\nLet's help Bob verify Alice's message"); 50 | console.log( 51 | "To do this we need to verify the message signature and the message hash to see if they return Alice's address", 52 | ); 53 | const isMessageFromAliceAuthentic = bob.verify( 54 | signedMessageFromAlice, 55 | hashedMessageFromAlice, 56 | alice.wallet.address, 57 | ); 58 | console.log('Is the message authentic?'); 59 | console.log(isMessageFromAliceAuthentic); 60 | 61 | // Note 62 | console.log( 63 | '\nWhile this may seem like a silly example, message signing and verification allows us to securely connect to websites, download files from servers, and run any public blockchain network!\n', 64 | ); 65 | 66 | console.log('/////////////////////////////////'); 67 | console.log('// Initial Paypal Network Demo //'); 68 | console.log('/////////////////////////////////'); 69 | 70 | // Setup Paypal network 71 | // const paypal = TODO 72 | // console.log(paypal) 73 | // 74 | // // Generate transaction 75 | // const aliceTx = TODO 76 | // console.log('\nAlice sends a TX to Bob via the Paypal network\n', aliceTx) 77 | // 78 | // // Check transaction signature 79 | // const checkAliceTx = TODO 80 | // console.log('\nPaypal checks Alice\'s transaction to Bob. Is it valid?') 81 | // console.log(checkAliceTx) 82 | // 83 | // // Check user address 84 | // paypal.checkUserAddress(aliceTx) 85 | // console.log('\nPaypal checks if Alice and Bob have already opened accounts with Paypal. They have not, so Paypal adds their addresses to it\'s state\n', paypal) 86 | // 87 | // // Check transaction type 88 | // console.log('\nNow that Alice and Bob\'s addresses are in the Paypal network, Paypal checks to make sure that the transaction is valid. ') 89 | // console.log('Is it? ') 90 | // const checkAliceTxType = TODO 91 | // console.log('Alice has a balance of 0, but the transaction is trying to spend 10. In order to send tokens on the Paypal network Alice is going to have to have to buy them from Paypal Inc.') 92 | // console.log('Alice really wants to use Paypal\'s network so she sells her right kidney and gets enough money to buy 100 of Paypal\'s magic tokens. Now Alice can finally participate in the network!') 93 | // alice.buy(100) 94 | // const mintAlice100Tokens = TODO 95 | // paypal.processTx(mintAlice100Tokens) 96 | // console.log(paypal) 97 | // 98 | // // Check user balance 99 | // console.log('\nAlice checks her balance with Paypal') 100 | // const checkAliceAccountBalance = TODO 101 | // paypal.checkTxType(checkAliceAccountBalance) 102 | // 103 | // // Note 104 | // console.log('\n// Notice that all Alice can do is send a message to the network and ask what her balance is. With a central operator Alice is trusting that the balance that she is told (and the balance in the database) is accurate. With a public blockchain like Bitcoin or Ethereum Alice can see the entire history of the network and verify transactions herself to make sure that everything is accurate.') 105 | // 106 | // // Sending Tokens 107 | // console.log('\nNow that Alice has verified that she has some tokens she wants to pay Bob back for the gallon of Ketchup he gave her. To do this Alice sends a transaction on the Paypal network') 108 | // const payBobBackForKetchup = TODO 109 | // console.log(payBobBackForKetchup) 110 | // console.log('\nPaypal sees this transaction and processes it.') 111 | // paypal.processTx(payBobBackForKetchup) 112 | // console.log(paypal) 113 | // 114 | // console.log('\nYay! Now Bob has been made whole, Paypal sold some of their magic tokens, and Alice gets to live life on the edge with only one kidney. Everyone wins :)') 115 | // console.log('') 116 | -------------------------------------------------------------------------------- /ch1/1.2/paypal.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./client.js'); 3 | 4 | // Our naive implementation of a centralized payment processor 5 | class Paypal extends Client { 6 | constructor() { 7 | super(); 8 | // the state of the network (accounts and balances) 9 | this.state = { 10 | [this.wallet.address]: { 11 | balance: 1000000, 12 | }, 13 | }; 14 | // the history of transactions 15 | this.txHistory = []; 16 | } 17 | 18 | // Checks that the sender of a transaction is the same as the signer 19 | checkTxSignature(tx) { 20 | // get the signature from the transaction 21 | // TODO 22 | // if the signature is invalid print an error to the console and return false 23 | // TODO 24 | // return true if the transaction is valid 25 | // TODO 26 | } 27 | 28 | // Checks if the user's address is already in the state, and if not, adds the user's address to the state 29 | checkUserAddress(tx) { 30 | // check if the sender is in the state 31 | // TODO 32 | // if the sender is not in the state, create an account for them 33 | // TODO 34 | // check if the receiver is in the state 35 | // TODO 36 | // if the receiver is not in the state, create an account for them 37 | // TODO 38 | // once the checks on both accounts pass (they're both in the state), return true 39 | // TODO 40 | } 41 | 42 | // Checks the transaction type and ensures that the transaction is valid based on that type 43 | checkTxType(tx) { 44 | // if the transaction type is 'mint' 45 | // TODO 46 | // check that the sender is PayPal 47 | // TODO 48 | // if the check fails, print an error to the concole stating why and return false so that the transaction is not processed 49 | // TODO 50 | // if the check passes, return true 51 | // TODO 52 | // if the transaction type is 'check' 53 | // TODO 54 | // print the balance of the sender to the console 55 | // TODO 56 | // return false so that the stateTransitionFunction does not process the tx 57 | // TODO 58 | // if the transaction type is 'send' 59 | // TODO 60 | // check that the transaction amount is positive and the sender has an account balance greater than or equal to the transaction amount 61 | // if a check fails, print an error to the console stating why and return false 62 | // TODO 63 | // if the check passes, return true 64 | // TODO 65 | } 66 | 67 | // Checks if a transaction is valid, adds it to the transaction history, and updates the state of accounts and balances 68 | checkTx(tx) { 69 | // check that the transaction signature is valid 70 | // TODO 71 | // check that the transaction sender and receiver are in the state 72 | // TODO 73 | // check that the transaction type is valid 74 | // TODO 75 | // if all checks pass return true 76 | // TODO 77 | // if any checks fail return false 78 | // TODO 79 | } 80 | 81 | // Updates account balances according to a transaction and adds the transaction to the history 82 | applyTx(tx) { 83 | // decrease the balance of the transaction sender/signer 84 | // TODO 85 | // increase the balance of the transaction receiver 86 | // TODO 87 | // add the transaction to the transaction history 88 | // TODO 89 | // return true once the transaction is processed 90 | // TODO 91 | } 92 | 93 | // Process a transaction 94 | processTx(tx) { 95 | // check the transaction is valid 96 | // TODO 97 | // apply the transaction to Paypal's state 98 | // TODO 99 | } 100 | } 101 | 102 | module.exports = Paypal; 103 | -------------------------------------------------------------------------------- /ch1/1.2/solutions/client.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | 3 | // The client that end-users will use to interact with our central payment processor 4 | class Client { 5 | // Initializes a public/private key pair for the user 6 | constructor() { 7 | this.wallet = EthCrypto.createIdentity(); 8 | } 9 | 10 | // Creates a keccak256/SHA3 hash of some data 11 | hash(data) { 12 | return EthCrypto.hash.keccak256(data); 13 | } 14 | 15 | // Signs a hash of data with the client's private key 16 | sign(message) { 17 | const messageHash = this.hash(message); 18 | return EthCrypto.sign(this.wallet.privateKey, messageHash); 19 | } 20 | 21 | // Verifies that a messageHash is signed by a certain address 22 | verify(signature, messageHash, address) { 23 | const signer = EthCrypto.recover(signature, messageHash); 24 | return signer === address; 25 | } 26 | 27 | // Buys tokens from Paypal 28 | buy(amount) { 29 | // Let the user know that they just exchanged off-network goods for network tokens 30 | console.log(`You bought ${amount} magic tokens from Paypal`); 31 | } 32 | 33 | // Generates new transactions 34 | generateTx(to, amount, type) { 35 | // create an unsigned transaction 36 | const unsignedTx = { 37 | type, 38 | amount, 39 | from: this.wallet.address, 40 | to, 41 | }; 42 | // create a signature of the transaction 43 | const tx = { 44 | contents: unsignedTx, 45 | sig: this.sign(unsignedTx), 46 | }; 47 | // return a Javascript object with the unsigned transaction and transaction signature 48 | return tx; 49 | } 50 | } 51 | 52 | module.exports = Client; 53 | -------------------------------------------------------------------------------- /ch1/1.2/solutions/paypal.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./client.js'); 3 | 4 | // Our naive implementation of a centralized payment processor 5 | class Paypal extends Client { 6 | constructor() { 7 | super(); 8 | // the state of the network (accounts and balances) 9 | this.state = { 10 | [this.wallet.address]: { 11 | balance: 1000000, 12 | }, 13 | }; 14 | // the history of transactions 15 | this.txHistory = []; 16 | } 17 | 18 | // Checks that the sender of a transaction is the same as the signer 19 | checkTxSignature(tx) { 20 | // get the signature from the transaction 21 | const sig = this.verify(tx.sig, this.hash(tx.contents), tx.contents.from); 22 | // if the signature is invalid print an error to the console and return false 23 | if (!sig) { 24 | console.log('Invalid Signature'); 25 | return false; 26 | // return true if the transaction is valid 27 | } 28 | return true; 29 | } 30 | 31 | // Checks if the user's address is already in the state, and if not, adds the user's address to the state 32 | checkUserAddress(tx) { 33 | // check if the sender is in the state 34 | if (!(tx.contents.to in this.state)) { 35 | // if the sender is not in the state, create an account for them 36 | this.state[tx.contents.to] = { 37 | balance: 0, 38 | }; 39 | } 40 | // check if the receiver is in the state 41 | if (!(tx.contents.from in this.state)) { 42 | // if the receiver is not in the state, create an account for them 43 | this.state[tx.contents.from] = { 44 | balance: 0, 45 | }; 46 | } 47 | // once the checks on both accounts pass (they're both in the state), return true 48 | return true; 49 | } 50 | 51 | // Checks the transaction type and ensures that the transaction is valid based on that type 52 | checkTxType(tx) { 53 | // if the transaction type is 'mint' 54 | if (tx.contents.type === 'mint') { 55 | // check that the sender is PayPal 56 | if (tx.contents.from !== this.wallet.address) { 57 | // if the check fails, print an error to the concole stating why and return false so that the transaction is not processed 58 | console.log("Non-Paypal Clients can't mint!"); 59 | return false; 60 | } 61 | // if a check passes, return true 62 | return true; 63 | } 64 | // if the transaction type is 'check' 65 | if (tx.contents.type === 'check') { 66 | // print the balance of the sender to the console 67 | const user = tx.contents.from; 68 | console.log(`Your balance is: ${this.state[user].balance}`); 69 | // return false so that Paypal's processTx function does not process the tx 70 | return false; 71 | } 72 | // if the transaction type is 'send' 73 | if (tx.contents.type === 'send') { 74 | // check that the transaction amount is positive and the sender has an account balance greater than or equal to the transaction amount 75 | if (this.state[tx.contents.from].balance - tx.contents.amount < 0) { 76 | // if a check fails, print an error to the console stating why and return false 77 | console.log('Not enough money!'); 78 | return false; 79 | } 80 | // if the check passes, return true 81 | return true; 82 | } 83 | } 84 | 85 | // Checks if a transaction is valid, adds it to the transaction history, and updates the state of accounts and balances 86 | checkTx(tx) { 87 | // check that the transaction signature is valid 88 | if (this.checkTxSignature(tx)) { 89 | // check that the transaction sender and receiver are in the state 90 | if (this.checkUserAddress(tx)) { 91 | // check that the transaction type is valid 92 | if (this.checkTxType(tx)) { 93 | // if all checks pass return true 94 | return true; 95 | } 96 | } 97 | } 98 | // if any checks fail return false 99 | return false; 100 | } 101 | 102 | // Updates account balances according to a transaction and adds the transaction to the history 103 | applyTx(tx) { 104 | // decrease the balance of the transaction sender/signer 105 | this.state[tx.contents.from].balance -= tx.contents.amount; 106 | // increase the balance of the transaction receiver 107 | this.state[tx.contents.to].balance += tx.contents.amount; 108 | // add the transaction to the transaction history 109 | this.txHistory.push(tx); 110 | // return true once the transaction is processed 111 | return true; 112 | } 113 | 114 | // Process a transaction 115 | processTx(tx) { 116 | // check the transaction is valid 117 | if (this.checkTx(tx)) { 118 | // apply the transaction to Paypal's state 119 | this.applyTx(tx); 120 | } 121 | } 122 | } 123 | 124 | module.exports = Paypal; 125 | -------------------------------------------------------------------------------- /ch1/1.3/client.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | 3 | // The client that end-users will use to interact with our central payment processor 4 | class Client { 5 | // Initializes a public/private key pair for the user 6 | constructor() { 7 | this.wallet = EthCrypto.createIdentity(); 8 | // initialize the nonce 9 | // TODO 10 | } 11 | 12 | // Creates a keccak256/SHA3 hash of some data 13 | hash(data) { 14 | return EthCrypto.hash.keccak256(data); 15 | } 16 | 17 | // Signs a hash of data with the client's private key 18 | sign(message) { 19 | const messageHash = this.hash(message); 20 | return EthCrypto.sign(this.wallet.privateKey, messageHash); 21 | } 22 | 23 | // Verifies that a messageHash is signed by a certain address 24 | verify(signature, messageHash, address) { 25 | const signer = EthCrypto.recover(signature, messageHash); 26 | return signer === address; 27 | } 28 | 29 | // Buys tokens from Paypal 30 | buy(amount) { 31 | // Let the user know that they just exchanged off-network goods for network tokens 32 | console.log(`You bought ${amount} magic tokens from Paypal`); 33 | } 34 | 35 | // Generates new transactions 36 | generateTx(to, amount, type) { 37 | // create an unsigned transaction 38 | const unsignedTx = { 39 | type, 40 | amount, 41 | from: this.wallet.address, 42 | to, 43 | // add wallet nonce to tx 44 | // TODO 45 | }; 46 | // create a signature of the transaction 47 | const tx = { 48 | contents: unsignedTx, 49 | sig: this.sign(unsignedTx), 50 | }; 51 | // increment the wallet's nonce parameter AFTER the tx object is created 52 | // TODO 53 | // return a Javascript object with the unsigned transaction and transaction signature 54 | return tx; 55 | } 56 | } 57 | 58 | module.exports = Client; 59 | -------------------------------------------------------------------------------- /ch1/1.3/demo.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./client.js'); 3 | const Paypal = require('./paypal.js'); 4 | 5 | // Paypal Network Demo 6 | console.log('/////////////////////////////////'); 7 | console.log('// Paypal Network Demo w Nonces//'); 8 | console.log('/////////////////////////////////'); 9 | 10 | // Setup Paypal network 11 | const paypal = new Paypal(); 12 | console.log('\nInitial Paypal network:'); 13 | console.log(paypal); 14 | 15 | // Mint tokens for our users 16 | console.log( 17 | '\nToday Paypal has a promotion that new users get 100 free tokens for signing up', 18 | ); 19 | 20 | // Alice signs up for Paypal 21 | const alice = new Client(); 22 | const newUserAlice = paypal.generateTx(alice.wallet.address, 100, 'mint'); 23 | paypal.processTx(newUserAlice); 24 | 25 | // Bob signs up for Paypal 26 | const bob = new Client(); 27 | const newUserBob = paypal.generateTx(bob.wallet.address, 100, 'mint'); 28 | paypal.processTx(newUserBob); 29 | 30 | // Carol signs up for Paypal 31 | const carol = new Client(); 32 | const newUserCarol = paypal.generateTx(carol.wallet.address, 100, 'mint'); 33 | paypal.processTx(newUserCarol); 34 | 35 | // Paypal's first users 36 | console.log( 37 | "\nLet's look at the state of Paypal's network now that there are a few users:", 38 | ); 39 | console.log(paypal); 40 | 41 | // Generate transaction 42 | const aliceTx = 'TODO'; 43 | // console.log("\nAlice generates a transaction to send 10 tokens to Bob."); 44 | // console.log(aliceTx); 45 | 46 | // Mandatory waiting period, because... YOLO 47 | // console.log("\nPaypal does not process the transaction right away..."); 48 | 49 | // Generating another transaction 50 | // console.log("\nAlice gets impatient and submits the transaction again, because clicking things is more satisfying than waiting."); 51 | const aliceTx2 = 'TODO'; 52 | // console.log(aliceTx2); 53 | 54 | // Paypal gets the transactions 55 | // TODO: make paypal process aliceTx2 56 | // console.log("\nDue to a network error, Paypal gets Alice's second transaction first and it goes in the pendingTx pool"); 57 | // console.log(paypal); 58 | // TODO: make paypal process aliceTx 59 | // console.log("\nPaypal then gets Alice's first transaction, processes it, and then processes any transactions in the pendingTx pool"); 60 | // console.log(paypal); 61 | 62 | // SNAFU 63 | console.log( 64 | '\nOh no! Alice has actually sent Bob 20 tokens instead of the 10 she intended. What to do...', 65 | ); 66 | console.log( 67 | "Lucky for Alice, Paypal has a cancel transaction feature. Yes that's right! Alice can cancel her transaction, for a small fee of course...", 68 | ); 69 | console.log( 70 | 'Since the fee is smaller than the extra 10 tokens Alice sent, she sends a cancellation transaction to Paypal and gets her tokens back', 71 | ); 72 | // note: nonces are zero indexed, so they start at 0, then 1, then 2, etc... 73 | const aliceTx2Cancellation = 'TODO'; 74 | // TODO: make Paypal process aliceTx2Cancellation 75 | 76 | // All's well that ends well 77 | // console.log("\nNow let's look at Paypal's state to see everyone's accounts and balances"); 78 | // console.log(paypal); 79 | 80 | // Feature or bug? You decide! 81 | // console.log("note that when you're using a centralized payment processor's database, they set the rules and can manipulate the state arbitrarily. This can be good if you're worried about regulatory compliance or the ability to revert errors, but it also means that there are no guarantees that your account, funds, or transactions are valid. With decentralized networks like Bitcoin and Ethereum transactions are immutable and no one can stop them, not even you. Feature or bug? You decide!"); 82 | -------------------------------------------------------------------------------- /ch1/1.3/solutions/client.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | 3 | // The client that end-users will use to interact with our central payment processor 4 | class Client { 5 | // Initializes a public/private key pair for the user 6 | constructor() { 7 | this.wallet = EthCrypto.createIdentity(); 8 | // initialize the nonce 9 | this.nonce = 0; 10 | } 11 | 12 | // Creates a keccak256/SHA3 hash of some data 13 | hash(data) { 14 | return EthCrypto.hash.keccak256(data); 15 | } 16 | 17 | // Signs a hash of data with the client's private key 18 | sign(message) { 19 | const messageHash = this.hash(message); 20 | return EthCrypto.sign(this.wallet.privateKey, messageHash); 21 | } 22 | 23 | // Verifies that a messageHash is signed by a certain address 24 | verify(signature, messageHash, address) { 25 | const signer = EthCrypto.recover(signature, messageHash); 26 | return signer === address; 27 | } 28 | 29 | // Buys tokens from Paypal 30 | buy(amount) { 31 | // Let the user know that they just exchanged off-network goods for network tokens 32 | console.log(`You bought ${amount} magic tokens from Paypal`); 33 | } 34 | 35 | // Generates new transactions 36 | generateTx(to, amount, type) { 37 | // create an unsigned transaction 38 | const unsignedTx = { 39 | type, 40 | amount, 41 | from: this.wallet.address, 42 | to, 43 | // add wallet nonce to tx 44 | nonce: this.nonce, 45 | }; 46 | // create a signature of the transaction 47 | const tx = { 48 | contents: unsignedTx, 49 | sig: this.sign(unsignedTx), 50 | }; 51 | // increment the wallet's nonce parameter AFTER the tx object is created 52 | this.nonce++; 53 | // return a Javascript object with the unsigned transaction and transaction signature 54 | return tx; 55 | } 56 | } 57 | 58 | module.exports = Client; 59 | -------------------------------------------------------------------------------- /ch1/1.3/solutions/demo.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./client.js'); 3 | const Paypal = require('./paypal.js'); 4 | 5 | // Paypal Network Demo 6 | console.log('/////////////////////////////////'); 7 | console.log('// Paypal Network Demo w Nonces//'); 8 | console.log('/////////////////////////////////'); 9 | 10 | // Setup Paypal network 11 | const paypal = new Paypal(); 12 | console.log('\nInitial Paypal network:'); 13 | console.log(paypal); 14 | 15 | // Mint tokens for our users 16 | console.log( 17 | '\nToday Paypal has a promotion that new users get 100 free tokens for signing up', 18 | ); 19 | 20 | // Alice signs up for Paypal 21 | const alice = new Client(); 22 | const newUserAlice = paypal.generateTx(alice.wallet.address, 100, 'mint'); 23 | paypal.processTx(newUserAlice); 24 | 25 | // Bob signs up for Paypal 26 | const bob = new Client(); 27 | const newUserBob = paypal.generateTx(bob.wallet.address, 100, 'mint'); 28 | paypal.processTx(newUserBob); 29 | 30 | // Carol signs up for Paypal 31 | const carol = new Client(); 32 | const newUserCarol = paypal.generateTx(carol.wallet.address, 100, 'mint'); 33 | paypal.processTx(newUserCarol); 34 | 35 | // Paypal's first users 36 | console.log( 37 | "\nLet's look at the state of Paypal's network now that there are a few users:", 38 | ); 39 | console.log(paypal); 40 | 41 | // Generate transaction 42 | const aliceTx = alice.generateTx(bob.wallet.address, 10, 'send'); 43 | console.log('\nAlice generates a transaction to send 10 tokens to Bob.'); 44 | console.log(aliceTx); 45 | 46 | // Mandatory waiting period, because... YOLO 47 | console.log('\nPaypal does not process the transaction right away...'); 48 | 49 | // Generating another transaction 50 | console.log( 51 | '\nAlice gets impatient and submits the transaction again, because clicking things is more satisfying than waiting.', 52 | ); 53 | const aliceTx2 = alice.generateTx(bob.wallet.address, 10, 'send'); 54 | console.log(aliceTx2); 55 | 56 | // Paypal gets the transactions 57 | paypal.processTx(aliceTx2); 58 | console.log( 59 | "\nDue to a network error, Paypal gets Alice's second transaction first and it goes in the pendingTx pool", 60 | ); 61 | console.log(paypal); 62 | paypal.processTx(aliceTx); 63 | console.log( 64 | "\nPaypal then gets Alice's first transaction, processes it, and then processes any transactions in the pendingTx pool", 65 | ); 66 | console.log(paypal); 67 | 68 | // SNAFU 69 | console.log( 70 | '\nOh no! Alice has actually sent Bob 20 tokens instead of the 10 she intended. What to do...', 71 | ); 72 | console.log( 73 | "Lucky for Alice, Paypal has a cancel transaction feature. Yes that's right! Alice can cancel her transaction, for a small fee of course...", 74 | ); 75 | console.log( 76 | 'Since the fee is smaller than the extra 10 tokens Alice sent, she sends a cancellation transaction to Paypal and gets her tokens back', 77 | ); 78 | // note: nonces are zero indexed, so they start at 0, then 1, then 2, etc... 79 | const aliceTx2Cancellation = alice.generateTx( 80 | paypal.wallet.address, 81 | 0, 82 | 'cancel', 83 | ); 84 | paypal.processTx(aliceTx2Cancellation); 85 | 86 | // All's well that ends well 87 | console.log( 88 | "\nNow let's look at Paypal's state to see everyone's accounts and balances", 89 | ); 90 | console.log(paypal); 91 | 92 | // Feature or bug? You decide! 93 | console.log( 94 | "note that when you're using a centralized payment processor's database, they set the rules and can manipulate the state arbitrarily. This can be good if you're worried about regulatory compliance or the ability to revert errors, but it also means that there are no guarantees that your account, funds, or transactions are valid. With decentralized networks like Bitcoin and Ethereum transactions are immutable and no one can stop them, not even you. Feature or bug? You decide!", 95 | ); 96 | -------------------------------------------------------------------------------- /ch1/1.3/test/.test.js.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptoeconomics-study/code/4eacdac17890917d8129dc804483490a027a03b4/ch1/1.3/test/.test.js.swp -------------------------------------------------------------------------------- /ch1/1.3/test/test.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const assert = require('assert'); 3 | const Client = require('../client.js'); 4 | const Paypal = require('../paypal.js'); 5 | 6 | describe('Functioning Nonces', () => { 7 | // init params 8 | const paypal = new Paypal(); 9 | const alice = new Client(); 10 | const bob = new Client(); 11 | const tx1 = paypal.generateTx(alice.wallet.address, 100, 'mint'); 12 | const tx2 = alice.generateTx(bob.wallet.address, 10, 'send'); 13 | const tx3 = alice.generateTx(bob.wallet.address, 10, 'send'); 14 | 15 | // Generating Tx 16 | describe('generateTx in Client.js', () => { 17 | it('should properly set the first nonce with generateTx', () => { 18 | const unsignedTx = { 19 | type: 'send', 20 | amount: 10, 21 | from: alice.wallet.address, 22 | to: bob.wallet.address, 23 | nonce: 0, 24 | }; 25 | assert.deepEqual(tx2.contents, unsignedTx); 26 | const sig = alice.sign(unsignedTx); 27 | assert.equal(tx2.sig, sig); 28 | }); 29 | 30 | it('should properly increment the nonce with generateTx', () => { 31 | const unsignedTx = { 32 | type: 'send', 33 | amount: 10, 34 | from: alice.wallet.address, 35 | to: bob.wallet.address, 36 | nonce: 1, 37 | }; 38 | assert.deepEqual(tx3.contents, unsignedTx); 39 | const sig = alice.sign(unsignedTx); 40 | assert.equal(tx3.sig, sig); 41 | }); 42 | }); 43 | 44 | // Processing Tx 45 | describe('processTx in Paypal.js', () => { 46 | it('should correctly set nonce of a new sender', () => { 47 | paypal.processTx(tx1); 48 | assert.deepEqual(paypal.state[paypal.wallet.address], { 49 | balance: 1000000 - tx1.contents.amount, 50 | nonce: 1, 51 | }); 52 | }); 53 | 54 | it('should correctly set nonce of a new receiver', () => { 55 | assert.deepEqual(paypal.state[alice.wallet.address], { 56 | balance: 100, 57 | nonce: 0, 58 | }); 59 | }); 60 | 61 | it('Paypal should not apply transactions with a nonce greater than the nonce Paypal has for that user', () => { 62 | paypal.processTx(tx3); 63 | assert.equal(false, paypal.processTx(tx3)); 64 | }); 65 | 66 | it('should apply valid nonce transactions', () => { 67 | const paypal = new Paypal(); 68 | const alice = new Client(); 69 | const bob = new Client(); 70 | const tx1 = paypal.generateTx(alice.wallet.address, 100, 'mint'); 71 | const tx2 = alice.generateTx(bob.wallet.address, 10, 'send'); 72 | const tx3 = alice.generateTx(bob.wallet.address, 10, 'send'); 73 | paypal.processTx(tx1); 74 | paypal.processTx(tx2); 75 | assert.equal(true, paypal.processTx(tx3)); 76 | }); 77 | 78 | it('should apply pending transactions when they are ready', () => { 79 | const paypal = new Paypal(); 80 | const alice = new Client(); 81 | const bob = new Client(); 82 | const tx1 = paypal.generateTx(alice.wallet.address, 100, 'mint'); 83 | const tx2 = alice.generateTx(bob.wallet.address, 10, 'send'); 84 | const tx3 = alice.generateTx(bob.wallet.address, 10, 'send'); 85 | paypal.processTx(tx3); 86 | paypal.processTx(tx2); 87 | assert.equal(true, paypal.processTx(tx1)); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /ch1/1.4/README.md: -------------------------------------------------------------------------------- 1 | > This code challenge is a bonus round. It does not build on the prior course work and is not necessary to understand future course work. That being said, UTXOs are a common thing in the world of cryptoeconomics and blockchains, so we recommend recommend that you explore them. 2 | 3 | ## UTXOs 4 | 5 | Since UTXOs have been around since Bitcoin was first created (2008), there are many great resources on the internet that explain them. There's even Javascript code tutorials and examples that will walk you through how to roll your own. Rather than reinventing the wheel, we're just going to link to a few that we recommend: 6 | 7 | ### [Blockchain in JS](https://blockchain.nambrot.com/) 8 | 9 | A Bitcoin style interactive blockchain demo by [nambrot](https://github.com/nambrot). There's even a [tutorial to build your own](https://github.com/nambrot/blockchain-in-js) so you can recreate the demo. 10 | 11 | ### [BitcoinJS](https://github.com/bitcoinjs/bitcoinjs-lib) 12 | 13 | A simple [UTXO example](https://github.com/bitcoinjs/utxo) in Javascript. Their [other repos](https://github.com/bitcoinjs) have lots of great examples as well. 14 | 15 | ### [How To Code A Bitcoin Blockchain In Javascript](https://blockgeeks.com/guides/code-a-bitcoin-blockchain-in-javascript/) 16 | 17 | As advertised, a tutorial on how to code a bitcoin blockchain in Javascript. [Blockgeeks](https://blockgeeks.com/) has more great content, so we recommend you check them out if you want a different perspective on many of the concepts explored in this course. 18 | 19 | -------------------------------------------------------------------------------- /ch1/1.5/client.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | 3 | // The client that end-users will use to interact with our central payment processor 4 | class Client { 5 | // Initializes a public/private key pair for the user 6 | constructor() { 7 | this.wallet = EthCrypto.createIdentity(); 8 | // initialize the nonce 9 | this.nonce = 0; 10 | } 11 | 12 | // Creates a keccak256/SHA3 hash of some data 13 | hash(data) { 14 | return EthCrypto.hash.keccak256(data); 15 | } 16 | 17 | // Signs a hash of data with the client's private key 18 | sign(message) { 19 | const messageHash = this.hash(message); 20 | return EthCrypto.sign(this.wallet.privateKey, messageHash); 21 | } 22 | 23 | // Verifies that a messageHash is signed by a certain address 24 | verify(signature, messageHash, address) { 25 | const signer = EthCrypto.recover(signature, messageHash); 26 | return signer === address; 27 | } 28 | 29 | // Buys tokens from Paypal 30 | buy(amount) { 31 | // Let the user know that they just exchanged off-network goods for network tokens 32 | console.log(`You bought ${amount} magic tokens from Paypal`); 33 | } 34 | 35 | // Generates new transactions 36 | generateTx(to, amount, type) { 37 | // create an unsigned transaction 38 | const unsignedTx = { 39 | type, 40 | amount, 41 | from: this.wallet.address, 42 | to, 43 | // add wallet nonce to tx 44 | nonce: this.nonce, 45 | }; 46 | // create a signature of the transaction 47 | const tx = { 48 | contents: unsignedTx, 49 | sig: this.sign(unsignedTx), 50 | }; 51 | // increment the wallet's nonce parameter AFTER the tx object is created 52 | this.nonce++; 53 | // return a Javascript object with the unsigned transaction and transaction signature 54 | return tx; 55 | } 56 | } 57 | 58 | module.exports = Client; 59 | -------------------------------------------------------------------------------- /ch1/1.5/demo.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./Client.js'); 3 | const Paypal = require('./Paypal.js'); 4 | 5 | // Paypal Network Demo 6 | console.log('//////////////////////////////////////////'); 7 | console.log('// Paypal Network Demo w Rent Extraction//'); 8 | console.log('//////////////////////////////////////////'); 9 | 10 | // Setup Paypal network 11 | const paypal = new Paypal(); 12 | console.log('\nInitial Paypal network:'); 13 | console.log(paypal); 14 | 15 | // Mint tokens for our users 16 | console.log( 17 | '\nToday Paypal has a promotion that new users get 100 free tokens for signing up', 18 | ); 19 | 20 | // Alice signs up for Paypal 21 | const alice = new Client(); 22 | const newUserAlice = paypal.generateTx(alice.wallet.address, 1000, 'mint'); 23 | paypal.processTx(newUserAlice); 24 | 25 | // Bob signs up for Paypal 26 | const bob = new Client(); 27 | const newUserBob = paypal.generateTx(bob.wallet.address, 1000, 'mint'); 28 | paypal.processTx(newUserBob); 29 | 30 | // Carol signs up for Paypal 31 | const carol = new Client(); 32 | const newUserCarol = paypal.generateTx(carol.wallet.address, 1000, 'mint'); 33 | paypal.processTx(newUserCarol); 34 | 35 | // Dave signs up for Paypal 36 | const dave = new Client(); 37 | const newUserDave = paypal.generateTx(dave.wallet.address, 1000, 'mint'); 38 | paypal.processTx(newUserDave); 39 | 40 | // Earl signs up for Paypal 41 | const eve = new Client(); 42 | const newUserEve = paypal.generateTx(eve.wallet.address, 1000, 'mint'); 43 | paypal.processTx(newUserEve); 44 | 45 | // Frank signs up for Paypal 46 | const frank = new Client(); 47 | const newUserFrank = paypal.generateTx(frank.wallet.address, 1000, 'mint'); 48 | paypal.processTx(newUserFrank); 49 | 50 | // George signs up for Paypal 51 | const george = new Client(); 52 | const newUserGeorge = paypal.generateTx(george.wallet.address, 1000, 'mint'); 53 | paypal.processTx(newUserGeorge); 54 | 55 | // Harry signs up for Paypal 56 | const harry = new Client(); 57 | const newUserHarry = paypal.generateTx(harry.wallet.address, 1000, 'mint'); 58 | paypal.processTx(newUserHarry); 59 | 60 | // Ian signs up for Paypal 61 | const ian = new Client(); 62 | const newUserIan = paypal.generateTx(ian.wallet.address, 1000, 'mint'); 63 | paypal.processTx(newUserIan); 64 | 65 | // Jill signs up for Paypal 66 | const jill = new Client(); 67 | const newUserJill = paypal.generateTx(jill.wallet.address, 1000, 'mint'); 68 | paypal.processTx(newUserJill); 69 | 70 | // Paypal's first users 71 | console.log( 72 | "\nLet's look at the state of Paypal's network now that there are a few users:", 73 | ); 74 | console.log(paypal); 75 | 76 | // The only constant is change 77 | console.log( 78 | "\nLet's imagine that some time has passed... Paypal is doing well and has lots of users. Naturally, Paypal decides to implement fees. Users are already using Paypal and don't want to switch, so the fee isn't THAT big of a deal. Let's see how it affects their balances over time", 79 | ); 80 | 81 | // BONUS TODO (not directly related to understanding cryptoeconomic mechanisms, but a good programming exercise non the less) 82 | // Death by 1000 transaction fees 83 | function financialAttrition(...users) { 84 | // simulate 1000 transactions 85 | // TODO 86 | // pick a random value between 1 and 10 87 | // TODO 88 | // choose two users at random, but excluding Paypal 89 | // TODO 90 | // create a transaction from one random user to another 91 | // TODO 92 | // process the transaction 93 | // TODO 94 | // print Paypal's balance and/or the full state to the console every 100 iterations so we can see the progress 95 | // TODO 96 | } 97 | 98 | financialAttrition( 99 | alice, 100 | bob, 101 | carol, 102 | dave, 103 | eve, 104 | frank, 105 | george, 106 | harry, 107 | ian, 108 | jill, 109 | ); 110 | 111 | // The truth will set you free 112 | console.log( 113 | "\nWow! Shocking... Paypal made money while everyone else's balance went down. One could argue that this is because Paypal provides a valuable service and is compensated for that, which is a fair and reasonable this to say. IF, however, Paypal gained a monopoly and started raising the fees... well then that would be a different story. Try playing with the model to see what happens with different fees.", 114 | ); 115 | 116 | // The plot thickens... 117 | console.log( 118 | "\nWell that was fun. But wait! There's more. It turns out that Eve was actually a space pirate, and Paypal is forbidden from serving space pirates. Upon hearing this news, Paypal adds Eve's address to the blacklist (duhn duhn duhn...). Eve is now banned from using Paypal or their services.", 119 | ); 120 | paypal.blacklist.push(eve.wallet.address); 121 | 122 | console.log("Let's see what happens now..."); 123 | financialAttrition( 124 | alice, 125 | bob, 126 | carol, 127 | dave, 128 | eve, 129 | frank, 130 | george, 131 | harry, 132 | ian, 133 | jill, 134 | ); 135 | 136 | console.log( 137 | "\nLooks like most of Paypal's users are now too broke or too outlawed to use the network... Hmmm if only there was a way for everyone to transact without a central operator. Oh wait, there is! Bitcoin, Ethereum, and other crypto-currencies allow users to send and receive value with (relatively) low fees. Best of all, anyone can participate! In the next chapter we'll explore what some of these networks look like :)", 138 | ); 139 | -------------------------------------------------------------------------------- /ch1/1.5/solutions/client.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | 3 | // The client that end-users will use to interact with our central payment processor 4 | class Client { 5 | // Initializes a public/private key pair for the user 6 | constructor() { 7 | this.wallet = EthCrypto.createIdentity(); 8 | // initialize the nonce 9 | this.nonce = 0; 10 | } 11 | 12 | // Creates a keccak256/SHA3 hash of some data 13 | hash(data) { 14 | return EthCrypto.hash.keccak256(data); 15 | } 16 | 17 | // Signs a hash of data with the client's private key 18 | sign(message) { 19 | const messageHash = this.hash(message); 20 | return EthCrypto.sign(this.wallet.privateKey, messageHash); 21 | } 22 | 23 | // Verifies that a messageHash is signed by a certain address 24 | verify(signature, messageHash, address) { 25 | const signer = EthCrypto.recover(signature, messageHash); 26 | return signer === address; 27 | } 28 | 29 | // Buys tokens from Paypal 30 | buy(amount) { 31 | // Let the user know that they just exchanged off-network goods for network tokens 32 | console.log(`You bought ${amount} magic tokens from Paypal`); 33 | } 34 | 35 | // Generates new transactions 36 | generateTx(to, amount, type) { 37 | // create an unsigned transaction 38 | const unsignedTx = { 39 | type, 40 | amount, 41 | from: this.wallet.address, 42 | to, 43 | // add wallet nonce to tx 44 | nonce: this.nonce, 45 | }; 46 | // create a signature of the transaction 47 | const tx = { 48 | contents: unsignedTx, 49 | sig: this.sign(unsignedTx), 50 | }; 51 | // increment the wallet's nonce parameter AFTER the tx object is created 52 | this.nonce++; 53 | // return a Javascript object with the unsigned transaction and transaction signature 54 | return tx; 55 | } 56 | } 57 | 58 | module.exports = Client; 59 | -------------------------------------------------------------------------------- /ch1/1.5/solutions/demo.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./Client.js'); 3 | const Paypal = require('./Paypal.js'); 4 | 5 | // Paypal Network Demo 6 | console.log('//////////////////////////////////////////'); 7 | console.log('// Paypal Network Demo w Rent Extraction//'); 8 | console.log('//////////////////////////////////////////'); 9 | 10 | // Setup Paypal network 11 | const paypal = new Paypal(); 12 | console.log('\nInitial Paypal network:'); 13 | console.log(paypal); 14 | 15 | // Mint tokens for our users 16 | console.log( 17 | '\nToday Paypal has a promotion that new users get 100 free tokens for signing up', 18 | ); 19 | 20 | // Alice signs up for Paypal 21 | const alice = new Client(); 22 | const newUserAlice = paypal.generateTx(alice.wallet.address, 1000, 'mint'); 23 | paypal.processTx(newUserAlice); 24 | 25 | // Bob signs up for Paypal 26 | const bob = new Client(); 27 | const newUserBob = paypal.generateTx(bob.wallet.address, 1000, 'mint'); 28 | paypal.processTx(newUserBob); 29 | 30 | // Carol signs up for Paypal 31 | const carol = new Client(); 32 | const newUserCarol = paypal.generateTx(carol.wallet.address, 1000, 'mint'); 33 | paypal.processTx(newUserCarol); 34 | 35 | // Dave signs up for Paypal 36 | const dave = new Client(); 37 | const newUserDave = paypal.generateTx(dave.wallet.address, 1000, 'mint'); 38 | paypal.processTx(newUserDave); 39 | 40 | // Earl signs up for Paypal 41 | const eve = new Client(); 42 | const newUserEve = paypal.generateTx(eve.wallet.address, 1000, 'mint'); 43 | paypal.processTx(newUserEve); 44 | 45 | // Frank signs up for Paypal 46 | const frank = new Client(); 47 | const newUserFrank = paypal.generateTx(frank.wallet.address, 1000, 'mint'); 48 | paypal.processTx(newUserFrank); 49 | 50 | // George signs up for Paypal 51 | const george = new Client(); 52 | const newUserGeorge = paypal.generateTx(george.wallet.address, 1000, 'mint'); 53 | paypal.processTx(newUserGeorge); 54 | 55 | // Harry signs up for Paypal 56 | const harry = new Client(); 57 | const newUserHarry = paypal.generateTx(harry.wallet.address, 1000, 'mint'); 58 | paypal.processTx(newUserHarry); 59 | 60 | // Ian signs up for Paypal 61 | const ian = new Client(); 62 | const newUserIan = paypal.generateTx(ian.wallet.address, 1000, 'mint'); 63 | paypal.processTx(newUserIan); 64 | 65 | // Jill signs up for Paypal 66 | const jill = new Client(); 67 | const newUserJill = paypal.generateTx(jill.wallet.address, 1000, 'mint'); 68 | paypal.processTx(newUserJill); 69 | 70 | // Paypal's first users 71 | console.log( 72 | "\nLet's look at the state of Paypal's network now that there are a few users:", 73 | ); 74 | console.log(paypal); 75 | 76 | // The only constant is change 77 | console.log( 78 | "\nLet's imagine that some time has passed... Paypal is doing well and has lots of users. Naturally, Paypal decides to implement fees. Users are already using Paypal and don't want to switch, so the fee isn't THAT big of a deal. Let's see how it affects their balances over time", 79 | ); 80 | 81 | // Death by 1000 transaction fees 82 | function financialAttrition(...users) { 83 | // 1000 transactions 84 | for (let i = 0; i < 1000; i++) { 85 | // pick a random value between 1 and 10 86 | const txValue = Math.floor(Math.random() * 10) + 1; 87 | // choose two users at random, but excluding Paypal 88 | const user1 = users[Math.floor(Math.random() * users.length)]; 89 | const user2 = users[Math.floor(Math.random() * users.length)]; 90 | // create a transaction from one random user to another 91 | const tx = user1.generateTx(user2, txValue, 'send'); 92 | // process the transaction 93 | paypal.processTx(tx); 94 | // print the state to the console every 100 iterations so we can see the progress 95 | if (i % 100 === 0) { 96 | console.log('\nITER: ', i); 97 | console.log( 98 | "Paypal's balance: ", 99 | paypal.state[paypal.wallet.address].balance, 100 | ); 101 | // uncomment if you want to view the full state 102 | // console.log(paypal); 103 | } 104 | } 105 | } 106 | 107 | financialAttrition( 108 | alice, 109 | bob, 110 | carol, 111 | dave, 112 | eve, 113 | frank, 114 | george, 115 | harry, 116 | ian, 117 | jill, 118 | ); 119 | 120 | // The truth will set you free 121 | console.log( 122 | "\nWow! Shocking... Paypal made money while everyone else's balance went down. One could argue that this is because Paypal provides a valuable service and is compensated for that, which is a fair and reasonable this to say. IF, however, Paypal gained a monopoly and started raising the fees... well then that would be a different story. Try playing with the model to see what happens with different fees.", 123 | ); 124 | 125 | // The plot thickens... 126 | console.log( 127 | "\nWell that was fun. But wait! There's more. It turns out that Eve was actually a space pirate, and Paypal is forbidden from serving space pirates. Upon hearing this news, Paypal adds Eve's address to the blacklist (duhn duhn duhn...). Eve is now banned from using Paypal or their services.", 128 | ); 129 | paypal.blacklist.push(eve.wallet.address); 130 | 131 | console.log("Let's see what happens now..."); 132 | financialAttrition( 133 | alice, 134 | bob, 135 | carol, 136 | dave, 137 | eve, 138 | frank, 139 | george, 140 | harry, 141 | ian, 142 | jill, 143 | ); 144 | 145 | console.log( 146 | "\nLooks like most of Paypal's users are now too broke or too outlawed to use the network... Hmmm if only there was a way for everyone to transact without a central operator. Oh wait, there is! Bitcoin, Ethereum, and other crypto-currencies allow users to send and receive value with (relatively) low fees. Best of all, anyone can participate! In the next chapter we'll explore what some of these networks look like :)", 147 | ); 148 | -------------------------------------------------------------------------------- /ch1/1.5/test/test.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const assert = require('assert'); 3 | const Client = require('../client.js'); 4 | const Paypal = require('../paypal.js'); 5 | 6 | // Testing Paypal's fee feature 7 | describe('Rent Extraction', () => { 8 | // init params 9 | const paypal = new Paypal(); 10 | const alice = new Client(); 11 | const bob = new Client(); 12 | const carol = new Client(); 13 | const mintAliceTokens = paypal.generateTx(alice.wallet.address, 100, 'mint'); 14 | paypal.processTx(mintAliceTokens); 15 | const tx1 = alice.generateTx(bob.wallet.address, 10, 'send'); 16 | paypal.processTx(tx1); 17 | const tx2 = bob.generateTx(carol.wallet.address, 5, 'send'); 18 | paypal.processTx(tx2); 19 | 20 | it('should extract $1 fees from users', () => { 21 | assert.equal(89, paypal.state[alice.wallet.address].balance); 22 | assert.equal(4, paypal.state[bob.wallet.address].balance); 23 | }); 24 | }); 25 | 26 | // Testing Paypal's blacklist feature 27 | describe('Censorship', () => { 28 | // init params 29 | const paypal = new Paypal(); 30 | const alice = new Client(); 31 | const bob = new Client(); 32 | const eve = new Client(); 33 | paypal.blacklist.push(eve.wallet.address); 34 | const mintAliceTokens = paypal.generateTx(alice.wallet.address, 100, 'mint'); 35 | paypal.processTx(mintAliceTokens); 36 | const aliceTx2Bob = alice.generateTx(bob.wallet.address, 10, 'send'); 37 | const aliceTx2Eve = alice.generateTx(eve.wallet.address, 10, 'send'); 38 | console.log(paypal); 39 | // send a transaction to and from non-blacklisted accounts 40 | it('should allow transactions from non-blacklisted addresses', () => { 41 | assert.equal(true, paypal.processTx(aliceTx2Bob)); 42 | }); 43 | // try to send transaction to a blacklisted account 44 | it('should return false and not process the transaction if the tx is to or from blacklisted address', () => { 45 | assert.equal(false, paypal.processTx(aliceTx2Eve)); 46 | }); 47 | }); 48 | 49 | // Testing Paypal's theft feature 50 | describe('Steal All Funds Fraud', () => { 51 | // init params 52 | const paypal = new Paypal(); 53 | const alice = new Client(); 54 | const bob = new Client(); 55 | const tx1 = paypal.generateTx(alice.wallet.address, 100, 'mint'); 56 | const tx2 = alice.generateTx(bob.wallet.address, 30, 'send'); 57 | // steal all funds 58 | it('should steal all funds', () => { 59 | paypal.processTx(tx1); 60 | paypal.processTx(tx2); 61 | paypal.stealAllFunds(); 62 | assert.equal(0, paypal.state[alice.wallet.address].balance); 63 | assert.equal(0, paypal.state[bob.wallet.address].balance); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /ch2/2.1/README.md: -------------------------------------------------------------------------------- 1 | > The code challenges in this course build upon each other. It's highly recommended that you start from the beginning. If you haven't already, get started with our [Installation Instructions](https://cryptoeconomics.study/docs/en/sync/getting-started-development-setup). 2 | 3 | > The chapter 2 coding challenges use a network simulation. This is created by nodeAgent.js, networkSim.js, and netTest.js in the `ch2` directory. You do not need to understand how this network simulation works to complete the chapter two coding challenges, but we highly recommend it. The README in the `ch2` directory will give you a high level overview of the network demo. Each file also has detailed code comments explaining how each function works. 4 | 5 |
6 | 7 | ## Networks and Synchrony 8 | 9 | In chapter one we had a self contained payments processor demo. This worked fine since the state and all the logic of the state transition function was contained within a centralized file. Now we're going to explore what it's like to process transactions on a network where each node verifies transactions and propagates them to other nodes on the network. 10 | 11 |
12 | 13 | ## Spender Nodes 14 | 15 | As mentioned in the disclaimer, the network demo has already been set up for us in the `ch2` directory. In this section we will be creating a Spender class that extends the basic Node class. Spender represents a user on the network who is running a node and sending transactions. These spenders have a unique property in that they are altruistic (or naive) and only send transactions when they have money to spend. As we'll see, this allows the network to function for a while, but due to latency issues there is a problem. 16 | ``` 17 | // Spender is a Node that sends a random transaction at every tick() 18 | // Spender extends the Node class in nodeAgent.js 19 | // - this means that everything that is available to the Node class is imported and available to the Spender class as well 20 | class Spender extends Node { 21 | // returns a random wallet address (excluding the Spender) 22 | getRandomReceiver() { 23 | // TODO 24 | // create array of Node addresses that does not include this Node 25 | // pick a node at random from the nodes that are not this node 26 | // return the address of that random node 27 | } 28 | 29 | // tick() makes stuff happen 30 | // in this case we're simulating agents performing actions on a network 31 | // available options are 32 | // - do nothing 33 | // - send a transaction 34 | tick() { 35 | // TODO 36 | // check if we have money 37 | // if we have no money, don't do anything 38 | // print a fun message to the console stating that we're not doing anything 39 | // return to exit the function 40 | // if we do have money 41 | // Generate a random transaction 42 | // add the transaction to our historical transaction list 43 | // process the transaction 44 | // broadcast the transaction to the network 45 | } 46 | } 47 | 48 | ``` 49 | 50 |
51 | 52 | ## Testing The Spender Nodes 53 | 54 | To test our new Spender class we're going to try running it on our network simulation. Since the majority of the logic for the Node class (which Spender extends) has already been written, and what we really want to test is the latency, we're going to run the file itself. In the `invalidWithHonestNodesTest.js` file you'll see a demo at the end of the file. If you run `node invalidWithHonestNodesTest.js` in the `2.1` directory you'll see the network run for a bit, then... snafu! Due to the latency we introduced the system should fail and show us why. If you get stuck check the `solutions` directory or ask for help on the [forum](https://forum.cryptoeconomics.study/). 55 | 56 |
57 | -------------------------------------------------------------------------------- /ch2/2.1/solutions/spenderNode.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../../networkSim'); 3 | const { Node, getTxHash } = require('../../nodeAgent'); 4 | 5 | // Spender is a Node that sends a random transaction at every tick() 6 | // Spender extends the Node class in nodeAgent.js 7 | // - this means that everything that is available to the Node class is imported and available to the Spender class as well 8 | class Spender extends Node { 9 | // initialize the Spender class 10 | constructor(wallet, genesis, network, nodes) { 11 | super(wallet, genesis, network); 12 | // tell the Spender Node about the other nodes on the network 13 | this.nodes = nodes; 14 | 15 | // returns a random wallet address (excluding the Spender) 16 | getRandomReceiver() { 17 | // create array of Node addresses that does not include this Node 18 | const otherNodes = this.nodes.filter( 19 | n => n.wallet.address !== this.wallet.address, 20 | ); 21 | // pick a node at random from the nodes that are not this node 22 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 23 | // return the address of that random node 24 | return randomNode.wallet.address; 25 | } 26 | 27 | // tick() makes stuff happen 28 | // in this case we're simulating agents performing actions on a network 29 | // available options are 30 | // - do nothing 31 | // - send a transaction 32 | tick() { 33 | // If we have no money, don't do anything! 34 | if (this.state[this.wallet.address].balance < 10) { 35 | // print a fun message to the console 36 | console.log('We are honest so we wont send anything :)'); 37 | // return 38 | return; 39 | } 40 | // Generate a random transaction 41 | const tx = this.generateTx(this.getRandomReceiver(), 10); 42 | // add the transaction to our historical transaction list 43 | this.transactions.push(tx); 44 | // process the transaction 45 | this.applyTransaction(tx); 46 | // broadcast the transaction to the network 47 | this.network.broadcast(this.pid, tx); 48 | } 49 | } 50 | 51 | module.exports = Spender; 52 | -------------------------------------------------------------------------------- /ch2/2.1/spenderNode.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../networkSim'); 3 | const { Node, getTxHash } = require('../nodeAgent'); 4 | 5 | // Spender is a Node that sends a random transaction at every tick() 6 | // Spender extends the Node class in nodeAgent.js 7 | // - this means that everything that is available to the Node class is imported and available to the Spender class as well 8 | class Spender extends Node { 9 | // returns a random wallet address (excluding the Spender) 10 | getRandomReceiver() { 11 | // TODO 12 | // create array of Node addresses that does not include this Node 13 | // pick a node at random from the nodes that are not this node 14 | // return the address of that random node 15 | } 16 | 17 | // tick() makes stuff happen 18 | // in this case we're simulating agents performing actions on a network 19 | // available options are 20 | // - do nothing 21 | // - send a transaction 22 | tick() { 23 | // TODO 24 | // check if we have money 25 | // if we have no money, don't do anything 26 | // print a fun message to the console stating that we're not doing anything 27 | // return to exit the function 28 | // if we do have money 29 | // Generate a random transaction 30 | // add the transaction to our historical transaction list 31 | // process the transaction 32 | // broadcast the transaction to the network 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ch2/2.1/test/invalidWithHonestNodesTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../../networkSim'); 3 | const { Node, getTxHash } = require('../../nodeAgent'); 4 | const Spender = require('../solutions/spenderNode'); 5 | 6 | // //////////////////////////////////////////////////////// 7 | // ****** Test this out using a simulated network ****** // 8 | // //////////////////////////////////////////////////////// 9 | 10 | // you don't need to do anything here 11 | // this will just create a network simulation with some nodes 12 | // then simulate the nodes sending transactions and interacting on the network 13 | 14 | // Initialize network simulation parameters 15 | const numNodes = 5; 16 | const numConnections = 2; 17 | const wallets = []; 18 | const genesis = {}; 19 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0.1)); 20 | for (let i = 0; i < numNodes; i++) { 21 | wallets.push(EthCrypto.createIdentity()); 22 | genesis[wallets[i].address] = { 23 | balance: 100, 24 | nonce: 0, 25 | }; 26 | } 27 | 28 | // Create new Spender nodes based on our wallets, and connect them to the network 29 | const nodes = []; 30 | for (let i = 0; i < numNodes; i++) { 31 | nodes.push( 32 | new Spender(wallets[i], JSON.parse(JSON.stringify(genesis)), network, nodes), 33 | ); 34 | network.connectPeer(nodes[i], numConnections); 35 | } 36 | 37 | // PROBLEM 38 | // `nodes` is defined here, but the Spender class needs to know what nodes there are in order to connect to them. Originally this test was written in the same file as the Spender class, but now that they are separated this is broken. 39 | // MOVING GET RANDOM RECEIVER FROM SPENDERNODE.JS 40 | const getRandomReceiver = (address) => { 41 | // create array without this Node 42 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 43 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 44 | return randomNode.wallet.address; 45 | }; 46 | const tx = nodes[0].generateTx(getRandomReceiver(nodes[0].wallet.address), 10); 47 | nodes[0].onReceive(tx); 48 | // Broadcast this tx to the network 49 | nodes[0].network.broadcast(nodes[0].pid, tx); 50 | 51 | // Run the network simulation until a transaction fails 52 | try { 53 | network.run((steps = 300)); 54 | } catch (e) { 55 | console.log( 56 | 'One of our honest nodes had a transaction fail because of network latency!', 57 | ); 58 | console.log('err:', e); 59 | for (let i = 0; i < numNodes; i++) { 60 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 61 | console.log(nodes[i].state); 62 | } 63 | console.log(nodes[1].invalidNonceTxs[wallets[0].address]); 64 | } 65 | -------------------------------------------------------------------------------- /ch2/2.2/README.md: -------------------------------------------------------------------------------- 1 | > The code challenges in this course build upon each other. It's highly recommended that you start from the beginning. If you haven't already, get started with our [Installation Instructions](https://cryptoeconomics.study/docs/en/sync/getting-started-development-setup). 2 | 3 | > ! The chapter 2 coding challenges use a network simulation. This is created by nodeAgent.js, networkSim.js, and netTest.js in the `ch2` directory. You do not need to understand how this network simulation works to complete the chapter two coding challenges, but we highly recommend it. The README in the `ch2` directory will give you a high level overview of the network demo. Each file also has detailed code comments explaining how each function works. 4 | 5 |
6 | 7 | ## Double Spending 8 | 9 | As we mentioned in 2.1, the Spender nodes are altruistic (or naive) in that they only try to send transactions when they have funds. They could, however, try to send multiple transactions at the same time, and... if network latency was great enough, they might be able to fool multiple nodes into thinking they've both received the same transaction. 10 | 11 | The way to do this would be to duplicate the same transaction (including the nonce), but with different receivers. To each recipient the transactions would look valid, but when broadcast to the network and compared to the state of other nodes there would be a conflict. At best this would result in some disgruntled nodes and a reversion of one of the transactions, but this could potentially also create a network fork where one set of nodes accepts one of the transactions and another set of nodes accepts another. This would create 2 different versions of the state, and thus 2 different networks. 12 | 13 | To better understand double spends let's try to implement a double spend attack ourselves! In `doubleSpendTest.js` the network has already been setup, but it's up to you to perform the double spend. 14 | ``` 15 | // Attempting to double spend! 16 | // TODO: designate the node that will double spend 17 | // TODO: designate the nodes that we will perform the attack on 18 | // TODO: create 2 identical transactions that have different recipients 19 | // TODO: broadcast both transaction to the network at the same time 20 | ``` 21 | 22 |
23 | 24 | ## Testing The Attack 25 | 26 | You can run the network simulation at any time to check the results of your attack. This is done by running `node doubleSpendTest.js` in the `2.2` directory. As always, if you have questions or get stuck please hit up the community on the [forum!](https://forum.cryptoeconomics.study) 27 | 28 | > Throughout the rest of chapter 2 we will use slightly modified versions of `doubleSpendTest.js` to see how our different networks handle double spending 29 | -------------------------------------------------------------------------------- /ch2/2.2/doubleSpendTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../networkSim'); 3 | const { Node } = require('../nodeAgent'); 4 | 5 | // Double Spending Network Simulation 6 | // this extends the NetworkSimulator class by logging important information related to double spending 7 | class DoubleSpendNetSim extends NetworkSimulator { 8 | // initialize initial parameters 9 | constructor(latency, packetLoss) { 10 | super(latency, packetLoss); 11 | } 12 | 13 | // do stuff and log the results 14 | tick() { 15 | // call NetworkSimulator tick() 16 | super.tick(); 17 | const victimBalances = []; 18 | // for all the victims (network participants) 19 | for (const v of victims) { 20 | victimBalances.push([ 21 | v.state[evilNode.wallet.address].balance, 22 | v.state[v.wallet.address].balance, 23 | ]); 24 | // keep track of the balances of network participants 25 | console.log( 26 | `Victim ${victimBalances.length} has balance ${ 27 | victimBalances[victimBalances.length - 1][1] 28 | }`, 29 | ); 30 | // if a successful double spend is detected (nodes accepted both transactions to the victims), then stop the simulation and log the results to the console 31 | if (Object.keys(v.invalidNonceTxs).length > 0) { 32 | console.log( 33 | `Double spend propagated to victim ${victimBalances.length}`, 34 | ); 35 | } 36 | } 37 | if (victimBalances[0][1] === 100 && victimBalances[1][1] === 100) { 38 | console.log('Double spend detected!!!!! Ahh!!!'); 39 | throw new Error('Double spend was successful!'); 40 | } 41 | } 42 | } 43 | 44 | // //////////////////////////////////////////////////////// 45 | // ****** Test this out using a simulated network ****** // 46 | // //////////////////////////////////////////////////////// 47 | 48 | // Initialize network parameters 49 | const numNodes = 5; 50 | const wallets = []; 51 | const genesis = {}; 52 | const network = new DoubleSpendNetSim((latency = 5), (packetLoss = 0.1)); 53 | 54 | // Create some wallets representing users on the network 55 | for (let i = 0; i < numNodes; i++) { 56 | // create new identity 57 | wallets.push(EthCrypto.createIdentity()); 58 | // add that node to our genesis block & give them an allocation 59 | genesis[wallets[i].address] = { 60 | balance: 0, 61 | nonce: 0, 62 | }; 63 | } 64 | 65 | // Give wallet 0 (the double spending node) some money at genesis 66 | genesis[wallets[0].address] = { 67 | balance: 100, 68 | nonce: 0, 69 | }; 70 | 71 | // Create new nodes based on our wallets, and connect them to the network 72 | const nodes = []; 73 | for (let i = 0; i < numNodes; i++) { 74 | nodes.push( 75 | new Node(wallets[i], JSON.parse(JSON.stringify(genesis)), network), 76 | ); 77 | // connect everyone to everyone 78 | network.connectPeer(nodes[i], (numConnections = 3)); 79 | } 80 | 81 | // Attempting to double spend! 82 | // TODO: designate the node that will double spend 83 | // TODO: designate the nodes that we will perform the attack on 84 | // TODO: create 2 identical transactions that have different recipients 85 | // TODO: broadcast both transaction to the network at the same time 86 | 87 | // Running the simulation 88 | // due to network latency some nodes will receive one transaction before the other 89 | // let's run the network until an invalid spend is detected 90 | // we will also detect if the two victim nodes, for a short time, both believe they have been sent money by our evil node. That's our double spend! 91 | network.run((steps = 20)); 92 | 93 | // If the network was able to run without an error, that means that the double spend was not successful 94 | console.log( 95 | 'Looks like the double spend was foiled by network latency! Victim 1 propagated their transaction ' 96 | + "to Victim 2 before Victim 2 received the evil node's attempt to double spend (or vise-versa)!", 97 | ); 98 | -------------------------------------------------------------------------------- /ch2/2.2/solutions/doubleSpendTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../../networkSim'); 3 | const { Node } = require('../../nodeAgent'); 4 | 5 | // Double Spending Network Simulation 6 | // this extends the NetworkSimulator class by logging important information related to double spending 7 | class DoubleSpendNetSim extends NetworkSimulator { 8 | // initialize initial parameters 9 | constructor(latency, packetLoss) { 10 | super(latency, packetLoss); 11 | } 12 | 13 | // do stuff and log the results 14 | tick() { 15 | // call NetworkSimulator tick() 16 | super.tick(); 17 | const victimBalances = []; 18 | // for all the victims (network participants) 19 | for (const v of victims) { 20 | victimBalances.push([ 21 | v.state[evilNode.wallet.address].balance, 22 | v.state[v.wallet.address].balance, 23 | ]); 24 | // keep track of the balances of network participants 25 | console.log( 26 | `Victim ${victimBalances.length} has balance ${ 27 | victimBalances[victimBalances.length - 1][1] 28 | }`, 29 | ); 30 | // if a successful double spend is detected (nodes accepted both transactions to the victims), then stop the simulation and log the results to the console 31 | if (Object.keys(v.invalidNonceTxs).length > 0) { 32 | console.log( 33 | `Double spend propagated to victim ${victimBalances.length}`, 34 | ); 35 | } 36 | } 37 | if (victimBalances[0][1] === 100 && victimBalances[1][1] === 100) { 38 | console.log('Double spend detected!!!!! Ahh!!!'); 39 | throw new Error('Double spend was successful!'); 40 | } 41 | } 42 | } 43 | 44 | // //////////////////////////////////////////////////////// 45 | // ****** Test this out using a simulated network ****** // 46 | // //////////////////////////////////////////////////////// 47 | 48 | // Initialize network parameters 49 | const numNodes = 5; 50 | const wallets = []; 51 | const genesis = {}; 52 | const network = new DoubleSpendNetSim((latency = 5), (packetLoss = 0.1)); 53 | 54 | // Create some wallets representing users on the network 55 | for (let i = 0; i < numNodes; i++) { 56 | // create new identity 57 | wallets.push(EthCrypto.createIdentity()); 58 | // add that node to our genesis block & give them an allocation 59 | genesis[wallets[i].address] = { 60 | balance: 0, 61 | nonce: 0, 62 | }; 63 | } 64 | 65 | // Give wallet 0 (the double spending node) some money at genesis 66 | genesis[wallets[0].address] = { 67 | balance: 100, 68 | nonce: 0, 69 | }; 70 | 71 | // Create new nodes based on our wallets, and connect them to the network 72 | const nodes = []; 73 | for (let i = 0; i < numNodes; i++) { 74 | nodes.push( 75 | new Node(wallets[i], JSON.parse(JSON.stringify(genesis)), network), 76 | ); 77 | // connect everyone to everyone 78 | network.connectPeer(nodes[i], (numConnections = 3)); 79 | } 80 | 81 | // Attempting to double spend! 82 | // designate the node that will double spend 83 | const evilNode = nodes[0]; 84 | // designate the nodes that we will perform the attack on 85 | const victims = [ 86 | network.peers[evilNode.pid][0], 87 | network.peers[evilNode.pid][1], 88 | ]; 89 | // create 2 identical transactions that have different recipients 90 | const spends = [ 91 | evilNode.generateTx(victims[0].wallet.address, (amount = 100)), 92 | evilNode.generateTx(victims[1].wallet.address, (amount = 100)), 93 | ]; 94 | // broadcast both transaction to the network at the same time 95 | network.broadcastTo(evilNode.pid, victims[0], spends[0]); 96 | network.broadcastTo(evilNode.pid, victims[1], spends[1]); 97 | 98 | // Running the simulation 99 | // due to network latency some nodes will receive one transaction before the other 100 | // let's run the network until an invalid spend is detected 101 | // we will also detect if the two victim nodes, for a short time, both believe they have been sent money by our evil node. That's our double spend! 102 | network.run((steps = 20)); 103 | 104 | // If the network was able to run without an error, that means that the double spend was not successful 105 | console.log( 106 | 'Looks like the double spend was foiled by network latency! Victim 1 propagated their transaction ' 107 | + "to Victim 2 before Victim 2 received the evil node's attempt to double spend (or vise-versa)!", 108 | ); 109 | -------------------------------------------------------------------------------- /ch2/2.3/README.md: -------------------------------------------------------------------------------- 1 | > The code challenges in this course build upon each other. It's highly recommended that you start from the beginning. If you haven't already, get started with our [Installation Instructions](https://cryptoeconomics.study/docs/en/sync/getting-started-development-setup). 2 | 3 | > ! The chapter 2 coding challenges use a network simulation. This is created by nodeAgent.js, networkSim.js, and netTest.js in the `ch2` directory. You do not need to understand how this network simulation works to complete the chapter two coding challenges, but we highly recommend it. The README in the `ch2` directory will give you a high level overview of the network demo. Each file also has detailed code comments explaining how each function works. 4 | 5 |
6 | 7 | ## Latency-Based Consensus 8 | 9 | In order to implement latency based consensus we're going to need to extend our Node class. We will do this, then construct a few tests. In these tests we will try to attack the network, but our new nodes will prevent any damage. This will give you an appreciation for how these attacks work as well as the mechanisms that can help us prevent them. 10 | 11 |
12 | 13 | ## Fault Tolerant Node 14 | 15 | In order to extend the vanilla node simulation with logic to handle fault tolerant consensus we will modify the `onReceive()` function. 16 | ``` 17 | // what the node does when it receives a transaction 18 | onReceive(tx) { 19 | // TODO 20 | // if we're already seen (and processed the transaction), jut return since there's nothing to do 21 | // get the signature from the transaction 22 | // return and do not process the transaction if the first signee is not tx sender 23 | // take note that we've seen the transaction 24 | // check that each signee is actually a peer in the network 25 | // add to pending ( we'll apply this transaction once we hit finalTimeout) 26 | // choice rule: if the node has two transactions with same sender, nonce, and timestamp then apply the one with lower sig first 27 | // add this node's signature to the transaction 28 | // broadcast the transaction to the rest of the network so that another node can sign it 29 | } 30 | ``` 31 | 32 |
33 | 34 | ## Time Stamps 35 | 36 | Time stamps are a crucial part of latency based consensus. In `timestampTest.js` we will extend `NetworkSimulator` to allow for timestamps. We will then create a few attacks to test out our fault tolerant nodes and make sure they are working correctly. 37 | ``` 38 | // TODO 39 | // create two transactions with the same amount, but with different timestamps 40 | // broadcast both transactions to the network at the same time 41 | ``` 42 | 43 |
44 | 45 | ## Testing 46 | 47 | To test our new fault tolerant nodes we will run `doubleSpendTest.js`, `faultTolerantTest.js`, and `timestampTest.js`. This will test that our fault tolerant node is functioning correctly, that it's blocking double spends, and that it's blocking timestamp attacks. 48 | 49 | > As always, if you have questions or get stuck please hit up the community on the [forum!](https://forum.cryptoeconomics.study) 50 | 51 | -------------------------------------------------------------------------------- /ch2/2.3/doubleSpendTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const { getTxHash } = require('../nodeAgent'); 3 | const FaultTolerant = require('./faultTolerant'); 4 | const NetworkSimulator = require('./networkSimFaultTolerant'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 11 | for (let i = 0; i < numNodes; i++) { 12 | // Create new identity 13 | wallets.push(EthCrypto.createIdentity()); 14 | // Add that node to our genesis block & give them an allocation 15 | genesis[wallets[i].address] = { 16 | balance: 0, 17 | nonce: 0, 18 | }; 19 | } 20 | // Give wallet 0 some money at genesis 21 | genesis[wallets[0].address] = { 22 | balance: 100, 23 | nonce: 0, 24 | }; 25 | const nodes = []; 26 | // Create new nodes based on our wallets, and connect them to the network 27 | for (let i = 0; i < numNodes; i++) { 28 | nodes.push( 29 | // this time we're going to create fault tolerant nodes rather than altruistic spender nodes 30 | new FaultTolerant( 31 | wallets[i], 32 | JSON.parse(JSON.stringify(genesis)), 33 | network, 34 | (delta = 5), 35 | ), 36 | ); 37 | // Connect everyone to everyone 38 | network.connectPeer(nodes[i], (numConnections = 3)); 39 | } 40 | 41 | // Attempt double spend 42 | const evilNode = nodes[0]; 43 | const victims = [ 44 | network.peers[evilNode.pid][0], 45 | network.peers[evilNode.pid][1], 46 | ]; 47 | const spends = [ 48 | evilNode.generateTx(victims[0].wallet.address, (amount = 100)), 49 | evilNode.generateTx(victims[1].wallet.address, (amount = 100)), 50 | ]; 51 | console.log('transactions:', spends); 52 | network.broadcastTo(evilNode.pid, victims[0], spends[0]); 53 | network.broadcastTo(evilNode.pid, victims[1], spends[1]); 54 | // Now run the network until an invalid spend is detected. 55 | // We will also detect if the two victim nodes, for a short time, both believe they have been sent money 56 | // by our evil node. That's our double spend! 57 | try { 58 | network.run((steps = 70)); 59 | } catch (e) { 60 | console.err(e); 61 | for (let i = 0; i < numNodes; i++) { 62 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 63 | console.log(nodes[i].state); 64 | } 65 | } 66 | console.log('all nodes stayed in consensus!'); 67 | -------------------------------------------------------------------------------- /ch2/2.3/faultTolerant.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const _ = require('lodash'); 3 | const { Node, getTxHash } = require('../nodeAgent'); 4 | 5 | class FaultTolerant extends Node { 6 | constructor(wallet, genesis, network, delta) { 7 | super(wallet, genesis, network); 8 | this.delta = delta; 9 | this.pendingTxs = {}; 10 | this.seen = []; 11 | } 12 | 13 | timeout(timestamp, numObservers) { 14 | return timestamp + (numObservers - 0.5) * 2 * this.delta; 15 | } 16 | 17 | addressesFromSigs(tx) { 18 | const addressSet = new Set(); 19 | for (let i = 0; i < tx.sigs.length; i++) { 20 | const sig = tx.sigs[i]; 21 | const slicedTx = { 22 | contents: tx.contents, 23 | sigs: tx.sigs.slice(0, i), 24 | }; 25 | const messageHash = getTxHash(slicedTx); 26 | const address = EthCrypto.recover(sig, messageHash); 27 | if (i === 0 && address !== tx.contents.from) throw new Error('Invalid first signature!'); 28 | addressSet.add(address); 29 | } 30 | return addressSet; 31 | } 32 | 33 | // what the node does when it receives a transaction 34 | onReceive(tx) { 35 | // TODO 36 | // if we're already seen (and processed the transaction), jut return since there's nothing to do 37 | // get the signature from the transaction 38 | // return and do not process the transaction if the first signee is not tx sender 39 | // take note that we've seen the transaction 40 | // check that each signee is actually a peer in the network 41 | // add to pending ( we'll apply this transaction once we hit finalTimeout) 42 | // choice rule: if the node has two transactions with same sender, nonce, and timestamp then apply the one with lower sig first 43 | // add this node's signature to the transaction 44 | // broadcast the transaction to the rest of the network so that another node can sign it 45 | } 46 | 47 | // do stuff 48 | tick() { 49 | const { time } = this.network; 50 | const toApply = this.pendingTxs[time]; 51 | if (!toApply) return; 52 | for (const tx of toApply) { 53 | this.applyTransaction(tx); 54 | } 55 | delete this.pendingTxs[time]; 56 | } 57 | 58 | generateTx(to, amount) { 59 | const unsignedTx = { 60 | type: 'send', 61 | amount, 62 | from: this.wallet.address, 63 | to, 64 | nonce: this.state[this.wallet.address].nonce, 65 | timestamp: this.network.time, 66 | }; 67 | const tx = { 68 | contents: unsignedTx, 69 | sigs: [], 70 | }; 71 | tx.sigs.push(EthCrypto.sign(this.wallet.privateKey, getTxHash(tx))); 72 | return tx; 73 | } 74 | 75 | applyTransaction(tx) { 76 | console.log('~~~~~~~~~APPLY TRANSACTION~~~~~~~~~~', tx); 77 | // If we don't have a record for this address, create one 78 | if (!(tx.contents.to in this.state)) { 79 | this.state[tx.contents.to] = { 80 | balance: 0, 81 | nonce: 0, 82 | }; 83 | } 84 | // Check that the nonce is correct for replay protection 85 | if (tx.contents.nonce > this.state[tx.contents.from].nonce) { 86 | if (!(tx.contents.from in this.invalidNonceTxs)) { 87 | this.invalidNonceTxs[tx.contents.from] = {}; 88 | } 89 | this.invalidNonceTxs[tx.contents.from][tx.contents.nonce] = tx; 90 | return; 91 | } 92 | if (tx.contents.nonce < this.state[tx.contents.from].nonce) { 93 | console.log('passed nonce tx rejected'); 94 | return; 95 | } 96 | // Apply send to balances 97 | if (tx.contents.type === 'send') { 98 | // Send coins 99 | if (this.state[tx.contents.from].balance - tx.contents.amount < 0) { 100 | throw new Error('Not enough money!'); 101 | } 102 | this.state[tx.contents.from].balance -= tx.contents.amount; 103 | this.state[tx.contents.to].balance += tx.contents.amount; 104 | } else { 105 | throw new Error('Invalid transaction type!'); 106 | } 107 | // update sender nonce 108 | this.state[tx.contents.from].nonce += 1; 109 | } 110 | } 111 | 112 | module.exports = FaultTolerant; 113 | -------------------------------------------------------------------------------- /ch2/2.3/faultTolerantTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimFaultTolerant'); 3 | const FaultTolerant = require('./faultTolerant'); 4 | 5 | // ****** Test this out using a simulated network ****** // 6 | const numNodes = 5; 7 | const wallets = []; 8 | const genesis = {}; 9 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 10 | for (let i = 0; i < numNodes; i++) { 11 | // Create new identity 12 | wallets.push(EthCrypto.createIdentity()); 13 | // Add that node to our genesis block & give them an allocation 14 | genesis[wallets[i].address] = { 15 | balance: 100, 16 | nonce: 0, 17 | }; 18 | } 19 | const nodes = []; 20 | // Create new nodes based on our wallets, and connect them to the network 21 | for (let i = 0; i < numNodes; i++) { 22 | nodes.push( 23 | new FaultTolerant( 24 | wallets[i], 25 | JSON.parse(JSON.stringify(genesis)), 26 | network, 27 | (delta = 5), 28 | ), 29 | ); 30 | network.connectPeer(nodes[i], (numConnections = 2)); 31 | } 32 | 33 | // Create a quick test 34 | // randomly choose a node in the network 35 | const getRandomReceiver = (address) => { 36 | // create array without this Node 37 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 38 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 39 | return randomNode.wallet.address; 40 | }; 41 | // create a transaction to a random node in the network 42 | const tx = nodes[0].generateTx(getRandomReceiver(nodes[0].wallet.address), 10); 43 | nodes[0].onReceive(tx); 44 | // Broadcast this tx to the network 45 | nodes[0].network.broadcast(nodes[0].pid, tx); 46 | 47 | // Run the network simulation test 48 | try { 49 | network.run((steps = 80)); 50 | } catch (e) { 51 | console.err(e); 52 | for (let i = 0; i < numNodes; i++) { 53 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 54 | console.log(nodes[i].state); 55 | } 56 | } 57 | // success message 58 | console.log('all nodes stayed in consensus!'); 59 | -------------------------------------------------------------------------------- /ch2/2.3/networkSimFaultTolerant.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../networkSim'); 3 | 4 | class NetworkSimFT extends NetworkSimulator { 5 | constructor(latency, packetLoss) { 6 | super(latency, packetLoss); 7 | } 8 | 9 | tick() { 10 | // call NetworkSimulator tick() 11 | super.tick(); 12 | console.log( 13 | `~~~~~~~~~~~~~~~~~~~~~~~~~~~~time: ${this.time}~~~~~~~~~~~~~~~~~~~~~~~~`, 14 | ); 15 | for (let i = 0; i < this.agents.length; i++) { 16 | console.log('~~~~~~~~~ Node', i, '~~~~~~~~~'); 17 | console.log(this.agents[i].state); 18 | if ( 19 | JSON.stringify(this.agents[i].state) 20 | !== JSON.stringify(this.agents[0].state) 21 | ) throw new Error('Nodes have fallen out of consensus'); 22 | } 23 | } 24 | } 25 | 26 | module.exports = NetworkSimFT; 27 | -------------------------------------------------------------------------------- /ch2/2.3/solutions/doubleSpendTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const { getTxHash } = require('../../nodeAgent'); 3 | const FaultTolerant = require('./faultTolerant'); 4 | const NetworkSimulator = require('./networkSimFaultTolerant'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 11 | for (let i = 0; i < numNodes; i++) { 12 | // Create new identity 13 | wallets.push(EthCrypto.createIdentity()); 14 | // Add that node to our genesis block & give them an allocation 15 | genesis[wallets[i].address] = { 16 | balance: 0, 17 | nonce: 0, 18 | }; 19 | } 20 | // Give wallet 0 some money at genesis 21 | genesis[wallets[0].address] = { 22 | balance: 100, 23 | nonce: 0, 24 | }; 25 | const nodes = []; 26 | // Create new nodes based on our wallets, and connect them to the network 27 | for (let i = 0; i < numNodes; i++) { 28 | nodes.push( 29 | // this time we're going to create fault tolerant nodes rather than altruistic spender nodes 30 | new FaultTolerant( 31 | wallets[i], 32 | JSON.parse(JSON.stringify(genesis)), 33 | network, 34 | (delta = 5), 35 | ), 36 | ); 37 | // Connect everyone to everyone 38 | network.connectPeer(nodes[i], (numConnections = 3)); 39 | } 40 | 41 | // Attempt double spend 42 | const evilNode = nodes[0]; 43 | const victims = [ 44 | network.peers[evilNode.pid][0], 45 | network.peers[evilNode.pid][1], 46 | ]; 47 | const spends = [ 48 | evilNode.generateTx(victims[0].wallet.address, (amount = 100)), 49 | evilNode.generateTx(victims[1].wallet.address, (amount = 100)), 50 | ]; 51 | console.log('transactions:', spends); 52 | network.broadcastTo(evilNode.pid, victims[0], spends[0]); 53 | network.broadcastTo(evilNode.pid, victims[1], spends[1]); 54 | // Now run the network until an invalid spend is detected. 55 | // We will also detect if the two victim nodes, for a short time, both believe they have been sent money 56 | // by our evil node. That's our double spend! 57 | try { 58 | network.run((steps = 70)); 59 | } catch (e) { 60 | console.err(e); 61 | for (let i = 0; i < numNodes; i++) { 62 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 63 | console.log(nodes[i].state); 64 | } 65 | } 66 | console.log('all nodes stayed in consensus!'); 67 | -------------------------------------------------------------------------------- /ch2/2.3/solutions/faultTolerant.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const _ = require('lodash'); 3 | const { Node, getTxHash } = require('../../nodeAgent'); 4 | 5 | class FaultTolerant extends Node { 6 | constructor(wallet, genesis, network, delta) { 7 | super(wallet, genesis, network); 8 | this.delta = delta; 9 | this.pendingTxs = {}; 10 | this.seen = []; 11 | } 12 | 13 | timeout(timestamp, numObservers) { 14 | return timestamp + (numObservers - 0.5) * 2 * this.delta; 15 | } 16 | 17 | addressesFromSigs(tx) { 18 | const addressSet = new Set(); 19 | for (let i = 0; i < tx.sigs.length; i++) { 20 | const sig = tx.sigs[i]; 21 | const slicedTx = { 22 | contents: tx.contents, 23 | sigs: tx.sigs.slice(0, i), 24 | }; 25 | const messageHash = getTxHash(slicedTx); 26 | const address = EthCrypto.recover(sig, messageHash); 27 | if (i === 0 && address !== tx.contents.from) throw new Error('Invalid first signature!'); 28 | addressSet.add(address); 29 | } 30 | return addressSet; 31 | } 32 | 33 | // what the node does when it receives a transaction 34 | onReceive(tx) { 35 | // if we're already seen (and processed the transaction), jut return since there's nothing to do 36 | if (this.seen.includes(tx.contents)) return; 37 | // get the signature from the transaction 38 | const sigs = this.addressesFromSigs(tx); 39 | // return and do not process the transaction if the first signee is not tx sender 40 | if (this.network.time >= this.timeout(tx.contents.timestamp, sigs.size)) return; 41 | // take note that we've seen the transaction 42 | this.seen.push(tx.contents); 43 | // check that each signee is actually a peer in the network 44 | const finalTimeout = this.timeout( 45 | tx.contents.timestamp, 46 | this.network.agents.length, 47 | ); 48 | if (!this.pendingTxs[finalTimeout]) this.pendingTxs[finalTimeout] = []; 49 | // add to pending ( we'll apply this transaction once we hit finalTimeout) 50 | this.pendingTxs[finalTimeout].push(tx); 51 | // choice rule: if the node has two transactions with same sender, nonce, and timestamp then apply the one with lower sig first 52 | this.pendingTxs[finalTimeout].sort((a, b) => a.sigs[0] - b.sigs[0]); 53 | // add this node's signature to the transaction 54 | tx.sigs.push(EthCrypto.sign(this.wallet.privateKey, getTxHash(tx))); 55 | // broadcast the transaction to the rest of the network so that another node can sign it 56 | this.network.broadcast(this.pid, tx); 57 | } 58 | 59 | // do stuff 60 | tick() { 61 | const { time } = this.network; 62 | const toApply = this.pendingTxs[time]; 63 | if (!toApply) return; 64 | for (const tx of toApply) { 65 | this.applyTransaction(tx); 66 | } 67 | delete this.pendingTxs[time]; 68 | } 69 | 70 | generateTx(to, amount) { 71 | const unsignedTx = { 72 | type: 'send', 73 | amount, 74 | from: this.wallet.address, 75 | to, 76 | nonce: this.state[this.wallet.address].nonce, 77 | timestamp: this.network.time, 78 | }; 79 | const tx = { 80 | contents: unsignedTx, 81 | sigs: [], 82 | }; 83 | tx.sigs.push(EthCrypto.sign(this.wallet.privateKey, getTxHash(tx))); 84 | return tx; 85 | } 86 | 87 | applyTransaction(tx) { 88 | console.log('~~~~~~~~~APPLY TRANSACTION~~~~~~~~~~', tx); 89 | // If we don't have a record for this address, create one 90 | if (!(tx.contents.to in this.state)) { 91 | this.state[tx.contents.to] = { 92 | balance: 0, 93 | nonce: 0, 94 | }; 95 | } 96 | // Check that the nonce is correct for replay protection 97 | if (tx.contents.nonce > this.state[tx.contents.from].nonce) { 98 | if (!(tx.contents.from in this.invalidNonceTxs)) { 99 | this.invalidNonceTxs[tx.contents.from] = {}; 100 | } 101 | this.invalidNonceTxs[tx.contents.from][tx.contents.nonce] = tx; 102 | return; 103 | } 104 | if (tx.contents.nonce < this.state[tx.contents.from].nonce) { 105 | console.log('passed nonce tx rejected'); 106 | return; 107 | } 108 | // Apply send to balances 109 | if (tx.contents.type === 'send') { 110 | // Send coins 111 | if (this.state[tx.contents.from].balance - tx.contents.amount < 0) { 112 | throw new Error('Not enough money!'); 113 | } 114 | this.state[tx.contents.from].balance -= tx.contents.amount; 115 | this.state[tx.contents.to].balance += tx.contents.amount; 116 | } else { 117 | throw new Error('Invalid transaction type!'); 118 | } 119 | // update sender nonce 120 | this.state[tx.contents.from].nonce += 1; 121 | } 122 | } 123 | 124 | module.exports = FaultTolerant; 125 | -------------------------------------------------------------------------------- /ch2/2.3/solutions/faultTolerantTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimFaultTolerant'); 3 | const FaultTolerant = require('./faultTolerant'); 4 | 5 | // ****** Test this out using a simulated network ****** // 6 | const numNodes = 5; 7 | const wallets = []; 8 | const genesis = {}; 9 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 10 | for (let i = 0; i < numNodes; i++) { 11 | // Create new identity 12 | wallets.push(EthCrypto.createIdentity()); 13 | // Add that node to our genesis block & give them an allocation 14 | genesis[wallets[i].address] = { 15 | balance: 100, 16 | nonce: 0, 17 | }; 18 | } 19 | const nodes = []; 20 | // Create new nodes based on our wallets, and connect them to the network 21 | for (let i = 0; i < numNodes; i++) { 22 | nodes.push( 23 | new FaultTolerant( 24 | wallets[i], 25 | JSON.parse(JSON.stringify(genesis)), 26 | network, 27 | (delta = 5), 28 | ), 29 | ); 30 | network.connectPeer(nodes[i], (numConnections = 2)); 31 | } 32 | 33 | // Create a quick test 34 | // randomly choose a node in the network 35 | const getRandomReceiver = (address) => { 36 | // create array without this Node 37 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 38 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 39 | return randomNode.wallet.address; 40 | }; 41 | // create a transaction to a random node in the network 42 | const tx = nodes[0].generateTx(getRandomReceiver(nodes[0].wallet.address), 10); 43 | nodes[0].onReceive(tx); 44 | // Broadcast this tx to the network 45 | nodes[0].network.broadcast(nodes[0].pid, tx); 46 | 47 | // Run the network simulation test 48 | try { 49 | network.run((steps = 80)); 50 | } catch (e) { 51 | console.err(e); 52 | for (let i = 0; i < numNodes; i++) { 53 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 54 | console.log(nodes[i].state); 55 | } 56 | } 57 | // success message 58 | console.log('all nodes stayed in consensus!'); 59 | -------------------------------------------------------------------------------- /ch2/2.3/solutions/networkSimFaultTolerant.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../../networkSim'); 3 | 4 | class NetworkSimFT extends NetworkSimulator { 5 | constructor(latency, packetLoss) { 6 | super(latency, packetLoss); 7 | } 8 | 9 | tick() { 10 | // call NetworkSimulator tick() 11 | super.tick(); 12 | console.log( 13 | `~~~~~~~~~~~~~~~~~~~~~~~~~~~~time: ${ 14 | this.time 15 | }~~~~~~~~~~~~~~~~~~~~~~~~`, 16 | ); 17 | for (let i = 0; i < this.agents.length; i++) { 18 | console.log('~~~~~~~~~ Node', i, '~~~~~~~~~'); 19 | console.log(this.agents[i].state); 20 | if ( 21 | JSON.stringify(this.agents[i].state) 22 | !== JSON.stringify(this.agents[0].state) 23 | ) throw new Error('Nodes have fallen out of consensus'); 24 | } 25 | } 26 | } 27 | 28 | module.exports = NetworkSimFT; 29 | -------------------------------------------------------------------------------- /ch2/2.3/solutions/timestampTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimFaultTolerant.js'); 3 | const FaultTolerant = require('./faultTolerant'); 4 | const { getTxHash } = require('../../nodeAgent'); 5 | 6 | class TimestampSimulator extends NetworkSimulator { 7 | constructor(latency, packetLoss) { 8 | super(latency, packetLoss); 9 | } 10 | 11 | tick() { 12 | // call NetworkSimulator tick() 13 | super.tick(); 14 | 15 | // at the 5th timestep try faking an early spend 16 | if (this.time === 5) { 17 | network.broadcast(evilNode.pid, fakeEarlySpend); 18 | } 19 | } 20 | } 21 | // ****** Test this out using a simulated network ****** // 22 | const numNodes = 5; 23 | const wallets = []; 24 | const genesis = {}; 25 | const network = new TimestampSimulator((latency = 5), (packetLoss = 0)); 26 | for (let i = 0; i < numNodes; i++) { 27 | // Create new identity 28 | wallets.push(EthCrypto.createIdentity()); 29 | // Add that node to our genesis block & give them an allocation 30 | genesis[wallets[i].address] = { 31 | balance: 0, 32 | nonce: 0, 33 | }; 34 | } 35 | // Give wallet 0 some money at genesis 36 | genesis[wallets[0].address] = { 37 | balance: 100, 38 | nonce: 0, 39 | }; 40 | // init nodes 41 | const nodes = []; 42 | // Create new nodes based on our wallets, and connect them to the network 43 | for (let i = 0; i < numNodes; i++) { 44 | nodes.push( 45 | // push a new fault tolerant node to the network 46 | new FaultTolerant( 47 | wallets[i], 48 | JSON.parse(JSON.stringify(genesis)), 49 | network, 50 | (delta = 5), 51 | ), 52 | ); 53 | // Connect everyone to everyone 54 | network.connectPeer(nodes[i], (numConnections = 3)); 55 | } 56 | 57 | // Attempt double spend 58 | const evilNode = nodes[0]; 59 | const victims = [ 60 | network.peers[evilNode.pid][0], 61 | network.peers[evilNode.pid][1], 62 | ]; 63 | const generateCustomTx = (to, amount, timestamp, node) => { 64 | const unsignedTx = { 65 | type: 'send', 66 | amount, 67 | from: node.wallet.address, 68 | to, 69 | nonce: node.state[node.wallet.address].nonce, 70 | timestamp, 71 | }; 72 | const tx = { 73 | contents: unsignedTx, 74 | sigs: [], 75 | }; 76 | tx.sigs.push(EthCrypto.sign(node.wallet.privateKey, getTxHash(tx))); 77 | return tx; 78 | }; 79 | // const spends = [evilNode.generateTx(victims[0].wallet.address, amount = 100), evilNode.generateTx(victims[1].wallet.address, amount = 100)] 80 | const spend = generateCustomTx( 81 | victims[0].wallet.address, 82 | (amount = 100), 83 | (timestamp = 10), 84 | evilNode, 85 | ); 86 | const fakeEarlySpend = generateCustomTx( 87 | victims[1].wallet.address, 88 | (amount = 100), 89 | (timestamp = 0), 90 | evilNode, 91 | ); 92 | network.broadcast(evilNode.pid, spend); 93 | // Now run the network until an invalid spend is detected. 94 | // We will also detect if the two victim nodes, for a short time, both believe they have been sent money 95 | // by our evil node. That's our double spend! 96 | try { 97 | network.run((steps = 100)); 98 | } catch (e) { 99 | console.log('err:', e); 100 | for (let i = 0; i < numNodes; i++) { 101 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 102 | console.log(nodes[i].state); 103 | } 104 | } 105 | console.log('all nodes stayed in consensus!'); 106 | -------------------------------------------------------------------------------- /ch2/2.3/timestampTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimFaultTolerant.js'); 3 | const FaultTolerant = require('./faultTolerant'); 4 | const { getTxHash } = require('../nodeAgent'); 5 | 6 | class TimestampSimulator extends NetworkSimulator { 7 | constructor(latency, packetLoss) { 8 | super(latency, packetLoss); 9 | } 10 | 11 | tick() { 12 | // call NetworkSimulator tick() 13 | super.tick(); 14 | 15 | // at the 5th timestep try faking an early spend 16 | if (this.time === 5) { 17 | network.broadcast(evilNode.pid, fakeEarlySpend); 18 | } 19 | } 20 | } 21 | // ****** Test this out using a simulated network ****** // 22 | const numNodes = 5; 23 | const wallets = []; 24 | const genesis = {}; 25 | const network = new TimestampSimulator((latency = 5), (packetLoss = 0)); 26 | for (let i = 0; i < numNodes; i++) { 27 | // Create new identity 28 | wallets.push(EthCrypto.createIdentity()); 29 | // Add that node to our genesis block & give them an allocation 30 | genesis[wallets[i].address] = { 31 | balance: 0, 32 | nonce: 0, 33 | }; 34 | } 35 | // Give wallet 0 some money at genesis 36 | genesis[wallets[0].address] = { 37 | balance: 100, 38 | nonce: 0, 39 | }; 40 | // init nodes 41 | const nodes = []; 42 | // Create new nodes based on our wallets, and connect them to the network 43 | for (let i = 0; i < numNodes; i++) { 44 | nodes.push( 45 | // push a new fault tolerant node to the network 46 | new FaultTolerant( 47 | wallets[i], 48 | JSON.parse(JSON.stringify(genesis)), 49 | network, 50 | (delta = 5), 51 | ), 52 | ); 53 | // Connect everyone to everyone 54 | network.connectPeer(nodes[i], (numConnections = 3)); 55 | } 56 | 57 | // Attempt double spend 58 | const evilNode = nodes[0]; 59 | const victims = [ 60 | network.peers[evilNode.pid][0], 61 | network.peers[evilNode.pid][1], 62 | ]; 63 | const generateCustomTx = (to, amount, timestamp, node) => { 64 | const unsignedTx = { 65 | type: 'send', 66 | amount, 67 | from: node.wallet.address, 68 | to, 69 | nonce: node.state[node.wallet.address].nonce, 70 | timestamp, 71 | }; 72 | const tx = { 73 | contents: unsignedTx, 74 | sigs: [], 75 | }; 76 | tx.sigs.push(EthCrypto.sign(node.wallet.privateKey, getTxHash(tx))); 77 | return tx; 78 | }; 79 | // TODO 80 | // create two transactions with the same amount, but with different timestamps 81 | // broadcast both transactions to the network at the same time 82 | 83 | // Now run the network until an invalid spend is detected. 84 | // We will also detect if the two victim nodes, for a short time, both believe they have been sent money by our evil node. That's our double spend! 85 | try { 86 | network.run((steps = 100)); 87 | } catch (e) { 88 | console.log('err:', e); 89 | for (let i = 0; i < numNodes; i++) { 90 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 91 | console.log(nodes[i].state); 92 | } 93 | } 94 | console.log('all nodes stayed in consensus!'); 95 | -------------------------------------------------------------------------------- /ch2/2.4/Authority.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const _ = require('lodash'); 3 | const NetworkSimulator = require('../networkSim'); 4 | const { Node, getTxHash } = require('../nodeAgent'); 5 | 6 | // Authority extends Node and provides functionality needed to receive, order, and broadcast transactions 7 | class Authority extends Node { 8 | constructor(wallet, genesis, network) { 9 | super(wallet, genesis, network); 10 | this.orderNonce = 0; 11 | } 12 | 13 | onReceive(tx) { 14 | if (this.transactions.includes(tx)) { 15 | return; 16 | } 17 | this.transactions.push(tx); 18 | this.applyTransaction(tx); 19 | this.applyInvalidNonceTxs(tx.contents.from); 20 | } 21 | 22 | // TODO 23 | // Order transactions and broadcast that ordering to the network 24 | orderAndBroadcast(tx) { 25 | // give the transactiona the latest nonce in the Authority node's state 26 | // increment the nonce 27 | // sign the transaction to give it "proof of authority" 28 | // add the signed transaction to the history 29 | // broadcast the transaction to the network 30 | } 31 | 32 | applyTransaction(tx) { 33 | // Check the from address matches the signature 34 | const slicedTx = { 35 | contents: tx.contents, 36 | sigs: [], 37 | }; 38 | const signer = EthCrypto.recover(tx.sigs[0], getTxHash(slicedTx)); 39 | if (signer !== tx.contents.from) { 40 | throw new Error('Invalid signature!'); 41 | } 42 | // If we don't have a record for this address, create one 43 | if (!(tx.contents.to in this.state)) { 44 | this.state[[tx.contents.to]] = { 45 | balance: 0, 46 | nonce: 0, 47 | }; 48 | } 49 | // Check that the nonce is correct for replay protection 50 | if (tx.contents.nonce > this.state[tx.contents.from].nonce) { 51 | if (!(tx.contents.from in this.invalidNonceTxs)) { 52 | this.invalidNonceTxs[tx.contents.from] = {}; 53 | } 54 | this.invalidNonceTxs[tx.contents.from][tx.contents.nonce] = tx; 55 | return; 56 | } 57 | if (tx.contents.nonce < this.state[tx.contents.from].nonce) { 58 | console.log('passed nonce tx rejected'); 59 | return; 60 | } 61 | if (tx.contents.type === 'send') { 62 | // Send coins 63 | if (this.state[[tx.contents.from]].balance - tx.contents.amount < 0) { 64 | throw new Error('Not enough money!'); 65 | } 66 | this.state[[tx.contents.from]].balance -= tx.contents.amount; 67 | this.state[[tx.contents.to]].balance += tx.contents.amount; 68 | // after applying, add orderNonce, sign, and broadcast 69 | this.orderAndBroadcast(tx); 70 | } else { 71 | throw new Error('Invalid transaction type!'); 72 | } 73 | this.state[[tx.contents.from]].nonce += 1; 74 | } 75 | } 76 | 77 | module.exports = Authority; 78 | -------------------------------------------------------------------------------- /ch2/2.4/PoAClient.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const _ = require('lodash'); 3 | const NetworkSimulator = require('../networkSim'); 4 | const { Node, getTxHash } = require('../nodeAgent'); 5 | 6 | class PoA extends Node { 7 | constructor(wallet, genesis, network, authority) { 8 | super(wallet, genesis, network); 9 | this.authority = authority; // Eth Address of the authority node 10 | this.orderNonce = 0; 11 | } 12 | 13 | onReceive(tx) { 14 | if (this.transactions.includes(tx)) { 15 | return; 16 | } 17 | this.transactions.push(tx); 18 | this.applyTransaction(tx); 19 | this.network.broadcast(this.pid, tx); 20 | this.applyInvalidNonceTxs(); 21 | } 22 | 23 | generateTx(to, amount) { 24 | const unsignedTx = { 25 | type: 'send', 26 | amount, 27 | from: this.wallet.address, 28 | to, 29 | nonce: this.state[this.wallet.address].nonce, 30 | }; 31 | const tx = { 32 | contents: unsignedTx, 33 | sigs: [], 34 | }; 35 | tx.sigs.push(EthCrypto.sign(this.wallet.privateKey, getTxHash(tx))); 36 | return tx; 37 | } 38 | 39 | applyInvalidNonceTxs() { 40 | if (this.orderNonce in this.invalidNonceTxs) { 41 | this.applyTransaction(this.invalidNonceTxs[this.orderNonce]); 42 | delete this.invalidNonceTxs[this.orderNonce - 1]; // -1 because we increment orderNonce in applyTransaction 43 | this.applyInvalidNonceTxs(); 44 | } 45 | } 46 | 47 | // TODO 48 | applyTransaction(tx) { 49 | // get the transaction from before the authority node added ordering and make a copy of it 50 | // delete the order nonce from the original transaction 51 | // clear the transaction signatures 52 | // get tx from before the auth node signed it 53 | // check the signer of the transaction and throw an error if the signature cannot be verified 54 | // check the autority for the network and throw an error if the transaction does not 55 | // Check that this is the next transaction in the Authority node's ordering 56 | // - hint: check if the nonce ordering is greater or less than it's supposed to be 57 | // if all checks pass... 58 | if (tx.contents.type === 'send') { 59 | // Send coins 60 | if (this.state[tx.contents.from].balance - tx.contents.amount < 0) { 61 | throw new Error('Not enough money!'); 62 | } 63 | this.state[tx.contents.from].balance -= tx.contents.amount; 64 | this.state[tx.contents.to].balance += tx.contents.amount; 65 | } else { 66 | throw new Error('Invalid transaction type!'); 67 | } 68 | // increment nonce 69 | this.state[tx.contents.from].nonce++; 70 | this.orderNonce++; 71 | } 72 | } 73 | 74 | module.exports = PoA; 75 | -------------------------------------------------------------------------------- /ch2/2.4/PoATest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimPoA'); 3 | const Authority = require('./Authority'); 4 | const PoAClient = require('./PoAClient'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 11 | for (let i = 0; i < numNodes + 1; i++) { 12 | // Create new identity 13 | wallets.push(EthCrypto.createIdentity()); 14 | // Add that node to our genesis block & give them an allocation 15 | genesis[wallets[i].address] = { 16 | balance: 100, 17 | nonce: 0, 18 | }; 19 | } 20 | const nodes = []; 21 | // Create new nodes based on our wallets, and connect them to the network 22 | const authority = new Authority( 23 | wallets[numNodes], 24 | JSON.parse(JSON.stringify(genesis)), 25 | network, 26 | ); 27 | for (let i = 0; i < numNodes; i++) { 28 | nodes.push( 29 | new PoAClient( 30 | wallets[i], 31 | JSON.parse(JSON.stringify(genesis)), 32 | network, 33 | authority.wallet.address, 34 | ), 35 | ); 36 | network.connectPeer(nodes[i], (numConnections = 2)); 37 | } 38 | nodes.push(authority); 39 | network.connectPeer(authority, (numConnections = 0)); 40 | for (let i = 0; i < network.agents.length; i++) { 41 | network.peers[authority.pid].push(network.agents[i]); 42 | } 43 | 44 | const getRandomReceiver = (address) => { 45 | // create array without this Node 46 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 47 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 48 | return randomNode.wallet.address; 49 | }; 50 | 51 | const tx = nodes[0].generateTx(getRandomReceiver(nodes[0].wallet.address), 10); 52 | // Broadcast this tx to the network 53 | nodes[0].network.broadcastTo(nodes[0].pid, authority, tx); 54 | 55 | try { 56 | network.run((steps = 30)); 57 | } catch (e) { 58 | console.log(e); 59 | for (let i = 0; i < numNodes; i++) { 60 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 61 | console.log(nodes[i].state); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ch2/2.4/README.md: -------------------------------------------------------------------------------- 1 | > The code challenges in this course build upon each other. It's highly recommended that you start from the beginning. If you haven't already, get started with our [Installation Instructions](https://cryptoeconomics.study/docs/en/sync/getting-started-development-setup). 2 | 3 | > ! The chapter 2 coding challenges use a network simulation. This is created by nodeAgent.js, networkSim.js, and netTest.js in the `ch2` directory. You do not need to understand how this network simulation works to complete the chapter two coding challenges, but we highly recommend it. The README in the `ch2` directory will give you a high level overview of the network demo. Each file also has detailed code comments explaining how each function works. 4 | 5 |
6 | 7 | ## Proof of Authority 8 | 9 | Proof of Authority networks are a hybrid between completely open public blockchains and completely private blockchains. This is possible thanks to permissioning. Anyone can participate in the network, but only certain nodes have the permission (authority) to process transactions and create state transitions (new blocks). This allows participants to verify that everything is working correctly, while also allowing for faster transaction processing because we just trust the validators rather than making them perform a complex algorithm to earn the right to be the next validator. 10 | 11 |
12 | 13 | ## Authority Client 14 | 15 | `Authority.js` extends the Node class to create a node that has the authority to, verify, order, and sign transactions as valid. 16 | ``` 17 | // TODO 18 | // Order transactions and broadcast that ordering to the network 19 | orderAndBroadcast(tx) { 20 | // give the transactiona the latest nonce in the Authority node's state 21 | // increment the nonce 22 | // sign the transaction to give it "proof of authority" 23 | // add the signed transaction to the history 24 | // broadcast the transaction to the network 25 | } 26 | ``` 27 | 28 |
29 | 30 | ## PoA Client 31 | 32 | `PoAClient.js` extends the Node class to create nodes that can participate in the Proof of Authority network. This means that PoA client nodes can send, receive, and verify transactions, but cannot order and then sign them as "verified" because they don't have the authority. 33 | ``` 34 | // TODO 35 | applyTransaction(tx) { 36 | // get the transaction from before the authority node added ordering and make a copy of it 37 | // delete the order nonce from the original transaction 38 | // clear the transaction signatures 39 | // get tx from before the auth node signed it 40 | // check the signer of the transaction and throw an error if the signature cannot be verified 41 | // check the autority for the network and throw an error if the transaction does not 42 | // Check that this is the next transaction in the Authority node's ordering 43 | // - hint: check if the nonce ordering is greater or less than it's supposed to be 44 | // if all checks pass... 45 | if (tx.contents.type === 'send') { 46 | // Send coins 47 | if (this.state[tx.contents.from].balance - tx.contents.amount < 0) { 48 | throw new Error('Not enough money!'); 49 | } 50 | this.state[tx.contents.from].balance -= tx.contents.amount; 51 | this.state[tx.contents.to].balance += tx.contents.amount; 52 | } else { 53 | throw new Error('Invalid transaction type!'); 54 | } 55 | // increment nonce 56 | this.state[tx.contents.from].nonce++; 57 | this.orderNonce++; 58 | } 59 | ``` 60 | 61 |
62 | 63 | ## Testing 64 | 65 | In this last section we will use a few tests to make sure that our Proof of Authority network is functioning as intended. 66 | - doubleSpendTest.js => same as usual 67 | - orderNonceTest.js => sends transactions out of order on purpose to check that our PoA nodes can correctly process and order them 68 | - PoATest.js => just send random tx to check basic PoA node functionality 69 | 70 | > As always, if you have questions or get stuck please hit up the community on the [forum!](https://forum.cryptoeconomics.study) 71 | 72 | -------------------------------------------------------------------------------- /ch2/2.4/doubleSpendTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimPoA'); 3 | const Authority = require('./Authority'); 4 | const PoAClient = require('./PoAClient'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 11 | for (let i = 0; i < numNodes + 1; i++) { 12 | // Create new identity 13 | wallets.push(EthCrypto.createIdentity()); 14 | // Add that node to our genesis block & give them an allocation 15 | genesis[wallets[i].address] = { 16 | balance: 100, 17 | nonce: 0, 18 | }; 19 | } 20 | const nodes = []; 21 | // Create new nodes based on our wallets, and connect them to the network 22 | const authority = new Authority( 23 | wallets[numNodes], 24 | JSON.parse(JSON.stringify(genesis)), 25 | network, 26 | ); 27 | for (let i = 0; i < numNodes; i++) { 28 | nodes.push( 29 | new PoAClient( 30 | wallets[i], 31 | JSON.parse(JSON.stringify(genesis)), 32 | network, 33 | authority.wallet.address, 34 | ), 35 | ); 36 | network.connectPeer(nodes[i], (numConnections = 2)); 37 | } 38 | nodes.push(authority); 39 | network.connectPeer(authority, (numConnections = 0)); 40 | for (let i = 0; i < network.agents.length; i++) { 41 | network.peers[authority.pid].push(network.agents[i]); 42 | } 43 | 44 | const getRandomReceiver = (address) => { 45 | // create array without this Node 46 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 47 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 48 | return randomNode.wallet.address; 49 | }; 50 | 51 | // Attempt double spend 52 | const evilNode = nodes[0]; 53 | const victims = [nodes[1], nodes[2]]; 54 | const spends = [ 55 | evilNode.generateTx(victims[0].wallet.address, (amount = 100)), 56 | evilNode.generateTx(victims[1].wallet.address, (amount = 100)), 57 | ]; 58 | console.log('transactions:', spends); 59 | // Broadcast the txs to the network 60 | nodes[0].network.broadcastTo(nodes[0].pid, authority, spends[0]); 61 | nodes[0].network.broadcastTo(nodes[0].pid, authority, spends[1]); 62 | 63 | try { 64 | network.run((steps = 30)); 65 | } catch (e) { 66 | console.log(e); 67 | for (let i = 0; i < numNodes; i++) { 68 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 69 | console.log(nodes[i].state); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ch2/2.4/networkSimPoA.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../networkSim'); 3 | 4 | class NetworkSimPoA extends NetworkSimulator { 5 | constructor(latency, packetLoss) { 6 | super(latency, packetLoss); 7 | } 8 | 9 | tick() { 10 | super.tick(); 11 | console.log( 12 | `~~~~~~~~~~~~~~~~~~~~~~~~~~~~time: ${this.time}~~~~~~~~~~~~~~~~~~~~~~~~`, 13 | ); 14 | for (let i = 0; i < this.agents.length; i++) { 15 | if (this.agents[i].constructor.name === 'Authority') { 16 | console.log('~~~~~~~~~ AUTHORITY', '~~~~~~~~~'); 17 | } else { 18 | console.log('~~~~~~~~~ Node', i, '~~~~~~~~~'); 19 | } 20 | console.log(this.agents[i].state); 21 | } 22 | } 23 | } 24 | 25 | module.exports = NetworkSimPoA; 26 | -------------------------------------------------------------------------------- /ch2/2.4/orderNonceTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimPoA'); 3 | const Authority = require('./Authority'); 4 | const { getTxHash } = require('../nodeAgent'); 5 | const PoAClient = require('./PoAClient'); 6 | 7 | // ****** Test this out using a simulated network ****** // 8 | const numNodes = 5; 9 | const wallets = []; 10 | const genesis = {}; 11 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 12 | for (let i = 0; i < numNodes + 1; i++) { 13 | // Create new identity 14 | wallets.push(EthCrypto.createIdentity()); 15 | // Add that node to our genesis block & give them an allocation 16 | genesis[wallets[i].address] = { 17 | balance: 100, 18 | nonce: 0, 19 | }; 20 | } 21 | const nodes = []; 22 | // Create new nodes based on our wallets, and connect them to the network 23 | const authority = new Authority( 24 | wallets[numNodes], 25 | JSON.parse(JSON.stringify(genesis)), 26 | network, 27 | ); 28 | for (let i = 0; i < numNodes; i++) { 29 | nodes.push( 30 | new PoAClient( 31 | wallets[i], 32 | JSON.parse(JSON.stringify(genesis)), 33 | network, 34 | authority.wallet.address, 35 | ), 36 | ); 37 | network.connectPeer(nodes[i], (numConnections = 2)); 38 | } 39 | nodes.push(authority); 40 | network.connectPeer(authority, (numConnections = 0)); 41 | for (let i = 0; i < network.agents.length; i++) { 42 | network.peers[authority.pid].push(network.agents[i]); 43 | } 44 | 45 | const getRandomReceiver = (address) => { 46 | // create array without this Node 47 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 48 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 49 | return randomNode.wallet.address; 50 | }; 51 | 52 | const generateCustomTx = (to, amount, nonce, node) => { 53 | const unsignedTx = { 54 | type: 'send', 55 | amount, 56 | from: node.wallet.address, 57 | to, 58 | nonce, 59 | }; 60 | const tx = { 61 | contents: unsignedTx, 62 | sigs: [], 63 | }; 64 | tx.sigs.push(EthCrypto.sign(node.wallet.privateKey, getTxHash(tx))); 65 | return tx; 66 | }; 67 | 68 | // Send 10 random txs 69 | // These will be ordered by nonce by Authority node, but will be received out of order by the PoAClients 70 | // This test checks that PoAClients reorder transactions correctly 71 | for (let i = 0; i < 10; i++) { 72 | const tx = generateCustomTx( 73 | getRandomReceiver(nodes[0].wallet.address), 74 | 10, 75 | i, 76 | nodes[0], 77 | ); 78 | nodes[0].network.broadcastTo(nodes[0].pid, authority, tx); 79 | } 80 | 81 | // Run the network simulation to test order nonce functionality 82 | try { 83 | network.run((steps = 100)); 84 | } catch (e) { 85 | console.log(e); 86 | for (let i = 0; i < numNodes; i++) { 87 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 88 | console.log(nodes[i].state); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ch2/2.4/solutions/Authority.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const _ = require('lodash'); 3 | const NetworkSimulator = require('../networkSim'); 4 | const { Node, getTxHash } = require('../../nodeAgent'); 5 | 6 | // Authority extends Node and provides functionality needed to receive, order, and broadcast transactions 7 | class Authority extends Node { 8 | constructor(wallet, genesis, network) { 9 | super(wallet, genesis, network); 10 | this.orderNonce = 0; 11 | } 12 | 13 | onReceive(tx) { 14 | if (this.transactions.includes(tx)) { 15 | return; 16 | } 17 | this.transactions.push(tx); 18 | this.applyTransaction(tx); 19 | this.applyInvalidNonceTxs(tx.contents.from); 20 | } 21 | 22 | // Order transactions and broadcast that ordering to the network 23 | orderAndBroadcast(tx) { 24 | // give the transactiona the latest nonce in the Authority node's state 25 | tx.contents.orderNonce = this.orderNonce; 26 | // increment the nonce 27 | this.orderNonce++; 28 | // sign the transaction to give it "proof of authority" 29 | const authSig = EthCrypto.sign(this.wallet.privateKey, getTxHash(tx)); 30 | // add the signed transaction to the history 31 | tx.sigs.push(authSig); 32 | // broadcast the transaction to the network 33 | this.network.broadcast(this.pid, tx); 34 | } 35 | 36 | applyTransaction(tx) { 37 | // Check the from address matches the signature 38 | const slicedTx = { 39 | contents: tx.contents, 40 | sigs: [], 41 | }; 42 | const signer = EthCrypto.recover(tx.sigs[0], getTxHash(slicedTx)); 43 | if (signer !== tx.contents.from) { 44 | throw new Error('Invalid signature!'); 45 | } 46 | // If we don't have a record for this address, create one 47 | if (!(tx.contents.to in this.state)) { 48 | this.state[[tx.contents.to]] = { 49 | balance: 0, 50 | nonce: 0, 51 | }; 52 | } 53 | // Check that the nonce is correct for replay protection 54 | if (tx.contents.nonce > this.state[tx.contents.from].nonce) { 55 | if (!(tx.contents.from in this.invalidNonceTxs)) { 56 | this.invalidNonceTxs[tx.contents.from] = {}; 57 | } 58 | this.invalidNonceTxs[tx.contents.from][tx.contents.nonce] = tx; 59 | return; 60 | } 61 | if (tx.contents.nonce < this.state[tx.contents.from].nonce) { 62 | console.log('passed nonce tx rejected'); 63 | return; 64 | } 65 | if (tx.contents.type === 'send') { 66 | // Send coins 67 | if (this.state[[tx.contents.from]].balance - tx.contents.amount < 0) { 68 | throw new Error('Not enough money!'); 69 | } 70 | this.state[[tx.contents.from]].balance -= tx.contents.amount; 71 | this.state[[tx.contents.to]].balance += tx.contents.amount; 72 | // after applying, add orderNonce, sign, and broadcast 73 | this.orderAndBroadcast(tx); 74 | } else { 75 | throw new Error('Invalid transaction type!'); 76 | } 77 | this.state[[tx.contents.from]].nonce += 1; 78 | } 79 | } 80 | 81 | module.exports = Authority; 82 | -------------------------------------------------------------------------------- /ch2/2.4/solutions/PoAClient.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const _ = require('lodash'); 3 | const NetworkSimulator = require('../../networkSim'); 4 | const { Node, getTxHash } = require('../../nodeAgent'); 5 | 6 | class PoA extends Node { 7 | constructor(wallet, genesis, network, authority) { 8 | super(wallet, genesis, network); 9 | this.authority = authority; // Eth Address of the authority node 10 | this.orderNonce = 0; 11 | } 12 | 13 | onReceive(tx) { 14 | if (this.transactions.includes(tx)) { 15 | return; 16 | } 17 | this.transactions.push(tx); 18 | this.applyTransaction(tx); 19 | this.network.broadcast(this.pid, tx); 20 | this.applyInvalidNonceTxs(); 21 | } 22 | 23 | generateTx(to, amount) { 24 | const unsignedTx = { 25 | type: 'send', 26 | amount, 27 | from: this.wallet.address, 28 | to, 29 | nonce: this.state[this.wallet.address].nonce, 30 | }; 31 | const tx = { 32 | contents: unsignedTx, 33 | sigs: [], 34 | }; 35 | tx.sigs.push(EthCrypto.sign(this.wallet.privateKey, getTxHash(tx))); 36 | return tx; 37 | } 38 | 39 | applyInvalidNonceTxs() { 40 | if (this.orderNonce in this.invalidNonceTxs) { 41 | this.applyTransaction(this.invalidNonceTxs[this.orderNonce]); 42 | delete this.invalidNonceTxs[this.orderNonce - 1]; // -1 because we increment orderNonce in applyTransaction 43 | this.applyInvalidNonceTxs(); 44 | } 45 | } 46 | 47 | applyTransaction(tx) { 48 | // get tx from before the authority node added ordering 49 | const originalTx = _.cloneDeep(tx); 50 | delete originalTx.contents.orderNonce; 51 | originalTx.sigs = []; 52 | // get tx from before the auth node signed it 53 | const slicedTx = { 54 | contents: tx.contents, 55 | sigs: tx.sigs.slice(0, 1), 56 | }; 57 | // check the signer of the transaction and throw an error if the signature cannot be verified 58 | const signer = EthCrypto.recover(tx.sigs[0], getTxHash(originalTx)); 59 | if (signer !== tx.contents.from) { 60 | throw new Error('Invalid signature!'); 61 | } 62 | // check the autority for the network and throw an error if the transaction does not 63 | const authority = EthCrypto.recover(tx.sigs[1], getTxHash(slicedTx)); 64 | if (authority !== this.authority) { 65 | throw new Error('Invalid signature!'); 66 | } 67 | // If we don't have a record for this address, create one 68 | if (!(tx.contents.to in this.state)) { 69 | this.state[tx.contents.to] = { 70 | balance: 0, 71 | nonce: 0, 72 | }; 73 | } 74 | // Check that this is the next transaction in the Authority node's ordering 75 | if (tx.contents.orderNonce > this.orderNonce) { 76 | this.invalidNonceTxs[tx.contents.orderNonce] = tx; 77 | return; 78 | } 79 | if (tx.contents.nonce < this.orderNonce) { 80 | console.log('passed nonce. tx rejected'); 81 | return; 82 | } 83 | // if all checks pass... 84 | if (tx.contents.type === 'send') { 85 | // Send coins 86 | if (this.state[tx.contents.from].balance - tx.contents.amount < 0) { 87 | throw new Error('Not enough money!'); 88 | } 89 | this.state[tx.contents.from].balance -= tx.contents.amount; 90 | this.state[tx.contents.to].balance += tx.contents.amount; 91 | } else { 92 | throw new Error('Invalid transaction type!'); 93 | } 94 | // increment nonce 95 | this.state[tx.contents.from].nonce++; 96 | this.orderNonce++; 97 | } 98 | } 99 | 100 | module.exports = PoA; 101 | -------------------------------------------------------------------------------- /ch2/2.4/solutions/PoATest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimPoA'); 3 | const Authority = require('./Authority'); 4 | const PoAClient = require('./PoAClient'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 11 | for (let i = 0; i < numNodes + 1; i++) { 12 | // Create new identity 13 | wallets.push(EthCrypto.createIdentity()); 14 | // Add that node to our genesis block & give them an allocation 15 | genesis[wallets[i].address] = { 16 | balance: 100, 17 | nonce: 0, 18 | }; 19 | } 20 | const nodes = []; 21 | // Create new nodes based on our wallets, and connect them to the network 22 | const authority = new Authority( 23 | wallets[numNodes], 24 | JSON.parse(JSON.stringify(genesis)), 25 | network, 26 | ); 27 | for (let i = 0; i < numNodes; i++) { 28 | nodes.push( 29 | new PoAClient( 30 | wallets[i], 31 | JSON.parse(JSON.stringify(genesis)), 32 | network, 33 | authority.wallet.address, 34 | ), 35 | ); 36 | network.connectPeer(nodes[i], (numConnections = 2)); 37 | } 38 | nodes.push(authority); 39 | network.connectPeer(authority, (numConnections = 0)); 40 | for (let i = 0; i < network.agents.length; i++) { 41 | network.peers[authority.pid].push(network.agents[i]); 42 | } 43 | 44 | const getRandomReceiver = (address) => { 45 | // create array without this Node 46 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 47 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 48 | return randomNode.wallet.address; 49 | }; 50 | 51 | const tx = nodes[0].generateTx(getRandomReceiver(nodes[0].wallet.address), 10); 52 | // Broadcast this tx to the network 53 | nodes[0].network.broadcastTo(nodes[0].pid, authority, tx); 54 | 55 | try { 56 | network.run((steps = 30)); 57 | } catch (e) { 58 | console.log(e); 59 | for (let i = 0; i < numNodes; i++) { 60 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 61 | console.log(nodes[i].state); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ch2/2.4/solutions/doubleSpendTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimPoA'); 3 | const Authority = require('./Authority'); 4 | const PoAClient = require('./PoAClient'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 11 | for (let i = 0; i < numNodes + 1; i++) { 12 | // Create new identity 13 | wallets.push(EthCrypto.createIdentity()); 14 | // Add that node to our genesis block & give them an allocation 15 | genesis[wallets[i].address] = { 16 | balance: 100, 17 | nonce: 0, 18 | }; 19 | } 20 | const nodes = []; 21 | // Create new nodes based on our wallets, and connect them to the network 22 | const authority = new Authority( 23 | wallets[numNodes], 24 | JSON.parse(JSON.stringify(genesis)), 25 | network, 26 | ); 27 | for (let i = 0; i < numNodes; i++) { 28 | nodes.push( 29 | new PoAClient( 30 | wallets[i], 31 | JSON.parse(JSON.stringify(genesis)), 32 | network, 33 | authority.wallet.address, 34 | ), 35 | ); 36 | network.connectPeer(nodes[i], (numConnections = 2)); 37 | } 38 | nodes.push(authority); 39 | network.connectPeer(authority, (numConnections = 0)); 40 | for (let i = 0; i < network.agents.length; i++) { 41 | network.peers[authority.pid].push(network.agents[i]); 42 | } 43 | 44 | const getRandomReceiver = (address) => { 45 | // create array without this Node 46 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 47 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 48 | return randomNode.wallet.address; 49 | }; 50 | 51 | // Attempt double spend 52 | const evilNode = nodes[0]; 53 | const victims = [nodes[1], nodes[2]]; 54 | const spends = [ 55 | evilNode.generateTx(victims[0].wallet.address, (amount = 100)), 56 | evilNode.generateTx(victims[1].wallet.address, (amount = 100)), 57 | ]; 58 | console.log('transactions:', spends); 59 | // Broadcast the txs to the network 60 | nodes[0].network.broadcastTo(nodes[0].pid, authority, spends[0]); 61 | nodes[0].network.broadcastTo(nodes[0].pid, authority, spends[1]); 62 | 63 | try { 64 | network.run((steps = 30)); 65 | } catch (e) { 66 | console.log(e); 67 | for (let i = 0; i < numNodes; i++) { 68 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 69 | console.log(nodes[i].state); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ch2/2.4/solutions/networkSimPoA.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../networkSim'); 3 | 4 | class NetworkSimPoA extends NetworkSimulator { 5 | constructor(latency, packetLoss) { 6 | super(latency, packetLoss); 7 | } 8 | 9 | tick() { 10 | super.tick(); 11 | console.log( 12 | `~~~~~~~~~~~~~~~~~~~~~~~~~~~~time: ${this.time}~~~~~~~~~~~~~~~~~~~~~~~~`, 13 | ); 14 | for (let i = 0; i < this.agents.length; i++) { 15 | if (this.agents[i].constructor.name === 'Authority') { 16 | console.log('~~~~~~~~~ AUTHORITY', '~~~~~~~~~'); 17 | } else { 18 | console.log('~~~~~~~~~ Node', i, '~~~~~~~~~'); 19 | } 20 | console.log(this.agents[i].state); 21 | } 22 | } 23 | } 24 | 25 | module.exports = NetworkSimPoA; 26 | -------------------------------------------------------------------------------- /ch2/2.4/solutions/orderNonceTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('./networkSimPoA'); 3 | const Authority = require('./Authority'); 4 | const { getTxHash } = require('../../nodeAgent'); 5 | const PoAClient = require('./PoAClient'); 6 | 7 | // ****** Test this out using a simulated network ****** // 8 | const numNodes = 5; 9 | const wallets = []; 10 | const genesis = {}; 11 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 12 | for (let i = 0; i < numNodes + 1; i++) { 13 | // Create new identity 14 | wallets.push(EthCrypto.createIdentity()); 15 | // Add that node to our genesis block & give them an allocation 16 | genesis[wallets[i].address] = { 17 | balance: 100, 18 | nonce: 0, 19 | }; 20 | } 21 | const nodes = []; 22 | // Create new nodes based on our wallets, and connect them to the network 23 | const authority = new Authority( 24 | wallets[numNodes], 25 | JSON.parse(JSON.stringify(genesis)), 26 | network, 27 | ); 28 | for (let i = 0; i < numNodes; i++) { 29 | nodes.push( 30 | new PoAClient( 31 | wallets[i], 32 | JSON.parse(JSON.stringify(genesis)), 33 | network, 34 | authority.wallet.address, 35 | ), 36 | ); 37 | network.connectPeer(nodes[i], (numConnections = 2)); 38 | } 39 | nodes.push(authority); 40 | network.connectPeer(authority, (numConnections = 0)); 41 | for (let i = 0; i < network.agents.length; i++) { 42 | network.peers[authority.pid].push(network.agents[i]); 43 | } 44 | 45 | const getRandomReceiver = (address) => { 46 | // create array without this Node 47 | const otherNodes = nodes.filter(n => n.wallet.address !== address); 48 | const randomNode = otherNodes[Math.floor(Math.random() * otherNodes.length)]; 49 | return randomNode.wallet.address; 50 | }; 51 | 52 | const generateCustomTx = (to, amount, nonce, node) => { 53 | const unsignedTx = { 54 | type: 'send', 55 | amount, 56 | from: node.wallet.address, 57 | to, 58 | nonce, 59 | }; 60 | const tx = { 61 | contents: unsignedTx, 62 | sigs: [], 63 | }; 64 | tx.sigs.push(EthCrypto.sign(node.wallet.privateKey, getTxHash(tx))); 65 | return tx; 66 | }; 67 | 68 | // Send 10 random txs 69 | // These will be ordered by nonce by Authority node, but will be received out of order by the PoAClients 70 | // This test checks that PoAClients reorder transactions correctly 71 | for (let i = 0; i < 10; i++) { 72 | const tx = generateCustomTx( 73 | getRandomReceiver(nodes[0].wallet.address), 74 | 10, 75 | i, 76 | nodes[0], 77 | ); 78 | nodes[0].network.broadcastTo(nodes[0].pid, authority, tx); 79 | } 80 | 81 | // Run the network simulation to test order nonce functionality 82 | try { 83 | network.run((steps = 100)); 84 | } catch (e) { 85 | console.log(e); 86 | for (let i = 0; i < numNodes; i++) { 87 | console.log('~~~~~~~~~~~ Node', i, '~~~~~~~~~~~'); 88 | console.log(nodes[i].state); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ch2/netTest.js: -------------------------------------------------------------------------------- 1 | const NetworkSimulator = require('./networkSim'); 2 | 3 | // netTest.js creates testing agents using the Node class from nodeAgent.js 4 | // then connects those agents to a network simulation created by networkSim.js 5 | 6 | // Some testing agents (users) for our network simulation 7 | const testAgents = [ 8 | { 9 | pid: 'karl', 10 | onReceive(message) { 11 | console.log(this.pid, 'got', message); 12 | }, 13 | tick() {}, 14 | }, 15 | { 16 | pid: 'aparna', 17 | onReceive(message) { 18 | console.log(this.pid, 'got', message); 19 | }, 20 | tick() {}, 21 | }, 22 | { 23 | pid: 'jing', 24 | onReceive(message) { 25 | console.log(this.pid, 'got', message); 26 | }, 27 | tick() {}, 28 | }, 29 | { 30 | pid: 'bob', 31 | onReceive(message) { 32 | console.log(this.pid, 'got', message); 33 | }, 34 | tick() {}, 35 | }, 36 | { 37 | pid: 'phil', 38 | onReceive(message) { 39 | console.log(this.pid, 'got', message); 40 | }, 41 | tick() {}, 42 | }, 43 | { 44 | pid: 'vitalik', 45 | onReceive(message) { 46 | console.log(this.pid, 'got', message); 47 | }, 48 | tick() {}, 49 | }, 50 | ]; 51 | 52 | // Network testing parameters 53 | // initialize the network 54 | const network = new NetworkSimulator((latency = 5), (packetLoss = 0)); 55 | // connect the testing agents to the network 56 | for (const a of testAgents) { 57 | network.connectPeer(a, 1); 58 | } 59 | // test a broadcast to make sure the agents are connected 60 | network.broadcast('karl', 'testing!'); 61 | network.broadcast('aparna', 'testing!'); 62 | // log the state of the network test to the console 63 | console.log(network); 64 | // run the network 65 | network.run(100); 66 | -------------------------------------------------------------------------------- /ch2/networkSim.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const d3 = require('d3-random'); 3 | const seedrandom = require('seedrandom'); 4 | 5 | // Create a network simulation 6 | class NetworkSimulator { 7 | // the global network parameters 8 | constructor(latency, packetLoss) { 9 | const normalRandom = d3.randomNormal.source(seedrandom('a22ebc7c488a3a47'))( 10 | latency, 11 | (latency * 2) / 5, 12 | ); 13 | this.agents = []; 14 | this.latencyDistribution = () => Math.floor(Math.max(normalRandom(), 0)); 15 | this.time = 0; 16 | this.messageQueue = {}; 17 | this.peers = {}; 18 | this.packetLoss = packetLoss; 19 | } 20 | 21 | // connect new peers (Nodes) to the network 22 | connectPeer(newPeer, numConnections) { 23 | newPeer.network = this; 24 | const shuffledAgents = _.shuffle(this.agents); 25 | this.agents.push(newPeer); 26 | this.peers[newPeer.pid] = []; 27 | for (const a of shuffledAgents.slice(0, numConnections)) { 28 | this.peers[newPeer.pid].push(a); 29 | this.peers[a.pid].push(newPeer); 30 | } 31 | } 32 | 33 | // broadcast messages from a node to the rest of the network 34 | broadcast(sender, message) { 35 | for (const pid of this.peers[sender]) { 36 | this.broadcastTo(sender, pid, message); 37 | } 38 | } 39 | 40 | // broadcast a message from one node to another node 41 | broadcastTo(sender, receiver, message) { 42 | const recvTime = this.time + this.latencyDistribution(); 43 | if (!(recvTime in this.messageQueue)) { 44 | this.messageQueue[recvTime] = []; 45 | } 46 | this.messageQueue[recvTime].push({ recipient: receiver, message }); 47 | } 48 | 49 | // simulate message broadcasting (transactions) on the network and introduce random latency 50 | tick() { 51 | if (this.time in this.messageQueue) { 52 | for (const { recipient, message } of this.messageQueue[this.time]) { 53 | if (Math.random() > this.packetLoss) { 54 | recipient.onReceive(message); 55 | } 56 | } 57 | delete this.messageQueue[this.time]; 58 | } 59 | for (const a of this.agents) { 60 | a.tick(); 61 | } 62 | this.time += 1; 63 | } 64 | 65 | // how many steps (iterations) to run the network simulation with latency 66 | run(steps) { 67 | for (let i = 0; i < steps; i++) { 68 | this.tick(); 69 | } 70 | } 71 | } 72 | 73 | module.exports = NetworkSimulator; 74 | -------------------------------------------------------------------------------- /ch3/.networksim.js.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptoeconomics-study/code/4eacdac17890917d8129dc804483490a027a03b4/ch3/.networksim.js.swp -------------------------------------------------------------------------------- /ch3/3.1/PoWClient.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const hexToBinary = require('hex-to-binary'); 3 | const { Node, getTxHash } = require('../nodeAgent'); 4 | 5 | // Add Client class which generates & sends transactions 6 | class Client extends Node { 7 | constructor(wallet, genesis, network) { 8 | super(wallet, genesis, network); 9 | this.blockchain = []; // longest chain 10 | this.allBlocks = []; // all blocks 11 | this.blockNumber = 0; // keep track of blocks added to blockchain despite getState() 12 | this.difficulty = 13; 13 | 14 | const genesisBlock = { 15 | nonce: 0, 16 | number: 0, 17 | coinbase: 0, 18 | difficulty: 9000, 19 | parentHash: 0, 20 | timestamp: 0, 21 | contents: { 22 | type: 'block', 23 | txList: [], 24 | }, 25 | }; 26 | this.blockchain.push(genesisBlock); 27 | this.allBlocks.push(genesisBlock); 28 | } 29 | 30 | // Check if a message is a transaction or a block 31 | onReceive(message) { 32 | // check the message.contents.type 33 | // if it's 'send', receiveTx(message) 34 | // if it's 'block', receiveBlock(message) 35 | } 36 | 37 | // Process an incoming transaction 38 | receiveTx(tx) { 39 | // if we already have the transaction in our state, return to do nothing 40 | // add the transaction to the pending transaction pool (this is often called the mempool) 41 | // broadcast the transaction to the res of the network 42 | } 43 | 44 | // Check the hash of an incoming block 45 | isValidBlockHash(block) { 46 | // hash the block 47 | // convert the hex string to binary 48 | // check how many leading zeros the hash has 49 | // compare the amount of leading zeros in the block hash to the network difficulty and return a boolean if they match 50 | } 51 | 52 | // Processing the transactions in a block 53 | applyBlock(block) { 54 | // get all the transactions in block.contents 55 | // for every transaction in the transaction list 56 | // process the transaction to update our view of the state 57 | // if the transaction does not come from the 0 address (which is a mint transaction for miners and has no sender) 58 | // check any pending transactions with invalid nonces to see if they are now valid 59 | } 60 | 61 | // Update the state with transactions which are contained in the longest chain and return the resulting state object (this process is often referred to as the "fork choice" rule) 62 | updateState() { 63 | // create an array to represent a temp chain 64 | // create a variable to represent all the blocks that we have already processed 65 | // find the highest block number in all the blocks 66 | // add the highestBlockNumber to tempChain using blockNumber 67 | // add max number of blocks to tempChain using parentHash 68 | // save the ordered sequence 69 | // apply all txs from ordered list of blocks 70 | // return the new state 71 | } 72 | 73 | // Receiving a block, making sure it's valid, and then processing it 74 | receiveBlock(block) { 75 | // if we've already seen the block return to do nothing 76 | // if the blockhash is not valid return to do nothing 77 | // if checks pass, add block to all blocks received 78 | // if the block builds directly on the current head of the chain, append to chain 79 | // incriment the block number 80 | // add the block to our view of the blockchain 81 | // process the block 82 | // update our state with the new block 83 | // broadcast the block to the network 84 | } 85 | } 86 | 87 | module.exports = Client; 88 | -------------------------------------------------------------------------------- /ch3/3.1/test/PoWAllMinerTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../../networksim'); 3 | const Miner = require('./PoWMiner'); 4 | const { getTxHash } = require('../../nodeAgent'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const latency = 15; // 10-15 ticks per message 11 | const packetLoss = 0; 12 | const network = new NetworkSimulator(latency, packetLoss); 13 | for (let i = 0; i < numNodes; i++) { 14 | wallets.push(EthCrypto.createIdentity()); 15 | genesis[wallets[i].address] = { 16 | balance: 0, 17 | nonce: 0, 18 | }; 19 | } 20 | // Give wallet 0 some money at genesis 21 | genesis[wallets[0].address] = { 22 | balance: 100, 23 | nonce: 0, 24 | }; 25 | 26 | const nodes = []; 27 | // Create new nodes based on our wallets, and connect them to the network 28 | for (let i = 0; i < numNodes; i++) { 29 | nodes.push( 30 | new Miner(wallets[i], JSON.parse(JSON.stringify(genesis)), network), 31 | ); 32 | network.connectPeer(nodes[i], 3); 33 | } 34 | 35 | const tx0 = nodes[0].generateTx(nodes[1].wallet.address, 10); 36 | network.broadcast(nodes[0].pid, tx0); 37 | console.log('sent tx0'); 38 | for (let i = 0; i < 800; i++) { 39 | network.tick(); 40 | } 41 | const tx1 = nodes[0].generateTx(nodes[2].wallet.address, 5); 42 | network.broadcast(nodes[0].pid, tx1); 43 | console.log('sent tx1'); 44 | for (let i = 0; i < 800; i++) { 45 | network.tick(); 46 | } 47 | const tx2 = nodes[0].generateTx(nodes[3].wallet.address, 6); 48 | network.broadcast(nodes[0].pid, tx2); 49 | console.log('sent tx2'); 50 | for (let i = 0; i < 800; i++) { 51 | network.tick(); 52 | } 53 | const tx3 = nodes[1].generateTx(nodes[4].wallet.address, 3); 54 | network.broadcast(nodes[1].pid, tx3); 55 | console.log('sent tx3'); 56 | for (let i = 0; i < 1500; i++) { 57 | network.tick(); 58 | } 59 | 60 | for (let i = 0; i < numNodes; i++) { 61 | console.log( 62 | 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 63 | ); 64 | console.log('node: ', nodes[i].p2pNodeId.address); 65 | // console.log('my chain',nodes[i].blockchain) 66 | // console.log('all blocks',nodes[i].allBlocks) 67 | console.log('chain len', nodes[i].blockchain.length); 68 | for (let j = 0; j < nodes[i].blockchain.length; j++) { 69 | const block = nodes[i].blockchain[j]; 70 | console.log('block ', block.number, ':', getTxHash(block)); 71 | if (nodes[i].blockchain[j].contents.txList.length) { 72 | console.log('tx:', nodes[i].blockchain[j].contents.txList[0].contents); 73 | } 74 | } 75 | console.log('node state: ', nodes[i].state); 76 | } 77 | -------------------------------------------------------------------------------- /ch3/3.1/test/PoWMiner.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('../PoWClient'); 3 | const { getTxHash } = require('../../nodeAgent'); 4 | 5 | class Miner extends Client { 6 | constructor(wallet, genesis, network) { 7 | super(wallet, genesis, network); 8 | this.blockAttempt = this.createBlock(); 9 | this.hashRate = 5; // hashRate = # hashes miner can try at each tick 10 | } 11 | 12 | tick() { 13 | // Mining 14 | for (let i = 0; i < this.hashRate; i++) { 15 | if (this.isValidBlockHash(this.blockAttempt)) { 16 | const blockHash = getTxHash(this.blockAttempt); 17 | console.log( 18 | this.pid.substring(0, 6), 19 | 'found a block:', 20 | blockHash.substring(0, 10), 21 | 'at height', 22 | this.blockAttempt.number, 23 | ); 24 | const validBlock = this.blockAttempt; 25 | this.receiveBlock(validBlock); 26 | return; 27 | } 28 | this.blockAttempt.nonce++; 29 | } 30 | } 31 | 32 | receiveBlock(block) { 33 | const { parentHash } = this.blockAttempt; 34 | super.receiveBlock(block); 35 | const newHead = this.blockchain.slice(-1)[0]; 36 | // if the block head has changed, mine on top of the new head 37 | if (getTxHash(newHead) !== parentHash) { 38 | this.blockAttempt = this.createBlock(); // Start mining new block 39 | } 40 | } 41 | 42 | createBlock() { 43 | const tx = this.transactions.shift(); 44 | const txList = []; 45 | if (tx) txList.push(tx); 46 | 47 | const timestamp = Math.round(new Date().getTime() / 1000); 48 | const newBlock = { 49 | nonce: 0, 50 | number: this.blockNumber + 1, 51 | coinbase: this.wallet.address, 52 | difficulty: this.difficulty, 53 | parentHash: getTxHash(this.blockchain.slice(-1)[0]), 54 | timestamp, 55 | contents: { 56 | type: 'block', 57 | txList, 58 | }, 59 | }; 60 | return newBlock; 61 | } 62 | } 63 | 64 | module.exports = Miner; 65 | -------------------------------------------------------------------------------- /ch3/3.1/test/PoWSingleMinerTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../networksim'); 3 | const Miner = require('./PoWMiner'); 4 | const Client = require('../PoWClient'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const latency = 15; // 10-15 ticks per message 11 | const packetLoss = 0; 12 | const network = new NetworkSimulator(latency, packetLoss); 13 | for (let i = 0; i < numNodes; i++) { 14 | wallets.push(EthCrypto.createIdentity()); 15 | genesis[wallets[i].address] = { 16 | balance: 0, 17 | nonce: 0, 18 | }; 19 | } 20 | // Give wallet 0 some money at genesis 21 | genesis[wallets[0].address] = { 22 | balance: 100, 23 | nonce: 0, 24 | }; 25 | 26 | const nodes = []; 27 | // Create new nodes based on our wallets, and connect them to the network 28 | for (let i = 0; i < numNodes - 1; i++) { 29 | nodes.push( 30 | new Client(wallets[i], JSON.parse(JSON.stringify(genesis)), network), 31 | ); 32 | network.connectPeer(nodes[i], 3); 33 | } 34 | nodes.push( 35 | new Miner(wallets[numNodes - 1], JSON.parse(JSON.stringify(genesis)), network), 36 | ); 37 | network.connectPeer(nodes[numNodes - 1], 3); 38 | 39 | const tx0 = nodes[0].generateTx(nodes[1].wallet.address, 10); 40 | network.broadcast(nodes[0].pid, tx0); 41 | console.log('sent tx0'); 42 | for (let i = 0; i < 800; i++) { 43 | network.tick(); 44 | } 45 | const tx1 = nodes[0].generateTx(nodes[2].wallet.address, 5); 46 | network.broadcast(nodes[0].pid, tx1); 47 | console.log('sent tx1'); 48 | for (let i = 0; i < 800; i++) { 49 | network.tick(); 50 | } 51 | const tx2 = nodes[0].generateTx(nodes[3].wallet.address, 6); 52 | network.broadcast(nodes[0].pid, tx2); 53 | console.log('sent tx2'); 54 | for (let i = 0; i < 800; i++) { 55 | network.tick(); 56 | } 57 | const tx3 = nodes[1].generateTx(nodes[4].wallet.address, 3); 58 | network.broadcast(nodes[1].pid, tx3); 59 | console.log('sent tx3'); 60 | for (let i = 0; i < 1500; i++) { 61 | network.tick(); 62 | } 63 | 64 | for (let i = 0; i < numNodes; i++) { 65 | console.log('node: ', nodes[i].p2pNodeId.address); 66 | // console.log('my chain',nodes[i].blockchain) 67 | // console.log('all blocks',nodes[i].allBlocks) 68 | console.log('chain len', nodes[i].blockchain.length); 69 | for (let j = 0; j < nodes[i].blockchain.length; j++) { 70 | console.log('block number', nodes[i].blockchain[j].number); 71 | console.log('block parentHash', nodes[i].blockchain[j].parentHash); 72 | console.log('txs:', nodes[i].blockchain[j].contents.txList); 73 | } 74 | console.log('node state: ', nodes[i].state); 75 | // console.log('invalidNonceTxs: ', nodes[i].invalidNonceTxs) 76 | console.log('xxxxxxxxx0xxxxxxxxx'); 77 | // nodes[i].getState() 78 | // console.log('new node state: ', nodes[i].state) 79 | } 80 | -------------------------------------------------------------------------------- /ch3/3.1/test/test.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const hexToBinary = require('hex-to-binary'); 3 | const Client = require('PoWClient'); 4 | const { Node, getTxHash } = require('../nodeAgent'); 5 | 6 | // test the PoWClient 7 | describe('PoWClient Tests', () => { 8 | // testing onReceive() to process incoming messages 9 | describe('onReceive()', () => { 10 | it('should run receiveTx() because the message is a transaction', () => { 11 | const testingClient = new Client(); 12 | const message = 'tbd'; 13 | assert.equal(true, testingClient.receiveTx(message)); 14 | }); 15 | }); 16 | // testing receiveTx() to process incoming transactions 17 | describe('receiveTx()', () => {}); 18 | // testing receiveBlock() to process incoming blocks 19 | describe('receiveBlock()', () => { 20 | // testing isValidBlockHash() to check the hash of the block meets the network's difficulty requirements 21 | describe('isValidBlockHash()', () => {}); 22 | // testing applyBlock() to process a block and update the state 23 | describe('applyBlock()', () => {}); 24 | }); 25 | // testing updateState() to choose the longest chain 26 | describe('updateState()', () => {}); 27 | }); 28 | -------------------------------------------------------------------------------- /ch3/3.2/PoWClient.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const hexToBinary = require('hex-to-binary'); 3 | const { Node, getTxHash } = require('../nodeAgent'); 4 | 5 | // Add Client class which generates & sends transactions 6 | class Client extends Node { 7 | constructor(wallet, genesis, network) { 8 | super(wallet, genesis, network); 9 | this.blockchain = []; // longest chain 10 | this.allBlocks = []; // all blocks 11 | this.blockNumber = 0; // keep track of blocks added to blockchain despite getState() 12 | this.difficulty = 13; 13 | 14 | const genesisBlock = { 15 | nonce: 0, 16 | number: 0, 17 | coinbase: 0, 18 | difficulty: 9000, 19 | parentHash: 0, 20 | timestamp: 0, 21 | contents: { 22 | type: 'block', 23 | txList: [], 24 | }, 25 | }; 26 | this.blockchain.push(genesisBlock); 27 | this.allBlocks.push(genesisBlock); 28 | } 29 | 30 | onReceive(message) { 31 | switch (message.contents.type) { 32 | case 'send': 33 | this.receiveTx(message); 34 | break; 35 | case 'block': 36 | this.receiveBlock(message); 37 | break; 38 | } 39 | } 40 | 41 | receiveTx(tx) { 42 | if (this.transactions.includes(tx)) return; 43 | this.transactions.push(tx); // add tx to mempool 44 | this.network.broadcast(this.pid, tx); 45 | } 46 | 47 | isValidBlockHash(block) { 48 | const blockHash = getTxHash(block); 49 | // Found block 50 | const binBlockHash = hexToBinary(blockHash.substr(2)); 51 | const leadingZeros = parseInt(binBlockHash.substring(0, this.difficulty)); 52 | return leadingZeros === 0; 53 | } 54 | 55 | receiveBlock(block) { 56 | if (this.allBlocks.includes(block)) return; 57 | if (!this.isValidBlockHash(block)) return; 58 | this.allBlocks.push(block); // add block to all blocks received 59 | // if the block builds directly on the current head of the chain, append to chain 60 | if (block.parentHash === getTxHash(this.blockchain.slice(-1)[0])) { 61 | this.blockNumber++; // increment 62 | this.blockchain.push(block); // add block to own blockchain 63 | this.applyBlock(block); 64 | } else { 65 | this.allBlocks.push(block); 66 | this.updateState(); // check if blockchain is the longest sequence 67 | } 68 | this.network.broadcast(this.pid, block); // broadcast new block to network 69 | } 70 | 71 | // Fork choice 72 | // Only apply transactions which are contained in the longest chain 73 | // and returns the resulting state object. 74 | updateState() { 75 | // a temp chain 76 | const tempChain = []; 77 | const { allBlocks } = this; 78 | // return max blocknumber from allBlocks 79 | const max = Math.max.apply(Math, allBlocks.map(block => block.number)); 80 | // add the highestBlockNumber to tempChain using blockNumber 81 | for (const block of allBlocks) { 82 | // TODO optimize this...Once there are many blocks in allBlocks, this may be SLOW af 83 | if (block.number === max) { 84 | tempChain.push(block); 85 | break; 86 | } 87 | } 88 | // add max number of blocks to tempChain using parentHash 89 | // i is the current block number 90 | for (let i = max; i > 0; i--) { 91 | const prevHash = tempChain[0].parentHash; 92 | for (const block of allBlocks) { 93 | if (getTxHash(block) === prevHash) { 94 | // TODO verify blockhash before adding to allBlocks 95 | tempChain.unshift(block); // add block to front of array 96 | break; 97 | } 98 | } 99 | } 100 | // save the ordered sequence 101 | this.blockchain = tempChain; 102 | // apply all txs from ordered list of blocks 103 | for (const block of this.blockchain) { 104 | this.applyBlock(block); 105 | } 106 | return this.state; 107 | } 108 | 109 | applyBlock(block) { 110 | const { txList } = block.contents; 111 | for (const tx of txList) { 112 | this.applyTransaction(tx); // update state 113 | if (tx.contents.from !== 0) { 114 | // mint tx is excluded 115 | this.applyInvalidNonceTxs(tx.contents.from); // update state 116 | } 117 | } 118 | } 119 | } 120 | 121 | module.exports = Client; 122 | -------------------------------------------------------------------------------- /ch3/3.2/PoWMiner.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./PoWClient'); 3 | const { getTxHash } = require('../nodeAgent'); 4 | 5 | // This Miner class will extend the client we created in 3.1 6 | class Miner extends Client { 7 | constructor(wallet, genesis, network) { 8 | super(wallet, genesis, network); 9 | // create a block in the constructor so that we have something to start mining on 10 | this.blockAttempt = this.createBlock(); 11 | // hashRate = # hashes miner can try at each tick 12 | this.hashRate = 5; 13 | } 14 | 15 | // Create a new block 16 | createBlock() { 17 | // get all the transactions for this block 18 | // get the timestamp 19 | // create a new block 20 | // - nonce 21 | // - block number 22 | // - give ourselves the coinbase reward for finding the block 23 | // - log the difficulty of the network 24 | // - show the hash of the block that matches the network difficulty 25 | // - timestamp 26 | // - transactions 27 | // return the block 28 | } 29 | 30 | // What we do when we get a new block (from ourselves or the network) 31 | receiveBlock(block) { 32 | // create a variable to hold the hash of the old block so that we can mine on top of it 33 | // use the receiveBlock() function from the client to check the block and broadcast it to to the network 34 | // update the head of the local state of our blockchain 35 | // if the block head has changed, mine on top of the new head 36 | // - start creating/mining a new block 37 | } 38 | 39 | // Start mining 40 | tick() { 41 | // for every instance we try to mine (where hashRate determines the amount of computations a GPU or ASIC could process in parrallel) 42 | // - if we find a valid block 43 | // -- get the valid blockhash 44 | // -- log the results to the console 45 | // -- create the valid block 46 | // -- process the valid block 47 | // -- end the loop and keep mining 48 | // - if we did not find a block, incriment the nonce and try again 49 | } 50 | } 51 | 52 | module.exports = Miner; 53 | -------------------------------------------------------------------------------- /ch3/3.2/README.md: -------------------------------------------------------------------------------- 1 | > The code challenges in this course build upon each other. It's highly recommended that you start from the beginning. If you haven't already, get started with our [Installation Instructions](https://cryptoeconomics.study/docs/en/sync/getting-started-development-setup). 2 | 3 | > ! The chapter 3 coding challenges use a network simulation. This is created by nodeAgent.js and networkSim.js in the `ch3` directory. You do not need to understand how this network simulation works to complete the chapter two coding challenges, but we highly recommend it. The README in the `ch3` directory will give you a high level overview of the network demo. Each file also has detailed code comments explaining how each function works. 4 | 5 |
6 | 7 | # Bitcoin and Nakamoto Consensus 8 | 9 | In this coding challenge we're going to extend the work we did in 3.1 to create a proof of work miner node. While hashes and networking have existed for a long time, the core innovation of Bitcoin is that nodes are incentivized to do work to prove that they can submit new blocks. This aligns incentives for all parties on the network and ends up creating a public good even though they are behaving economically rationally. Because miners do the work to find "proofs of work," we know that anyone can't just modify the state arbitrarily. Because the network is public all the other miners can check every proof of work (and every tx in every block) to make sure it's legit. Miners are engaged in a competitive game and they don't want to give other miners extra rewards if they haven't earned them. Also, if they did and the public found out the value of the network would go to 0 so everyone is incentivized to work hard and to also to be honest otherwise they'll lose their rewards. 10 | 11 |
12 | 13 | ## Creating Blocks 14 | 15 | The reason miners are rewarded for creating valid proofs of work is that we want them to produce valid blocks for the network. The `createBlock()` function will aggregate all the valid transactions we have into a block. 16 | ``` 17 | // Create a new block 18 | createBlock() { 19 | // get all the transactions for this block 20 | // get the timestamp 21 | // create a new block 22 | // - nonce 23 | // - block number 24 | // - give ourselves the coinbase reward for finding the block 25 | // - log the difficulty of the network 26 | // - show the hash of the block that matches the network difficulty 27 | // - timestamp 28 | // - transactions 29 | // return the block 30 | } 31 | ``` 32 | 33 |
34 | 35 | ## Receiving and Processing Blocks 36 | 37 | When we receive new blocks (or create them) we need to add them to our view of the state (blockchain). With `receiveBlock()` we will take in a block, check it, and if it's valid broadcast it to the rest of the network. Then we'll add it to our view of the blockchain and start mining on top of it to get another coinbase reward. 38 | ``` 39 | // What we do when we get a new block (from ourselves or the network) 40 | receiveBlock(block) { 41 | // create a variable to hold the hash of the old block so that we can mine on top of it 42 | // use the receiveBlock() function from the client to check the block and broadcast it to to the network 43 | // update the head of the local state of our blockchain 44 | // if the block head has changed, mine on top of the new head 45 | // - start creating/mining a new block 46 | } 47 | ``` 48 | 49 |
50 | 51 | ## Mining 52 | 53 | While the network wants miners to create new valid blocks, miners just want to get paid. As such, they are perpetually engaged in mining to find valid block hashes. `tick()` simulates that mining activity by trying to find a valid block hash on top of the most recent block. 54 | ``` 55 | // Start mining 56 | tick() { 57 | // for every instance we try to mine (where hashRate determines the amount of computations a GPU or ASIC could process in parrallel) 58 | // - if we find a valid block 59 | // -- get the valid blockhash 60 | // -- log the results to the console 61 | // -- create the valid block 62 | // -- process the valid block 63 | // -- end the loop and keep mining 64 | // - if we did not find a block, incriment the nonce and try again 65 | } 66 | ``` 67 | 68 |
69 | 70 | ## Testing 71 | 72 | To test our proof of work miner we're going to use the `PoWSingleMinerTest.js` and `PoWAllMinerTest.js` network simulation in the `solutions` folder. These tests will simulate a network of miners mining blocks and creating transactions for your PoWClient to process. 73 | 74 |
75 | 76 | > As always, if you have questions or get stuck please hit up the community on the [forum!](https://forum.cryptoeconomics.study) 77 | -------------------------------------------------------------------------------- /ch3/3.2/solutions/PoWClient.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const hexToBinary = require('hex-to-binary'); 3 | const { Node, getTxHash } = require('../../nodeAgent'); 4 | 5 | // Add Client class which generates & sends transactions 6 | class Client extends Node { 7 | constructor(wallet, genesis, network) { 8 | super(wallet, genesis, network); 9 | this.blockchain = []; // longest chain 10 | this.allBlocks = []; // all blocks 11 | this.blockNumber = 0; // keep track of blocks added to blockchain despite getState() 12 | this.difficulty = 13; 13 | 14 | const genesisBlock = { 15 | nonce: 0, 16 | number: 0, 17 | coinbase: 0, 18 | difficulty: 9000, 19 | parentHash: 0, 20 | timestamp: 0, 21 | contents: { 22 | type: 'block', 23 | txList: [], 24 | }, 25 | }; 26 | this.blockchain.push(genesisBlock); 27 | this.allBlocks.push(genesisBlock); 28 | } 29 | 30 | onReceive(message) { 31 | switch (message.contents.type) { 32 | case 'send': 33 | this.receiveTx(message); 34 | break; 35 | case 'block': 36 | this.receiveBlock(message); 37 | break; 38 | } 39 | } 40 | 41 | receiveTx(tx) { 42 | if (this.transactions.includes(tx)) return; 43 | this.transactions.push(tx); // add tx to mempool 44 | this.network.broadcast(this.pid, tx); 45 | } 46 | 47 | isValidBlockHash(block) { 48 | const blockHash = getTxHash(block); 49 | // Found block 50 | const binBlockHash = hexToBinary(blockHash.substr(2)); 51 | const leadingZeros = parseInt(binBlockHash.substring(0, this.difficulty)); 52 | return leadingZeros === 0; 53 | } 54 | 55 | receiveBlock(block) { 56 | if (this.allBlocks.includes(block)) return; 57 | if (!this.isValidBlockHash(block)) return; 58 | this.allBlocks.push(block); // add block to all blocks received 59 | // if the block builds directly on the current head of the chain, append to chain 60 | if (block.parentHash === getTxHash(this.blockchain.slice(-1)[0])) { 61 | this.blockNumber++; // increment 62 | this.blockchain.push(block); // add block to own blockchain 63 | this.applyBlock(block); 64 | } else { 65 | this.allBlocks.push(block); 66 | this.updateState(); // check if blockchain is the longest sequence 67 | } 68 | this.network.broadcast(this.pid, block); // broadcast new block to network 69 | } 70 | 71 | // Fork choice 72 | // Only apply transactions which are contained in the longest chain 73 | // and returns the resulting state object. 74 | updateState() { 75 | // a temp chain 76 | const tempChain = []; 77 | const { allBlocks } = this; 78 | // return max blocknumber from allBlocks 79 | const max = Math.max.apply(Math, allBlocks.map(block => block.number)); 80 | // add the highestBlockNumber to tempChain using blockNumber 81 | for (const block of allBlocks) { 82 | // TODO optimize this...Once there are many blocks in allBlocks, this may be SLOW af 83 | if (block.number === max) { 84 | tempChain.push(block); 85 | break; 86 | } 87 | } 88 | // add max number of blocks to tempChain using parentHash 89 | // i is the current block number 90 | for (let i = max; i > 0; i--) { 91 | const prevHash = tempChain[0].parentHash; 92 | for (const block of allBlocks) { 93 | if (getTxHash(block) === prevHash) { 94 | // TODO verify blockhash before adding to allBlocks 95 | tempChain.unshift(block); // add block to front of array 96 | break; 97 | } 98 | } 99 | } 100 | // save the ordered sequence 101 | this.blockchain = tempChain; 102 | // apply all txs from ordered list of blocks 103 | for (const block of this.blockchain) { 104 | this.applyBlock(block); 105 | } 106 | return this.state; 107 | } 108 | 109 | applyBlock(block) { 110 | const { txList } = block.contents; 111 | for (const tx of txList) { 112 | this.applyTransaction(tx); // update state 113 | if (tx.contents.from !== 0) { 114 | // mint tx is excluded 115 | this.applyInvalidNonceTxs(tx.contents.from); // update state 116 | } 117 | } 118 | } 119 | } 120 | 121 | module.exports = Client; 122 | -------------------------------------------------------------------------------- /ch3/3.2/solutions/PoWMiner.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./PoWClient'); 3 | const { getTxHash } = require('../nodeAgent'); 4 | 5 | // This Miner class will extend the client we created in 3.1 6 | class Miner extends Client { 7 | constructor(wallet, genesis, network) { 8 | super(wallet, genesis, network); 9 | // create a block in the constructor so that we have something to start mining on 10 | this.blockAttempt = this.createBlock(); 11 | // hashRate = # hashes miner can try at each tick 12 | this.hashRate = 5; 13 | } 14 | 15 | // Create a new block 16 | createBlock() { 17 | // get all the transactions for this block 18 | const tx = this.transactions.shift(); 19 | const txList = []; 20 | if (tx) txList.push(tx); 21 | 22 | // get the timestamp 23 | const timestamp = Math.round(new Date().getTime() / 1000); 24 | // create a new block 25 | const newBlock = { 26 | // nonce 27 | nonce: 0, 28 | // block number 29 | number: this.blockNumber + 1, 30 | // give ourselves the coinbase reward for finding the block 31 | coinbase: this.wallet.address, 32 | // log the difficulty of the network 33 | difficulty: this.difficulty, 34 | // show the hash of the block that matches the network difficulty 35 | parentHash: getTxHash(this.blockchain.slice(-1)[0]), 36 | // timestamp 37 | timestamp, 38 | // transactions 39 | contents: { 40 | type: 'block', 41 | txList, 42 | }, 43 | }; 44 | // return the block 45 | return newBlock; 46 | } 47 | 48 | // What we do when we get a new block (from ourselves or the network) 49 | receiveBlock(block) { 50 | // create a variable to hold the hash of the old block so that we can mine on top of it 51 | const { parentHash } = this.blockAttempt; 52 | // use the receiveBlock() function from the client to check the block and broadcast it to to the network 53 | super.receiveBlock(block); 54 | // update the head of the local state of our blockchain 55 | const newHead = this.blockchain.slice(-1)[0]; 56 | // if the block head has changed, mine on top of the new head 57 | if (getTxHash(newHead) !== parentHash) { 58 | // Start creating/mining a new block 59 | this.blockAttempt = this.createBlock(); 60 | } 61 | } 62 | 63 | // Start mining 64 | tick() { 65 | for (let i = 0; i < this.hashRate; i++) { 66 | // if we find a valid block 67 | if (this.isValidBlockHash(this.blockAttempt)) { 68 | // get the valid blockhash 69 | const blockHash = getTxHash(this.blockAttempt); 70 | // log the results to the console 71 | console.log( 72 | this.pid.substring(0, 6), 73 | 'found a block:', 74 | blockHash.substring(0, 10), 75 | 'at height', 76 | this.blockAttempt.number, 77 | ); 78 | // create the valid block 79 | const validBlock = this.blockAttempt; 80 | // process the valid block 81 | this.receiveBlock(validBlock); 82 | // end the loop and keep mining 83 | return; 84 | } 85 | // if we did not find a block, incriment the nonce and try again 86 | this.blockAttempt.nonce++; 87 | } 88 | } 89 | } 90 | 91 | module.exports = Miner; 92 | -------------------------------------------------------------------------------- /ch3/3.2/test/PoWAllMinerTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../../networksim'); 3 | const Miner = require('../PoWMiner'); 4 | const { getTxHash } = require('../../nodeAgent'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const latency = 15; // 10-15 ticks per message 11 | const packetLoss = 0; 12 | const network = new NetworkSimulator(latency, packetLoss); 13 | for (let i = 0; i < numNodes; i++) { 14 | wallets.push(EthCrypto.createIdentity()); 15 | genesis[wallets[i].address] = { 16 | balance: 0, 17 | nonce: 0, 18 | }; 19 | } 20 | // Give wallet 0 some money at genesis 21 | genesis[wallets[0].address] = { 22 | balance: 100, 23 | nonce: 0, 24 | }; 25 | 26 | const nodes = []; 27 | // Create new nodes based on our wallets, and connect them to the network 28 | for (let i = 0; i < numNodes; i++) { 29 | nodes.push( 30 | new Miner(wallets[i], JSON.parse(JSON.stringify(genesis)), network), 31 | ); 32 | network.connectPeer(nodes[i], 3); 33 | } 34 | 35 | const tx0 = nodes[0].generateTx(nodes[1].wallet.address, 10); 36 | network.broadcast(nodes[0].pid, tx0); 37 | console.log('sent tx0'); 38 | for (let i = 0; i < 800; i++) { 39 | network.tick(); 40 | } 41 | const tx1 = nodes[0].generateTx(nodes[2].wallet.address, 5); 42 | network.broadcast(nodes[0].pid, tx1); 43 | console.log('sent tx1'); 44 | for (let i = 0; i < 800; i++) { 45 | network.tick(); 46 | } 47 | const tx2 = nodes[0].generateTx(nodes[3].wallet.address, 6); 48 | network.broadcast(nodes[0].pid, tx2); 49 | console.log('sent tx2'); 50 | for (let i = 0; i < 800; i++) { 51 | network.tick(); 52 | } 53 | const tx3 = nodes[1].generateTx(nodes[4].wallet.address, 3); 54 | network.broadcast(nodes[1].pid, tx3); 55 | console.log('sent tx3'); 56 | for (let i = 0; i < 1500; i++) { 57 | network.tick(); 58 | } 59 | 60 | for (let i = 0; i < numNodes; i++) { 61 | console.log( 62 | 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 63 | ); 64 | console.log('node: ', nodes[i].p2pNodeId.address); 65 | // console.log('my chain',nodes[i].blockchain) 66 | // console.log('all blocks',nodes[i].allBlocks) 67 | console.log('chain len', nodes[i].blockchain.length); 68 | for (let j = 0; j < nodes[i].blockchain.length; j++) { 69 | const block = nodes[i].blockchain[j]; 70 | console.log('block ', block.number, ':', getTxHash(block)); 71 | if (nodes[i].blockchain[j].contents.txList.length) { 72 | console.log('tx:', nodes[i].blockchain[j].contents.txList[0].contents); 73 | } 74 | } 75 | console.log('node state: ', nodes[i].state); 76 | } 77 | -------------------------------------------------------------------------------- /ch3/3.2/test/PoWClient.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const hexToBinary = require('hex-to-binary'); 3 | const { Node, getTxHash } = require('../../nodeAgent'); 4 | 5 | // Add Client class which generates & sends transactions 6 | class Client extends Node { 7 | constructor(wallet, genesis, network) { 8 | super(wallet, genesis, network); 9 | this.blockchain = []; // longest chain 10 | this.allBlocks = []; // all blocks 11 | this.blockNumber = 0; // keep track of blocks added to blockchain despite getState() 12 | this.difficulty = 13; 13 | 14 | const genesisBlock = { 15 | nonce: 0, 16 | number: 0, 17 | coinbase: 0, 18 | difficulty: 9000, 19 | parentHash: 0, 20 | timestamp: 0, 21 | contents: { 22 | type: 'block', 23 | txList: [], 24 | }, 25 | }; 26 | this.blockchain.push(genesisBlock); 27 | this.allBlocks.push(genesisBlock); 28 | } 29 | 30 | onReceive(message) { 31 | switch (message.contents.type) { 32 | case 'send': 33 | this.receiveTx(message); 34 | break; 35 | case 'block': 36 | this.receiveBlock(message); 37 | break; 38 | } 39 | } 40 | 41 | receiveTx(tx) { 42 | if (this.transactions.includes(tx)) return; 43 | this.transactions.push(tx); // add tx to mempool 44 | this.network.broadcast(this.pid, tx); 45 | } 46 | 47 | isValidBlockHash(block) { 48 | const blockHash = getTxHash(block); 49 | // Found block 50 | const binBlockHash = hexToBinary(blockHash.substr(2)); 51 | const leadingZeros = parseInt(binBlockHash.substring(0, this.difficulty)); 52 | return leadingZeros === 0; 53 | } 54 | 55 | receiveBlock(block) { 56 | if (this.allBlocks.includes(block)) return; 57 | if (!this.isValidBlockHash(block)) return; 58 | this.allBlocks.push(block); // add block to all blocks received 59 | // if the block builds directly on the current head of the chain, append to chain 60 | if (block.parentHash === getTxHash(this.blockchain.slice(-1)[0])) { 61 | this.blockNumber++; // increment 62 | this.blockchain.push(block); // add block to own blockchain 63 | this.applyBlock(block); 64 | } else { 65 | this.allBlocks.push(block); 66 | this.updateState(); // check if blockchain is the longest sequence 67 | } 68 | this.network.broadcast(this.pid, block); // broadcast new block to network 69 | } 70 | 71 | // Fork choice 72 | // Only apply transactions which are contained in the longest chain 73 | // and returns the resulting state object. 74 | updateState() { 75 | // a temp chain 76 | const tempChain = []; 77 | const { allBlocks } = this; 78 | // return max blocknumber from allBlocks 79 | const max = Math.max.apply(Math, allBlocks.map(block => block.number)); 80 | // add the highestBlockNumber to tempChain using blockNumber 81 | for (const block of allBlocks) { 82 | // TODO optimize this...Once there are many blocks in allBlocks, this may be SLOW af 83 | if (block.number === max) { 84 | tempChain.push(block); 85 | break; 86 | } 87 | } 88 | // add max number of blocks to tempChain using parentHash 89 | // i is the current block number 90 | for (let i = max; i > 0; i--) { 91 | const prevHash = tempChain[0].parentHash; 92 | for (const block of allBlocks) { 93 | if (getTxHash(block) === prevHash) { 94 | // TODO verify blockhash before adding to allBlocks 95 | tempChain.unshift(block); // add block to front of array 96 | break; 97 | } 98 | } 99 | } 100 | // save the ordered sequence 101 | this.blockchain = tempChain; 102 | // apply all txs from ordered list of blocks 103 | for (const block of this.blockchain) { 104 | this.applyBlock(block); 105 | } 106 | return this.state; 107 | } 108 | 109 | applyBlock(block) { 110 | const { txList } = block.contents; 111 | for (const tx of txList) { 112 | this.applyTransaction(tx); // update state 113 | if (tx.contents.from !== 0) { 114 | // mint tx is excluded 115 | this.applyInvalidNonceTxs(tx.contents.from); // update state 116 | } 117 | } 118 | } 119 | } 120 | 121 | module.exports = Client; 122 | -------------------------------------------------------------------------------- /ch3/3.2/test/PoWSingleMinerTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../../networksim'); 3 | const Miner = require('../PoWMiner'); 4 | const Client = require('./PoWClient'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const latency = 15; // 10-15 ticks per message 11 | const packetLoss = 0; 12 | const network = new NetworkSimulator(latency, packetLoss); 13 | for (let i = 0; i < numNodes; i++) { 14 | wallets.push(EthCrypto.createIdentity()); 15 | genesis[wallets[i].address] = { 16 | balance: 0, 17 | nonce: 0, 18 | }; 19 | } 20 | // Give wallet 0 some money at genesis 21 | genesis[wallets[0].address] = { 22 | balance: 100, 23 | nonce: 0, 24 | }; 25 | 26 | const nodes = []; 27 | // Create new nodes based on our wallets, and connect them to the network 28 | for (let i = 0; i < numNodes - 1; i++) { 29 | nodes.push( 30 | new Client(wallets[i], JSON.parse(JSON.stringify(genesis)), network), 31 | ); 32 | network.connectPeer(nodes[i], 3); 33 | } 34 | nodes.push( 35 | new Miner(wallets[numNodes - 1], JSON.parse(JSON.stringify(genesis)), network), 36 | ); 37 | network.connectPeer(nodes[numNodes - 1], 3); 38 | 39 | const tx0 = nodes[0].generateTx(nodes[1].wallet.address, 10); 40 | network.broadcast(nodes[0].pid, tx0); 41 | console.log('sent tx0'); 42 | for (let i = 0; i < 800; i++) { 43 | network.tick(); 44 | } 45 | const tx1 = nodes[0].generateTx(nodes[2].wallet.address, 5); 46 | network.broadcast(nodes[0].pid, tx1); 47 | console.log('sent tx1'); 48 | for (let i = 0; i < 800; i++) { 49 | network.tick(); 50 | } 51 | const tx2 = nodes[0].generateTx(nodes[3].wallet.address, 6); 52 | network.broadcast(nodes[0].pid, tx2); 53 | console.log('sent tx2'); 54 | for (let i = 0; i < 800; i++) { 55 | network.tick(); 56 | } 57 | const tx3 = nodes[1].generateTx(nodes[4].wallet.address, 3); 58 | network.broadcast(nodes[1].pid, tx3); 59 | console.log('sent tx3'); 60 | for (let i = 0; i < 1500; i++) { 61 | network.tick(); 62 | } 63 | 64 | for (let i = 0; i < numNodes; i++) { 65 | console.log('node: ', nodes[i].p2pNodeId.address); 66 | // console.log('my chain',nodes[i].blockchain) 67 | // console.log('all blocks',nodes[i].allBlocks) 68 | console.log('chain len', nodes[i].blockchain.length); 69 | for (let j = 0; j < nodes[i].blockchain.length; j++) { 70 | console.log('block number', nodes[i].blockchain[j].number); 71 | console.log('block parentHash', nodes[i].blockchain[j].parentHash); 72 | console.log('txs:', nodes[i].blockchain[j].contents.txList); 73 | } 74 | console.log('node state: ', nodes[i].state); 75 | // console.log('invalidNonceTxs: ', nodes[i].invalidNonceTxs) 76 | console.log('xxxxxxxxx0xxxxxxxxx'); 77 | // nodes[i].getState() 78 | // console.log('new node state: ', nodes[i].state) 79 | } 80 | -------------------------------------------------------------------------------- /ch3/3.3/MerkleTree.js: -------------------------------------------------------------------------------- 1 | class MerkleTree { 2 | constructor(leaves, concat) { 3 | this.leaves = leaves; 4 | this.concat = concat; 5 | } 6 | reduceLayers(layer) { 7 | } 8 | getProof(index, layer = this.leaves, proof = []) { 9 | } 10 | getRoot() { 11 | } 12 | } 13 | 14 | module.exports = MerkleTree; 15 | -------------------------------------------------------------------------------- /ch3/3.3/solutions/merkleTree.js: -------------------------------------------------------------------------------- 1 | //Forked from Chainshot Merkle Tree Lesson: https://github.com/ChainShot/Content/tree/master/projects/Merkle%20Tree/JavaScript 2 | class MerkleTree { 3 | constructor(leaves, concat) { 4 | this.leaves = leaves; 5 | this.concat = concat; 6 | } 7 | reduceLayers(layer) { 8 | if (layer.length === 1) return layer[0]; 9 | const newLayer = []; 10 | for (let i = 0; i < layer.length; i += 2) { 11 | let left = layer[i]; 12 | let right = layer[i + 1]; 13 | if (!right) { 14 | newLayer.push(left); 15 | } 16 | else { 17 | newLayer.push(this.concat(left, right)); 18 | } 19 | } 20 | return this.reduceLayers(newLayer); 21 | } 22 | getProof(index, layer = this.leaves, proof = []) { 23 | if (layer.length === 1) return proof; 24 | const newLayer = []; 25 | for (let i = 0; i < layer.length; i += 2) { 26 | let left = layer[i]; 27 | let right = layer[i + 1]; 28 | let value; 29 | if (!right) { 30 | newLayer.push(left); 31 | } 32 | else { 33 | newLayer.push(this.concat(left, right)); 34 | 35 | if (i === index || i === index - 1) { 36 | let isLeft = !(index % 2); 37 | proof.push({ 38 | data: isLeft ? right : left, 39 | left: !isLeft 40 | }); 41 | } 42 | } 43 | } 44 | return this.getProof(Math.floor(index / 2), newLayer, proof); 45 | } 46 | getRoot() { 47 | return this.reduceLayers(this.leaves); 48 | } 49 | } 50 | 51 | module.exports = MerkleTree; 52 | -------------------------------------------------------------------------------- /ch3/3.3/solutions/verifyProof.js: -------------------------------------------------------------------------------- 1 | function verifyProof(proof, node, root, concat) { 2 | let data = node; 3 | for (let i = 0; i < proof.length; i++) { 4 | const arr = (proof[i].left) ? [proof[i].data, data] : [data, proof[i].data]; 5 | data = concat(...arr); 6 | } 7 | return data.equals(root); 8 | } 9 | 10 | module.exports = verifyProof; 11 | -------------------------------------------------------------------------------- /ch3/3.3/test/testUtil.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | const assert = require("assert"); 3 | 4 | // use the crypto module to create a sha256 hash from the data passed in 5 | function sha256(data) { 6 | return crypto.createHash('sha256').update(data).digest(); 7 | } 8 | 9 | // the concat function we use to hash together merkle leaves 10 | function concatHash(left, right) { 11 | if (!left) throw new Error("The concat function expects two hash arguments, the first was not receieved."); 12 | if (!right) throw new Error("The concat function expects two hash arguments, the second was not receieved."); 13 | return sha256(Buffer.concat([left, right])); 14 | } 15 | 16 | // the concat function we use to show the merkle root calculation 17 | function concatLetters(left, right) { 18 | return `Hash(${left} + ${right})`; 19 | } 20 | 21 | // given a proof, finds the merkle root 22 | function hashProof(node, proof) { 23 | let data = sha256(node); 24 | for (let i = 0; i < proof.length; i++) { 25 | const buffers = (proof[i].left) ? [proof[i].data, data] : [data, proof[i].data]; 26 | data = sha256(Buffer.concat(buffers)); 27 | } 28 | return data; 29 | } 30 | 31 | // a helper function that will check to see if the tree generates the correct merkle root 32 | // the complexity of this function is mostly to help debug when something has gone wrong. 33 | // In the case where a bad hash is returned, the calculation is logged 34 | function testTree(MerkleTree, leaves, expectedHash, expectedLetters) { 35 | const hashTree = new MerkleTree(leaves.map(sha256), concatHash); 36 | const lettersTree = new MerkleTree(leaves, concatLetters); 37 | if (!hashTree.getRoot()) { 38 | assert(false, "Did not recieve a hash from the getRoot() function"); 39 | } 40 | const actualHash = hashTree.getRoot().toString('hex'); 41 | const actualLetters = lettersTree.getRoot().toString('hex'); 42 | if (actualHash !== expectedHash) { 43 | console.log( 44 | `Merkle Leaves were: ${leaves.toString()} \n\n` + 45 | 46 | `We were expecting the calculated hash to be: ${expectedHash}\n` + 47 | `This would be the result of this calculation: ${expectedLetters}\n\n` + 48 | 49 | `We recieved this hash from getRoot(): ${actualHash}\n` + 50 | `Which is a result of this calculation: ${actualLetters}`) 51 | } 52 | assert.equal(actualHash, expectedHash, 53 | `Your hash was calculated ${actualLetters}, we were expecting ${expectedLetters}` 54 | ); 55 | } 56 | 57 | module.exports = {concatHash, concatLetters, testTree, hashProof, sha256} 58 | -------------------------------------------------------------------------------- /ch3/3.3/verifyProof.js: -------------------------------------------------------------------------------- 1 | function verifyProof(proof, node, root, concat) { 2 | 3 | } 4 | 5 | module.exports = verifyProof; 6 | -------------------------------------------------------------------------------- /ch3/3.4/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Game Theory in Bitcoin 4 | 5 | This section is more of an academic exercise. As such, here's a paper to read! 6 | - [A brief introduction to the basics of game theory](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=1968579) 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /ch3/3.5/PoWMiner.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./PoWClient'); 3 | const { getTxHash } = require('../nodeAgent'); 4 | 5 | class Miner extends Client { 6 | constructor(wallet, genesis, network) { 7 | super(wallet, genesis, network); 8 | this.blockAttempt = this.createBlock(); 9 | this.hashRate = 5; // hashRate = # hashes miner can try at each tick 10 | } 11 | 12 | tick() { 13 | // Mining 14 | for (let i = 0; i < this.hashRate; i++) { 15 | if (this.isValidBlockHash(this.blockAttempt)) { 16 | const blockHash = getTxHash(this.blockAttempt); 17 | console.log( 18 | this.pid.substring(0, 6), 19 | 'found a block:', 20 | blockHash.substring(0, 10), 21 | 'at height', 22 | this.blockAttempt.number, 23 | ); 24 | const validBlock = this.blockAttempt; 25 | this.receiveBlock(validBlock); 26 | return; 27 | } this.blockAttempt.nonce++; 28 | } 29 | } 30 | 31 | receiveBlock(block) { 32 | const { parentHash } = this.blockAttempt; 33 | super.receiveBlock(block); 34 | const newHead = this.blockchain.slice(-1)[0]; 35 | // if the block head has changed, mine on top of the new head 36 | if (getTxHash(newHead) !== parentHash) { 37 | this.blockAttempt = this.createBlock(); // Start mining new block 38 | } 39 | } 40 | 41 | createBlock() { 42 | const tx = this.transactions.shift(); 43 | const txList = []; 44 | if (tx) txList.push(tx); 45 | const timestamp = Math.round(new Date().getTime() / 1000); 46 | const newBlock = { 47 | nonce: 0, 48 | number: this.blockNumber + 1, 49 | coinbase: this.wallet.address, 50 | difficulty: this.difficulty, 51 | parentHash: getTxHash(this.blockchain.slice(-1)[0]), 52 | timestamp, 53 | contents: { 54 | type: 'block', 55 | txList, 56 | }, 57 | }; 58 | return newBlock; 59 | } 60 | } 61 | 62 | module.exports = Miner; 63 | -------------------------------------------------------------------------------- /ch3/3.5/README.md: -------------------------------------------------------------------------------- 1 | > The code challenges in this course build upon each other. It's highly recommended that you start from the beginning. If you haven't already, get started with our [Installation Instructions](https://cryptoeconomics.study/docs/en/sync/getting-started-development-setup). 2 | 3 | > ! The chapter 3 coding challenges use a network simulation. This is created by nodeAgent.js and networkSim.js in the `ch3` directory. You do not need to understand how this network simulation works to complete the chapter two coding challenges, but we highly recommend it. The README in the `ch3` directory will give you a high level overview of the network demo. Each file also has detailed code comments explaining how each function works. 4 | 5 |
6 | 7 | # Selfish Mining 8 | 9 | Selfish mining is a strategy miners can use to iteratively take a larger share of the mining rewards than their peers. This strategy has a griefing factor that hurts the miner in the short therm though, so we haven't seen this in practice yet. Non the less, it's a very real strategy and we'll reconstruct it here to see how it works. To do this we're going to extend our Miner class from 3.2 so that they can keep a private fork of the blockchain to mine on. 10 | 11 | > Note: this attack is not possible in Proof of Stake systems as validators are chosen randomly and there is no way to be "ahead" of the main chain. 12 | 13 |
14 | 15 | ## Mining 16 | 17 | The mining function is the same as before, but now we're going to keep track of a private fork and push all of our valid blocks there. The goal being to have a private chain that is longer than the public chain. We build up our private chain and wait for other miners expend resources building blocks. When they have a valid block we broadcast our longer chain to make the network throw their work away. This way we get all the rewards and they lost money mining an invalid block. This sometimes works, but sometimes it results on us mining on a fork that falls behind that we have to discard. 18 | ``` 19 | // Mining 20 | tick() { 21 | for (let i = 0; i < this.hashRate; i++) { 22 | if (this.isValidBlockHash(this.blockAttempt)) { 23 | const blockHash = getTxHash(this.blockAttempt); 24 | console.log( 25 | this.pid.substring(0, 6), 26 | 'found a private block:', 27 | blockHash.substring(0, 10), 28 | 'at height', 29 | this.blockAttempt.number, 30 | ); 31 | const validBlock = this.blockAttempt; 32 | // record height of the miner's private fork 33 | // add our latest valid block to our fork 34 | // start creating a new block and then return to exit the loop 35 | } 36 | this.blockAttempt.nonce++; 37 | } 38 | } 39 | ``` 40 | 41 |
42 | 43 | ## Receiving Blocks 44 | 45 | When we receive blocks we still update our view of the chain, but then we need to compare that new state to our private chain. If we have an advantage we capitalize on that by broadcasting out private chain, otherwise we just keep mining. 46 | ``` 47 | // Receiving blocks 48 | receiveBlock(block) { 49 | const { parentHash } = this.blockAttempt; 50 | super.receiveBlock(block); 51 | const newHead = this.blockchain.slice(-1)[0]; 52 | // if the block head has changed due to a new block coming in, mine on top of the new head 53 | // then check to see if the other miners have caught up 54 | // - our new block extends our private chain start mining a new block 55 | // - public Fork has length 1 and private Fork has length 1, so broadcast 56 | // - public Fork has length 3 and private fork has just reached length 2, so broadcast 57 | // - public blockchain has grown longer than private fork. Discard private fork and start mining on the public chain (longest chain) 58 | } 59 | ``` 60 | 61 |
62 | 63 | ## Broadcasting Our Private Fork 64 | 65 | When we're ready to capitalize upon our work, we broadcast our private chain to the network. 66 | ``` 67 | // Broadcasting our private fork 68 | broadcastPrivateFork() { 69 | // log the private fork to the console 70 | // resize our view of the blockchain to add the length of our private fork 71 | // broadcast all the blocks on our private fork to the network 72 | // - add the private block to our view of the blockchain 73 | // - add the private block to our list of all the blocks 74 | // - broadcast the private block to the network 75 | // update block number 76 | // reset private fork 77 | // start mining a new block 78 | } 79 | ``` 80 | 81 |
82 | 83 | ## Testing 84 | 85 | To test our proof of work miner we're going to use the `SelfishTest.js` network simulation in the `solutions` folder. This test will simulate a network of miners mining blocks and creating transactions for your Selfish PoWClient to process. 86 | 87 |
88 | 89 | > As always, if you have questions or get stuck please hit up the community on the [forum!](https://forum.cryptoeconomics.study) 90 | -------------------------------------------------------------------------------- /ch3/3.5/SelfishMiner.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('./PoWClient'); 3 | const { getTxHash } = require('../nodeAgent'); 4 | 5 | class SelfishMiner extends Client { 6 | constructor(wallet, genesis, network) { 7 | super(wallet, genesis, network); 8 | this.hashRate = 15; 9 | // the miner's secret fork of the network 10 | this.privateFork = []; 11 | // amount of valid blocks the miner has created ahead of the main chain 12 | this.privateForkHeight = 0; 13 | this.blockAttempt = this.createBlock(); 14 | } 15 | 16 | // Mining 17 | tick() { 18 | for (let i = 0; i < this.hashRate; i++) { 19 | if (this.isValidBlockHash(this.blockAttempt)) { 20 | const blockHash = getTxHash(this.blockAttempt); 21 | console.log( 22 | this.pid.substring(0, 6), 23 | 'found a private block:', 24 | blockHash.substring(0, 10), 25 | 'at height', 26 | this.blockAttempt.number, 27 | ); 28 | const validBlock = this.blockAttempt; 29 | // record height of the miner's private fork 30 | // add our latest valid block to our fork 31 | // start creating a new block and then return to exit the loop 32 | } 33 | this.blockAttempt.nonce++; 34 | } 35 | } 36 | 37 | // Receiving blocks 38 | receiveBlock(block) { 39 | const { parentHash } = this.blockAttempt; 40 | super.receiveBlock(block); 41 | const newHead = this.blockchain.slice(-1)[0]; 42 | // if the block head has changed due to a new block coming in, mine on top of the new head 43 | // then check to see if the other miners have caught up 44 | // - our new block extends our private chain start mining a new block 45 | // - public Fork has length 1 and private Fork has length 1, so broadcast 46 | // - public Fork has length 3 and private fork has just reached length 2, so broadcast 47 | // - public blockchain has grown longer than private fork. Discard private fork and start mining on the public chain (longest chain) 48 | } 49 | 50 | // Creating a new block 51 | createBlock() { 52 | const tx = this.transactions.shift(); 53 | const txList = []; 54 | if (tx) txList.push(tx); 55 | const timestamp = Math.round(new Date().getTime() / 1000); 56 | const pfLength = this.privateFork.length; 57 | const pfHead = this.privateFork.slice(-1)[0]; 58 | const blockNum = pfLength > 0 ? pfHead.number : this.blockNumber; 59 | const parentHash = pfLength > 0 60 | ? getTxHash(pfHead) 61 | : getTxHash(this.blockchain.slice(-1)[0]); 62 | const newBlock = { 63 | nonce: 0, 64 | number: blockNum + 1, 65 | coinbase: this.wallet.address, 66 | difficulty: this.difficulty, 67 | parentHash, 68 | timestamp, 69 | contents: { 70 | type: 'block', 71 | txList, 72 | }, 73 | }; 74 | return newBlock; 75 | } 76 | 77 | // Broadcasting our private fork 78 | broadcastPrivateFork() { 79 | // log the private fork to the console 80 | // resize our view of the blockchain to add the length of our private fork 81 | // broadcast all the blocks on our private fork to the network 82 | // - add the private block to our view of the blockchain 83 | // - add the private block to our list of all the blocks 84 | // - broadcast the private block to the network 85 | // update block number 86 | // reset private fork 87 | // start mining a new block 88 | } 89 | } 90 | 91 | module.exports = SelfishMiner; 92 | -------------------------------------------------------------------------------- /ch3/3.5/solutions/SelfishMiner.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const Client = require('../PoWClient'); 3 | const { getTxHash } = require('../../nodeAgent'); 4 | 5 | class SelfishMiner extends Client { 6 | constructor(wallet, genesis, network) { 7 | super(wallet, genesis, network); 8 | this.hashRate = 15; 9 | // the miner's secret fork of the network 10 | this.privateFork = []; 11 | // amount of valid blocks the miner has created ahead of the main chain 12 | this.privateForkHeight = 0; 13 | this.blockAttempt = this.createBlock(); 14 | } 15 | 16 | // Mining 17 | tick() { 18 | for (let i = 0; i < this.hashRate; i++) { 19 | if (this.isValidBlockHash(this.blockAttempt)) { 20 | const blockHash = getTxHash(this.blockAttempt); 21 | console.log( 22 | this.pid.substring(0, 6), 23 | 'found a private block:', 24 | blockHash.substring(0, 10), 25 | 'at height', 26 | this.blockAttempt.number, 27 | ); 28 | const validBlock = this.blockAttempt; 29 | // record height of the miner's private fork 30 | if (!this.privateFork.length) this.privateForkHeight = this.blockNumber; 31 | // add our latest valid block to our fork 32 | this.privateFork.push(validBlock); 33 | // start creating a new block 34 | this.blockAttempt = this.createBlock(); 35 | return; 36 | } 37 | this.blockAttempt.nonce++; 38 | } 39 | } 40 | 41 | // Receiving blocks 42 | receiveBlock(block) { 43 | const { parentHash } = this.blockAttempt; 44 | super.receiveBlock(block); 45 | const newHead = this.blockchain.slice(-1)[0]; 46 | // if the block head has changed, mine on top of the new head 47 | // check to see if the other miners have caught up 48 | const publicForkLength = newHead.number - this.privateForkHeight; 49 | const privateForkLength = this.privateFork.length; 50 | if (getTxHash(newHead) !== parentHash) { 51 | if (privateForkLength === 0) { 52 | // new block extends chain, and have no private fork so start mining a new block 53 | this.blockAttempt = this.createBlock(); 54 | } else if ( 55 | privateForkLength === 1 56 | && publicForkLength === privateForkLength 57 | ) { 58 | // public Fork has length 1 and private Fork has length 1, so broadcast 59 | this.broadcastPrivateFork(); 60 | } else if ( 61 | privateForkLength > 1 62 | && publicForkLength === privateForkLength - 1 63 | ) { 64 | // e.g. public Fork has length 3 and private fork has just reached length 2, so broadcast 65 | this.broadcastPrivateFork(); 66 | } else if ( 67 | privateForkLength > 0 68 | && publicForkLength > privateForkLength 69 | ) { 70 | // public blockchain has grown longer than private fork. Discard private fork and start mining on the public chain (longest chain) 71 | this.privateFork = []; 72 | this.blockAttempt = this.createBlock(); 73 | } 74 | } 75 | } 76 | 77 | // Creating a new block 78 | createBlock() { 79 | const tx = this.transactions.shift(); 80 | const txList = []; 81 | if (tx) txList.push(tx); 82 | const timestamp = Math.round(new Date().getTime() / 1000); 83 | const pfLength = this.privateFork.length; 84 | const pfHead = this.privateFork.slice(-1)[0]; 85 | const blockNum = pfLength > 0 ? pfHead.number : this.blockNumber; 86 | const parentHash = pfLength > 0 87 | ? getTxHash(pfHead) 88 | : getTxHash(this.blockchain.slice(-1)[0]); 89 | const newBlock = { 90 | nonce: 0, 91 | number: blockNum + 1, 92 | coinbase: this.wallet.address, 93 | difficulty: this.difficulty, 94 | parentHash, 95 | timestamp, 96 | contents: { 97 | type: 'block', 98 | txList, 99 | }, 100 | }; 101 | return newBlock; 102 | } 103 | 104 | // Broadcasting our private fork 105 | broadcastPrivateFork() { 106 | console.log( 107 | 'Broadcasting private fork:', 108 | this.privateFork.length, 109 | 'blocks', 110 | ); 111 | // resize our view of the blockchain to add the length of our private fork 112 | this.blockchain = this.blockchain.slice(0, this.privateForkHeight); 113 | // broadcast all the blocks on our private fork to the network 114 | for (const block of this.privateFork) { 115 | // add the private block to our view of the blockchain 116 | this.blockchain.push(block); 117 | // add the private block to our list of all the blocks 118 | this.allBlocks.push(block); 119 | // broadcast the private block to the network 120 | this.network.broadcast(this.pid, block); 121 | } 122 | // update block number 123 | this.blockNumber = this.blockchain.slice(-1)[0].number; 124 | // reset private fork 125 | this.privateFork = []; 126 | // start mining a new block 127 | this.blockAttempt = this.createBlock(); 128 | } 129 | } 130 | 131 | module.exports = SelfishMiner; 132 | -------------------------------------------------------------------------------- /ch3/3.5/test/SelfishTest.js: -------------------------------------------------------------------------------- 1 | const EthCrypto = require('eth-crypto'); 2 | const NetworkSimulator = require('../../networksim'); 3 | const Miner = require('../PoWMiner'); 4 | const SelfishMiner = require('../SelfishMiner'); 5 | 6 | // ****** Test this out using a simulated network ****** // 7 | const numNodes = 5; 8 | const wallets = []; 9 | const genesis = {}; 10 | const latency = 9; // 6-9 ticks per message 11 | const packetLoss = 0; 12 | const network = new NetworkSimulator(latency, packetLoss); 13 | for (let i = 0; i < numNodes; i++) { 14 | wallets.push(EthCrypto.createIdentity()); 15 | genesis[wallets[i].address] = { 16 | balance: 0, 17 | nonce: 0, 18 | }; 19 | } 20 | // Give wallet 0 some money at genesis 21 | genesis[wallets[0].address] = { 22 | balance: 100, 23 | nonce: 0, 24 | }; 25 | 26 | const nodes = []; 27 | // Create new nodes based on our wallets, and connect them to the network 28 | for (let i = 0; i < numNodes - 1; i++) { 29 | nodes.push( 30 | new Miner(wallets[i], JSON.parse(JSON.stringify(genesis)), network), 31 | ); 32 | network.connectPeer(nodes[i], 3); 33 | } 34 | // Add Selfish Miner 35 | nodes.push( 36 | new SelfishMiner( 37 | wallets[numNodes - 1], 38 | JSON.parse(JSON.stringify(genesis)), 39 | network, 40 | ), 41 | ); 42 | network.connectPeer(nodes[numNodes - 1], 3); 43 | 44 | const tx0 = nodes[0].generateTx(nodes[1].wallet.address, 10); 45 | nodes[0].network.broadcast(nodes[0].pid, tx0); 46 | for (let i = 0; i < 2000; i++) { 47 | network.tick(); 48 | } 49 | const tx1 = nodes[0].generateTx(nodes[2].wallet.address, 5); 50 | nodes[0].network.broadcast(nodes[0].pid, tx1); 51 | for (let i = 0; i < 2000; i++) { 52 | network.tick(); 53 | } 54 | const tx2 = nodes[0].generateTx(nodes[3].wallet.address, 6); 55 | nodes[0].network.broadcast(nodes[0].pid, tx2); 56 | for (let i = 0; i < 2000; i++) { 57 | network.tick(); 58 | } 59 | const tx3 = nodes[1].generateTx(nodes[4].wallet.address, 3); 60 | nodes[1].network.broadcast(nodes[1].pid, tx3); 61 | for (let i = 0; i < 10000; i++) { 62 | network.tick(); 63 | } 64 | 65 | const selfishAddress = nodes[numNodes - 1].wallet.address; 66 | console.log(selfishAddress); 67 | let totalHashRate = 0; 68 | for (let i = 0; i < numNodes; i++) { 69 | totalHashRate += nodes[i].hashRate; 70 | console.log('node: ', nodes[i].p2pNodeId.address); 71 | // console.log('my chain',nodes[i].blockchain) 72 | // console.log('all blocks',nodes[i].allBlocks) 73 | console.log('chain len', nodes[i].blockchain.length); 74 | // for (let j = 0; j < nodes[i].blockchain.length; j++) { 75 | // console.log('block number', nodes[i].blockchain[j].number) 76 | // console.log('block parentHash', nodes[i].blockchain[j].parentHash) 77 | // } 78 | console.log('node state: ', nodes[i].state); 79 | // console.log('invalidNonceTxs: ', nodes[i].invalidNonceTxs) 80 | console.log('xxxxxxxxx0xxxxxxxxx'); 81 | // nodes[i].getState() 82 | // console.log('new node state: ', nodes[i].state) 83 | } 84 | const totalBlocks = nodes[0].blockchain.length; 85 | let selfishBlocks = 0; 86 | for (const block of nodes[0].blockchain) { 87 | if (block.coinbase === selfishAddress) { 88 | selfishBlocks++; 89 | console.log('Block', block.number, 'SELFISH'); 90 | } else { 91 | console.log('Block', block.number, 'honest'); 92 | } 93 | } 94 | console.log( 95 | 'Selfish % Hash rate:', 96 | (100 * nodes[numNodes - 1].hashRate) / totalHashRate, 97 | ); 98 | console.log('Selfish % Blocks mined:', (100 * selfishBlocks) / totalBlocks); 99 | -------------------------------------------------------------------------------- /ch3/networksim.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const d3 = require('d3-random') 3 | const seedrandom = require('seedrandom') 4 | 5 | class NetworkSimulator { 6 | constructor (latency, packetLoss) { 7 | const normalRandom = d3.randomNormal.source(seedrandom('a22ebc7c488a3a47'))(latency, (latency * 2) / 5) 8 | this.agents = [] 9 | this.latencyDistribution = () => Math.floor(Math.max(normalRandom(), 0)) 10 | this.time = 0 11 | this.messageQueue = {} 12 | this.peers = {} 13 | this.packetLoss = packetLoss 14 | } 15 | 16 | connectPeer (newPeer, numConnections) { 17 | newPeer.network = this 18 | const shuffledAgents = _.shuffle(this.agents) 19 | this.agents.push(newPeer) 20 | this.peers[newPeer.pid] = [] 21 | for (let a of shuffledAgents.slice(0, numConnections)) { 22 | this.peers[newPeer.pid].push(a) 23 | this.peers[a.pid].push(newPeer) 24 | } 25 | } 26 | 27 | broadcast (sender, message) { 28 | for (let pid of this.peers[sender]) { 29 | this.broadcastTo(sender, pid, message) 30 | } 31 | } 32 | 33 | broadcastTo (sender, receiver, message) { 34 | const recvTime = this.time + this.latencyDistribution() 35 | if (!(recvTime in this.messageQueue)) { 36 | this.messageQueue[recvTime] = [] 37 | } 38 | this.messageQueue[recvTime].push({recipient: receiver, message}) 39 | } 40 | 41 | tick () { 42 | if (this.time in this.messageQueue) { 43 | for (let {recipient, message} of this.messageQueue[this.time]) { 44 | if (Math.random() > this.packetLoss) { 45 | recipient.onReceive(message) 46 | } 47 | } 48 | delete this.messageQueue[this.time] 49 | } 50 | for (let a of this.agents) { 51 | a.tick() 52 | } 53 | this.time += 1 54 | } 55 | 56 | run (steps) { 57 | for (let i = 0; i < steps; i++) { 58 | this.tick() 59 | } 60 | } 61 | } 62 | 63 | module.exports = NetworkSimulator 64 | -------------------------------------------------------------------------------- /ch3/nodeAgent.js: -------------------------------------------------------------------------------- 1 | var EthCrypto = require('eth-crypto') 2 | 3 | function getTxHash (tx) { 4 | return EthCrypto.hash.keccak256(JSON.stringify(tx)) 5 | } 6 | 7 | class Node { 8 | constructor (wallet, genesis, network) { 9 | // Blockchain identity 10 | this.wallet = wallet 11 | // P2P Node identity -- used for connecting to peers 12 | this.p2pNodeId = EthCrypto.createIdentity() 13 | this.pid = this.p2pNodeId.address 14 | this.network = network 15 | this.state = genesis 16 | this.transactions = [] 17 | this.invalidNonceTxs = {} 18 | this.nonce = 0 19 | } 20 | 21 | onReceive (tx) { 22 | if (this.transactions.includes(tx)) { 23 | return 24 | } 25 | this.transactions.push(tx) 26 | this.applyTransaction(tx) 27 | this.network.broadcast(this.pid, tx) 28 | this.applyInvalidNonceTxs(tx.contents.from) 29 | } 30 | 31 | applyInvalidNonceTxs (address) { 32 | const targetNonce = this.state[address].nonce 33 | if (address in this.invalidNonceTxs && targetNonce in this.invalidNonceTxs[address]) { 34 | this.applyTransaction(this.invalidNonceTxs[address][targetNonce]) 35 | delete this.invalidNonceTxs[address][targetNonce] 36 | this.applyInvalidNonceTxs(address) 37 | } 38 | } 39 | 40 | tick () {} 41 | 42 | generateTx (to, amount) { 43 | const unsignedTx = { 44 | type: 'send', 45 | amount: amount, 46 | from: this.wallet.address, 47 | to: to, 48 | nonce: this.nonce 49 | } 50 | const tx = { 51 | contents: unsignedTx, 52 | sig: EthCrypto.sign(this.wallet.privateKey, getTxHash(unsignedTx)) 53 | } 54 | this.nonce++ //added so a node can send multiple txs before applying them (before they are included in a block) 55 | return tx 56 | } 57 | 58 | applyTransaction (tx) { 59 | // Check the from address matches the signature 60 | const signer = EthCrypto.recover(tx.sig, getTxHash(tx.contents)) 61 | if (signer !== tx.contents.from) { 62 | throw new Error('Invalid signature!') 63 | } 64 | // If we don't have a record for this address, create one 65 | if (!(tx.contents.to in this.state)) { 66 | this.state[[tx.contents.to]] = { 67 | balance: 0, 68 | nonce: 0 69 | } 70 | } 71 | // Check that the nonce is correct for replay protection 72 | if (tx.contents.nonce !== this.state[[tx.contents.from]].nonce) { 73 | // If it isn't correct, then we should add the transaction to invalidNonceTxs 74 | if (!(tx.contents.from in this.invalidNonceTxs)) { 75 | this.invalidNonceTxs[tx.contents.from] = {} 76 | } 77 | this.invalidNonceTxs[tx.contents.from][tx.contents.nonce] = tx 78 | return 79 | } 80 | if (tx.contents.type === 'send') { // Send coins 81 | if (this.state[[tx.contents.from]].balance - tx.contents.amount < 0) { 82 | console.log('not enough money...yet') 83 | return 84 | // throw new Error('Not enough money!') 85 | } 86 | this.state[[tx.contents.from]].balance -= tx.contents.amount 87 | this.state[[tx.contents.to]].balance += tx.contents.amount 88 | } else if (tx.contents.type === 'mint') { //Block reward 89 | // Check the from address matches the signature 90 | const signer = EthCrypto.recover(tx.sig, getTxHash(tx.contents)) 91 | if (signer !== tx.contents.to) { 92 | throw new Error('Invalid signature in mint tx!') 93 | } 94 | // Update state 95 | this.state[[tx.contents.to]].balance += tx.contents.amount 96 | } else { 97 | throw new Error('Invalid transaction type!') 98 | } 99 | this.state[[tx.contents.from]].nonce += 1 100 | } 101 | } 102 | 103 | module.exports = {Node, getTxHash} 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cryptoeconomics-an-introduction", 3 | "version": "1.0.0", 4 | "description": "Code for the course!", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "serve": "http-server public", 9 | "chapter1": "node c1_CentralPaymentOperator/paypalWithSigs.js", 10 | "build-c2": "browserify c2_NetworkDoubleSpends/paypalWithSigs.js -o public/bundle.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+ssh://git@github.com/cryptoeconomics-study/code.git" 15 | }, 16 | "keywords": [ 17 | "cryptoeconomics" 18 | ], 19 | "author": "Cryptoeconomics Study Team", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/cryptoeconomics-study/code/issues" 23 | }, 24 | "homepage": "https://github.com/cryptoeconomics-study/code#readme", 25 | "dependencies": { 26 | "crypto": "^1.0.1", 27 | "d3-random": "^1.1.2", 28 | "eslint-plugin-node": "^6.0.1", 29 | "eslint-plugin-promise": "^3.8.0", 30 | "eslint-plugin-standard": "^3.1.0", 31 | "eth-crypto": "^1.3.4", 32 | "hex-to-binary": "^1.0.1", 33 | "http-server": "^0.11.1", 34 | "lodash": "^4.17.14", 35 | "mocha": "^6.1.4", 36 | "npm": "^6.10.1", 37 | "seedrandom": "^2.4.4" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^5.16.0", 41 | "eslint-config-airbnb": "^17.1.1", 42 | "eslint-config-standard": "^11.0.0", 43 | "eslint-plugin-import": "^2.18.2", 44 | "eslint-plugin-jsx-a11y": "^6.2.3", 45 | "eslint-plugin-react": "^7.14.3" 46 | } 47 | } 48 | --------------------------------------------------------------------------------