marsxchain 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+

|
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
|
+
});
|