@ocap/tx-pipeline 1.18.162 → 1.18.164
Sign up to get free protection for your applications and to get access to all the features.
- package/lib/index.js +4 -0
- package/lib/pipes/take-state-snapshot.js +33 -0
- package/lib/pipes/verify-state-diff.js +192 -0
- package/package.json +9 -9
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.
|
6
|
+
"version": "1.18.164",
|
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.
|
33
|
-
"@arcblock/did-util": "1.18.
|
34
|
-
"@ocap/mcrypto": "1.18.
|
35
|
-
"@ocap/message": "1.18.
|
36
|
-
"@ocap/state": "1.18.
|
37
|
-
"@ocap/util": "1.18.
|
38
|
-
"@ocap/wallet": "1.18.
|
32
|
+
"@arcblock/did": "1.18.164",
|
33
|
+
"@arcblock/did-util": "1.18.164",
|
34
|
+
"@ocap/mcrypto": "1.18.164",
|
35
|
+
"@ocap/message": "1.18.164",
|
36
|
+
"@ocap/state": "1.18.164",
|
37
|
+
"@ocap/util": "1.18.164",
|
38
|
+
"@ocap/wallet": "1.18.164",
|
39
39
|
"debug": "^4.3.6",
|
40
40
|
"empty-value": "^1.0.1",
|
41
41
|
"lodash": "^4.17.21"
|
42
42
|
},
|
43
|
-
"gitHead": "
|
43
|
+
"gitHead": "f517d0620f7fb4acd26716ab76f68829587b6483"
|
44
44
|
}
|