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