@ocap/tx-protocols 1.13.57 → 1.13.61

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.
@@ -1,27 +1,27 @@
1
- const isEmpty = require('empty-value');
1
+ const Error = require('@ocap/util/lib/error');
2
2
  const { Runner, pipes } = require('@ocap/tx-pipeline');
3
- const { account } = require('@ocap/state');
3
+ const { account, Joi } = require('@ocap/state');
4
4
  const { toBase58 } = require('@ocap/util');
5
5
 
6
6
  const runner = new Runner();
7
7
 
8
8
  runner.use(pipes.VerifyMultiSig(0));
9
9
 
10
- // Verify itx
11
- runner.use(
12
- pipes.VerifyInfo([
13
- {
14
- error: 'INSUFFICIENT_DATA',
15
- message: 'Can not declare without moniker',
16
- fn: ({ itx }) => !isEmpty(itx.moniker),
17
- },
18
- {
19
- error: 'INVALID_MONIKER',
20
- message: 'Moniker is not valid',
21
- fn: ({ itx }) => /^[a-zA-Z0-9][-a-zA-Z0-9_]{2,40}$/.test(itx.moniker),
22
- },
23
- ])
24
- );
10
+ // verify itx
11
+ const schema = Joi.object({
12
+ issuer: Joi.DID().optional().allow(''),
13
+ moniker: Joi.string()
14
+ .regex(/^[a-zA-Z0-9][-a-zA-Z0-9_]{2,40}$/)
15
+ .required(),
16
+ data: Joi.any().optional(),
17
+ }).options({ stripUnknown: true, noDefaults: false });
18
+ runner.use(({ itx }, next) => {
19
+ const { error } = schema.validate(itx);
20
+ if (error) {
21
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
22
+ }
23
+ return next();
24
+ });
25
25
 
26
26
  // Ensure sender does not exist
27
27
  runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState' }));
@@ -1,8 +1,8 @@
1
1
  /* eslint-disable max-len */
2
2
  const get = require('lodash/get');
3
- const isEmpty = require('empty-value');
3
+ const Error = require('@ocap/util/lib/error');
4
4
  const getListField = require('@ocap/util/lib/get-list-field');
5
- const { delegation, account } = require('@ocap/state');
5
+ const { delegation, account, Joi } = require('@ocap/state');
6
6
  const { Runner, pipes } = require('@ocap/tx-pipeline');
7
7
  const { toDelegateAddress } = require('@arcblock/did-util');
8
8
 
@@ -13,6 +13,27 @@ const runner = new Runner();
13
13
 
14
14
  runner.use(pipes.VerifyMultiSig(0));
15
15
 
16
+ // verify itx
17
+ const schema = Joi.object({
18
+ address: Joi.DID().role('ROLE_DELEGATION').required(),
19
+ to: Joi.DID().required(),
20
+ opsList: Joi.array()
21
+ .items(
22
+ Joi.object({
23
+ typeUrl: Joi.string().required(),
24
+ })
25
+ )
26
+ .min(1)
27
+ .required(),
28
+ data: Joi.any().optional(),
29
+ }).options({ stripUnknown: true, noDefaults: false });
30
+ runner.use(({ itx }, next) => {
31
+ const { error } = schema.validate(itx);
32
+ if (error) {
33
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
34
+ }
35
+ return next();
36
+ });
16
37
  runner.use(
17
38
  pipes.VerifyInfo([
18
39
  {
@@ -22,11 +43,6 @@ runner.use(
22
43
  return true;
23
44
  },
24
45
  },
25
- {
26
- error: 'INSUFFICIENT_DATA',
27
- message: 'itx.address, itx.to and itx.ops should not be empty',
28
- fn: ({ itx, ops }) => !isEmpty(itx.address) && !isEmpty(itx.to) && ops.length > 0,
29
- },
30
46
  {
31
47
  error: 'INVALID_TX',
32
48
  message: 'Delegation address does not match',
@@ -1,9 +1,8 @@
1
- const isEmpty = require('empty-value');
2
1
  const Error = require('@ocap/util/lib/error');
3
2
  const getRelatedAddresses = require('@ocap/util/lib/get-related-addr');
4
3
  const { Runner, pipes } = require('@ocap/tx-pipeline');
5
4
  const { fromPublicKey, toTypeInfo } = require('@arcblock/did');
6
- const { account } = require('@ocap/state');
5
+ const { account, Joi } = require('@ocap/state');
7
6
 
8
7
  // eslint-disable-next-line global-require
9
8
  const debug = require('debug')(`${require('../../../package.json').name}:migrate`);
@@ -12,14 +11,21 @@ const runner = new Runner();
12
11
 
13
12
  runner.use(pipes.VerifyMultiSig(0));
14
13
 
15
- // Verify itx
14
+ // verify itx
15
+ const schema = Joi.object({
16
+ address: Joi.DID().required(),
17
+ pk: Joi.any().required(),
18
+ data: Joi.any().optional(),
19
+ }).options({ stripUnknown: true, noDefaults: false });
20
+ runner.use(({ itx }, next) => {
21
+ const { error } = schema.validate(itx);
22
+ if (error) {
23
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
24
+ }
25
+ return next();
26
+ });
16
27
  runner.use(
17
28
  pipes.VerifyInfo([
18
- {
19
- error: 'INSUFFICIENT_DATA',
20
- message: 'itx.address and itx.pk should not be empty',
21
- fn: ({ itx }) => !isEmpty(itx.pk) && !isEmpty(itx.address),
22
- },
23
29
  {
24
30
  error: 'INVALID_RECEIVER_STATE',
25
31
  message: 'Receiver pk and address does not match',
@@ -1,9 +1,8 @@
1
1
  const get = require('lodash/get');
2
2
  const cloneDeep = require('lodash/cloneDeep');
3
- const isEmpty = require('empty-value');
4
3
  const getListField = require('@ocap/util/lib/get-list-field');
5
4
  const { Runner, pipes } = require('@ocap/tx-pipeline');
6
- const { delegation, account } = require('@ocap/state');
5
+ const { delegation, account, Joi } = require('@ocap/state');
7
6
  const { toDelegateAddress } = require('@arcblock/did-util');
8
7
 
9
8
  // eslint-disable-next-line global-require
@@ -14,6 +13,19 @@ const runner = new Runner();
14
13
  runner.use(pipes.VerifyMultiSig(0));
15
14
 
16
15
  // Verify itx
16
+ const schema = Joi.object({
17
+ address: Joi.DID().role('ROLE_DELEGATION').required(),
18
+ to: Joi.DID().required(),
19
+ typeUrlsList: Joi.array().items(Joi.string().required()).min(1).required(),
20
+ data: Joi.any().optional(),
21
+ }).options({ stripUnknown: true, noDefaults: false });
22
+ runner.use(({ itx }, next) => {
23
+ const { error } = schema.validate(itx);
24
+ if (error) {
25
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
26
+ }
27
+ return next();
28
+ });
17
29
  runner.use(
18
30
  pipes.VerifyInfo([
19
31
  {
@@ -23,11 +35,6 @@ runner.use(
23
35
  return true;
24
36
  },
25
37
  },
26
- {
27
- error: 'INSUFFICIENT_DATA',
28
- message: 'itx.address, itx.to and itx.typeUrls should not be empty',
29
- fn: ({ itx, typeUrls }) => !isEmpty(itx.address) && !isEmpty(itx.to) && typeUrls.length > 0,
30
- },
31
38
  {
32
39
  error: 'INVALID_TX',
33
40
  message: 'Delegation address does not match',
@@ -1,8 +1,8 @@
1
1
  /* eslint-disable indent */
2
- const isEmpty = require('empty-value');
2
+ const Error = require('@ocap/util/lib/error');
3
3
  const cloneDeep = require('lodash/cloneDeep');
4
4
  const { Runner, pipes } = require('@ocap/tx-pipeline');
5
- const { account, asset } = require('@ocap/state');
5
+ const { account, asset, Joi } = require('@ocap/state');
6
6
  const { toAssetAddress } = require('@arcblock/did-util');
7
7
 
8
8
  // eslint-disable-next-line global-require
@@ -14,18 +14,26 @@ const runner = new Runner();
14
14
 
15
15
  runner.use(pipes.VerifyMultiSig(0));
16
16
 
17
+ // Verify itx
18
+ const schema = Joi.object({
19
+ moniker: Joi.string().min(2).max(255).required(),
20
+ data: Joi.any().required(),
21
+ readonly: Joi.boolean().default(false),
22
+ transferrable: Joi.boolean().default(false),
23
+ ttl: Joi.number().min(0).default(0),
24
+ parent: Joi.DID().optional().allow(''),
25
+ address: Joi.DID().role('ROLE_ASSET').required(),
26
+ issuer: Joi.DID().optional().allow(''),
27
+ }).options({ stripUnknown: true, noDefaults: false });
28
+ runner.use(({ itx }, next) => {
29
+ const { error } = schema.validate(itx);
30
+ if (error) {
31
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
32
+ }
33
+ return next();
34
+ });
17
35
  runner.use(
18
36
  pipes.VerifyInfo([
19
- {
20
- error: 'INSUFFICIENT_DATA',
21
- message: 'itx.address, itx.moniker and itx.data must not be empty',
22
- fn: ({ itx }) => itx.address && itx.moniker && itx.data,
23
- },
24
- {
25
- error: 'INVALID_MONIKER',
26
- message: 'Length of asset moniker should between 2 and 255 characters',
27
- fn: ({ itx }) => itx.moniker.length >= 2 && itx.moniker.length <= 255,
28
- },
29
37
  {
30
38
  error: 'INVALID_ASSET',
31
39
  message: 'Asset address is not valid',
@@ -50,7 +58,7 @@ runner.use(
50
58
  {
51
59
  error: 'INVALID_ASSET',
52
60
  message: 'This asset already exist on chain',
53
- fn: (context) => isEmpty(context.assetState),
61
+ fn: (context) => !context.assetState,
54
62
  },
55
63
  ])
56
64
  );
@@ -1,5 +1,6 @@
1
+ const Error = require('@ocap/util/lib/error');
1
2
  const { Runner, pipes } = require('@ocap/tx-pipeline');
2
- const { account, asset } = require('@ocap/state');
3
+ const { account, asset, Joi } = require('@ocap/state');
3
4
 
4
5
  // eslint-disable-next-line global-require
5
6
  const debug = require('debug')(`${require('../../../package.json').name}:update-asset`);
@@ -10,20 +11,19 @@ const runner = new Runner();
10
11
 
11
12
  runner.use(pipes.VerifyMultiSig(0));
12
13
 
13
- runner.use(
14
- pipes.VerifyInfo([
15
- {
16
- error: 'INSUFFICIENT_DATA',
17
- message: 'itx.data, itx.moniker and itx.address must not be empty',
18
- fn: ({ itx }) => itx.address && itx.moniker && itx.data,
19
- },
20
- {
21
- error: 'INVALID_MONIKER',
22
- message: 'Length of asset moniker should between 2 and 255 characters',
23
- fn: ({ itx }) => itx.moniker.length >= 2 && itx.moniker.length <= 255,
24
- },
25
- ])
26
- );
14
+ // Verify itx
15
+ const schema = Joi.object({
16
+ address: Joi.DID().role('ROLE_ASSET').required(),
17
+ moniker: Joi.string().min(2).max(255).required(),
18
+ data: Joi.any().optional(),
19
+ }).options({ stripUnknown: true, noDefaults: false });
20
+ runner.use(({ itx }, next) => {
21
+ const { error } = schema.validate(itx);
22
+ if (error) {
23
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
24
+ }
25
+ return next();
26
+ });
27
27
 
28
28
  // Ensure asset exist owned by sender and can be modified
29
29
  runner.use(pipes.ExtractState({ from: 'itx.address', to: 'assetState', status: 'INVALID_ASSET' }));
@@ -9,10 +9,10 @@ const { account, stake, evidence, rollupBlock, Joi } = require('@ocap/state');
9
9
  // eslint-disable-next-line global-require
10
10
  const debug = require('debug')(`${require('../../../package.json').name}:claim-block-reward`);
11
11
 
12
+ const { toStakeAddress } = require('@arcblock/did-util');
12
13
  const VerifySigners = require('./pipes/verify-signers');
13
- const { applyTokenChange, splitBlockReward, getBlockRewardLocker } = require('../../util');
14
+ const { applyTokenChange, splitTxFee, getBNSum, getRewardLocker, RATE_BASE } = require('../../util');
14
15
 
15
- const ZERO = new BN(0);
16
16
  const runner = new Runner();
17
17
 
18
18
  // 1. verify itx
@@ -39,12 +39,21 @@ runner.use((context, next) => {
39
39
  return next(new Error('INVALID_TX', 'itx.publisher must be same with tx.from'));
40
40
  }
41
41
 
42
- context.lockerAddress = getBlockRewardLocker(itx.rollup);
42
+ context.lockerAddress = getRewardLocker(itx.rollup);
43
43
 
44
44
  return next();
45
45
  });
46
46
 
47
- // 2. verify rollup and block state
47
+ // 2. ensure evidence not exist
48
+ runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceSeen', status: 'OK', table: 'evidence' }));
49
+ runner.use(pipes.ExtractState({ from: 'itx.blockHash', to: 'blockClaimed', status: 'OK', table: 'evidence' }));
50
+ runner.use(({ evidenceSeen, blockClaimed }, next) => {
51
+ if (evidenceSeen) return next(new Error('INVALID_TX', 'Claim evidence already seen on this chain'));
52
+ if (blockClaimed) return next(new Error('INVALID_TX', 'Block reward already claimed before this tx'));
53
+ return next();
54
+ });
55
+
56
+ // 3. verify rollup and block state
48
57
  runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
49
58
  runner.use(pipes.ExtractState({ from: 'itx.blockHash', to: 'blockState', status: 'INVALID_ROLLUP_BLOCK', table: 'rollupBlock' })); // prettier-ignore
50
59
  runner.use((context, next) => {
@@ -55,24 +64,28 @@ runner.use((context, next) => {
55
64
  if (blockState.height !== itx.blockHeight) {
56
65
  return next(new Error('INVALID_TX', 'Rollup block height does not match'));
57
66
  }
67
+
68
+ // If the publisher is not the producer, he should wait at least rollupState.publishWaitingPeriod
69
+ if (blockState.proposer !== itx.publisher) {
70
+ const proposedAt = +new Date(blockState.context.genesisTime);
71
+ const publishedAt = +new Date(context.txTime);
72
+ if (proposedAt + rollupState.publishWaitingPeriod * 1000 > publishedAt) {
73
+ return next(new Error('INVALID_TX', 'Rollup block can only be published by producer during waiting period'));
74
+ }
75
+ }
76
+
77
+ context.producerStake = toStakeAddress(blockState.proposer, itx.rollup);
78
+
58
79
  return next();
59
80
  });
60
81
 
61
- // 3. verify tx signers & signatures
82
+ // 4. verify tx signers & signatures
62
83
  runner.use(VerifySigners({ signersKey: 'tx.signaturesList', allowSender: true }));
63
84
  runner.use(pipes.VerifyMultiSigV2({ signersKey: 'signers' }));
64
85
 
65
- // 4. verify block reward locker
86
+ // 5. verify block reward locker
66
87
  runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'INVALID_LOCKER_STATE', table: 'stake' })); // prettier-ignore
67
-
68
- // 5. ensure evidence not exist
69
- runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceSeen', status: 'OK', table: 'evidence' }));
70
- runner.use(pipes.ExtractState({ from: 'itx.blockHash', to: 'blockClaimed', status: 'OK', table: 'evidence' }));
71
- runner.use(({ evidenceSeen, blockClaimed }, next) => {
72
- if (evidenceSeen) return next(new Error('INVALID_TX', 'Claim evidence already seen on this chain'));
73
- if (blockClaimed) return next(new Error('INVALID_TX', 'Block reward already claimed before this tx'));
74
- return next();
75
- });
88
+ runner.use(pipes.ExtractState({ from: 'producerStake', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
76
89
 
77
90
  // 6. verify sender and signer states
78
91
  runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
@@ -85,8 +98,7 @@ runner.use(pipes.ExtractState({ from: 'rollupState.tokenAddress', to: 'tokenStat
85
98
  // 8. split and aggregate block reward for each tx and the block
86
99
  runner.use(pipes.ExtractState({ from: 'blockState.txs', to: 'txs', status: 'INVALID_TX', table: 'tx' }));
87
100
  runner.use(async (context, next) => {
88
- const { itx, txs, rollupState, blockState, lockerState, tokenState } = context;
89
- const { depositFeeRate, withdrawFeeRate, minDepositFee, maxDepositFee, minWithdrawFee, maxWithdrawFee } = rollupState;
101
+ const { itx, txs, rollupState, blockState, lockerState, tokenState, stakeState } = context;
90
102
  const { proposerFeeShare, publisherFeeShare } = rollupState;
91
103
 
92
104
  const shares = {
@@ -95,23 +107,28 @@ runner.use(async (context, next) => {
95
107
  validator: 10000 - publisherFeeShare - proposerFeeShare,
96
108
  };
97
109
 
98
- // 1. loop through the tx and split reward
99
110
  const changes = { account: [], stake: [] };
111
+
112
+ // 0. handle slash case
113
+ // TODO: the producer stake balance should be enough for slashing
114
+ const shouldSlash = blockState.proposer !== itx.publisher;
115
+ if (shouldSlash) {
116
+ const slashAmount = new BN(rollupState.minStakeAmount).mul(new BN(rollupState.publishSlashRate)).div(RATE_BASE);
117
+ const slashShare = slashAmount.div(new BN(2));
118
+ changes.stake.push({ address: stakeState.address, delta: `-${slashAmount.toString(10)}`, action: 'slash' });
119
+ changes.stake.push({ address: lockerState.address, delta: slashShare.toString(10), action: 'slash' });
120
+ changes.account.push({ address: itx.publisher, delta: slashShare.toString(10), action: 'reward' });
121
+ }
122
+
123
+ // 1. loop through the tx and split reward
100
124
  txs.forEach((x) => {
101
- const amount = x.tx.itxJson.token.value;
125
+ const { actualFee } = x.tx.itxJson;
126
+ const { publisher, proposer, validator } = splitTxFee({ total: actualFee, shares, stringify: false });
127
+
128
+ changes.stake.push({ address: lockerState.address, delta: `-${actualFee}`, action: 'claim' });
129
+ changes.account.push({ address: itx.publisher, delta: publisher.toString(10), action: 'fee' });
130
+
102
131
  if (x.type === 'deposit_token_v2') {
103
- const { fee, feeShares } = splitBlockReward({
104
- total: amount,
105
- feeRate: depositFeeRate,
106
- maxFee: maxDepositFee,
107
- minFee: minDepositFee,
108
- shares,
109
- stringify: false,
110
- });
111
- changes.stake.push({ address: lockerState.address, delta: `-${fee}`, action: 'claim' });
112
-
113
- const { publisher, proposer, validator } = feeShares;
114
- changes.account.push({ address: itx.publisher, delta: publisher.toString(10), action: 'fee' });
115
132
  changes.account.push({ address: x.tx.itxJson.proposer, delta: proposer.toString(10), action: 'fee' });
116
133
 
117
134
  // block and tx signers share the validator part
@@ -122,18 +139,6 @@ runner.use(async (context, next) => {
122
139
  changes.account.push({ address: v, delta: validatorShare.toString(10), action: 'fee' })
123
140
  );
124
141
  } else if (x.type === 'withdraw_token_v2') {
125
- const { fee, feeShares } = splitBlockReward({
126
- total: amount,
127
- feeRate: withdrawFeeRate,
128
- maxFee: maxWithdrawFee,
129
- minFee: minWithdrawFee,
130
- shares,
131
- stringify: false,
132
- });
133
- changes.stake.push({ address: lockerState.address, delta: `-${fee}`, action: 'claim' });
134
-
135
- const { publisher, proposer, validator } = feeShares;
136
- changes.account.push({ address: itx.publisher, delta: publisher.toString(10), action: 'fee' });
137
142
  changes.account.push({ address: blockState.proposer, delta: proposer.toString(10), action: 'fee' });
138
143
 
139
144
  // block signers share the validator part
@@ -157,7 +162,7 @@ runner.use(async (context, next) => {
157
162
  acc[x] = {
158
163
  address: x,
159
164
  token: tokenState.address,
160
- delta: grouped.stake[x].reduce((a, u) => a.add(new BN(u.delta)), ZERO),
165
+ delta: getBNSum(...grouped.stake[x].map((c) => c.delta)),
161
166
  action: grouped.stake[x][grouped.stake[x].length - 1].action,
162
167
  };
163
168
  return acc;
@@ -166,7 +171,7 @@ runner.use(async (context, next) => {
166
171
  acc[x] = {
167
172
  address: x,
168
173
  token: tokenState.address,
169
- delta: grouped.account[x].reduce((a, u) => a.add(new BN(u.delta)), ZERO),
174
+ delta: getBNSum(...grouped.account[x].map((c) => c.delta)),
170
175
  action: grouped.account[x][grouped.account[x].length - 1].action,
171
176
  };
172
177
  return acc;
@@ -179,8 +184,6 @@ runner.use(async (context, next) => {
179
184
  return next();
180
185
  });
181
186
 
182
- // TODO: verify locker balance
183
-
184
187
  // 9. extract all accounts that will be updated exist
185
188
  runner.use((context, next) => {
186
189
  const { updates, senderState, signerStates } = context;
@@ -208,17 +211,23 @@ runner.use((context, next) => {
208
211
  });
209
212
 
210
213
  // 10. update state
211
- // FIXME: (future) we have a low probability of encounter QLDB 40 documents in a single transaction limit
212
214
  runner.use(
213
215
  async (context, next) => {
214
- const { tx, itx, updates, statedb, senderState, blockState, lockerState, accountStates } = context;
215
-
216
- // This balance from the locker will be checked when apply updates
217
- const lockerUpdates = applyTokenChange(lockerState, updates.stake[lockerState.address]);
216
+ const { tx, itx, updates, statedb, senderState, blockState, lockerState, stakeState, accountStates } = context;
218
217
 
219
- const [newAccountStates, newLockerState, newBlockState] = await Promise.all([
220
- // update locker state
221
- statedb.stake.update(lockerState.address, stake.update(lockerState, lockerUpdates, context), context),
218
+ const [newStakeStates, newAccountStates, newBlockState] = await Promise.all([
219
+ // update stake states
220
+ Promise.all(
221
+ [lockerState, stakeState].map((x) =>
222
+ updates.stake[x.address]
223
+ ? statedb.stake.update(
224
+ x.address,
225
+ stake.update(x, applyTokenChange(x, updates.stake[x.address]), context),
226
+ context
227
+ )
228
+ : x
229
+ )
230
+ ),
222
231
 
223
232
  // update accounts(proposer, publisher, validator)
224
233
  Promise.all(
@@ -254,12 +263,12 @@ runner.use(
254
263
  ),
255
264
  ]);
256
265
 
257
- context.updatedAccounts = cloneDeep([...Object.values(updates.account), ...Object.values(updates.stake)]);
258
- context.updatedAccounts.forEach((x) => (x.delta = x.delta.toString(10))); // eslint-disable-line
266
+ context.stakeStates = newStakeStates;
259
267
  context.accountStates = newAccountStates;
260
- context.stakeState = newLockerState;
261
268
  context.blockState = newBlockState;
262
269
 
270
+ context.updatedAccounts = cloneDeep([...Object.values(updates.account), ...Object.values(updates.stake)]);
271
+
263
272
  debug('claim-block-reward', itx);
264
273
 
265
274
  next();