├── .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 |
--------------------------------------------------------------------------------