@ocap/tx-protocols 1.6.3 → 1.6.10

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.
Files changed (52) hide show
  1. package/README.md +36 -0
  2. package/lib/execute.js +97 -30
  3. package/lib/index.js +56 -14
  4. package/lib/protocols/account/declare.js +36 -30
  5. package/lib/protocols/account/delegate.js +78 -40
  6. package/lib/protocols/account/migrate.js +65 -49
  7. package/lib/protocols/account/revoke-delegate.js +39 -23
  8. package/lib/protocols/asset/acquire-v2.js +159 -0
  9. package/lib/protocols/asset/acquire-v3.js +242 -0
  10. package/lib/protocols/asset/calls/README.md +5 -0
  11. package/lib/protocols/asset/calls/transfer-token.js +37 -0
  12. package/lib/protocols/asset/calls/transfer.js +29 -0
  13. package/lib/protocols/asset/create.js +85 -36
  14. package/lib/protocols/asset/mint.js +133 -0
  15. package/lib/protocols/asset/pipes/exec-mint-hook.js +59 -0
  16. package/lib/protocols/asset/pipes/extract-factory-tokens.js +18 -0
  17. package/lib/protocols/asset/pipes/verify-acquire-params.js +30 -0
  18. package/lib/protocols/asset/pipes/verify-itx-address.js +41 -0
  19. package/lib/protocols/asset/pipes/verify-itx-assets.js +49 -0
  20. package/lib/protocols/asset/pipes/verify-itx-variables.js +26 -0
  21. package/lib/protocols/asset/pipes/verify-mint-limit.js +13 -0
  22. package/lib/protocols/asset/update.js +76 -44
  23. package/lib/protocols/factory/create.js +146 -0
  24. package/lib/protocols/governance/claim-stake.js +219 -0
  25. package/lib/protocols/governance/revoke-stake.js +136 -0
  26. package/lib/protocols/governance/stake.js +176 -0
  27. package/lib/protocols/rollup/claim-reward.js +283 -0
  28. package/lib/protocols/rollup/create-block.js +333 -0
  29. package/lib/protocols/rollup/create.js +169 -0
  30. package/lib/protocols/rollup/join.js +156 -0
  31. package/lib/protocols/rollup/leave.js +127 -0
  32. package/lib/protocols/rollup/migrate-contract.js +53 -0
  33. package/lib/protocols/rollup/migrate-token.js +66 -0
  34. package/lib/protocols/rollup/pause.js +52 -0
  35. package/lib/protocols/rollup/pipes/ensure-service-fee.js +37 -0
  36. package/lib/protocols/rollup/pipes/ensure-validator.js +10 -0
  37. package/lib/protocols/rollup/pipes/verify-evidence.js +37 -0
  38. package/lib/protocols/rollup/pipes/verify-paused.js +10 -0
  39. package/lib/protocols/rollup/pipes/verify-signers.js +88 -0
  40. package/lib/protocols/rollup/resume.js +52 -0
  41. package/lib/protocols/rollup/update.js +98 -0
  42. package/lib/protocols/token/create.js +150 -0
  43. package/lib/protocols/token/deposit-v2.js +241 -0
  44. package/lib/protocols/token/withdraw-v2.js +255 -0
  45. package/lib/protocols/trade/exchange-v2.js +179 -0
  46. package/lib/protocols/trade/transfer-v2.js +136 -0
  47. package/lib/protocols/trade/transfer-v3.js +241 -0
  48. package/lib/util.js +325 -2
  49. package/package.json +23 -16
  50. package/lib/protocols/misc/poke.js +0 -106
  51. package/lib/protocols/trade/exchange.js +0 -139
  52. package/lib/protocols/trade/transfer.js +0 -101
@@ -0,0 +1,333 @@
1
+ /* eslint-disable indent */
2
+ const pick = require('lodash/pick');
3
+ const Error = require('@ocap/util/lib/error');
4
+ const MerkleTree = require('@ocap/merkle-tree');
5
+ const Joi = require('@arcblock/validator');
6
+ const { formatMessage } = require('@ocap/message');
7
+ const { toStakeAddress } = require('@arcblock/did-util');
8
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
9
+ const { account, stake, rollup, rollupBlock, tx: Tx } = require('@ocap/state');
10
+
11
+ // eslint-disable-next-line global-require
12
+ const debug = require('debug')(`${require('../../../package.json').name}:create-rollup-block`);
13
+
14
+ const VerifySigners = require('./pipes/verify-signers');
15
+ const VerifyEvidence = require('./pipes/verify-evidence');
16
+ const VerifyPaused = require('./pipes/verify-paused');
17
+ const { applyTokenChange, ensureBlockReward, getRewardLocker } = require('../../util');
18
+
19
+ const runner = new Runner();
20
+
21
+ // 1. verify itx
22
+ const schema = Joi.object({
23
+ hash: Joi.string().regex(Joi.patterns.txHash).required(),
24
+ height: Joi.number().integer().greater(0).required(),
25
+ merkleRoot: Joi.string().regex(Joi.patterns.txHash).required(),
26
+ previousHash: Joi.string().when('height', {
27
+ is: 1,
28
+ then: Joi.string().optional().allow(''),
29
+ otherwise: Joi.string().regex(Joi.patterns.txHash).required(),
30
+ }),
31
+ txsHash: Joi.string().regex(Joi.patterns.txHash).required(),
32
+ txsList: Joi.array().items(Joi.string().regex(Joi.patterns.txHash).required()).min(1).unique().required(),
33
+ proposer: Joi.DID().wallet('ethereum').required(),
34
+ signaturesList: Joi.schemas.multiSig.min(1).required(),
35
+ rollup: Joi.DID().role('ROLE_ROLLUP').required(),
36
+ minReward: Joi.BN().min(0).required(),
37
+ data: Joi.any().optional(),
38
+ }).options({ stripUnknown: true, noDefaults: false });
39
+
40
+ runner.use((context, next) => {
41
+ const { tx, itx } = context;
42
+
43
+ context.formattedItx = formatMessage('CreateRollupBlockTx', itx);
44
+
45
+ const { error } = schema.validate(itx);
46
+ if (error) {
47
+ return next(new Error('INVALID_TX', error.message));
48
+ }
49
+
50
+ // ensure proposer same with tx.from
51
+ if (tx.from !== itx.proposer) {
52
+ return next(new Error('INVALID_TX', 'itx.proposer must be same with tx.from'));
53
+ }
54
+
55
+ // ensure proposer in signer list
56
+ if (tx.signaturesList.some((x) => x.signer === itx.proposer) === false) {
57
+ return next(new Error('INVALID_TX', 'itx.proposer must exist in signatures'));
58
+ }
59
+
60
+ // ensure tx hash match with txs
61
+ const txsHash = MerkleTree.getListHash(itx.txsList);
62
+ if (txsHash !== itx.txsHash) {
63
+ return next(new Error('INVALID_TX', 'itx.txsHash does not match with itx.txs'));
64
+ }
65
+
66
+ // ensure block hash is correct
67
+ const blockHash = MerkleTree.getBlockHash({
68
+ ...pick(itx, ['height', 'merkleRoot', 'txsHash']),
69
+ previousHash: itx.height === 1 ? '' : itx.previousHash,
70
+ });
71
+ if (blockHash !== itx.hash) {
72
+ return next(new Error('INVALID_TX', 'itx.hash invalid'));
73
+ }
74
+
75
+ return next();
76
+ });
77
+
78
+ // 2. verify rollup state & height
79
+ runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
80
+ runner.use(VerifyPaused());
81
+ runner.use((context, next) => {
82
+ const { itx, rollupState } = context;
83
+ if (itx.height !== rollupState.blockHeight + 1) {
84
+ return next(
85
+ new Error('INVALID_TX', `Expect new block height to be ${rollupState.blockHeight + 1}, got ${itx.height}`)
86
+ );
87
+ }
88
+
89
+ // ensure proposer in validator list
90
+ if (rollupState.validators.some((x) => x.address === itx.proposer) === false) {
91
+ return next(new Error('INVALID_TX', 'itx.proposer does not exist in validators'));
92
+ }
93
+
94
+ // ensure block size
95
+ const blockSize = itx.txsList.length;
96
+ if (blockSize < rollupState.minBlockSize) {
97
+ return next(
98
+ new Error('INVALID_TX', `Expect at least ${rollupState.minBlockSize} tx in the block, got ${blockSize}`)
99
+ );
100
+ }
101
+ if (blockSize > rollupState.maxBlockSize) {
102
+ return next(
103
+ new Error('INVALID_TX', `Expect at most ${rollupState.maxBlockSize} tx in the block, got ${blockSize}`)
104
+ );
105
+ }
106
+
107
+ return next();
108
+ });
109
+
110
+ // 4. verify evidence signers, signatures, state
111
+ runner.use(VerifySigners({ signersKey: 'itx.signaturesList', allowSender: true }));
112
+ runner.use(VerifyEvidence({ evidenceKey: 'itx.hash', signaturesKey: 'itx.signaturesList', verifyMethod: 'ethVerify' }));
113
+
114
+ // 3. verify signers & signatures
115
+ runner.use(VerifySigners({ signersKey: 'tx.signaturesList', allowSender: true }));
116
+ runner.use(pipes.VerifyMultiSigV2({ signersKey: 'signers' }));
117
+
118
+ // 4. verify sender and signer states
119
+ runner.use(
120
+ pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })
121
+ );
122
+ runner.use(
123
+ pipes.ExtractState({ from: 'signers', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })
124
+ );
125
+ runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
126
+
127
+ // 5. verify previous block
128
+ runner.use(pipes.ExtractState({ from: 'itx.previousHash', to: 'previousBlock', status: 'OK', table: 'rollupBlock' }));
129
+ runner.use((context, next) => {
130
+ const { txTime, itx, previousBlock, rollupState } = context;
131
+ if (itx.height > 1) {
132
+ if (!previousBlock) {
133
+ return next(new Error('INVALID_TX', `Rollup block with height ${itx.height} does not exist`));
134
+ }
135
+
136
+ if (itx.height !== previousBlock.height + 1) {
137
+ return next(
138
+ new Error(
139
+ 'INVALID_TX',
140
+ `Unexpected previous block height, expected ${itx.height - 1}, got ${previousBlock.height}`
141
+ )
142
+ );
143
+ }
144
+
145
+ // ensure minBlockInterval
146
+ const minBlockTime = +new Date(previousBlock.context.genesisTime) + rollupState.minBlockInterval * 1000;
147
+ const newBlockTime = +new Date(txTime);
148
+ if (newBlockTime < minBlockTime) {
149
+ return next(
150
+ new Error('INVALID_TX', `Block does not comply with minBlockInterval: ${new Date(minBlockTime).toISOString()}`)
151
+ );
152
+ }
153
+ }
154
+
155
+ return next();
156
+ });
157
+
158
+ // 6. verify new block
159
+ runner.use(pipes.ExtractState({ from: 'itx.hash', to: 'newBlock', status: 'OK', table: 'rollupBlock' }));
160
+ runner.use((context, next) => {
161
+ if (context.newBlock) {
162
+ return next(new Error('INVALID_TX', `Rollup block with height ${context.itx.height} already exists`));
163
+ }
164
+
165
+ return next();
166
+ });
167
+
168
+ // 7. verify token state
169
+ runner.use(
170
+ pipes.ExtractState({ from: 'rollupState.tokenAddress', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })
171
+ );
172
+
173
+ // 8. verify txs
174
+ runner.use(pipes.ExtractState({ from: 'itx.txsList', to: 'txStates', status: 'INVALID_TX', table: 'tx' }));
175
+ runner.use(({ itx, txStates }, next) => {
176
+ // ensure not finalized
177
+ const finalizedTx = txStates.some((x) => x.finalized);
178
+ if (finalizedTx) {
179
+ return next(new Error('INVALID_TX', 'Should not include finalized tx in later block'));
180
+ }
181
+
182
+ // ensure tx are successfully executed
183
+ const invalidStatusTx = txStates.some((x) => x.code !== 'OK');
184
+ if (invalidStatusTx) {
185
+ return next(new Error('INVALID_TX', 'Rollup block can only include successful executed transactions'));
186
+ }
187
+
188
+ // ensure tx type
189
+ const invalidTypeTx = txStates.some((x) => ['deposit_token_v2', 'withdraw_token_v2'].includes(x.type) === false);
190
+ if (invalidTypeTx) {
191
+ return next(new Error('INVALID_TX', "Rollup block can only include 'withdraw_token' and 'deposit_token' tx"));
192
+ }
193
+
194
+ // ensure rollup belonging
195
+ const invalidRollupTx = txStates.some((x) => x.tx.itxJson.rollup !== itx.rollup);
196
+ if (invalidRollupTx) {
197
+ return next(new Error('INVALID_TX', `Rollup block can only include tx from rollup ${itx.rollup}`));
198
+ }
199
+
200
+ // ensure merkleRoot
201
+ const merkleTree = MerkleTree.getBlockMerkleTree(
202
+ txStates
203
+ .filter((x) => x.type === 'withdraw_token_v2')
204
+ .map((x) => ({
205
+ hash: x.hash,
206
+ to: x.tx.itxJson.to,
207
+ amount: x.tx.itxJson.token.value,
208
+ }))
209
+ );
210
+ if (merkleTree.getHexRoot() !== itx.merkleRoot) {
211
+ return next(new Error('INVALID_TX', 'Invalid rollup block merkle root'));
212
+ }
213
+
214
+ return next();
215
+ });
216
+
217
+ // 9. verify staking: used for minting & burning
218
+ runner.use((context, next) => {
219
+ const { itx, txStates } = context;
220
+
221
+ const depositProposerStakeAddr = txStates
222
+ .filter((x) => x.type === 'deposit_token_v2')
223
+ .map((x) => toStakeAddress(x.tx.itxJson.proposer, itx.rollup));
224
+
225
+ const withdrawLockerStakeAddr = txStates
226
+ .filter((x) => x.type === 'withdraw_token_v2')
227
+ .map((x) => toStakeAddress(x.tx.from, itx.rollup));
228
+
229
+ context.stakeAddress = [...depositProposerStakeAddr, ...withdrawLockerStakeAddr, getRewardLocker(itx.rollup)];
230
+
231
+ return next();
232
+ });
233
+ runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeStates', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
234
+
235
+ // 10. ensure block reward and dynamic tx fees
236
+ runner.use((context, next) => {
237
+ const { itx, txStates, rollupState } = context;
238
+ try {
239
+ const result = ensureBlockReward(rollupState, itx.minReward, txStates);
240
+ context.senders = Object.keys(result.accountUpdates);
241
+ Object.assign(context, result);
242
+ return next();
243
+ } catch (err) {
244
+ return next(err);
245
+ }
246
+ });
247
+ runner.use(pipes.ExtractState({ from: 'senders', to: 'senderStates', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
248
+
249
+ // 11. update state: mint tokens for deposits, burn tokens for withdraws
250
+ runner.use(
251
+ async (context, next) => {
252
+ const { tx, itx, statedb, rollupState, senderState, stakeStates, txStates, formattedItx } = context;
253
+ const senderStates = context.senderStates || [];
254
+
255
+ const [newSenderState, newStakeStates, newSenderStates, newRollupState, rollupBlockState, newTxStates] =
256
+ await Promise.all([
257
+ // update sender(proposer) account
258
+ statedb.account.update(
259
+ senderState.address,
260
+ account.update(senderState, Object.assign({ nonce: tx.nonce }), context),
261
+ context
262
+ ),
263
+
264
+ // update stake states
265
+ Promise.all(
266
+ stakeStates.map((x) =>
267
+ context.stakeUpdates[x.address]
268
+ ? statedb.stake.update(
269
+ x.address,
270
+ stake.update(x, applyTokenChange(x, context.stakeUpdates[x.address]), context),
271
+ context
272
+ )
273
+ : Promise.resolve(x)
274
+ )
275
+ ),
276
+
277
+ // update tx owner states: those may get refund
278
+ Promise.all(
279
+ senderStates.map((x) =>
280
+ statedb.account.update(
281
+ x.address,
282
+ account.update(x, applyTokenChange(x, context.accountUpdates[x.address]), context),
283
+ context
284
+ )
285
+ )
286
+ ),
287
+
288
+ // Update rollup state
289
+ statedb.rollup.update(
290
+ rollupState.address,
291
+ rollup.update(rollupState, { blockHeight: itx.height, blockHash: itx.hash }, context),
292
+ context
293
+ ),
294
+
295
+ // Create rollup block
296
+ statedb.rollupBlock.create(
297
+ itx.hash,
298
+ rollupBlock.create(
299
+ {
300
+ ...formattedItx,
301
+ ...['rewardAmount', 'mintedAmount', 'burnedAmount'].reduce((acc, x) => {
302
+ acc[x] = context[x].toString(10);
303
+ return acc;
304
+ }, {}),
305
+ },
306
+ context
307
+ ),
308
+ context
309
+ ),
310
+
311
+ // Update tx states: mark as finalized
312
+ Promise.all(
313
+ txStates.map((x) => statedb.tx.update(x.hash, Tx.update(x, { finalized: true, tx: x.tx }), context))
314
+ ),
315
+ ]);
316
+
317
+ context.senderState = newSenderState;
318
+ context.stakeStates = newStakeStates.filter((x) => context.stakeUpdates[x.address]);
319
+ context.newSenderStates = newSenderStates;
320
+ context.rollupState = newRollupState;
321
+ context.rollupBlockState = rollupBlockState;
322
+ context.txStates = newTxStates;
323
+
324
+ context.updatedAccounts = [...Object.values(context.stakeUpdates), ...Object.values(context.accountUpdates)];
325
+
326
+ debug('create-rollup-block', { itx, updatedAccounts: context.updatedAccounts });
327
+
328
+ next();
329
+ },
330
+ { persistError: true }
331
+ );
332
+
333
+ module.exports = runner;
@@ -0,0 +1,169 @@
1
+ /* eslint-disable indent */
2
+ const isEmpty = require('empty-value');
3
+ const cloneDeep = require('lodash/cloneDeep');
4
+ const Error = require('@ocap/util/lib/error');
5
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
6
+ const { account, rollup } = require('@ocap/state');
7
+ const { formatMessage } = require('@ocap/message');
8
+ const { isFromPublicKey, isEthereumDid } = require('@arcblock/did');
9
+ const { toRollupAddress } = require('@arcblock/did-util');
10
+
11
+ // eslint-disable-next-line global-require
12
+ const debug = require('debug')(`${require('../../../package.json').name}:create-rollup`);
13
+
14
+ const { decodeAnySafe } = require('../../util');
15
+ const ensureServiceFee = require('./pipes/ensure-service-fee');
16
+
17
+ const runner = new Runner();
18
+
19
+ runner.use(pipes.VerifyMultiSig(0));
20
+
21
+ // 0. verify itx
22
+ runner.use((context, next) => {
23
+ const { itx } = context;
24
+ context.formattedItx = formatMessage('CreateRollupTx', itx);
25
+
26
+ try {
27
+ rollup.create(context.formattedItx, context);
28
+ } catch (err) {
29
+ return next(new Error('INVALID_TX', err.message));
30
+ }
31
+
32
+ return next();
33
+ });
34
+ runner.use(
35
+ pipes.VerifyInfo([
36
+ {
37
+ error: 'INVALID_TX',
38
+ message: 'Sum of itx.proposerFeeShare and itx.publisherFeeShare must be less than 10000',
39
+ fn: ({ itx }) => itx.proposerFeeShare + itx.publisherFeeShare < 10000,
40
+ },
41
+ {
42
+ error: 'INVALID_TX',
43
+ message: 'Seed validator pk and address not match',
44
+ fn: ({ formattedItx }) => formattedItx.seedValidators.every(({ pk, address }) => isFromPublicKey(address, pk)),
45
+ },
46
+ {
47
+ error: 'INVALID_TX',
48
+ message: 'Seed validator must use ethereum compatible address',
49
+ fn: ({ formattedItx }) => formattedItx.seedValidators.every(({ address }) => isEthereumDid(address)),
50
+ },
51
+ {
52
+ error: 'INVALID_TX',
53
+ message: 'Rollup address is not valid',
54
+ fn: (context) => {
55
+ const itx = cloneDeep(context.itx);
56
+ itx.data = decodeAnySafe(itx.data);
57
+ itx.address = '';
58
+
59
+ // Save for later use
60
+ context.rollupData = itx.data;
61
+ context.seedValidators = itx.seedValidatorsList.map((x) => x.address);
62
+
63
+ return toRollupAddress(itx) === context.itx.address;
64
+ },
65
+ },
66
+ ])
67
+ );
68
+
69
+ // 1. ensure sender, validators, delegation
70
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
71
+ runner.use(pipes.ExtractState({ from: 'seedValidators', to: 'validatorStates', status: 'INVALID_VALIDATOR_STATE', table: 'account' })); // prettier-ignore
72
+ runner.use(pipes.VerifyAccountMigration({ signerKey: 'validatorStates', senderKey: 'senderState' }));
73
+ runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
74
+ runner.use(pipes.VerifyDelegation({ type: 'signature', signerKey: 'senderState', delegatorKey: 'delegatorState' }));
75
+
76
+ // 2. ensure token exist and match
77
+ runner.use(pipes.ExtractState({ from: 'itx.tokenAddress', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' }));
78
+ runner.use(
79
+ pipes.VerifyInfo([
80
+ {
81
+ error: 'INVALID_TOKEN',
82
+ message: 'Token does not have foreignToken attached',
83
+ fn: ({ tokenState }) => isEmpty(tokenState.foreignToken) === false,
84
+ },
85
+ ])
86
+ );
87
+
88
+ // 3. ensure only 1 rollup for a token
89
+ runner.use(async (context, next) => {
90
+ const { statedb, itx } = context;
91
+ const exist = await statedb.rollup.existByToken(itx.tokenAddress, context);
92
+ if (exist) {
93
+ return next(new Error('DUPLICATE_ROLLUP', 'Only 1 rollup can be created for the token'));
94
+ }
95
+
96
+ return next();
97
+ });
98
+
99
+ // 4. ensure rollup not exist
100
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'rollupState', status: 'OK', table: 'rollup' }));
101
+ runner.use(
102
+ pipes.VerifyInfo([
103
+ {
104
+ error: 'DUPLICATE_ROLLUP',
105
+ message: 'This rollup already exist on chain',
106
+ fn: (context) => isEmpty(context.rollupState),
107
+ },
108
+ ])
109
+ );
110
+
111
+ runner.use(ensureServiceFee);
112
+
113
+ // 5. create rollup state
114
+ runner.use(
115
+ async (context, next) => {
116
+ const {
117
+ tx,
118
+ formattedItx,
119
+ rollupData,
120
+ statedb,
121
+ senderState,
122
+ delegatorState,
123
+ senderUpdates,
124
+ vaultState,
125
+ vaultUpdates,
126
+ } = context;
127
+
128
+ const issuer = delegatorState ? delegatorState.address : senderState.address;
129
+ const [newSenderState, rollupState, newVaultState] = await Promise.all([
130
+ statedb.account.update(
131
+ senderState.address,
132
+ account.update(senderState, { nonce: tx.nonce, ...senderUpdates }, context),
133
+ context
134
+ ),
135
+
136
+ statedb.rollup.create(
137
+ formattedItx.address,
138
+ rollup.create(
139
+ {
140
+ ...formattedItx,
141
+ issuer,
142
+ validators: formattedItx.seedValidators,
143
+ data: rollupData,
144
+ },
145
+ context
146
+ ),
147
+ context
148
+ ),
149
+
150
+ isEmpty(vaultUpdates)
151
+ ? vaultState
152
+ : statedb.account.update(vaultState.address, account.update(vaultState, vaultUpdates, context), context),
153
+ ]);
154
+
155
+ // FIXME: create-rollup 的时候不能校验 stake,还是在这里创建 stake?如果在这里创建 stake,是否需要多签?
156
+ // FIXME: seed validator 无法离开, account migration 也需要测试下
157
+
158
+ context.senderState = newSenderState;
159
+ context.rollupState = rollupState;
160
+ context.vaultState = newVaultState;
161
+
162
+ debug('create-rollup', rollupState);
163
+
164
+ next();
165
+ },
166
+ { persistError: true }
167
+ );
168
+
169
+ module.exports = runner;
@@ -0,0 +1,156 @@
1
+ /* eslint-disable indent */
2
+ const MerkleTree = require('@ocap/merkle-tree');
3
+ const joinUrl = require('url-join');
4
+ const Error = require('@ocap/util/lib/error');
5
+ const Joi = require('@arcblock/validator');
6
+ const { BN, toHex } = require('@ocap/util');
7
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
8
+ const { account, rollup, stake, evidence } = require('@ocap/state');
9
+ const { toStakeAddress } = require('@arcblock/did-util');
10
+ const { isEthereumDid } = require('@arcblock/did');
11
+
12
+ // eslint-disable-next-line global-require
13
+ const debug = require('debug')(`${require('../../../package.json').name}:join-rollup`);
14
+
15
+ const VerifySigners = require('./pipes/verify-signers');
16
+ const VerifyEvidence = require('./pipes/verify-evidence');
17
+ const VerifyPaused = require('./pipes/verify-paused');
18
+
19
+ const runner = new Runner();
20
+
21
+ // 1. verify itx
22
+ const schema = Joi.object({
23
+ rollup: Joi.DID().role('ROLE_ROLLUP').required(),
24
+ endpoint: Joi.string()
25
+ .uri({ scheme: [/https?/] })
26
+ .required(),
27
+ evidence: Joi.object({
28
+ hash: Joi.string().regex(Joi.patterns.txHash).required(),
29
+ }).required(),
30
+ signaturesList: Joi.schemas.multiSig.min(1).required(),
31
+ data: Joi.any().optional(),
32
+ }).options({ stripUnknown: true, noDefaults: false });
33
+ runner.use(({ itx }, next) => {
34
+ const { error } = schema.validate(itx);
35
+ return next(error ? new Error('INVALID_TX', `Invalid itx: ${error.message}`) : null);
36
+ });
37
+
38
+ // 2. verify rollup
39
+ runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
40
+ runner.use(VerifyPaused());
41
+
42
+ // 3. verify new validator
43
+ runner.use((context, next) => {
44
+ const { tx, itx, rollupState } = context;
45
+ const validator = { pk: toHex(tx.pk), address: tx.from, endpoint: itx.endpoint };
46
+ if (isEthereumDid(tx.from) === false) {
47
+ return next(new Error('INVALID_TX', 'Invalid tx.from: only ethereum compatible address can join rollup'));
48
+ }
49
+
50
+ if (rollupState.validators.some((x) => x.address === validator.address)) {
51
+ return next(new Error('INVALID_TX', `Address ${validator.address} already exist in validators`));
52
+ }
53
+
54
+ // The endpoint should not contain any pathname
55
+ if (rollupState.validators.some((x) => joinUrl(x.endpoint, '/') === joinUrl(validator.endpoint, '/'))) {
56
+ return next(new Error('INVALID_TX', `Endpoint ${validator.endpoint} already exist in validators`));
57
+ }
58
+
59
+ context.newValidator = validator;
60
+ context.validatorsHash = MerkleTree.getListHash([validator.address]);
61
+
62
+ return next();
63
+ });
64
+
65
+ // 4. verify evidence signers, signatures, state
66
+ runner.use(VerifySigners({ signersKey: 'itx.signaturesList', allowSender: false }));
67
+ runner.use(
68
+ VerifyEvidence({ evidenceKey: 'validatorsHash', signaturesKey: 'itx.signaturesList', verifyMethod: 'ethVerify' })
69
+ );
70
+ runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceState', status: 'OK', table: 'evidence' }));
71
+ runner.use(({ evidenceState }, next) => {
72
+ if (evidenceState) return next(new Error('INVALID_TX', 'Join evidence already seen on this chain'));
73
+ return next();
74
+ });
75
+
76
+ // 5. verify tx signers and signatures
77
+ runner.use(VerifySigners({ signersKey: 'tx.signaturesList', allowSender: false }));
78
+ runner.use(pipes.VerifyMultiSigV2({ signersKey: 'signers' }));
79
+
80
+ // 6. verify staking: get address, extract state, verify amount
81
+ runner.use((context, next) => {
82
+ const { tx, itx } = context;
83
+ context.stakeAddress = toStakeAddress(tx.from, itx.rollup);
84
+ return next();
85
+ });
86
+ runner.use(
87
+ pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })
88
+ );
89
+ runner.use((context, next) => {
90
+ const { stakeState, rollupState } = context;
91
+ if (stakeState.revocable === false) {
92
+ return next(new Error('INVALID_STAKE_STATE', `Staking already locked: ${stakeState.address}`));
93
+ }
94
+
95
+ const actualStake = new BN(stakeState.tokens[rollupState.tokenAddress] || 0);
96
+ const requiredStake = new BN(rollupState.minStakeAmount);
97
+ if (actualStake.lt(requiredStake)) {
98
+ return next(new Error('INVALID_STAKE_STATE', `Staking does not match min stake amount: ${requiredStake}`));
99
+ }
100
+
101
+ return next();
102
+ });
103
+
104
+ // 7. verify sender and signer states
105
+ runner.use(
106
+ pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })
107
+ );
108
+ runner.use(
109
+ pipes.ExtractState({ from: 'signers', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })
110
+ );
111
+ runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
112
+
113
+ // 8. update state
114
+ runner.use(
115
+ async (context, next) => {
116
+ const { tx, itx, rollupState, statedb, senderState, stakeState, newValidator, stakeAddress } = context;
117
+ const newValidators = rollupState.validators.concat([newValidator]);
118
+
119
+ const [newSenderState, newRollupState, newStakeState, evidenceState] = await Promise.all([
120
+ statedb.account.update(senderState.address, account.update(senderState, { nonce: tx.nonce }, context), context),
121
+
122
+ // persist new validators list
123
+ statedb.rollup.update(
124
+ rollupState.address,
125
+ rollup.update(rollupState, { validators: newValidators }, context),
126
+ context
127
+ ),
128
+
129
+ // lock the stake from the new validator for defined period
130
+ statedb.stake.update(
131
+ stakeAddress,
132
+ stake.update(stakeState, { revocable: false, revokeWaitingPeriod: rollupState.leaveWaitingPeriod }, context),
133
+ context
134
+ ),
135
+
136
+ // Create evidence state
137
+ statedb.evidence.create(
138
+ itx.evidence.hash,
139
+ evidence.create({ hash: itx.evidence.hash, data: 'rollup-join' }, context),
140
+ context
141
+ ),
142
+ ]);
143
+
144
+ context.senderState = newSenderState;
145
+ context.rollupState = newRollupState;
146
+ context.stakeState = newStakeState;
147
+ context.evidenceState = evidenceState;
148
+
149
+ debug('join-rollup', newValidators);
150
+
151
+ next();
152
+ },
153
+ { persistError: true }
154
+ );
155
+
156
+ module.exports = runner;