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