@ocap/tx-protocols 1.18.17 → 1.18.19

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/lib/execute.js CHANGED
@@ -62,11 +62,13 @@ module.exports = ({ filter, runAsLambda }) => {
62
62
  // we should only flush events when retry is not supported
63
63
  // otherwise the outer caller should handle these 2
64
64
  if (shouldPersistTx(err) && isRetrySupported === false) {
65
- const txState = context.states.tx.create(context, err ? err.code || 'INTERNAL' : 'OK');
65
+ let txState;
66
66
  try {
67
+ txState = context.states.tx.create(context, err ? err.code || 'INTERNAL' : 'OK');
67
68
  await context.statedb.tx.create(txState.hash, txState, context);
68
69
  } catch (e) {
69
70
  console.error('Failed to save transaction to statedb', e);
71
+ return reject(e);
70
72
  }
71
73
 
72
74
  flushEvents(context, { txState });
package/lib/index.js CHANGED
@@ -17,6 +17,7 @@ const createToken = require('./protocols/token/create');
17
17
  const stake = require('./protocols/governance/stake');
18
18
  const revokeStake = require('./protocols/governance/revoke-stake');
19
19
  const claimStake = require('./protocols/governance/claim-stake');
20
+ const slashStake = require('./protocols/governance/slash-stake');
20
21
  const depositTokenV2 = require('./protocols/token/deposit-v2');
21
22
  const withdrawTokenV2 = require('./protocols/token/withdraw-v2');
22
23
  const createRollup = require('./protocols/rollup/create');
@@ -65,6 +66,7 @@ const createExecutor = ({ filter, runAsLambda }) => {
65
66
  stake,
66
67
  revokeStake,
67
68
  claimStake,
69
+ slashStake,
68
70
 
69
71
  // rollup
70
72
  createRollup,
@@ -43,7 +43,11 @@ module.exports = function CreateEnsureTxCostPipe({ attachSenderChanges = true }
43
43
  const gasStake = {};
44
44
  if (token && pk && JWT.verify(token, pk, { enforceTimestamp: true, tolerance: 5 })) {
45
45
  const decoded = JWT.decode(token);
46
- if (decoded.txHash === txHash) {
46
+ const txHashes = (decoded.txHash || '')
47
+ .split(',')
48
+ .map((x) => x.trim())
49
+ .filter(Boolean);
50
+ if (txHashes.includes(txHash)) {
47
51
  gasStake.owner = toAddress(decoded.iss);
48
52
  gasStake.stakeId = toStakeAddress(gasStake.owner, gasStake.owner);
49
53
  } else {
@@ -1,5 +1,7 @@
1
1
  const isEmpty = require('empty-value');
2
2
  const { BN } = require('@ocap/util');
3
+ const { Joi, schemas } = require('@arcblock/validator');
4
+ const { CustomError: Error } = require('@ocap/util/lib/error');
3
5
  const { Runner, pipes } = require('@ocap/tx-pipeline');
4
6
  const { account, stake } = require('@ocap/state');
5
7
  const { getRelatedAddresses } = require('@ocap/util/lib/get-related-addr');
@@ -15,6 +17,17 @@ const runner = new Runner();
15
17
 
16
18
  runner.use(pipes.VerifyMultiSig(0));
17
19
 
20
+ // 0. verify itx
21
+ const schema = Joi.object({
22
+ address: Joi.DID().role('ROLE_STAKE').required(),
23
+ outputsList: schemas.multiInput.min(1).required(),
24
+ data: Joi.any().optional(),
25
+ }).options({ stripUnknown: true, noDefaults: false });
26
+ runner.use(({ itx }, next) => {
27
+ const { error } = schema.validate(itx);
28
+ return next(error ? new Error('INVALID_TX', `Invalid itx: ${error.message}`) : null);
29
+ });
30
+
18
31
  // 1. verify itx output
19
32
  runner.use(
20
33
  pipes.VerifyTxInput({
@@ -29,11 +42,6 @@ runner.use(
29
42
  // 2. verify itx address
30
43
  runner.use(
31
44
  pipes.VerifyInfo([
32
- {
33
- error: 'INSUFFICIENT_DATA',
34
- message: 'Can not revoke stake without stake address',
35
- fn: ({ itx }) => itx.address,
36
- },
37
45
  {
38
46
  error: 'INSUFFICIENT_DATA',
39
47
  message: 'Can not revoke stake without any output',
@@ -64,6 +72,11 @@ runner.use(
64
72
  message: 'This stake address is not revocable',
65
73
  fn: ({ stakeState }) => stakeState.revocable,
66
74
  },
75
+ {
76
+ error: 'INVALID_TX',
77
+ message: 'Can not revoke assets that are not locked in the stake',
78
+ fn: ({ assets, stakeState }) => assets.every((x) => stakeState.assets.includes(x)),
79
+ },
67
80
  ])
68
81
  );
69
82
 
@@ -0,0 +1,249 @@
1
+ const isEmpty = require('empty-value');
2
+ const { CustomError: Error } = require('@ocap/util/lib/error');
3
+ const { BN } = require('@ocap/util');
4
+ const { Joi, schemas } = require('@arcblock/validator');
5
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
6
+ const { account, stake, asset } = require('@ocap/state');
7
+ const { getRelatedAddresses } = require('@ocap/util/lib/get-related-addr');
8
+
9
+ // eslint-disable-next-line global-require
10
+ const debug = require('debug')(`${require('../../../package.json').name}:slash-stake`);
11
+
12
+ const { applyTokenUpdates, applyTokenChange } = require('../../util');
13
+ const EnsureTxGas = require('../../pipes/ensure-gas');
14
+ const EnsureTxCost = require('../../pipes/ensure-cost');
15
+
16
+ const runner = new Runner();
17
+
18
+ runner.use(pipes.VerifyMultiSig(0));
19
+
20
+ // 0. verify itx
21
+ const schema = Joi.object({
22
+ address: Joi.DID().role('ROLE_STAKE').required(),
23
+ message: Joi.string().trim().min(1).max(256).required(),
24
+ outputsList: schemas.multiInput.min(1).required(),
25
+ data: Joi.any().optional(),
26
+ }).options({ stripUnknown: true, noDefaults: false });
27
+ runner.use(({ itx }, next) => {
28
+ const { error } = schema.validate(itx);
29
+ return next(error ? new Error('INVALID_TX', `Invalid itx: ${error.message}`) : null);
30
+ });
31
+
32
+ // 1. verify itx.outputs
33
+ runner.use(
34
+ pipes.VerifyTxInput({
35
+ fieldKey: 'itx.outputs',
36
+ inputsKey: 'outputs',
37
+ sendersKey: 'receivers',
38
+ tokensKey: 'tokens',
39
+ assetsKey: 'assets',
40
+ })
41
+ );
42
+
43
+ // 2. verify output
44
+ runner.use(
45
+ pipes.VerifyInfo([
46
+ {
47
+ error: 'INSUFFICIENT_DATA',
48
+ message: 'Can not slash stake without any output',
49
+ fn: ({ assets, tokens }) => !(isEmpty(tokens) && isEmpty(assets)),
50
+ },
51
+ {
52
+ error: 'INVALID_TX',
53
+ message: 'Can only send slashed token/assets to specified vault',
54
+ fn: ({ config, receivers }) => receivers.every((x) => x === config.vaults.slashedStake),
55
+ },
56
+ ])
57
+ );
58
+
59
+ // 3. verify itx size: set hard limit here because more output leads to longer tx execute time
60
+ runner.use(pipes.VerifyListSize({ listKey: ['outputs', 'receivers', 'tokens', 'assets'] }));
61
+
62
+ // 4. verify sender & receiver
63
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
64
+ runner.use(pipes.ExtractState({ from: 'receivers', to: 'receiverStates', status: 'INVALID_RECEIVER_STATE', table: 'account' })); // prettier-ignore
65
+ runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
66
+
67
+ // 5. verify stake state
68
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
69
+ runner.use(
70
+ pipes.VerifyInfo([
71
+ {
72
+ error: 'INVALID_TX',
73
+ message: 'You are not allowed to slash from this stake',
74
+ fn: ({ tx, stakeState }) =>
75
+ (isEmpty(stakeState.slashers) ? [stakeState.receiver] : stakeState.slashers).includes(tx.from),
76
+ },
77
+ ])
78
+ );
79
+
80
+ // 6. verify token state and balance from both staked and revoked
81
+ runner.use(pipes.ExtractState({ from: 'tokens', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
82
+ runner.use((context, next) => {
83
+ const { outputs, stakeState } = context;
84
+ const tokens = {};
85
+ outputs.forEach(({ tokensList }) => {
86
+ tokensList.forEach(({ address, value }) => {
87
+ if (typeof tokens[address] === 'undefined') {
88
+ tokens[address] = new BN(0);
89
+ }
90
+ tokens[address] = tokens[address].add(new BN(value));
91
+ });
92
+ });
93
+
94
+ // merge both staked and revoked as locked
95
+ const lockedState = {
96
+ address: stakeState.address,
97
+ tokens: { ...stakeState.tokens },
98
+ assets: [...stakeState.assets, ...stakeState.revokedAssets],
99
+ };
100
+ Object.keys(stakeState.revokedTokens).forEach((address) => {
101
+ lockedState.tokens[address] = new BN(stakeState.revokedTokens[address])
102
+ .add(new BN(stakeState.tokens[address]))
103
+ .toString(10);
104
+ });
105
+
106
+ context.lockedState = lockedState;
107
+ context.tokenCondition = {
108
+ owner: stakeState.address,
109
+ tokens: Object.keys(tokens).map((address) => ({ address, value: tokens[address] })),
110
+ };
111
+
112
+ next();
113
+ });
114
+ runner.use(pipes.VerifyTokenBalance({ ownerKey: 'lockedState', conditionKey: 'tokenCondition' }));
115
+ runner.use(
116
+ pipes.VerifyInfo([
117
+ {
118
+ error: 'INVALID_TX',
119
+ message: 'Can not slash assets that are not locked in the stake',
120
+ fn: ({ assets, lockedState }) => assets.every((x) => lockedState.assets.includes(x)),
121
+ },
122
+ ])
123
+ );
124
+
125
+ // 7. verify asset state and ownership
126
+ runner.use(pipes.ExtractState({ from: 'assets', to: 'assetStates', status: 'INVALID_ASSET_STATE', table: 'asset' }));
127
+ runner.use(pipes.VerifyTransferrable({ assets: 'assetStates' }));
128
+ runner.use(pipes.VerifyUpdater({ assetKey: 'assetStates', ownerKey: 'stakeState' }));
129
+
130
+ // ensure tx cost and gas
131
+ runner.use(
132
+ EnsureTxGas((context) => {
133
+ const result = { create: 0, update: 2, payment: 0 };
134
+ if (context.receiverStates) {
135
+ result.update += context.receiverStates.length;
136
+ }
137
+ if (context.assetStates) {
138
+ result.update += context.assetStates.length;
139
+ }
140
+
141
+ return result;
142
+ })
143
+ );
144
+ runner.use(EnsureTxCost({ attachSenderChanges: true }));
145
+
146
+ // 8. update statedb
147
+ runner.use(
148
+ async (context, next) => {
149
+ const {
150
+ tx,
151
+ itx,
152
+ outputs,
153
+ statedb,
154
+ senderState,
155
+ receiverStates = [],
156
+ assetStates = [],
157
+ stakeState,
158
+ updateVaults,
159
+ senderChange,
160
+ } = context;
161
+
162
+ const receiverUpdates = {};
163
+ const assetUpdates = {};
164
+ const stakeUpdates = {
165
+ tokens: stakeState.tokens || {},
166
+ assets: stakeState.assets || [],
167
+ revokedTokens: stakeState.revokedTokens || {},
168
+ revokedAssets: stakeState.revokedAssets || [],
169
+ };
170
+
171
+ outputs.forEach((x) => {
172
+ const { owner, tokensList, assetsList } = x;
173
+
174
+ // move assets to output
175
+ stakeUpdates.assets = stakeUpdates.assets.filter((a) => assetsList.includes(a) === false);
176
+ stakeUpdates.revokedAssets = stakeUpdates.revokedAssets.filter((a) => assetsList.includes(a) === false);
177
+
178
+ // move tokens and assets from staked or revoked to output
179
+ const [newTokens, newRevoked] = applyTokenUpdates(
180
+ tokensList,
181
+ [{ tokens: stakeUpdates.tokens }, { tokens: stakeUpdates.revokedTokens }],
182
+ 'sub'
183
+ );
184
+ stakeUpdates.tokens = newTokens.tokens;
185
+ stakeUpdates.revokedTokens = newRevoked.tokens;
186
+
187
+ // Update receiver balance
188
+ const ownerState = receiverStates.find((s) => getRelatedAddresses(s).includes(owner));
189
+ receiverUpdates[ownerState.address] = applyTokenUpdates(tokensList, ownerState, 'add');
190
+ if (senderChange && ownerState.address === senderChange.address) {
191
+ receiverUpdates[ownerState.address] = applyTokenChange(receiverUpdates[ownerState.address], senderChange);
192
+ }
193
+
194
+ // Update asset owner
195
+ assetsList.forEach((a) => {
196
+ assetUpdates[a] = { owner: ownerState.address };
197
+ });
198
+ });
199
+
200
+ const [newSenderState, newReceiverStates, newStakeState, newAssetStates] = await Promise.all([
201
+ // Update sender state
202
+ statedb.account.update(
203
+ senderState.address,
204
+ account.update(
205
+ senderState,
206
+ Object.assign(
207
+ { nonce: tx.nonce },
208
+ receiverUpdates[senderState.address] || (senderChange ? applyTokenChange(senderState, senderChange) : {})
209
+ ),
210
+ context
211
+ ),
212
+ context
213
+ ),
214
+
215
+ // Update receiver states
216
+ Promise.all(
217
+ receiverStates
218
+ .filter((x) => x.address !== senderState.address)
219
+ .map((x) =>
220
+ statedb.account.update(x.address, account.update(x, receiverUpdates[x.address], context), context)
221
+ )
222
+ ),
223
+
224
+ // Update stake state
225
+ statedb.stake.update(stakeState.address, stake.update(stakeState, stakeUpdates, context), context),
226
+
227
+ // Transfer assets to output account
228
+ Promise.all(
229
+ assetStates.map((x) =>
230
+ statedb.asset.update(x.address, asset.update(x, assetUpdates[x.address], context), context)
231
+ )
232
+ ),
233
+
234
+ updateVaults(),
235
+ ]);
236
+
237
+ context.senderState = newSenderState;
238
+ context.receiverStates = newReceiverStates;
239
+ context.stakeState = newStakeState;
240
+ context.assetStates = newAssetStates;
241
+
242
+ debug('slash-stake', { address: itx.address, stakeUpdates });
243
+
244
+ next();
245
+ },
246
+ { persistError: true }
247
+ );
248
+
249
+ module.exports = runner;
@@ -1,7 +1,7 @@
1
1
  const pick = require('lodash/pick');
2
2
  const { promisify } = require('util');
3
3
  const isEmpty = require('empty-value');
4
- const { Joi } = require('@arcblock/validator');
4
+ const { Joi, schemas } = require('@arcblock/validator');
5
5
  const { CustomError: Error } = require('@ocap/util/lib/error');
6
6
  const { BN, fromTokenToUnit } = require('@ocap/util');
7
7
  const { Runner, pipes } = require('@ocap/tx-pipeline');
@@ -24,7 +24,9 @@ const runner = new Runner();
24
24
  const schema = Joi.object({
25
25
  address: Joi.DID().role('ROLE_STAKE').required(),
26
26
  receiver: Joi.DID().required(),
27
+ slashersList: Joi.array().items(Joi.DID()).default([]),
27
28
  locked: Joi.boolean().default(false),
29
+ inputsList: schemas.multiInput.min(1).required(),
28
30
  message: Joi.string().trim().min(1).max(256).required(),
29
31
  revokeWaitingPeriod: Joi.number().integer().min(0).default(0),
30
32
  }).options({ stripUnknown: true, noDefaults: false });
@@ -50,6 +52,11 @@ runner.use(
50
52
  message: 'Invalid staking address',
51
53
  fn: ({ tx, itx }) => toStakeAddress(tx.from, itx.receiver) === itx.address,
52
54
  },
55
+ {
56
+ error: 'INVALID_TX',
57
+ message: 'Tx sender can not be slasher',
58
+ fn: ({ tx, itx }) => itx.slashersList.includes(tx.from) === false,
59
+ },
53
60
  {
54
61
  error: 'INSUFFICIENT_DATA',
55
62
  message: 'Can not stake without any token or asset',
@@ -104,9 +111,7 @@ runner.use(async (context, next) => {
104
111
  const { token, transaction } = config;
105
112
  const { minStake, maxStake } = transaction.txGas;
106
113
 
107
- // FIXME: delegation not supported here
108
114
  context.isGasStake = isGasStakeAddress(tx.from, itx.address) && isGasStakeInput(inputs, token.address);
109
-
110
115
  if (context.isGasStake) {
111
116
  const [tokenInput] = inputs[0].tokensList;
112
117
  const actualStake = new BN(tokenInput.value);
@@ -122,8 +127,10 @@ runner.use(async (context, next) => {
122
127
 
123
128
  if (context.isGasStake) {
124
129
  context.revokeWaitingPeriod = transaction.txGas.stakeLockPeriod;
130
+ context.slashers = [config.moderator.address];
125
131
  } else {
126
132
  context.revokeWaitingPeriod = itx.revokeWaitingPeriod;
133
+ context.slashers = itx.slashersList;
127
134
  }
128
135
 
129
136
  return next();
@@ -224,6 +231,7 @@ runner.use(
224
231
  {
225
232
  sender: tx.from,
226
233
  revocable: !itx.locked,
234
+ slashers: context.slashers,
227
235
  revokeWaitingPeriod: context.revokeWaitingPeriod,
228
236
  ...pick(itx, ['address', 'receiver', 'message', 'data']),
229
237
  ...stakeUpdates,
package/lib/util.js CHANGED
@@ -52,6 +52,8 @@ const decodeAnySafe = (encoded) => {
52
52
  }
53
53
  };
54
54
 
55
+ // If we are adding tokens to multiple states, same updates applied to all
56
+ // If we are removing tokens from multiple states, applied sequentially
55
57
  const applyTokenUpdates = (tokens, state, operator) => {
56
58
  if (['add', 'sub'].includes(operator) === false) {
57
59
  throw new Error('UNEXPECTED_OPERATOR', `Invalid operator when applyTokenUpdates: ${operator}`);
@@ -61,23 +63,40 @@ const applyTokenUpdates = (tokens, state, operator) => {
61
63
  return {};
62
64
  }
63
65
 
64
- const oldTokens = state.tokens || {};
65
- const newTokens = cloneDeep(oldTokens);
66
+ const states = cloneDeep((Array.isArray(state) ? state : [state]).map((x) => ({ tokens: x.tokens || {} })));
66
67
  for (let i = 0; i < tokens.length; i++) {
67
68
  const { address, value } = tokens[i];
68
- const requirement = new BN(value);
69
- const balance = new BN(oldTokens[address] || 0);
70
- const newBalance = balance[operator](requirement);
71
- if (newBalance.isNeg()) {
69
+ const expectedDelta = new BN(value);
70
+
71
+ let fulfilled = new BN(0);
72
+ for (let j = 0; j < states.length; j++) {
73
+ const origin = states[j].tokens;
74
+ const updated = cloneDeep(origin);
75
+ const balance = new BN(origin[address] || 0);
76
+
77
+ let newBalance;
78
+ if (operator === 'add') {
79
+ fulfilled = fulfilled.add(expectedDelta);
80
+ newBalance = balance.add(expectedDelta);
81
+ } else {
82
+ const unfulfilledDelta = expectedDelta.sub(fulfilled);
83
+ const actualDelta = balance.lt(unfulfilledDelta) ? balance : unfulfilledDelta;
84
+ fulfilled = fulfilled.add(actualDelta);
85
+ newBalance = balance.sub(actualDelta);
86
+ }
87
+
88
+ updated[address] = newBalance.toString(10);
89
+ states[j].tokens = updated;
90
+ }
91
+
92
+ if (fulfilled.lt(expectedDelta)) {
72
93
  throw new Error('NEGATIVE_TOKEN_BALANCE', `Negative token balance when applyTokenUpdates for ${address}`);
73
94
  }
74
- newTokens[address] = newBalance.toString(10);
75
95
  }
76
96
 
77
- return {
78
- tokens: newTokens,
79
- };
97
+ return Array.isArray(state) ? states : states[0];
80
98
  };
99
+
81
100
  const applyTokenChange = (state, change) => {
82
101
  const delta = typeof change.delta === 'string' ? new BN(change.delta) : change.delta;
83
102
  if (delta.gt(ZERO)) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.18.17",
6
+ "version": "1.18.19",
7
7
  "description": "Predefined tx pipeline sets to execute certain type of transactions",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -21,18 +21,18 @@
21
21
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
- "@arcblock/did": "1.18.17",
25
- "@arcblock/did-util": "1.18.17",
26
- "@arcblock/jwt": "1.18.17",
27
- "@arcblock/validator": "1.18.17",
28
- "@ocap/asset": "1.18.17",
29
- "@ocap/mcrypto": "1.18.17",
30
- "@ocap/merkle-tree": "1.18.17",
31
- "@ocap/message": "1.18.17",
32
- "@ocap/state": "1.18.17",
33
- "@ocap/tx-pipeline": "1.18.17",
34
- "@ocap/util": "1.18.17",
35
- "@ocap/wallet": "1.18.17",
24
+ "@arcblock/did": "1.18.19",
25
+ "@arcblock/did-util": "1.18.19",
26
+ "@arcblock/jwt": "1.18.19",
27
+ "@arcblock/validator": "1.18.19",
28
+ "@ocap/asset": "1.18.19",
29
+ "@ocap/mcrypto": "1.18.19",
30
+ "@ocap/merkle-tree": "1.18.19",
31
+ "@ocap/message": "1.18.19",
32
+ "@ocap/state": "1.18.19",
33
+ "@ocap/tx-pipeline": "1.18.19",
34
+ "@ocap/util": "1.18.19",
35
+ "@ocap/wallet": "1.18.19",
36
36
  "debug": "^4.3.4",
37
37
  "deep-diff": "^1.0.2",
38
38
  "empty-value": "^1.0.1",
@@ -47,5 +47,5 @@
47
47
  "jest": "^27.5.1",
48
48
  "start-server-and-test": "^1.14.0"
49
49
  },
50
- "gitHead": "be4f07d40599521e3b5364dd7d0ace22ad5f58d6"
50
+ "gitHead": "d13bcea225809e0c0a926cfc246c7a46bd4185f3"
51
51
  }