marsxchain 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ });