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
@@ -0,0 +1,244 @@
|
|
1
|
+
const uuid = require('uuid/v4');
|
2
|
+
const Account = require('../account');
|
3
|
+
const Interpreter = require('../interpreter');
|
4
|
+
const { MINING_REWARD } = require('../config');
|
5
|
+
|
6
|
+
const TRANSACTION_TYPE_MAP = {
|
7
|
+
CREATE_ACCOUNT: 'CREATE_ACCOUNT',
|
8
|
+
TRANSACT: 'TRANSACT',
|
9
|
+
MINING_REWARD: 'MINING_REWARD'
|
10
|
+
};
|
11
|
+
|
12
|
+
class Transaction {
|
13
|
+
constructor({ id, from, to, value, data, signature, gasLimit }) {
|
14
|
+
this.id = id || uuid();
|
15
|
+
this.from = from || '-';
|
16
|
+
this.to = to || '-';
|
17
|
+
this.value = value || 0;
|
18
|
+
this.data = data || '-';
|
19
|
+
this.signature = signature || '-';
|
20
|
+
this.gasLimit = gasLimit || 0;
|
21
|
+
}
|
22
|
+
|
23
|
+
static createTransaction({ account, to, value, beneficiary, gasLimit }) {
|
24
|
+
if (beneficiary) {
|
25
|
+
return new Transaction({
|
26
|
+
to: beneficiary,
|
27
|
+
value: MINING_REWARD,
|
28
|
+
gasLimit,
|
29
|
+
data: { type: TRANSACTION_TYPE_MAP.MINING_REWARD }
|
30
|
+
});
|
31
|
+
}
|
32
|
+
|
33
|
+
if (to) {
|
34
|
+
const transactionData = {
|
35
|
+
id: uuid(),
|
36
|
+
from: account.address,
|
37
|
+
to,
|
38
|
+
value: value || 0,
|
39
|
+
gasLimit: gasLimit || 0,
|
40
|
+
data: { type: TRANSACTION_TYPE_MAP.TRANSACT }
|
41
|
+
};
|
42
|
+
|
43
|
+
return new Transaction({
|
44
|
+
...transactionData,
|
45
|
+
signature: account.sign(transactionData)
|
46
|
+
});
|
47
|
+
}
|
48
|
+
|
49
|
+
return new Transaction({
|
50
|
+
data: {
|
51
|
+
type: TRANSACTION_TYPE_MAP.CREATE_ACCOUNT,
|
52
|
+
accountData: account.toJSON()
|
53
|
+
}
|
54
|
+
});
|
55
|
+
}
|
56
|
+
|
57
|
+
static validateStandardTransaction({ state, transaction }) {
|
58
|
+
return new Promise((resolve, reject) => {
|
59
|
+
const { id, from, signature, value, to, gasLimit } = transaction;
|
60
|
+
const transactionData = { ...transaction };
|
61
|
+
delete transactionData.signature;
|
62
|
+
|
63
|
+
if (!Account.verifySignature({
|
64
|
+
publicKey: from,
|
65
|
+
data: transactionData,
|
66
|
+
signature
|
67
|
+
})) {
|
68
|
+
return reject(new Error(`Transaction ${id} signature is invalid`));
|
69
|
+
}
|
70
|
+
|
71
|
+
const fromBalance = state.getAccount({ address: from }).balance;
|
72
|
+
|
73
|
+
if ((value + gasLimit) > fromBalance) {
|
74
|
+
return reject(new Error(
|
75
|
+
`Transaction value and gasLimit: ${value} exceeds balance: ${fromBalance}`
|
76
|
+
));
|
77
|
+
}
|
78
|
+
|
79
|
+
const toAccount = state.getAccount({ address: to });
|
80
|
+
|
81
|
+
if (!toAccount) {
|
82
|
+
return reject(new Error(
|
83
|
+
`The to field: ${to} does not exist`
|
84
|
+
));
|
85
|
+
}
|
86
|
+
|
87
|
+
if (toAccount.codeHash) {
|
88
|
+
const { gasUsed } = new Interpreter({
|
89
|
+
storageTrie: state.storageTrieMap[toAccount.codeHash]
|
90
|
+
}).runCode(toAccount.code);
|
91
|
+
|
92
|
+
if (gasUsed > gasLimit) {
|
93
|
+
return reject(new Error(
|
94
|
+
`Transaction needs more gas. Provided: ${gasLimit}. Needs: ${gasUsed}.`
|
95
|
+
));
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
return resolve();
|
100
|
+
});
|
101
|
+
}
|
102
|
+
|
103
|
+
static validateCreateAccountTransaction({ transaction }) {
|
104
|
+
return new Promise((resolve, reject) => {
|
105
|
+
const expectedAccountDataFields = Object.keys(new Account().toJSON());
|
106
|
+
const fields = Object.keys(transaction.data.accountData);
|
107
|
+
|
108
|
+
if (fields.length !== expectedAccountDataFields.length) {
|
109
|
+
return reject(
|
110
|
+
new Error(`The transaction account data has an incorrect number of fields`)
|
111
|
+
);
|
112
|
+
}
|
113
|
+
|
114
|
+
fields.forEach(field => {
|
115
|
+
if (!expectedAccountDataFields.includes(field)) {
|
116
|
+
return reject(new Error(
|
117
|
+
`The field: ${field}, is unexpected for account data`
|
118
|
+
));
|
119
|
+
}
|
120
|
+
});
|
121
|
+
|
122
|
+
return resolve();
|
123
|
+
});
|
124
|
+
}
|
125
|
+
|
126
|
+
static validateMiningRewardTransaction({ transaction }) {
|
127
|
+
return new Promise((resolve, reject) => {
|
128
|
+
const { value } = transaction;
|
129
|
+
|
130
|
+
if (value !== MINING_REWARD) {
|
131
|
+
return reject(new Error(
|
132
|
+
`The provided mining reward value: ${value} does not equal ` +
|
133
|
+
`the official value: ${MINING_REWARD}`
|
134
|
+
));
|
135
|
+
}
|
136
|
+
|
137
|
+
return resolve();
|
138
|
+
});
|
139
|
+
}
|
140
|
+
|
141
|
+
static validateTransactionSeries({ transactionSeries, state }) {
|
142
|
+
return new Promise(async (resolve, reject) => {
|
143
|
+
for (let transaction of transactionSeries) {
|
144
|
+
try {
|
145
|
+
switch (transaction.data.type) {
|
146
|
+
case TRANSACTION_TYPE_MAP.CREATE_ACCOUNT:
|
147
|
+
await Transaction.validateCreateAccountTransaction({
|
148
|
+
transaction
|
149
|
+
});
|
150
|
+
break;
|
151
|
+
case TRANSACTION_TYPE_MAP.TRANSACT:
|
152
|
+
await Transaction.validateStandardTransaction({
|
153
|
+
state,
|
154
|
+
transaction
|
155
|
+
});
|
156
|
+
break;
|
157
|
+
case TRANSACTION_TYPE_MAP.MINING_REWARD:
|
158
|
+
await Transaction.validateMiningRewardTransaction({
|
159
|
+
state,
|
160
|
+
transaction
|
161
|
+
});
|
162
|
+
break;
|
163
|
+
default:
|
164
|
+
break;
|
165
|
+
}
|
166
|
+
} catch (error) {
|
167
|
+
return reject(error);
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
return resolve();
|
172
|
+
});
|
173
|
+
}
|
174
|
+
|
175
|
+
static runTransaction({ state, transaction }) {
|
176
|
+
switch(transaction.data.type) {
|
177
|
+
case TRANSACTION_TYPE_MAP.TRANSACT:
|
178
|
+
Transaction.runStandardTransaction({ state, transaction });
|
179
|
+
console.log(
|
180
|
+
' -- Updated account data to reflect the standard transaction'
|
181
|
+
);
|
182
|
+
break;
|
183
|
+
case TRANSACTION_TYPE_MAP.CREATE_ACCOUNT:
|
184
|
+
Transaction.runCreateAccountTransaction({ state, transaction });
|
185
|
+
console.log(' -- Stored the account data');
|
186
|
+
break;
|
187
|
+
case TRANSACTION_TYPE_MAP.MINING_REWARD:
|
188
|
+
Transaction.runMiningRewardTransaction({ state, transaction });
|
189
|
+
console.log(' -- Updated account data to reflect the mining reward');
|
190
|
+
break;
|
191
|
+
default:
|
192
|
+
break;
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
static runStandardTransaction({ state, transaction }) {
|
197
|
+
const fromAccount = state.getAccount({ address: transaction.from });
|
198
|
+
const toAccount = state.getAccount({ address: transaction.to });
|
199
|
+
|
200
|
+
let gasUsed = 0;
|
201
|
+
let result;
|
202
|
+
|
203
|
+
if (toAccount.codeHash) {
|
204
|
+
const interpreter = new Interpreter({
|
205
|
+
storageTrie: state.storageTrieMap[toAccount.codeHash]
|
206
|
+
});
|
207
|
+
({ gasUsed, result } = interpreter.runCode(toAccount.code));
|
208
|
+
|
209
|
+
console.log(
|
210
|
+
` -*- Smart contract execution: ${transaction.id} - RESULT: ${result}`
|
211
|
+
);
|
212
|
+
}
|
213
|
+
|
214
|
+
const { value, gasLimit } = transaction;
|
215
|
+
const refund = gasLimit - gasUsed;
|
216
|
+
|
217
|
+
fromAccount.balance -= value;
|
218
|
+
fromAccount.balance -= gasLimit;
|
219
|
+
fromAccount.balance += refund;
|
220
|
+
toAccount.balance += value;
|
221
|
+
toAccount.balance += gasUsed;
|
222
|
+
|
223
|
+
state.putAccount({ address: transaction.from, accountData: fromAccount });
|
224
|
+
state.putAccount({ address: transaction.to, accountData: toAccount });
|
225
|
+
}
|
226
|
+
|
227
|
+
static runCreateAccountTransaction({ state, transaction }) {
|
228
|
+
const { accountData } = transaction.data;
|
229
|
+
const { address, codeHash } = accountData;
|
230
|
+
|
231
|
+
state.putAccount({ address: codeHash ? codeHash : address, accountData });
|
232
|
+
}
|
233
|
+
|
234
|
+
static runMiningRewardTransaction({ state, transaction }) {
|
235
|
+
const { to, value } = transaction;
|
236
|
+
const accountData = state.getAccount({ address: to });
|
237
|
+
|
238
|
+
accountData.balance += value;
|
239
|
+
|
240
|
+
state.putAccount({ address: to, accountData });
|
241
|
+
}
|
242
|
+
}
|
243
|
+
|
244
|
+
module.exports = Transaction;
|
@@ -0,0 +1,140 @@
|
|
1
|
+
const Transaction = require('./index');
|
2
|
+
const Account = require('../account');
|
3
|
+
const State = require('../store/state');
|
4
|
+
|
5
|
+
describe('Transaction', () => {
|
6
|
+
let account;
|
7
|
+
let standardTransaction;
|
8
|
+
let createAccountTransaction;
|
9
|
+
let state;
|
10
|
+
let toAccount;
|
11
|
+
let miningRewardTransaction;
|
12
|
+
|
13
|
+
beforeEach(() => {
|
14
|
+
account = new Account();
|
15
|
+
toAccount = new Account();
|
16
|
+
state = new State();
|
17
|
+
state.putAccount({ address: account.address, accountData: account });
|
18
|
+
state.putAccount({ address: toAccount.address, accountData: toAccount });
|
19
|
+
|
20
|
+
standardTransaction = Transaction.createTransaction({
|
21
|
+
account,
|
22
|
+
to: toAccount.address,
|
23
|
+
value: 50
|
24
|
+
});
|
25
|
+
createAccountTransaction = Transaction.createTransaction({
|
26
|
+
account
|
27
|
+
});
|
28
|
+
miningRewardTransaction = Transaction.createTransaction({
|
29
|
+
beneficiary: account.address
|
30
|
+
})
|
31
|
+
});
|
32
|
+
|
33
|
+
describe('validateStandardTransaction()', () => {
|
34
|
+
it('validates a valid transaction', () => {
|
35
|
+
expect(Transaction.validateStandardTransaction({
|
36
|
+
transaction: standardTransaction,
|
37
|
+
state
|
38
|
+
})).resolves;
|
39
|
+
});
|
40
|
+
|
41
|
+
it('does not validate a malformed transaction', () => {
|
42
|
+
standardTransaction.to = 'different-recipient';
|
43
|
+
|
44
|
+
expect(Transaction.validateStandardTransaction({
|
45
|
+
transaction: standardTransaction,
|
46
|
+
state
|
47
|
+
})).rejects.toMatchObject({ message: /invalid/ });
|
48
|
+
});
|
49
|
+
|
50
|
+
it('does not validate when the value exceeds the balance', () => {
|
51
|
+
standardTransaction = Transaction.createTransaction({
|
52
|
+
account,
|
53
|
+
to: toAccount.address,
|
54
|
+
value: 9001
|
55
|
+
});
|
56
|
+
|
57
|
+
expect(Transaction.validateStandardTransaction({
|
58
|
+
transaction: standardTransaction,
|
59
|
+
state
|
60
|
+
})).rejects.toMatchObject({ message: /exceeds/ });
|
61
|
+
});
|
62
|
+
|
63
|
+
it('does not validate when the `to` address does not exist', () => {
|
64
|
+
standardTransaction = Transaction.createTransaction({
|
65
|
+
account,
|
66
|
+
to: 'foo-recipient',
|
67
|
+
value: 50
|
68
|
+
});
|
69
|
+
|
70
|
+
expect(Transaction.validateStandardTransaction({
|
71
|
+
transaction: standardTransaction,
|
72
|
+
state
|
73
|
+
})).rejects.toMatchObject({ message: /does not exist/ });
|
74
|
+
});
|
75
|
+
|
76
|
+
it('does not validate when the gasLimit exceeds the balance', () => {
|
77
|
+
standardTransaction = Transaction.createTransaction({
|
78
|
+
account,
|
79
|
+
to: 'foo-recipient',
|
80
|
+
gasLimit: 9001
|
81
|
+
});
|
82
|
+
|
83
|
+
expect(Transaction.validateStandardTransaction({
|
84
|
+
transaction: standardTransaction,
|
85
|
+
state
|
86
|
+
})).rejects.toMatchObject({ message: /exceeds/ });
|
87
|
+
});
|
88
|
+
|
89
|
+
it('does not validate when the gasUsed for the code exceeds the gasLimit', () => {
|
90
|
+
const codeHash = 'foo-codeHash';
|
91
|
+
const code = ['PUSH', 1, 'PUSH', 2, 'ADD', 'STOP'];
|
92
|
+
|
93
|
+
state.putAccount({
|
94
|
+
address: codeHash,
|
95
|
+
accountData: { code, codeHash }
|
96
|
+
});
|
97
|
+
|
98
|
+
standardTransaction = Transaction.createTransaction({
|
99
|
+
account,
|
100
|
+
to: codeHash,
|
101
|
+
gasLimit: 0
|
102
|
+
});
|
103
|
+
|
104
|
+
expect(Transaction.validateStandardTransaction({
|
105
|
+
transaction: standardTransaction,
|
106
|
+
state
|
107
|
+
})).rejects.toMatchObject({ message: /Transaction needs more gas/ });
|
108
|
+
});
|
109
|
+
});
|
110
|
+
|
111
|
+
describe('validateCreateAccountTransaction()', () => {
|
112
|
+
it('validates a create account transaction', () => {
|
113
|
+
expect(Transaction.validateCreateAccountTransaction({
|
114
|
+
transaction: createAccountTransaction
|
115
|
+
})).resolves;
|
116
|
+
});
|
117
|
+
|
118
|
+
it('does not validate a non create account transaction', () => {
|
119
|
+
expect(Transaction.validateCreateAccountTransaction({
|
120
|
+
transaction: standardTransaction
|
121
|
+
})).rejects.toMatchObject({ message: /incorrect/ });
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
describe('validateMiningRewardTransaction', () => {
|
126
|
+
it('validates a mining reward transaction', () => {
|
127
|
+
expect(Transaction.validateMiningRewardTransaction({
|
128
|
+
transaction: miningRewardTransaction
|
129
|
+
})).resolves;
|
130
|
+
});
|
131
|
+
|
132
|
+
it('does not validate a tampered with mining reward transaction', () => {
|
133
|
+
miningRewardTransaction.value = 9001;
|
134
|
+
|
135
|
+
expect(Transaction.validateMiningRewardTransaction({
|
136
|
+
transaction: miningRewardTransaction
|
137
|
+
})).rejects.toMatchObject({ message: /does not equal the official/ });
|
138
|
+
});
|
139
|
+
});
|
140
|
+
});
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class TransactionQueue {
|
2
|
+
constructor() {
|
3
|
+
this.transactionMap = {};
|
4
|
+
}
|
5
|
+
|
6
|
+
add(transaction) {
|
7
|
+
this.transactionMap[transaction.id] = transaction;
|
8
|
+
}
|
9
|
+
|
10
|
+
getTransactionSeries() {
|
11
|
+
return Object.values(this.transactionMap);
|
12
|
+
}
|
13
|
+
|
14
|
+
clearBlockTransactions({ transactionSeries }) {
|
15
|
+
for (let transaction of transactionSeries) {
|
16
|
+
delete this.transactionMap[transaction.id];
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
module.exports = TransactionQueue;
|
package/util/index.js
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
const keccak256 = require('js-sha3').keccak256;
|
2
|
+
const EC = require('elliptic').ec;
|
3
|
+
|
4
|
+
const ec = new EC('secp256k1');
|
5
|
+
|
6
|
+
const sortCharacters = data => {
|
7
|
+
return JSON.stringify(data).split('').sort().join('');
|
8
|
+
}
|
9
|
+
|
10
|
+
const keccakHash = data => {
|
11
|
+
const hash = keccak256.create();
|
12
|
+
|
13
|
+
hash.update(sortCharacters(data));
|
14
|
+
|
15
|
+
return hash.hex();
|
16
|
+
}
|
17
|
+
|
18
|
+
module.exports = {
|
19
|
+
sortCharacters,
|
20
|
+
keccakHash,
|
21
|
+
ec
|
22
|
+
};
|
@@ -0,0 +1,22 @@
|
|
1
|
+
const { sortCharacters, keccakHash } = require('./index');
|
2
|
+
|
3
|
+
describe('util', () => {
|
4
|
+
describe('sortCharacters()', () => {
|
5
|
+
it('creates the same string for objects with the same properties in a different order', () => {
|
6
|
+
expect(sortCharacters({ foo: 'foo', bar: 'bar' }))
|
7
|
+
.toEqual(sortCharacters({ bar: 'bar', foo: 'foo' }));
|
8
|
+
});
|
9
|
+
|
10
|
+
it('creates a different string for different objects', () => {
|
11
|
+
expect(sortCharacters({ foo: 'foo' }))
|
12
|
+
.not.toEqual(sortCharacters({ bar: 'bar' }));
|
13
|
+
});
|
14
|
+
});
|
15
|
+
|
16
|
+
describe('keccakHash()', () => {
|
17
|
+
it('produces a keccak256 hash', () => {
|
18
|
+
expect(keccakHash('foo'))
|
19
|
+
.toEqual('b2a7ad9b4a2ee6d984cc5c2ad81d0c2b2902fa410670aa3f2f4f668a1f80611c');
|
20
|
+
});
|
21
|
+
});
|
22
|
+
});
|