@ocap/tx-pipeline 1.18.162 → 1.18.163

Sign up to get free protection for your applications and to get access to all the features.
package/lib/index.js CHANGED
@@ -23,6 +23,8 @@ const VerifyTxSize = require('./pipes/verify-tx-size');
23
23
  const VerifyTxInput = require('./pipes/verify-tx-input');
24
24
  const VerifyUpdater = require('./pipes/verify-updater');
25
25
  const VerifyTokenBalance = require('./pipes/verify-token-balance');
26
+ const VerifyStateDiff = require('./pipes/verify-state-diff');
27
+ const TakeStateSnapshot = require('./pipes/take-state-snapshot');
26
28
 
27
29
  module.exports = {
28
30
  Runner,
@@ -51,5 +53,7 @@ module.exports = {
51
53
  VerifyTxSize,
52
54
  VerifyTxInput,
53
55
  VerifyTokenBalance,
56
+ VerifyStateDiff,
57
+ TakeStateSnapshot,
54
58
  },
55
59
  };
@@ -0,0 +1,33 @@
1
+ const { cloneDeep } = require('lodash');
2
+ const { groupStatesByAddress } = require('./verify-state-diff');
3
+
4
+ module.exports = function CreateTakeStateSnapshotPipe({ key = 'stateSnapshot' } = {}) {
5
+ return async function TakeStateSnapshot(context, next) {
6
+ const { statedb, config } = context;
7
+
8
+ const statesMap = groupStatesByAddress(context);
9
+
10
+ // feeVaultState is usually add to context at the end, so we need to query it here
11
+ if (!statesMap[config.vaults.txFee]) {
12
+ statesMap[config.vaults.txFee] = [await statedb.account.get(config.vaults.txFee, context)];
13
+ }
14
+
15
+ // for acquire transactions, we should pre-fetch the account state in the mint hooks to verify their balance changes later
16
+ if (context.factoryState && ['fg:t:acquire_asset_v3', 'fg:t:acquire_asset_v2'].includes(context.txType)) {
17
+ const mintHook = context.factoryState.hooks?.find((x) => x.name === 'mint');
18
+ const states = await Promise.all(
19
+ (mintHook?.compiled || []).map((x) => {
20
+ const address = x.args.to;
21
+ return statesMap[address]?.[0] || statedb.account.get(address, context);
22
+ })
23
+ );
24
+ states.forEach((x) => {
25
+ statesMap[x.address] = [x];
26
+ });
27
+ }
28
+
29
+ context[key] = cloneDeep(statesMap);
30
+
31
+ return next();
32
+ };
33
+ };
@@ -0,0 +1,192 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ /* eslint-disable consistent-return */
3
+ const isEqual = require('lodash/isEqual');
4
+ const mergeWith = require('lodash/mergeWith');
5
+ const clone = require('lodash/clone');
6
+ const { BN, isSameDid } = require('@ocap/util');
7
+ const { CustomError: Error } = require('@ocap/util/lib/error');
8
+ const { groupReceiptTokenChanges, create: createTxState } = require('@ocap/state/lib/states/tx');
9
+ const { getRelatedAddresses } = require('@ocap/util/lib/get-related-addr');
10
+ const {
11
+ types: { RoleType },
12
+ } = require('@ocap/mcrypto');
13
+ const { toTypeInfo } = require('@arcblock/did');
14
+ const { pickBy } = require('lodash');
15
+
16
+ function groupStatesByAddress(context) {
17
+ /**
18
+ * a map of address -> state list
19
+ * @type {Record<string, []>}
20
+ */
21
+ const statesMap = {};
22
+
23
+ for (const [stateKey, stateOrStates] of Object.entries(context)) {
24
+ const states = [].concat(stateOrStates).filter((x) => x && x.address && x.tokens && x.context);
25
+
26
+ for (const state of states) {
27
+ // mintHookStates has the highest priority
28
+ if (stateKey === 'mintHookStates') {
29
+ statesMap[state.address] = [state];
30
+ continue;
31
+ }
32
+ // skip state with the same tokens as existing states
33
+ const curStates = statesMap[state.address] || [];
34
+ if (curStates.every((x) => !isEqual(x.tokens, state.tokens))) {
35
+ statesMap[state.address] = curStates.concat(state);
36
+ }
37
+ }
38
+ }
39
+
40
+ return statesMap;
41
+ }
42
+
43
+ function findStates(address, statesMap) {
44
+ if (statesMap[address]) {
45
+ return statesMap[address];
46
+ }
47
+ // Try to find state from migrated addresses
48
+ const matchedAddress = Object.keys(statesMap).find((x) => {
49
+ const state = statesMap[x][0];
50
+ const relatedAddresses = getRelatedAddresses(state);
51
+ return !state.migratedTo?.length && relatedAddresses.some((y) => isSameDid(y, address));
52
+ });
53
+ return statesMap[matchedAddress] || [];
54
+ }
55
+
56
+ function getStateTokens(state, txType) {
57
+ const isStakeAddress = toTypeInfo(state.address).role === RoleType.ROLE_STAKE;
58
+ if (txType === 'slash_stake' && isStakeAddress) {
59
+ return mergeTokenChange(state.revokedTokens, state.tokens);
60
+ }
61
+ if (txType === 'claim_stake' && isStakeAddress) {
62
+ return state.revokedTokens;
63
+ }
64
+ return state.tokens;
65
+ }
66
+
67
+ function mergeTokenChange(token1, token2, operator = 'add') {
68
+ return mergeWith(clone(token1), token2, (a, b) => {
69
+ return new BN(a || 0)[operator](new BN(b)).toString();
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Update migrated addresses in receipts to their latest addresses
75
+ */
76
+ function fixMigratedReceipts(receipts, statesMap) {
77
+ const states = Object.values(statesMap).flat();
78
+ const fixedReceipts = receipts.map((receipt) => {
79
+ const state = states.find((x) => {
80
+ const relatedAddresses = getRelatedAddresses(x);
81
+ return !x.migratedTo?.length && relatedAddresses.some((address) => isSameDid(address, receipt.address));
82
+ });
83
+ return {
84
+ ...receipt,
85
+ address: state?.address || receipt.address,
86
+ };
87
+ });
88
+ return fixedReceipts;
89
+ }
90
+
91
+ /**
92
+ * verify that account balances are updated correctly by comparing before and after states
93
+ */
94
+ module.exports = function CreateVerifyStateDiffPipe({ snapshotKey = 'stateSnapshot' } = {}) {
95
+ return async function VerifyStateDiff(context, next) {
96
+ const { logger, [snapshotKey]: snapshotStates } = context;
97
+
98
+ if (!snapshotStates) {
99
+ return next(
100
+ new Error('FORBIDDEN', `context.${snapshotKey} is missing, please call VerifyStateDiff after TakeStateSnapshot`)
101
+ );
102
+ }
103
+
104
+ const txState = createTxState(context, 'OK', false);
105
+ const contextStates = groupStatesByAddress(context);
106
+ const receipts = fixMigratedReceipts(txState.receipts, contextStates);
107
+ const receiptChanges = groupReceiptTokenChanges(receipts);
108
+
109
+ // verify: receipts -> state diff
110
+ // before tokens + receipt changes = after tokens
111
+ for (const [address, tokenChanges] of Object.entries(receiptChanges)) {
112
+ const beforeState = findStates(address, snapshotStates)[0];
113
+ const afterStates = findStates(address, contextStates);
114
+
115
+ if (!afterStates.length) {
116
+ logger?.error('INVALID_RECEIPT', 'Changed state not found for receipt', {
117
+ address,
118
+ context,
119
+ tokenChanges,
120
+ });
121
+ return next(new Error('INVALID_RECEIPT', 'Changed state not found for receipt'));
122
+ }
123
+
124
+ // if beforeState does not exist, it means afterState is created in this transaction,
125
+ // so its tokens should exactly match the receipts
126
+ const expectedTokens = beforeState
127
+ ? mergeTokenChange(getStateTokens(beforeState, txState.type), tokenChanges)
128
+ : tokenChanges;
129
+ const matchAfterState = afterStates.find((x) => isEqual(expectedTokens, getStateTokens(x, txState.type)));
130
+
131
+ if (!matchAfterState) {
132
+ logger?.error('INVALID_RECEIPT', 'Tx receipts does not match with state diff', {
133
+ txState,
134
+ expectedTokens,
135
+ beforeState,
136
+ afterStates,
137
+ tokenChanges,
138
+ });
139
+ return next(new Error('INVALID_RECEIPT', 'Tx receipts does not match with state diff'));
140
+ }
141
+ }
142
+
143
+ // verify: state diff -> receipts
144
+ // after tokens - before tokens = receipt changes
145
+ for (const [address, afterStates] of Object.entries(contextStates)) {
146
+ const beforeState = findStates(address, snapshotStates)[0];
147
+ const beforeTokens = beforeState ? getStateTokens(beforeState, txState.type) : {};
148
+ const roleType = toTypeInfo(address).role;
149
+
150
+ // there might be multiple afterStates found, e.g. when signerState and senderState have the same address,
151
+ // and it's possible that senderState has updated tokens but signerState hasn't.
152
+ // so we need to verify each afterState. there are two cases:
153
+ // 1. state with updated tokens: their tokens differ from beforeState.tokens, so they will enter verification logic
154
+ // 2. state with no updated tokens: their tokens match beforeState.tokens, so they will skip verification logic
155
+ for (const afterState of afterStates) {
156
+ const afterTokens = getStateTokens(afterState, txState.type);
157
+ if (!beforeState || !isEqual(afterTokens, beforeTokens)) {
158
+ const tokenChange = pickBy(
159
+ beforeState ? mergeTokenChange(afterTokens, beforeTokens, 'sub') : afterTokens,
160
+ (value) => value !== '0'
161
+ );
162
+ let expectedChange = receiptChanges[address] || {};
163
+
164
+ // revoke_stake only moves tokens from staked to revoked tokens and without generating any receipts
165
+ // after tokens - before tokens = before revoke tokens - after revoke tokens
166
+ if (txState.type === 'revoke_stake' && roleType === RoleType.ROLE_STAKE) {
167
+ expectedChange = pickBy(
168
+ mergeTokenChange(beforeState.revokedTokens, afterState.revokedTokens, 'sub'),
169
+ (value) => value !== '0'
170
+ );
171
+ }
172
+
173
+ if (!isEqual(tokenChange, expectedChange)) {
174
+ logger?.error('INVALID_RECEIPT', 'State diff does not match with tx receipts', {
175
+ txState,
176
+ beforeState,
177
+ afterState,
178
+ afterStates,
179
+ tokenChange,
180
+ expectedChange,
181
+ });
182
+ return next(new Error('INVALID_RECEIPT', 'State diff does not match with tx receipts'));
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ next();
189
+ };
190
+ };
191
+
192
+ module.exports.groupStatesByAddress = groupStatesByAddress;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.18.162",
6
+ "version": "1.18.163",
7
7
  "description": "Pipeline runner and common pipelines to process transactions",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -29,16 +29,16 @@
29
29
  "elliptic": "6.5.3"
30
30
  },
31
31
  "dependencies": {
32
- "@arcblock/did": "1.18.162",
33
- "@arcblock/did-util": "1.18.162",
34
- "@ocap/mcrypto": "1.18.162",
35
- "@ocap/message": "1.18.162",
36
- "@ocap/state": "1.18.162",
37
- "@ocap/util": "1.18.162",
38
- "@ocap/wallet": "1.18.162",
32
+ "@arcblock/did": "1.18.163",
33
+ "@arcblock/did-util": "1.18.163",
34
+ "@ocap/mcrypto": "1.18.163",
35
+ "@ocap/message": "1.18.163",
36
+ "@ocap/state": "1.18.163",
37
+ "@ocap/util": "1.18.163",
38
+ "@ocap/wallet": "1.18.163",
39
39
  "debug": "^4.3.6",
40
40
  "empty-value": "^1.0.1",
41
41
  "lodash": "^4.17.21"
42
42
  },
43
- "gitHead": "54c0d27c13d94fd758335e65648326a8d04dd30c"
43
+ "gitHead": "c4bb7fae24dbac7a56ffbb84d19f64b90ea770b7"
44
44
  }