marsxchain 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +28 -0
- package/account/index.js +43 -0
- package/account/index.test.js +29 -0
- package/api/index.js +96 -0
- package/api/pubsub.js +81 -0
- package/api-test.js +112 -0
- package/blockchain/block.js +144 -0
- package/blockchain/block.test.js +160 -0
- package/blockchain/index.js +53 -0
- package/config.js +27 -0
- package/course_logo_udemy.png +0 -0
- package/interpreter/index.js +251 -0
- package/interpreter/index.test.js +167 -0
- package/package.json +44 -0
- package/store/state.js +32 -0
- package/store/trie.js +60 -0
- package/store/trie.test.js +56 -0
- package/tmp.js +16 -0
- package/transaction/index.js +244 -0
- package/transaction/index.test.js +140 -0
- package/transaction/transaction-queue.js +21 -0
- package/util/index.js +22 -0
- package/util/index.test.js +22 -0
package/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
## Build Ethereum From Scratch - Smart Contracts and More
|
2
|
+
|
3
|
+
![Course Logo](course_logo_udemy.png)
|
4
|
+
|
5
|
+
This repository accompanies the "Build Ethereum From Scratch - Smart Contracts and More"
|
6
|
+
course by David Katz.
|
7
|
+
|
8
|
+
#### Take the course here:
|
9
|
+
|
10
|
+
[https://www.udemy.com/build-ethereum-from-scratch?couponCode=GITHUB](https://www.udemy.com/build-ethereum-from-scratch?couponCode=GITHUB)
|
11
|
+
|
12
|
+
In the course, you will build your own version of Ethereum. Ethereum can be described in two words. It's a:
|
13
|
+
|
14
|
+
#### Decentralized Computer.
|
15
|
+
|
16
|
+
A decentralized computer is like a normal computer. A normal computer executes a program using one machine.
|
17
|
+
|
18
|
+
But a decentralized computer executes a program using multiple machines. Every machine needs to agree upon the output of the program for its results to become official.
|
19
|
+
|
20
|
+
To build a decentralized computer, here are the essential elements:
|
21
|
+
|
22
|
+
1) A smart contract language.
|
23
|
+
2) A blockchain.
|
24
|
+
3) A network.
|
25
|
+
4) Transactions and accounts.
|
26
|
+
5) A state management data structure.
|
27
|
+
|
28
|
+
Definitely take the course if you're interested in exploring the concepts behind this project more deeply. The course is a line-by-line tutorial of this entire repository. And by the end of the course, you'll have your own version of Ethereum.
|
package/account/index.js
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
const { ec, keccakHash } = require('../util');
|
2
|
+
const { STARTING_BALANCE } = require('../config');
|
3
|
+
|
4
|
+
class Account {
|
5
|
+
constructor({ code } = {}) {
|
6
|
+
this.keyPair = ec.genKeyPair();
|
7
|
+
this.address = this.keyPair.getPublic().encode('hex');
|
8
|
+
this.balance = STARTING_BALANCE;
|
9
|
+
this.code = code || [];
|
10
|
+
this.generateCodeHash();
|
11
|
+
}
|
12
|
+
|
13
|
+
generateCodeHash() {
|
14
|
+
this.codeHash = this.code.length > 0
|
15
|
+
? keccakHash(this.address + this.code)
|
16
|
+
: null;
|
17
|
+
}
|
18
|
+
|
19
|
+
sign(data) {
|
20
|
+
return this.keyPair.sign(keccakHash(data));
|
21
|
+
}
|
22
|
+
|
23
|
+
toJSON() {
|
24
|
+
return {
|
25
|
+
address: this.address,
|
26
|
+
balance: this.balance,
|
27
|
+
code: this.code,
|
28
|
+
codeHash: this.codeHash
|
29
|
+
};
|
30
|
+
}
|
31
|
+
|
32
|
+
static verifySignature({ publicKey, data, signature }) {
|
33
|
+
const keyFromPublic = ec.keyFromPublic(publicKey, 'hex');
|
34
|
+
|
35
|
+
return keyFromPublic.verify(keccakHash(data), signature);
|
36
|
+
}
|
37
|
+
|
38
|
+
static calculateBalance({ address, state }) {
|
39
|
+
return state.getAccount({ address }).balance;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
module.exports = Account;
|
@@ -0,0 +1,29 @@
|
|
1
|
+
const Account = require('./index');
|
2
|
+
|
3
|
+
describe('Account', () => {
|
4
|
+
let account, data, signature;
|
5
|
+
|
6
|
+
beforeEach(() => {
|
7
|
+
account = new Account();
|
8
|
+
data = { foo: 'foo' };
|
9
|
+
signature = account.sign(data);
|
10
|
+
});
|
11
|
+
|
12
|
+
describe('verifySignature()', () => {
|
13
|
+
it('validates a signature generated by the account', () => {
|
14
|
+
expect(Account.verifySignature({
|
15
|
+
publicKey: account.address,
|
16
|
+
data,
|
17
|
+
signature
|
18
|
+
})).toBe(true);
|
19
|
+
});
|
20
|
+
|
21
|
+
it('invalidates a signature not generated by the account', () => {
|
22
|
+
expect(Account.verifySignature({
|
23
|
+
publicKey: new Account().address,
|
24
|
+
data,
|
25
|
+
signature
|
26
|
+
})).toBe(false);
|
27
|
+
});
|
28
|
+
});
|
29
|
+
});
|
package/api/index.js
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
const express = require('express');
|
2
|
+
const bodyParser = require('body-parser');
|
3
|
+
const request = require('request');
|
4
|
+
const Account = require('../account');
|
5
|
+
const Blockchain = require('../blockchain');
|
6
|
+
const Block = require('../blockchain/block');
|
7
|
+
const PubSub = require('./pubsub');
|
8
|
+
const State = require('../store/state');
|
9
|
+
const Transaction = require('../transaction');
|
10
|
+
const TransactionQueue = require('../transaction/transaction-queue');
|
11
|
+
|
12
|
+
const app = express();
|
13
|
+
app.use(bodyParser.json());
|
14
|
+
|
15
|
+
const state = new State();
|
16
|
+
const blockchain = new Blockchain({ state });
|
17
|
+
const transactionQueue = new TransactionQueue();
|
18
|
+
const pubsub = new PubSub({ blockchain, transactionQueue });
|
19
|
+
const account = new Account();
|
20
|
+
const transaction = Transaction.createTransaction({ account });
|
21
|
+
|
22
|
+
setTimeout(() => {
|
23
|
+
pubsub.broadcastTransaction(transaction);
|
24
|
+
}, 500);
|
25
|
+
|
26
|
+
app.get('/blockchain', (req, res, next) => {
|
27
|
+
const { chain } = blockchain;
|
28
|
+
|
29
|
+
res.json({ chain });
|
30
|
+
});
|
31
|
+
|
32
|
+
app.get('/blockchain/mine', (req, res, next) => {
|
33
|
+
const lastBlock = blockchain.chain[blockchain.chain.length-1];
|
34
|
+
const block = Block.mineBlock({
|
35
|
+
lastBlock,
|
36
|
+
beneficiary: account.address,
|
37
|
+
transactionSeries: transactionQueue.getTransactionSeries(),
|
38
|
+
stateRoot: state.getStateRoot()
|
39
|
+
});
|
40
|
+
|
41
|
+
blockchain.addBlock({ block, transactionQueue })
|
42
|
+
.then(() => {
|
43
|
+
pubsub.broadcastBlock(block);
|
44
|
+
|
45
|
+
res.json({ block });
|
46
|
+
})
|
47
|
+
.catch(next);
|
48
|
+
});
|
49
|
+
|
50
|
+
app.post('/account/transact', (req, res, next) => {
|
51
|
+
const { code, gasLimit, to, value } = req.body;
|
52
|
+
const transaction = Transaction.createTransaction({
|
53
|
+
account: !to ? new Account({ code }) : account,
|
54
|
+
gasLimit,
|
55
|
+
to,
|
56
|
+
value
|
57
|
+
});
|
58
|
+
|
59
|
+
pubsub.broadcastTransaction(transaction);
|
60
|
+
|
61
|
+
res.json({ transaction });
|
62
|
+
});
|
63
|
+
|
64
|
+
app.get('/account/balance', (req, res, next) => {
|
65
|
+
const { address } = req.query;
|
66
|
+
|
67
|
+
const balance = Account.calculateBalance({
|
68
|
+
address: address || account.address,
|
69
|
+
state
|
70
|
+
});
|
71
|
+
|
72
|
+
res.json({ balance });
|
73
|
+
});
|
74
|
+
|
75
|
+
app.use((err, req, res, next) => {
|
76
|
+
console.error('Internal server error:', err);
|
77
|
+
|
78
|
+
res.status(500).json({ message: err.message });
|
79
|
+
});
|
80
|
+
|
81
|
+
const peer = process.argv.includes('--peer');
|
82
|
+
const PORT = peer
|
83
|
+
? Math.floor(2000 + Math.random() * 1000)
|
84
|
+
: 3000;
|
85
|
+
|
86
|
+
if (peer) {
|
87
|
+
request('http://localhost:3000/blockchain', (error, response, body) => {
|
88
|
+
const { chain } = JSON.parse(body);
|
89
|
+
|
90
|
+
blockchain.replaceChain({ chain })
|
91
|
+
.then(() => console.log('Synchronized blockchain with the root node'))
|
92
|
+
.catch(error => console.error('Synchronization error:', error.message));
|
93
|
+
});
|
94
|
+
}
|
95
|
+
|
96
|
+
app.listen(PORT, () => console.log(`Listening at PORT: ${PORT}`));
|
package/api/pubsub.js
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
const PubNub = require('pubnub');
|
2
|
+
const Transaction = require('../transaction');
|
3
|
+
|
4
|
+
const credentials = {
|
5
|
+
publishKey: 'pub-c-1624883b-89a7-4a67-8438-447deac585a1',
|
6
|
+
subscribeKey: 'sub-c-94cf1061-53fc-447d-a7a6-dc2ce2cbc335',
|
7
|
+
secretKey: 'sec-c-OWUxOTFmNzctMTk1ZS00ZmM5LWJmOGYtZmE0YTIzMDNjNDY0'
|
8
|
+
};
|
9
|
+
|
10
|
+
const CHANNELS_MAP = {
|
11
|
+
TEST: 'TEST',
|
12
|
+
BLOCK: 'BLOCK',
|
13
|
+
TRANSACTION: 'TRANSACTION'
|
14
|
+
};
|
15
|
+
|
16
|
+
class PubSub {
|
17
|
+
constructor({ blockchain, transactionQueue }) {
|
18
|
+
this.pubnub = new PubNub(credentials);
|
19
|
+
this.blockchain = blockchain;
|
20
|
+
this.transactionQueue = transactionQueue;
|
21
|
+
this.subscribeToChannels();
|
22
|
+
this.listen();
|
23
|
+
}
|
24
|
+
|
25
|
+
subscribeToChannels() {
|
26
|
+
this.pubnub.subscribe({
|
27
|
+
channels: Object.values(CHANNELS_MAP)
|
28
|
+
});
|
29
|
+
}
|
30
|
+
|
31
|
+
publish({ channel, message }) {
|
32
|
+
this.pubnub.publish({ channel, message });
|
33
|
+
}
|
34
|
+
|
35
|
+
listen() {
|
36
|
+
this.pubnub.addListener({
|
37
|
+
message: messageObject => {
|
38
|
+
const { channel, message } = messageObject;
|
39
|
+
const parsedMessage = JSON.parse(message);
|
40
|
+
|
41
|
+
console.log('Message received. Channel:', channel);
|
42
|
+
|
43
|
+
switch (channel) {
|
44
|
+
case CHANNELS_MAP.BLOCK:
|
45
|
+
console.log('block message', message);
|
46
|
+
|
47
|
+
this.blockchain.addBlock({
|
48
|
+
block: parsedMessage,
|
49
|
+
transactionQueue: this.transactionQueue
|
50
|
+
}).then(() => console.log('New block accepted', parsedMessage))
|
51
|
+
.catch(error => console.error('New block rejected:', error.message));
|
52
|
+
break;
|
53
|
+
case CHANNELS_MAP.TRANSACTION:
|
54
|
+
console.log(`Received transaction: ${parsedMessage.id}`);
|
55
|
+
|
56
|
+
this.transactionQueue.add(new Transaction(parsedMessage));
|
57
|
+
|
58
|
+
break;
|
59
|
+
default:
|
60
|
+
return;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
});
|
64
|
+
}
|
65
|
+
|
66
|
+
broadcastBlock(block) {
|
67
|
+
this.publish({
|
68
|
+
channel: CHANNELS_MAP.BLOCK,
|
69
|
+
message: JSON.stringify(block)
|
70
|
+
});
|
71
|
+
}
|
72
|
+
|
73
|
+
broadcastTransaction(transaction) {
|
74
|
+
this.publish({
|
75
|
+
channel: CHANNELS_MAP.TRANSACTION,
|
76
|
+
message: JSON.stringify(transaction)
|
77
|
+
});
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
module.exports = PubSub;
|
package/api-test.js
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
const request = require('request');
|
2
|
+
|
3
|
+
const { OPCODE_MAP } = require('./interpreter');
|
4
|
+
const { STOP, ADD, PUSH, STORE, LOAD } = OPCODE_MAP;
|
5
|
+
|
6
|
+
const BASE_URL = 'http://localhost:3000';
|
7
|
+
|
8
|
+
const postTransact = ({ code, to, value, gasLimit }) => {
|
9
|
+
return new Promise((resolve, reject) => {
|
10
|
+
request(`${BASE_URL}/account/transact`, {
|
11
|
+
method: 'POST',
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
13
|
+
body: JSON.stringify({ code, to, value, gasLimit })
|
14
|
+
},(error, response, body) => {
|
15
|
+
return resolve(JSON.parse(body));
|
16
|
+
});
|
17
|
+
});
|
18
|
+
}
|
19
|
+
|
20
|
+
const getMine = () => {
|
21
|
+
return new Promise((resolve, reject) => {
|
22
|
+
setTimeout(() => {
|
23
|
+
request(`${BASE_URL}/blockchain/mine`, (error, response, body) => {
|
24
|
+
return resolve(JSON.parse(body));
|
25
|
+
});
|
26
|
+
}, 3000);
|
27
|
+
});
|
28
|
+
}
|
29
|
+
|
30
|
+
const getAccountBalance = ({ address } = {}) => {
|
31
|
+
return new Promise((resolve, reject) => {
|
32
|
+
request(
|
33
|
+
`${BASE_URL}/account/balance` + (address ? `?address=${address}` : ''),
|
34
|
+
(error, response, body) => {
|
35
|
+
return resolve(JSON.parse(body));
|
36
|
+
}
|
37
|
+
);
|
38
|
+
});
|
39
|
+
}
|
40
|
+
|
41
|
+
let toAccountData;
|
42
|
+
let smartContractAccountData;
|
43
|
+
|
44
|
+
postTransact({})
|
45
|
+
.then(postTransactResponse => {
|
46
|
+
console.log(
|
47
|
+
'postTransactResponse (Create Account Transaction)',
|
48
|
+
postTransactResponse
|
49
|
+
);
|
50
|
+
|
51
|
+
toAccountData = postTransactResponse.transaction.data.accountData;
|
52
|
+
|
53
|
+
return getMine();
|
54
|
+
}).then(getMineResponse => {
|
55
|
+
console.log('getMineResponse', getMineResponse);
|
56
|
+
|
57
|
+
return postTransact({ to: toAccountData.address, value: 20 });
|
58
|
+
})
|
59
|
+
.then(postTransactResponse2 => {
|
60
|
+
console.log(
|
61
|
+
'postTransactResponse2 (Standard Transaction)',
|
62
|
+
postTransactResponse2
|
63
|
+
);
|
64
|
+
|
65
|
+
const key = 'foo';
|
66
|
+
const value = 'bar';
|
67
|
+
const code = [PUSH, value, PUSH, key, STORE, PUSH, key, LOAD, STOP];
|
68
|
+
|
69
|
+
return postTransact({ code });
|
70
|
+
})
|
71
|
+
.then(postTransactResponse3 => {
|
72
|
+
console.log(
|
73
|
+
'postTransactResponse3 (Smart Contract)',
|
74
|
+
postTransactResponse3
|
75
|
+
);
|
76
|
+
|
77
|
+
smartContractAccountData = postTransactResponse3
|
78
|
+
.transaction
|
79
|
+
.data
|
80
|
+
.accountData;
|
81
|
+
|
82
|
+
return getMine();
|
83
|
+
})
|
84
|
+
.then(getMineResponse2 => {
|
85
|
+
console.log('getMineResponse2', getMineResponse2);
|
86
|
+
|
87
|
+
return postTransact({
|
88
|
+
to: smartContractAccountData.codeHash,
|
89
|
+
value: 0,
|
90
|
+
gasLimit: 100
|
91
|
+
});
|
92
|
+
})
|
93
|
+
.then(postTransactResponse4 => {
|
94
|
+
console.log(
|
95
|
+
'postTransactResponse4 (to the smart contract)',
|
96
|
+
postTransactResponse4
|
97
|
+
);
|
98
|
+
return getMine();
|
99
|
+
})
|
100
|
+
.then(getMineResponse3 => {
|
101
|
+
console.log('getMineResponse3', getMineResponse3);
|
102
|
+
|
103
|
+
return getAccountBalance();
|
104
|
+
})
|
105
|
+
.then(getAccountBalanceResponse => {
|
106
|
+
console.log('getAccountBalanceResponse', getAccountBalanceResponse);
|
107
|
+
|
108
|
+
return getAccountBalance({ address: toAccountData.address });
|
109
|
+
})
|
110
|
+
.then(getAccountBalanceResponse2 => {
|
111
|
+
console.log('getAccountBalanceResponse2', getAccountBalanceResponse2);
|
112
|
+
});
|
@@ -0,0 +1,144 @@
|
|
1
|
+
const { GENESIS_DATA, MINE_RATE } = require('../config');
|
2
|
+
const { keccakHash } = require('../util');
|
3
|
+
const Transaction = require('../transaction');
|
4
|
+
const Trie = require('../store/trie');
|
5
|
+
|
6
|
+
const HASH_LENGTH = 64;
|
7
|
+
const MAX_HASH_VALUE = parseInt('f'.repeat(HASH_LENGTH), 16);
|
8
|
+
const MAX_NONCE_VALUE = 2 ** 64;
|
9
|
+
|
10
|
+
class Block {
|
11
|
+
constructor({ blockHeaders, transactionSeries }) {
|
12
|
+
this.blockHeaders = blockHeaders;
|
13
|
+
this.transactionSeries = transactionSeries;
|
14
|
+
}
|
15
|
+
|
16
|
+
static calculateBlockTargetHash({ lastBlock }) {
|
17
|
+
const value = (MAX_HASH_VALUE / lastBlock.blockHeaders.difficulty).toString(16);
|
18
|
+
|
19
|
+
if (value.length > HASH_LENGTH) {
|
20
|
+
return 'f'.repeat(HASH_LENGTH);
|
21
|
+
}
|
22
|
+
|
23
|
+
return '0'.repeat(HASH_LENGTH - value.length) + value;
|
24
|
+
}
|
25
|
+
|
26
|
+
static adjustDifficulty({ lastBlock, timestamp }) {
|
27
|
+
const { difficulty } = lastBlock.blockHeaders;
|
28
|
+
|
29
|
+
if ((timestamp - lastBlock.blockHeaders.timestamp) > MINE_RATE) {
|
30
|
+
return difficulty - 1;
|
31
|
+
}
|
32
|
+
|
33
|
+
if (difficulty < 1) {
|
34
|
+
return 1;
|
35
|
+
}
|
36
|
+
|
37
|
+
return difficulty + 1;
|
38
|
+
}
|
39
|
+
|
40
|
+
static mineBlock({
|
41
|
+
lastBlock,
|
42
|
+
beneficiary,
|
43
|
+
transactionSeries,
|
44
|
+
stateRoot
|
45
|
+
}) {
|
46
|
+
const target = Block.calculateBlockTargetHash({ lastBlock });
|
47
|
+
const miningRewardTransaction = Transaction.createTransaction({
|
48
|
+
beneficiary
|
49
|
+
});
|
50
|
+
transactionSeries.push(miningRewardTransaction);
|
51
|
+
const transactionsTrie = Trie.buildTrie({ items: transactionSeries });
|
52
|
+
let timestamp, truncatedBlockHeaders, header, nonce, underTargetHash;
|
53
|
+
|
54
|
+
do {
|
55
|
+
timestamp = Date.now();
|
56
|
+
truncatedBlockHeaders = {
|
57
|
+
parentHash: keccakHash(lastBlock.blockHeaders),
|
58
|
+
beneficiary,
|
59
|
+
difficulty: Block.adjustDifficulty({ lastBlock, timestamp }),
|
60
|
+
number: lastBlock.blockHeaders.number + 1,
|
61
|
+
timestamp,
|
62
|
+
transactionsRoot: transactionsTrie.rootHash,
|
63
|
+
stateRoot
|
64
|
+
};
|
65
|
+
header = keccakHash(truncatedBlockHeaders);
|
66
|
+
nonce = Math.floor(Math.random() * MAX_NONCE_VALUE);
|
67
|
+
|
68
|
+
underTargetHash = keccakHash(header + nonce);
|
69
|
+
} while (underTargetHash > target);
|
70
|
+
|
71
|
+
return new this({
|
72
|
+
blockHeaders: { ...truncatedBlockHeaders, nonce },
|
73
|
+
transactionSeries
|
74
|
+
});
|
75
|
+
}
|
76
|
+
|
77
|
+
static genesis() {
|
78
|
+
return new Block(GENESIS_DATA);
|
79
|
+
}
|
80
|
+
|
81
|
+
static validateBlock({ lastBlock, block, state }) {
|
82
|
+
return new Promise((resolve, reject) => {
|
83
|
+
if (keccakHash(block) === keccakHash(Block.genesis())) {
|
84
|
+
return resolve();
|
85
|
+
}
|
86
|
+
|
87
|
+
if (keccakHash(lastBlock.blockHeaders) !== block.blockHeaders.parentHash) {
|
88
|
+
return reject(
|
89
|
+
new Error("The parent hash must be a hash of the last block's headers")
|
90
|
+
);
|
91
|
+
}
|
92
|
+
|
93
|
+
if (block.blockHeaders.number !== lastBlock.blockHeaders.number + 1) {
|
94
|
+
return reject(new Error('The block must increment the number by 1'));
|
95
|
+
}
|
96
|
+
|
97
|
+
if (
|
98
|
+
Math.abs(lastBlock.blockHeaders.difficulty - block.blockHeaders.difficulty) > 1
|
99
|
+
) {
|
100
|
+
return reject(new Error('The difficulty must only adjust by 1'));
|
101
|
+
}
|
102
|
+
|
103
|
+
const rebuiltTransactionsTrie = Trie.buildTrie({
|
104
|
+
items: block.transactionSeries
|
105
|
+
});
|
106
|
+
|
107
|
+
if (rebuiltTransactionsTrie.rootHash !== block.blockHeaders.transactionsRoot) {
|
108
|
+
return reject(
|
109
|
+
new Error(
|
110
|
+
`The rebuilt transactions root does not match the block's ` +
|
111
|
+
`transactions root: ${block.blockHeaders.transactionRoot}`
|
112
|
+
)
|
113
|
+
);
|
114
|
+
}
|
115
|
+
|
116
|
+
const target = Block.calculateBlockTargetHash({ lastBlock });
|
117
|
+
const { blockHeaders } = block;
|
118
|
+
const { nonce } = blockHeaders;
|
119
|
+
const truncatedBlockHeaders = { ...blockHeaders };
|
120
|
+
delete truncatedBlockHeaders.nonce;
|
121
|
+
const header = keccakHash(truncatedBlockHeaders);
|
122
|
+
const underTargetHash = keccakHash(header + nonce);
|
123
|
+
|
124
|
+
if (underTargetHash > target) {
|
125
|
+
return reject(new Error(
|
126
|
+
'The block does not meet the proof of work requirement'
|
127
|
+
));
|
128
|
+
}
|
129
|
+
|
130
|
+
Transaction.validateTransactionSeries({
|
131
|
+
state, transactionSeries: block.transactionSeries
|
132
|
+
}).then(resolve)
|
133
|
+
.catch(reject);
|
134
|
+
});
|
135
|
+
}
|
136
|
+
|
137
|
+
static runBlock({ block, state }) {
|
138
|
+
for (let transaction of block.transactionSeries) {
|
139
|
+
Transaction.runTransaction({ transaction, state });
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
module.exports = Block;
|
@@ -0,0 +1,160 @@
|
|
1
|
+
const Block = require('./block');
|
2
|
+
const State = require('../store/state');
|
3
|
+
const { keccakHash } = require('../util');
|
4
|
+
|
5
|
+
describe('Block', () => {
|
6
|
+
describe('calculateBlockTargetHash()', () => {
|
7
|
+
it('calculates the maximum hash when the last block difficulty is 1', () => {
|
8
|
+
expect(
|
9
|
+
Block
|
10
|
+
.calculateBlockTargetHash({ lastBlock: { blockHeaders: { difficulty: 1 } } })
|
11
|
+
).toEqual('f'.repeat(64));
|
12
|
+
});
|
13
|
+
|
14
|
+
it('calculates a low hash value when the last block difficulty is high', () => {
|
15
|
+
expect(
|
16
|
+
Block
|
17
|
+
.calculateBlockTargetHash({ lastBlock: { blockHeaders: { difficulty: 500 } } })
|
18
|
+
< '1'
|
19
|
+
).toBe(true);
|
20
|
+
});
|
21
|
+
});
|
22
|
+
|
23
|
+
describe('mineBlock()', () => {
|
24
|
+
let lastBlock, minedBlock;
|
25
|
+
|
26
|
+
beforeEach(() => {
|
27
|
+
lastBlock = Block.genesis();
|
28
|
+
minedBlock = Block.mineBlock({
|
29
|
+
lastBlock,
|
30
|
+
beneficiary: 'beneficiary',
|
31
|
+
transactionSeries: []
|
32
|
+
});
|
33
|
+
});
|
34
|
+
|
35
|
+
it('mines a block', () => {
|
36
|
+
expect(minedBlock).toBeInstanceOf(Block);
|
37
|
+
});
|
38
|
+
|
39
|
+
it('mines a block that meets the proof of work requirement', () => {
|
40
|
+
const target = Block.calculateBlockTargetHash({ lastBlock });
|
41
|
+
const { blockHeaders } = minedBlock;
|
42
|
+
const { nonce } = blockHeaders;
|
43
|
+
const truncatedBlockHeaders = { ...blockHeaders };
|
44
|
+
delete truncatedBlockHeaders.nonce;
|
45
|
+
const header = keccakHash(truncatedBlockHeaders);
|
46
|
+
const underTargetHash = keccakHash(header + nonce);
|
47
|
+
|
48
|
+
expect(underTargetHash < target).toBe(true);
|
49
|
+
});
|
50
|
+
});
|
51
|
+
|
52
|
+
describe('adjustDifficulty()', () => {
|
53
|
+
it('keeps the difficulty above 0', () => {
|
54
|
+
expect(
|
55
|
+
Block.adjustDifficulty({
|
56
|
+
lastBlock: { blockHeaders: { difficulty: 0 } },
|
57
|
+
timestamp: Date.now()
|
58
|
+
})
|
59
|
+
).toEqual(1);
|
60
|
+
});
|
61
|
+
|
62
|
+
it('increases the difficulty for a quickly mined block', () => {
|
63
|
+
expect(
|
64
|
+
Block.adjustDifficulty({
|
65
|
+
lastBlock: { blockHeaders: { difficulty: 5, timestamp: 1000 } },
|
66
|
+
timestamp: 3000
|
67
|
+
})
|
68
|
+
).toEqual(6);
|
69
|
+
});
|
70
|
+
|
71
|
+
it('decreases the difficulty for a slowly mined block', () => {
|
72
|
+
expect(
|
73
|
+
Block.adjustDifficulty({
|
74
|
+
lastBlock: { blockHeaders: { difficulty: 5, timestamp: 1000 } },
|
75
|
+
timestamp: 20000
|
76
|
+
})
|
77
|
+
).toEqual(4);
|
78
|
+
});
|
79
|
+
});
|
80
|
+
|
81
|
+
describe('validateBlock()', () => {
|
82
|
+
let block, lastBlock, state;
|
83
|
+
|
84
|
+
beforeEach(() => {
|
85
|
+
lastBlock = Block.genesis();
|
86
|
+
block = Block.mineBlock({
|
87
|
+
lastBlock,
|
88
|
+
beneficiary: 'beneficiary',
|
89
|
+
transactionSeries: []
|
90
|
+
});
|
91
|
+
state = new State();
|
92
|
+
});
|
93
|
+
|
94
|
+
it('resolves when the block is the genesis block', () => {
|
95
|
+
expect(Block.validateBlock({
|
96
|
+
block: Block.genesis(),
|
97
|
+
state
|
98
|
+
})).resolves;
|
99
|
+
});
|
100
|
+
|
101
|
+
it('resolves if block is valid', () => {
|
102
|
+
expect(Block.validateBlock({ lastBlock, block, state })).resolves;
|
103
|
+
});
|
104
|
+
|
105
|
+
it('rejects when the parentHash is invalid', () => {
|
106
|
+
block.blockHeaders.parentHash = 'foo';
|
107
|
+
|
108
|
+
expect(Block.validateBlock({ lastBlock, block, state }))
|
109
|
+
.rejects
|
110
|
+
.toMatchObject({
|
111
|
+
message: "The parent hash must be a hash of the last block's headers"
|
112
|
+
});
|
113
|
+
});
|
114
|
+
|
115
|
+
it('rejects when the number is not increased by one', () => {
|
116
|
+
block.blockHeaders.number = 500;
|
117
|
+
|
118
|
+
expect(Block.validateBlock({ lastBlock, block, state }))
|
119
|
+
.rejects
|
120
|
+
.toMatchObject({
|
121
|
+
message: 'The block must increment the number by 1'
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
it('rejects when the difficulty adjusts by more than 1', () => {
|
126
|
+
block.blockHeaders.difficulty = 999;
|
127
|
+
|
128
|
+
expect(Block.validateBlock({ lastBlock, block, state }))
|
129
|
+
.rejects
|
130
|
+
.toMatchObject({
|
131
|
+
message: 'The difficulty must only adjust by 1'
|
132
|
+
});
|
133
|
+
});
|
134
|
+
|
135
|
+
it('rejects when the proof of work requirement is not met', () => {
|
136
|
+
const originalCalculateBlockTargetHash = Block.calculateBlockTargetHash;
|
137
|
+
Block.calculateBlockTargetHash = () => {
|
138
|
+
return '0'.repeat(64);
|
139
|
+
}
|
140
|
+
|
141
|
+
expect(Block.validateBlock({ lastBlock, block, state }))
|
142
|
+
.rejects
|
143
|
+
.toMatchObject({
|
144
|
+
message: 'The block does not meet the proof of work requirement'
|
145
|
+
});
|
146
|
+
|
147
|
+
Block.calculateBlockTargetHash = originalCalculateBlockTargetHash;
|
148
|
+
});
|
149
|
+
|
150
|
+
it('rejects when the transactionSeries is not valid', () => {
|
151
|
+
block.transactionSeries = ['foo'];
|
152
|
+
|
153
|
+
expect(Block.validateBlock({ state, lastBlock, block }))
|
154
|
+
.rejects
|
155
|
+
.toMatchObject({
|
156
|
+
message: /rebuilt transactions root does not match/
|
157
|
+
});
|
158
|
+
});
|
159
|
+
});
|
160
|
+
});
|