@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.
- package/README.md +36 -0
- package/lib/execute.js +97 -30
- package/lib/index.js +56 -14
- package/lib/protocols/account/declare.js +36 -30
- package/lib/protocols/account/delegate.js +78 -40
- package/lib/protocols/account/migrate.js +65 -49
- package/lib/protocols/account/revoke-delegate.js +39 -23
- package/lib/protocols/asset/acquire-v2.js +159 -0
- package/lib/protocols/asset/acquire-v3.js +242 -0
- package/lib/protocols/asset/calls/README.md +5 -0
- package/lib/protocols/asset/calls/transfer-token.js +37 -0
- package/lib/protocols/asset/calls/transfer.js +29 -0
- package/lib/protocols/asset/create.js +85 -36
- package/lib/protocols/asset/mint.js +133 -0
- package/lib/protocols/asset/pipes/exec-mint-hook.js +59 -0
- package/lib/protocols/asset/pipes/extract-factory-tokens.js +18 -0
- package/lib/protocols/asset/pipes/verify-acquire-params.js +30 -0
- package/lib/protocols/asset/pipes/verify-itx-address.js +41 -0
- package/lib/protocols/asset/pipes/verify-itx-assets.js +49 -0
- package/lib/protocols/asset/pipes/verify-itx-variables.js +26 -0
- package/lib/protocols/asset/pipes/verify-mint-limit.js +13 -0
- package/lib/protocols/asset/update.js +76 -44
- package/lib/protocols/factory/create.js +146 -0
- package/lib/protocols/governance/claim-stake.js +219 -0
- package/lib/protocols/governance/revoke-stake.js +136 -0
- package/lib/protocols/governance/stake.js +176 -0
- package/lib/protocols/rollup/claim-reward.js +283 -0
- package/lib/protocols/rollup/create-block.js +333 -0
- package/lib/protocols/rollup/create.js +169 -0
- package/lib/protocols/rollup/join.js +156 -0
- package/lib/protocols/rollup/leave.js +127 -0
- package/lib/protocols/rollup/migrate-contract.js +53 -0
- package/lib/protocols/rollup/migrate-token.js +66 -0
- package/lib/protocols/rollup/pause.js +52 -0
- package/lib/protocols/rollup/pipes/ensure-service-fee.js +37 -0
- package/lib/protocols/rollup/pipes/ensure-validator.js +10 -0
- package/lib/protocols/rollup/pipes/verify-evidence.js +37 -0
- package/lib/protocols/rollup/pipes/verify-paused.js +10 -0
- package/lib/protocols/rollup/pipes/verify-signers.js +88 -0
- package/lib/protocols/rollup/resume.js +52 -0
- package/lib/protocols/rollup/update.js +98 -0
- package/lib/protocols/token/create.js +150 -0
- package/lib/protocols/token/deposit-v2.js +241 -0
- package/lib/protocols/token/withdraw-v2.js +255 -0
- package/lib/protocols/trade/exchange-v2.js +179 -0
- package/lib/protocols/trade/transfer-v2.js +136 -0
- package/lib/protocols/trade/transfer-v3.js +241 -0
- package/lib/util.js +325 -2
- package/package.json +23 -16
- package/lib/protocols/misc/poke.js +0 -106
- package/lib/protocols/trade/exchange.js +0 -139
- 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;
|