@aztec/p2p 4.1.0-rc.2 → 4.1.0-rc.3
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/dest/client/factory.d.ts +1 -1
- package/dest/client/factory.d.ts.map +1 -1
- package/dest/client/factory.js +6 -1
- package/dest/client/p2p_client.d.ts +1 -1
- package/dest/client/p2p_client.d.ts.map +1 -1
- package/dest/client/p2p_client.js +8 -2
- package/dest/config.d.ts +10 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +15 -0
- package/dest/mem_pools/tx_pool/priority.d.ts +2 -2
- package/dest/mem_pools/tx_pool/priority.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool/priority.js +4 -4
- package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts +1 -1
- package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool/tx_pool_test_suite.js +3 -1
- package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts +1 -1
- package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/allowed_public_setup.js +15 -29
- package/dest/msg_validators/tx_validator/allowed_setup_helpers.d.ts +17 -0
- package/dest/msg_validators/tx_validator/allowed_setup_helpers.d.ts.map +1 -0
- package/dest/msg_validators/tx_validator/allowed_setup_helpers.js +24 -0
- package/dest/msg_validators/tx_validator/factory.d.ts +15 -4
- package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/factory.js +13 -6
- package/dest/msg_validators/tx_validator/fee_payer_balance.d.ts +1 -1
- package/dest/msg_validators/tx_validator/fee_payer_balance.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/fee_payer_balance.js +6 -2
- package/dest/msg_validators/tx_validator/gas_validator.d.ts +13 -4
- package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/gas_validator.js +39 -9
- package/dest/msg_validators/tx_validator/index.d.ts +2 -1
- package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/index.js +1 -0
- package/dest/services/libp2p/libp2p_service.d.ts +1 -1
- package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
- package/dest/services/libp2p/libp2p_service.js +16 -8
- package/dest/test-helpers/testbench-utils.d.ts +1 -1
- package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
- package/dest/test-helpers/testbench-utils.js +2 -1
- package/package.json +14 -14
- package/src/client/factory.ts +6 -0
- package/src/client/p2p_client.ts +5 -4
- package/src/config.ts +25 -0
- package/src/mem_pools/tx_pool/priority.ts +4 -4
- package/src/mem_pools/tx_pool/tx_pool_test_suite.ts +3 -1
- package/src/msg_validators/tx_validator/allowed_public_setup.ts +9 -34
- package/src/msg_validators/tx_validator/allowed_setup_helpers.ts +31 -0
- package/src/msg_validators/tx_validator/factory.ts +15 -2
- package/src/msg_validators/tx_validator/fee_payer_balance.ts +6 -2
- package/src/msg_validators/tx_validator/gas_validator.ts +41 -8
- package/src/msg_validators/tx_validator/index.ts +1 -0
- package/src/services/libp2p/libp2p_service.ts +17 -7
- package/src/test-helpers/testbench-utils.ts +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
2
|
+
import { FunctionSelector, countArgumentsSize, getAllFunctionAbis } from '@aztec/stdlib/abi';
|
|
3
|
+
import type { ContractArtifact } from '@aztec/stdlib/abi';
|
|
4
|
+
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
5
|
+
import type { AllowedElement } from '@aztec/stdlib/interfaces/server';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds an AllowedElement from a contract artifact, deriving both the function selector
|
|
9
|
+
* and calldata length from the artifact instead of hardcoding signature strings.
|
|
10
|
+
*/
|
|
11
|
+
export async function buildAllowedElement(
|
|
12
|
+
artifact: ContractArtifact,
|
|
13
|
+
target: { address: AztecAddress } | { classId: Fr },
|
|
14
|
+
functionName: string,
|
|
15
|
+
opts?: { onlySelf?: boolean; rejectNullMsgSender?: boolean },
|
|
16
|
+
): Promise<AllowedElement> {
|
|
17
|
+
const allFunctions = getAllFunctionAbis(artifact);
|
|
18
|
+
const fn = allFunctions.find(f => f.name === functionName);
|
|
19
|
+
if (!fn) {
|
|
20
|
+
throw new Error(`Unknown function ${functionName} in artifact ${artifact.name}`);
|
|
21
|
+
}
|
|
22
|
+
const selector = await FunctionSelector.fromNameAndParameters(fn.name, fn.parameters);
|
|
23
|
+
const calldataLength = 1 + countArgumentsSize(fn);
|
|
24
|
+
return {
|
|
25
|
+
...target,
|
|
26
|
+
selector,
|
|
27
|
+
calldataLength,
|
|
28
|
+
...(opts?.onlySelf ? { onlySelf: true } : {}),
|
|
29
|
+
...(opts?.rejectNullMsgSender ? { rejectNullMsgSender: true } : {}),
|
|
30
|
+
} as AllowedElement;
|
|
31
|
+
}
|
|
@@ -97,6 +97,7 @@ export function createFirstStageTxValidationsForGossipedTransactions(
|
|
|
97
97
|
txsPermitted: boolean,
|
|
98
98
|
allowedInSetup: AllowedElement[] = [],
|
|
99
99
|
bindings?: LoggerBindings,
|
|
100
|
+
gasLimitOpts?: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number },
|
|
100
101
|
): Record<string, TransactionValidator> {
|
|
101
102
|
const merkleTree = worldStateSynchronizer.getCommitted();
|
|
102
103
|
|
|
@@ -158,6 +159,7 @@ export function createFirstStageTxValidationsForGossipedTransactions(
|
|
|
158
159
|
ProtocolContractAddress.FeeJuice,
|
|
159
160
|
gasFees,
|
|
160
161
|
bindings,
|
|
162
|
+
gasLimitOpts,
|
|
161
163
|
),
|
|
162
164
|
severity: PeerErrorSeverity.MidToleranceError,
|
|
163
165
|
},
|
|
@@ -278,6 +280,9 @@ export function createTxValidatorForAcceptingTxsOverRPC(
|
|
|
278
280
|
timestamp,
|
|
279
281
|
blockNumber,
|
|
280
282
|
txsPermitted,
|
|
283
|
+
rollupManaLimit,
|
|
284
|
+
maxBlockL2Gas,
|
|
285
|
+
maxBlockDAGas,
|
|
281
286
|
}: {
|
|
282
287
|
l1ChainId: number;
|
|
283
288
|
rollupVersion: number;
|
|
@@ -287,6 +292,9 @@ export function createTxValidatorForAcceptingTxsOverRPC(
|
|
|
287
292
|
timestamp: UInt64;
|
|
288
293
|
blockNumber: BlockNumber;
|
|
289
294
|
txsPermitted: boolean;
|
|
295
|
+
rollupManaLimit: number;
|
|
296
|
+
maxBlockL2Gas?: number;
|
|
297
|
+
maxBlockDAGas?: number;
|
|
290
298
|
},
|
|
291
299
|
bindings?: LoggerBindings,
|
|
292
300
|
): TxValidator<Tx> {
|
|
@@ -317,7 +325,11 @@ export function createTxValidatorForAcceptingTxsOverRPC(
|
|
|
317
325
|
|
|
318
326
|
if (!skipFeeEnforcement) {
|
|
319
327
|
validators.push(
|
|
320
|
-
new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees, bindings
|
|
328
|
+
new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees, bindings, {
|
|
329
|
+
rollupManaLimit,
|
|
330
|
+
maxBlockL2Gas,
|
|
331
|
+
maxBlockDAGas,
|
|
332
|
+
}),
|
|
321
333
|
);
|
|
322
334
|
}
|
|
323
335
|
|
|
@@ -403,6 +415,7 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool(
|
|
|
403
415
|
worldStateSynchronizer: WorldStateSynchronizer,
|
|
404
416
|
timestamp: bigint,
|
|
405
417
|
blockNumber: BlockNumber,
|
|
418
|
+
gasLimitOpts: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number },
|
|
406
419
|
bindings?: LoggerBindings,
|
|
407
420
|
): Promise<TxValidator<TxMetaData>> {
|
|
408
421
|
await worldStateSynchronizer.syncImmediate();
|
|
@@ -419,7 +432,7 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool(
|
|
|
419
432
|
},
|
|
420
433
|
};
|
|
421
434
|
return new AggregateTxValidator<TxMetaData>(
|
|
422
|
-
new GasLimitsValidator<TxMetaData>(bindings),
|
|
435
|
+
new GasLimitsValidator<TxMetaData>({ ...gasLimitOpts, bindings }),
|
|
423
436
|
new TimestampTxValidator<TxMetaData>({ timestamp, blockNumber }, bindings),
|
|
424
437
|
new DoubleSpendTxValidator<TxMetaData>(nullifierSource, bindings),
|
|
425
438
|
new BlockHeaderTxValidator<TxMetaData>(archiveSource, bindings),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { FeeJuiceArtifact } from '@aztec/protocol-contracts/fee-juice';
|
|
1
2
|
import { getCallRequestsWithCalldataByPhase } from '@aztec/simulator/server';
|
|
2
|
-
import { FunctionSelector } from '@aztec/stdlib/abi';
|
|
3
|
+
import { FunctionSelector, getAllFunctionAbis } from '@aztec/stdlib/abi';
|
|
3
4
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
4
5
|
import { type Tx, TxExecutionPhase } from '@aztec/stdlib/tx';
|
|
5
6
|
|
|
@@ -8,7 +9,10 @@ export type FeePayerBalanceDelta = {
|
|
|
8
9
|
claimAmount: bigint;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
const increasePublicBalanceSelectorPromise =
|
|
12
|
+
const increasePublicBalanceSelectorPromise = (() => {
|
|
13
|
+
const fn = getAllFunctionAbis(FeeJuiceArtifact).find(f => f.name === '_increase_public_balance')!;
|
|
14
|
+
return FunctionSelector.fromNameAndParameters(fn.name, fn.parameters);
|
|
15
|
+
})();
|
|
12
16
|
|
|
13
17
|
export function getTxFeeLimit(tx: Tx): bigint {
|
|
14
18
|
return tx.data.constants.txContext.gasSettings.getFeeLimit().toBigInt();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT,
|
|
2
3
|
MAX_PROCESSABLE_L2_GAS,
|
|
3
4
|
PRIVATE_TX_L2_GAS_OVERHEAD,
|
|
4
5
|
PUBLIC_TX_L2_GAS_OVERHEAD,
|
|
@@ -49,16 +50,31 @@ export interface HasGasLimitData {
|
|
|
49
50
|
*/
|
|
50
51
|
export class GasLimitsValidator<T extends HasGasLimitData> implements TxValidator<T> {
|
|
51
52
|
#log: Logger;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
#effectiveMaxL2Gas: number;
|
|
54
|
+
#effectiveMaxDAGas: number;
|
|
55
|
+
#rollupManaLimit: number;
|
|
56
|
+
#maxBlockL2Gas: number;
|
|
57
|
+
#maxBlockDAGas: number;
|
|
58
|
+
|
|
59
|
+
constructor(opts?: {
|
|
60
|
+
rollupManaLimit?: number;
|
|
61
|
+
maxBlockL2Gas?: number;
|
|
62
|
+
maxBlockDAGas?: number;
|
|
63
|
+
bindings?: LoggerBindings;
|
|
64
|
+
}) {
|
|
65
|
+
this.#log = createLogger('sequencer:tx_validator:tx_gas', opts?.bindings);
|
|
66
|
+
this.#rollupManaLimit = opts?.rollupManaLimit ?? Infinity;
|
|
67
|
+
this.#maxBlockL2Gas = opts?.maxBlockL2Gas ?? Infinity;
|
|
68
|
+
this.#maxBlockDAGas = opts?.maxBlockDAGas ?? Infinity;
|
|
69
|
+
this.#effectiveMaxL2Gas = Math.min(MAX_PROCESSABLE_L2_GAS, this.#rollupManaLimit, this.#maxBlockL2Gas);
|
|
70
|
+
this.#effectiveMaxDAGas = Math.min(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, this.#maxBlockDAGas);
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
validateTx(tx: T): Promise<TxValidationResult> {
|
|
58
74
|
return Promise.resolve(this.validateGasLimit(tx));
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
/** Checks gas limits are >= fixed minimums and <=
|
|
77
|
+
/** Checks gas limits are >= fixed minimums and <= effective max gas (L2 and DA). */
|
|
62
78
|
validateGasLimit(tx: T): TxValidationResult {
|
|
63
79
|
const gasLimits = tx.data.constants.txContext.gasSettings.gasLimits;
|
|
64
80
|
const minGasLimits = new Gas(
|
|
@@ -74,10 +90,21 @@ export class GasLimitsValidator<T extends HasGasLimitData> implements TxValidato
|
|
|
74
90
|
return { result: 'invalid', reason: [TX_ERROR_INSUFFICIENT_GAS_LIMIT] };
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
if (gasLimits.l2Gas >
|
|
78
|
-
this.#log.verbose(`Rejecting transaction due to the gas limit
|
|
93
|
+
if (gasLimits.l2Gas > this.#effectiveMaxL2Gas) {
|
|
94
|
+
this.#log.verbose(`Rejecting transaction due to the L2 gas limit being higher than the effective maximum`, {
|
|
79
95
|
gasLimits,
|
|
80
|
-
|
|
96
|
+
effectiveMaxL2Gas: this.#effectiveMaxL2Gas,
|
|
97
|
+
rollupManaLimit: this.#rollupManaLimit,
|
|
98
|
+
maxBlockL2Gas: this.#maxBlockL2Gas,
|
|
99
|
+
});
|
|
100
|
+
return { result: 'invalid', reason: [TX_ERROR_GAS_LIMIT_TOO_HIGH] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (gasLimits.daGas > this.#effectiveMaxDAGas) {
|
|
104
|
+
this.#log.verbose(`Rejecting transaction due to the DA gas limit being higher than the effective maximum`, {
|
|
105
|
+
gasLimits,
|
|
106
|
+
effectiveMaxDAGas: this.#effectiveMaxDAGas,
|
|
107
|
+
maxBlockDAGas: this.#maxBlockDAGas,
|
|
81
108
|
});
|
|
82
109
|
return { result: 'invalid', reason: [TX_ERROR_GAS_LIMIT_TOO_HIGH] };
|
|
83
110
|
}
|
|
@@ -106,21 +133,27 @@ export class GasTxValidator implements TxValidator<Tx> {
|
|
|
106
133
|
#publicDataSource: PublicStateSource;
|
|
107
134
|
#feeJuiceAddress: AztecAddress;
|
|
108
135
|
#gasFees: GasFees;
|
|
136
|
+
#gasLimitOpts?: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number };
|
|
109
137
|
|
|
110
138
|
constructor(
|
|
111
139
|
publicDataSource: PublicStateSource,
|
|
112
140
|
feeJuiceAddress: AztecAddress,
|
|
113
141
|
gasFees: GasFees,
|
|
114
142
|
private bindings?: LoggerBindings,
|
|
143
|
+
opts?: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number },
|
|
115
144
|
) {
|
|
116
145
|
this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings);
|
|
117
146
|
this.#publicDataSource = publicDataSource;
|
|
118
147
|
this.#feeJuiceAddress = feeJuiceAddress;
|
|
119
148
|
this.#gasFees = gasFees;
|
|
149
|
+
this.#gasLimitOpts = opts;
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
async validateTx(tx: Tx): Promise<TxValidationResult> {
|
|
123
|
-
const gasLimitValidation = new GasLimitsValidator(
|
|
153
|
+
const gasLimitValidation = new GasLimitsValidator({
|
|
154
|
+
...this.#gasLimitOpts,
|
|
155
|
+
bindings: this.bindings,
|
|
156
|
+
}).validateGasLimit(tx);
|
|
124
157
|
if (gasLimitValidation.result === 'invalid') {
|
|
125
158
|
return Promise.resolve(gasLimitValidation);
|
|
126
159
|
}
|
|
@@ -8,6 +8,7 @@ export * from './gas_validator.js';
|
|
|
8
8
|
export * from './phases_validator.js';
|
|
9
9
|
export * from './test_utils.js';
|
|
10
10
|
export * from './allowed_public_setup.js';
|
|
11
|
+
export * from './allowed_setup_helpers.js';
|
|
11
12
|
export * from './archive_cache.js';
|
|
12
13
|
export * from './tx_permitted_validator.js';
|
|
13
14
|
export * from './timestamp_validator.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EpochCacheInterface } from '@aztec/epoch-cache';
|
|
2
2
|
import { BlockNumber, type SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { maxBy } from '@aztec/foundation/collection';
|
|
3
4
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
5
|
import { type Logger, createLibp2pComponentLogger, createLogger } from '@aztec/foundation/log';
|
|
5
6
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
P2PMessage,
|
|
20
21
|
type ValidationResult as P2PValidationResult,
|
|
21
22
|
PeerErrorSeverity,
|
|
23
|
+
PeerErrorSeverityByHarshness,
|
|
22
24
|
TopicType,
|
|
23
25
|
createTopicString,
|
|
24
26
|
getTopicsForConfig,
|
|
@@ -224,7 +226,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
224
226
|
|
|
225
227
|
const proposalValidatorOpts = {
|
|
226
228
|
txsPermitted: !config.disableTransactions,
|
|
227
|
-
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
229
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock ?? config.validateMaxTxsPerCheckpoint,
|
|
228
230
|
};
|
|
229
231
|
this.blockProposalValidator = new BlockProposalValidator(epochCache, proposalValidatorOpts);
|
|
230
232
|
this.checkpointProposalValidator = new CheckpointProposalValidator(epochCache, proposalValidatorOpts);
|
|
@@ -235,11 +237,11 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
235
237
|
this.gossipSubEventHandler = this.handleGossipSubEvent.bind(this);
|
|
236
238
|
|
|
237
239
|
this.blockReceivedCallback = async (block: BlockProposal): Promise<boolean> => {
|
|
238
|
-
this.logger.
|
|
239
|
-
`Handler not yet registered
|
|
240
|
+
this.logger.warn(
|
|
241
|
+
`Handler for block received not yet registered on P2P service. Received block ${block.blockNumber} for slot ${block.slotNumber} from peer.`,
|
|
240
242
|
{ p2pMessageIdentifier: await block.p2pMessageLoggingIdentifier() },
|
|
241
243
|
);
|
|
242
|
-
return
|
|
244
|
+
return true;
|
|
243
245
|
};
|
|
244
246
|
|
|
245
247
|
this.checkpointReceivedCallback = (
|
|
@@ -1190,7 +1192,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1190
1192
|
// Note: Validators do NOT attest to individual blocks, only to checkpoint proposals.
|
|
1191
1193
|
const isValid = await this.blockReceivedCallback(block, sender);
|
|
1192
1194
|
if (!isValid) {
|
|
1193
|
-
this.logger.
|
|
1195
|
+
this.logger.info(`Block proposal validation failed for block ${block.blockNumber}`, block.toBlockInfo());
|
|
1194
1196
|
}
|
|
1195
1197
|
}
|
|
1196
1198
|
|
|
@@ -1624,6 +1626,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1624
1626
|
...(this.config.txPublicSetupAllowListExtend ?? []),
|
|
1625
1627
|
];
|
|
1626
1628
|
const blockNumber = BlockNumber(currentBlockNumber + 1);
|
|
1629
|
+
const l1Constants = await this.archiver.getL1Constants();
|
|
1627
1630
|
|
|
1628
1631
|
return createFirstStageTxValidationsForGossipedTransactions(
|
|
1629
1632
|
nextSlotTimestamp,
|
|
@@ -1637,6 +1640,11 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1637
1640
|
!this.config.disableTransactions,
|
|
1638
1641
|
allowedInSetup,
|
|
1639
1642
|
this.logger.getBindings(),
|
|
1643
|
+
{
|
|
1644
|
+
rollupManaLimit: l1Constants.rollupManaLimit,
|
|
1645
|
+
maxBlockL2Gas: this.config.validateMaxL2BlockGas,
|
|
1646
|
+
maxBlockDAGas: this.config.validateMaxDABlockGas,
|
|
1647
|
+
},
|
|
1640
1648
|
);
|
|
1641
1649
|
}
|
|
1642
1650
|
|
|
@@ -1662,8 +1670,10 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1662
1670
|
|
|
1663
1671
|
// A promise that resolves when all validations have been run
|
|
1664
1672
|
const allValidations = await Promise.all(validationPromises);
|
|
1665
|
-
const
|
|
1666
|
-
if (
|
|
1673
|
+
const failures = allValidations.filter(x => !x.isValid);
|
|
1674
|
+
if (failures.length > 0) {
|
|
1675
|
+
// Pick the most severe failure (lowest tolerance = harshest penalty)
|
|
1676
|
+
const failed = maxBy(failures, f => PeerErrorSeverityByHarshness.indexOf(f.severity))!;
|
|
1667
1677
|
return {
|
|
1668
1678
|
allPassed: false,
|
|
1669
1679
|
failure: {
|