@aztec/validator-client 0.0.1-commit.3e3d0c9cd → 0.0.1-commit.3f5453c7b
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/README.md +9 -13
- package/dest/checkpoint_builder.d.ts +10 -8
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +29 -28
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +0 -5
- package/dest/duties/validation_service.d.ts +3 -4
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +7 -12
- package/dest/factory.d.ts +7 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +5 -5
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/metrics.d.ts +6 -2
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +12 -0
- package/dest/proposal_handler.d.ts +107 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/{block_proposal_handler.js → proposal_handler.js} +392 -25
- package/dest/validator.d.ts +10 -15
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +44 -216
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +38 -33
- package/src/config.ts +0 -5
- package/src/duties/validation_service.ts +7 -14
- package/src/factory.ts +10 -4
- package/src/index.ts +1 -1
- package/src/metrics.ts +19 -1
- package/src/{block_proposal_handler.ts → proposal_handler.ts} +439 -23
- package/src/validator.ts +60 -235
- package/dest/block_proposal_handler.d.ts +0 -64
- package/dest/block_proposal_handler.d.ts.map +0 -1
|
@@ -20,6 +20,7 @@ import type { ContractDataSource } from '@aztec/stdlib/contract';
|
|
|
20
20
|
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
21
21
|
import { Gas } from '@aztec/stdlib/gas';
|
|
22
22
|
import {
|
|
23
|
+
type BlockBuilderOptions,
|
|
23
24
|
type BuildBlockInCheckpointResult,
|
|
24
25
|
type FullNodeBlockBuilderConfig,
|
|
25
26
|
FullNodeBlockBuilderConfigKeys,
|
|
@@ -46,6 +47,9 @@ export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/serv
|
|
|
46
47
|
export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
47
48
|
private log: Logger;
|
|
48
49
|
|
|
50
|
+
/** Persistent contracts DB shared across all blocks in this checkpoint. */
|
|
51
|
+
protected contractsDB: PublicContractsDB;
|
|
52
|
+
|
|
49
53
|
constructor(
|
|
50
54
|
private checkpointBuilder: LightweightCheckpointBuilder,
|
|
51
55
|
private fork: MerkleTreeWriteOperations,
|
|
@@ -60,6 +64,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
60
64
|
...bindings,
|
|
61
65
|
instanceId: `checkpoint-${checkpointBuilder.checkpointNumber}`,
|
|
62
66
|
});
|
|
67
|
+
this.contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
getConstantData(): CheckpointGlobalVariables {
|
|
@@ -74,7 +79,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
74
79
|
pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
|
|
75
80
|
blockNumber: BlockNumber,
|
|
76
81
|
timestamp: bigint,
|
|
77
|
-
opts:
|
|
82
|
+
opts: BlockBuilderOptions & { expectedEndState?: StateReference },
|
|
78
83
|
): Promise<BuildBlockInCheckpointResult> {
|
|
79
84
|
const slot = this.checkpointBuilder.constants.slotNumber;
|
|
80
85
|
|
|
@@ -104,6 +109,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
104
109
|
...this.capLimitsByCheckpointBudgets(opts),
|
|
105
110
|
};
|
|
106
111
|
|
|
112
|
+
// Create a block-level checkpoint on the contracts DB so we can roll back on failure
|
|
113
|
+
this.contractsDB.createCheckpoint();
|
|
107
114
|
// We execute all merkle tree operations on a world state fork checkpoint
|
|
108
115
|
// This enables us to discard all modifications in the event that we fail to successfully process sufficient transactions
|
|
109
116
|
const forkCheckpoint = await ForkCheckpoint.new(this.fork);
|
|
@@ -112,6 +119,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
112
119
|
const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
|
|
113
120
|
processor.process(pendingTxs, cappedOpts, validator),
|
|
114
121
|
);
|
|
122
|
+
|
|
115
123
|
// Throw before updating state if we don't have enough valid txs
|
|
116
124
|
const minValidTxs = opts.minValidTxs ?? 0;
|
|
117
125
|
if (processedTxs.length < minValidTxs) {
|
|
@@ -126,6 +134,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
126
134
|
expectedEndState: opts.expectedEndState,
|
|
127
135
|
});
|
|
128
136
|
|
|
137
|
+
this.contractsDB.commitCheckpoint();
|
|
138
|
+
|
|
129
139
|
this.log.debug('Built block within checkpoint', {
|
|
130
140
|
header: block.header.toInspect(),
|
|
131
141
|
processedTxs: processedTxs.map(tx => tx.hash.toString()),
|
|
@@ -140,6 +150,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
140
150
|
usedTxs,
|
|
141
151
|
};
|
|
142
152
|
} catch (err) {
|
|
153
|
+
// Revert all changes to contracts db
|
|
154
|
+
this.contractsDB.revertCheckpoint();
|
|
143
155
|
// If we reached the point of committing the checkpoint, this does nothing
|
|
144
156
|
// Otherwise it reverts any changes made to the fork for this failed block
|
|
145
157
|
await forkCheckpoint.revert();
|
|
@@ -167,11 +179,12 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
167
179
|
|
|
168
180
|
/**
|
|
169
181
|
* Caps per-block gas and blob field limits by remaining checkpoint-level budgets.
|
|
170
|
-
*
|
|
171
|
-
*
|
|
182
|
+
* When building a proposal (isBuildingProposal=true), computes a fair share of remaining budget
|
|
183
|
+
* across remaining blocks scaled by the multiplier. When validating, only caps by per-block limit
|
|
184
|
+
* and remaining checkpoint budget (no redistribution or multiplier).
|
|
172
185
|
*/
|
|
173
186
|
protected capLimitsByCheckpointBudgets(
|
|
174
|
-
opts:
|
|
187
|
+
opts: BlockBuilderOptions,
|
|
175
188
|
): Pick<PublicProcessorLimits, 'maxBlockGas' | 'maxBlobFields' | 'maxTransactions'> {
|
|
176
189
|
const existingBlocks = this.checkpointBuilder.getBlocks();
|
|
177
190
|
|
|
@@ -192,39 +205,31 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
192
205
|
const blockEndOverhead = getNumBlockEndBlobFields(isFirstBlock);
|
|
193
206
|
const maxBlobFieldsForTxs = totalBlobCapacity - usedBlobFields - blockEndOverhead;
|
|
194
207
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
let cappedMaxTransactions: number | undefined;
|
|
215
|
-
if (this.config.maxTxsPerCheckpoint !== undefined) {
|
|
216
|
-
const usedTxs = sum(existingBlocks.map(b => b.body.txEffects.length));
|
|
217
|
-
const remainingTxs = Math.max(0, this.config.maxTxsPerCheckpoint - usedTxs);
|
|
218
|
-
const fairShareTxs = redistribute ? Math.ceil((remainingTxs / remainingBlocks) * multiplier) : Infinity;
|
|
219
|
-
cappedMaxTransactions = Math.min(opts.maxTransactions ?? Infinity, fairShareTxs, remainingTxs);
|
|
220
|
-
} else {
|
|
221
|
-
cappedMaxTransactions = opts.maxTransactions;
|
|
208
|
+
// Remaining txs
|
|
209
|
+
const usedTxs = sum(existingBlocks.map(b => b.body.txEffects.length));
|
|
210
|
+
const remainingTxs = Math.max(0, (this.config.maxTxsPerCheckpoint ?? Infinity) - usedTxs);
|
|
211
|
+
|
|
212
|
+
// Cap by per-block limit + remaining checkpoint budget
|
|
213
|
+
let cappedL2Gas = Math.min(opts.maxBlockGas?.l2Gas ?? Infinity, remainingMana);
|
|
214
|
+
let cappedDAGas = Math.min(opts.maxBlockGas?.daGas ?? Infinity, remainingDAGas);
|
|
215
|
+
let cappedBlobFields = Math.min(opts.maxBlobFields ?? Infinity, maxBlobFieldsForTxs);
|
|
216
|
+
let cappedMaxTransactions = Math.min(opts.maxTransactions ?? Infinity, remainingTxs);
|
|
217
|
+
|
|
218
|
+
// Proposer mode: further cap by fair share of remaining budget across remaining blocks
|
|
219
|
+
if (opts.isBuildingProposal) {
|
|
220
|
+
const remainingBlocks = Math.max(1, opts.maxBlocksPerCheckpoint - existingBlocks.length);
|
|
221
|
+
const multiplier = opts.perBlockAllocationMultiplier;
|
|
222
|
+
|
|
223
|
+
cappedL2Gas = Math.min(cappedL2Gas, Math.ceil((remainingMana / remainingBlocks) * multiplier));
|
|
224
|
+
cappedDAGas = Math.min(cappedDAGas, Math.ceil((remainingDAGas / remainingBlocks) * multiplier));
|
|
225
|
+
cappedBlobFields = Math.min(cappedBlobFields, Math.ceil((maxBlobFieldsForTxs / remainingBlocks) * multiplier));
|
|
226
|
+
cappedMaxTransactions = Math.min(cappedMaxTransactions, Math.ceil((remainingTxs / remainingBlocks) * multiplier));
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
return {
|
|
225
230
|
maxBlockGas: new Gas(cappedDAGas, cappedL2Gas),
|
|
226
231
|
maxBlobFields: cappedBlobFields,
|
|
227
|
-
maxTransactions: cappedMaxTransactions,
|
|
232
|
+
maxTransactions: Number.isFinite(cappedMaxTransactions) ? cappedMaxTransactions : undefined,
|
|
228
233
|
};
|
|
229
234
|
}
|
|
230
235
|
|
|
@@ -233,7 +238,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
233
238
|
...(await getDefaultAllowedSetupFunctions()),
|
|
234
239
|
...(this.config.txPublicSetupAllowListExtend ?? []),
|
|
235
240
|
];
|
|
236
|
-
const contractsDB =
|
|
241
|
+
const contractsDB = this.contractsDB;
|
|
237
242
|
const guardedFork = new GuardedMerkleTreeOperations(fork);
|
|
238
243
|
|
|
239
244
|
const collectDebugLogs = this.debugLogStore.isEnabled;
|
package/src/config.ts
CHANGED
|
@@ -49,11 +49,6 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
|
|
|
49
49
|
description: 'Interval between polling for new attestations',
|
|
50
50
|
...numberConfigHelper(200),
|
|
51
51
|
},
|
|
52
|
-
validatorReexecute: {
|
|
53
|
-
env: 'VALIDATOR_REEXECUTE',
|
|
54
|
-
description: 'Re-execute transactions before attesting',
|
|
55
|
-
...booleanConfigHelper(true),
|
|
56
|
-
},
|
|
57
52
|
alwaysReexecuteBlockProposals: {
|
|
58
53
|
description:
|
|
59
54
|
'Whether to always reexecute block proposals, even for non-validator nodes (useful for monitoring network status).',
|
|
@@ -11,7 +11,6 @@ import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
|
11
11
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
12
12
|
import { createLogger } from '@aztec/foundation/log';
|
|
13
13
|
import type { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
14
|
-
import type { CreateCheckpointProposalLastBlockData } from '@aztec/stdlib/interfaces/server';
|
|
15
14
|
import {
|
|
16
15
|
BlockProposal,
|
|
17
16
|
type BlockProposalOptions,
|
|
@@ -86,7 +85,7 @@ export class ValidationService {
|
|
|
86
85
|
*
|
|
87
86
|
* @param checkpointHeader - The checkpoint header containing aggregated data
|
|
88
87
|
* @param archive - The archive of the checkpoint
|
|
89
|
-
* @param
|
|
88
|
+
* @param lastBlockProposal - Signed block proposal for the last block in the checkpoint, or undefined
|
|
90
89
|
* @param proposerAttesterAddress - The address of the proposer
|
|
91
90
|
* @param options - Checkpoint proposal options
|
|
92
91
|
*
|
|
@@ -96,13 +95,15 @@ export class ValidationService {
|
|
|
96
95
|
checkpointHeader: CheckpointHeader,
|
|
97
96
|
archive: Fr,
|
|
98
97
|
feeAssetPriceModifier: bigint,
|
|
99
|
-
|
|
98
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
100
99
|
proposerAttesterAddress: EthAddress | undefined,
|
|
101
100
|
options: CheckpointProposalOptions,
|
|
102
101
|
): Promise<CheckpointProposal> {
|
|
103
|
-
// For testing: change the archive to trigger state_mismatch validation failure
|
|
102
|
+
// For testing: change the archive to trigger state_mismatch validation failure.
|
|
103
|
+
// If there's a last block proposal, use its (already invalid) archive to keep signatures consistent
|
|
104
|
+
// so P2P validation passes and the slasher can detect the offense.
|
|
104
105
|
if (options.broadcastInvalidCheckpointProposal) {
|
|
105
|
-
archive = Fr.random();
|
|
106
|
+
archive = lastBlockProposal?.archiveRoot ?? Fr.random();
|
|
106
107
|
this.log.warn(`Creating INVALID checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -112,19 +113,11 @@ export class ValidationService {
|
|
|
112
113
|
return this.keyStore.signMessageWithAddress(address, payload, context);
|
|
113
114
|
};
|
|
114
115
|
|
|
115
|
-
// Last block to include in the proposal
|
|
116
|
-
const lastBlock = lastBlockInfo && {
|
|
117
|
-
blockHeader: lastBlockInfo.blockHeader,
|
|
118
|
-
indexWithinCheckpoint: lastBlockInfo.indexWithinCheckpoint,
|
|
119
|
-
txHashes: lastBlockInfo.txs.map(tx => tx.getTxHash()),
|
|
120
|
-
txs: options.publishFullTxs ? lastBlockInfo.txs : undefined,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
116
|
return CheckpointProposal.createProposalFromSigner(
|
|
124
117
|
checkpointHeader,
|
|
125
118
|
archive,
|
|
126
119
|
feeAssetPriceModifier,
|
|
127
|
-
|
|
120
|
+
lastBlockProposal,
|
|
128
121
|
payloadSigner,
|
|
129
122
|
);
|
|
130
123
|
}
|
package/src/factory.ts
CHANGED
|
@@ -7,13 +7,14 @@ import type { L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
|
7
7
|
import type { ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
8
8
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
9
9
|
import type { TelemetryClient } from '@aztec/telemetry-client';
|
|
10
|
+
import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
10
11
|
|
|
11
|
-
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
12
12
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
13
13
|
import { ValidatorMetrics } from './metrics.js';
|
|
14
|
+
import { ProposalHandler } from './proposal_handler.js';
|
|
14
15
|
import { ValidatorClient } from './validator.js';
|
|
15
16
|
|
|
16
|
-
export function
|
|
17
|
+
export function createProposalHandler(
|
|
17
18
|
config: ValidatorClientFullConfig,
|
|
18
19
|
deps: {
|
|
19
20
|
checkpointsBuilder: FullNodeCheckpointsBuilder;
|
|
@@ -22,6 +23,7 @@ export function createBlockProposalHandler(
|
|
|
22
23
|
l1ToL2MessageSource: L1ToL2MessageSource;
|
|
23
24
|
p2pClient: P2PClient;
|
|
24
25
|
epochCache: EpochCache;
|
|
26
|
+
blobClient: BlobClientInterface;
|
|
25
27
|
dateProvider: DateProvider;
|
|
26
28
|
telemetry: TelemetryClient;
|
|
27
29
|
},
|
|
@@ -29,9 +31,9 @@ export function createBlockProposalHandler(
|
|
|
29
31
|
const metrics = new ValidatorMetrics(deps.telemetry);
|
|
30
32
|
const blockProposalValidator = new BlockProposalValidator(deps.epochCache, {
|
|
31
33
|
txsPermitted: !config.disableTransactions,
|
|
32
|
-
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
34
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock ?? config.validateMaxTxsPerCheckpoint,
|
|
33
35
|
});
|
|
34
|
-
return new
|
|
36
|
+
return new ProposalHandler(
|
|
35
37
|
deps.checkpointsBuilder,
|
|
36
38
|
deps.worldState,
|
|
37
39
|
deps.blockSource,
|
|
@@ -40,9 +42,11 @@ export function createBlockProposalHandler(
|
|
|
40
42
|
blockProposalValidator,
|
|
41
43
|
deps.epochCache,
|
|
42
44
|
config,
|
|
45
|
+
deps.blobClient,
|
|
43
46
|
metrics,
|
|
44
47
|
deps.dateProvider,
|
|
45
48
|
deps.telemetry,
|
|
49
|
+
undefined,
|
|
46
50
|
);
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -59,6 +63,7 @@ export function createValidatorClient(
|
|
|
59
63
|
epochCache: EpochCache;
|
|
60
64
|
keyStoreManager: KeystoreManager | undefined;
|
|
61
65
|
blobClient: BlobClientInterface;
|
|
66
|
+
slashingProtectionDb?: SlashingProtectionDatabase;
|
|
62
67
|
},
|
|
63
68
|
) {
|
|
64
69
|
if (config.disableValidator || !deps.keyStoreManager) {
|
|
@@ -79,5 +84,6 @@ export function createValidatorClient(
|
|
|
79
84
|
deps.blobClient,
|
|
80
85
|
deps.dateProvider,
|
|
81
86
|
deps.telemetry,
|
|
87
|
+
deps.slashingProtectionDb,
|
|
82
88
|
);
|
|
83
89
|
}
|
package/src/index.ts
CHANGED
package/src/metrics.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
createUpDownCounterWithDefault,
|
|
12
12
|
} from '@aztec/telemetry-client';
|
|
13
13
|
|
|
14
|
-
import type { BlockProposalValidationFailureReason } from './
|
|
14
|
+
import type { BlockProposalValidationFailureReason } from './proposal_handler.js';
|
|
15
15
|
|
|
16
16
|
export class ValidatorMetrics {
|
|
17
17
|
private failedReexecutionCounter: UpDownCounter;
|
|
@@ -24,6 +24,8 @@ export class ValidatorMetrics {
|
|
|
24
24
|
private reexMana: Histogram;
|
|
25
25
|
private reexTx: Histogram;
|
|
26
26
|
private reexDuration: Gauge;
|
|
27
|
+
private checkpointProposalToPipelinedStateDuration: Histogram;
|
|
28
|
+
private checkpointProposalReceiveOffsetFromNextSlotBoundary: Histogram;
|
|
27
29
|
|
|
28
30
|
constructor(telemetryClient: TelemetryClient) {
|
|
29
31
|
const meter = telemetryClient.getMeter('Validator');
|
|
@@ -77,6 +79,12 @@ export class ValidatorMetrics {
|
|
|
77
79
|
this.reexTx = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_TX_COUNT);
|
|
78
80
|
|
|
79
81
|
this.reexDuration = meter.createGauge(Metrics.VALIDATOR_RE_EXECUTION_TIME);
|
|
82
|
+
this.checkpointProposalToPipelinedStateDuration = meter.createHistogram(
|
|
83
|
+
Metrics.VALIDATOR_CHECKPOINT_PROPOSAL_TO_PIPELINED_STATE_DURATION,
|
|
84
|
+
);
|
|
85
|
+
this.checkpointProposalReceiveOffsetFromNextSlotBoundary = meter.createHistogram(
|
|
86
|
+
Metrics.VALIDATOR_CHECKPOINT_PROPOSAL_RECEIVE_OFFSET_FROM_NEXT_SLOT_BOUNDARY,
|
|
87
|
+
);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
public recordReex(time: number, txs: number, mManaTotal: number) {
|
|
@@ -85,6 +93,16 @@ export class ValidatorMetrics {
|
|
|
85
93
|
this.reexMana.record(mManaTotal);
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
public recordCheckpointProposalToPipelinedStateDuration(durationMs: number) {
|
|
97
|
+
this.checkpointProposalToPipelinedStateDuration.record(Math.ceil(durationMs));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public recordCheckpointProposalReceiveOffsetFromNextSlotBoundary(offsetMs: number) {
|
|
101
|
+
this.checkpointProposalReceiveOffsetFromNextSlotBoundary.record(Math.ceil(Math.abs(offsetMs)), {
|
|
102
|
+
[Attributes.SLOT_BOUNDARY_SIDE]: offsetMs < 0 ? 'before' : 'after',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
public recordFailedReexecution(proposal: BlockProposal) {
|
|
89
107
|
const proposer = proposal.getSender();
|
|
90
108
|
this.failedReexecutionCounter.add(1, {
|