Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ci3/run_compose_test
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ trap down EXIT
# Start clean
down

docker compose -p "$name" up -d --force-recreate
# Image pulls can fail on transient registry errors (e.g. Docker Hub 502s); retry only those.
pull_flake='Bad Gateway|Service Unavailable|Gateway Time-?out|Internal Server Error|unexpected status from HEAD request|TLS handshake timeout|i/o timeout|connection reset|connection refused|Temporary failure in name resolution|unexpected EOF|toomanyrequests'
retry -p "$pull_flake" "docker compose -p \"$name\" up -d --force-recreate"

cid="$(docker ps -aq \
--filter "label=com.docker.compose.project=$name" \
Expand Down
1 change: 1 addition & 0 deletions yarn-project/archiver/src/l1/calldata_retriever.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ describe('CalldataRetriever', () => {
archiveRoot: Fr.random(),
header: CheckpointHeader.random(),
attestations: [],
verbatimAttestations: { signatureIndices: '0x', signaturesOrAddresses: '0x' },
blockHash,
feeAssetPriceModifier: 0n,
};
Expand Down
19 changes: 13 additions & 6 deletions yarn-project/archiver/src/l1/calldata_retriever.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ type CheckpointData = {
archiveRoot: Fr;
header: CheckpointHeader;
attestations: CommitteeAttestation[];
/**
* The exact packed `CommitteeAttestations` tuple as it appears in the propose calldata, preserved
* verbatim (never re-derived from {@link attestations}) so invalidation evidence stays byte-faithful to
* the on-chain `attestationsHash`.
*/
verbatimAttestations: ViemCommitteeAttestations;
blockHash: string;
feeAssetPriceModifier: bigint;
};
Expand Down Expand Up @@ -406,14 +412,14 @@ export class CalldataRetriever {
return undefined;
}

const [decodedArgs, packedAttestations] = args! as readonly [
const [decodedArgs, verbatimAttestations] = args! as readonly [
{ archive: Hex; oracleInput: { feeAssetPriceModifier: bigint }; header: ViemHeader },
ViemCommitteeAttestations,
...unknown[],
];

// Verify attestationsHash
const computedAttestationsHash = this.computeAttestationsHash(packedAttestations);
const computedAttestationsHash = this.computeAttestationsHash(verbatimAttestations);
if (
!Buffer.from(hexToBytes(computedAttestationsHash)).equals(
Buffer.from(hexToBytes(expectedHashes.attestationsHash)),
Expand Down Expand Up @@ -441,15 +447,15 @@ export class CalldataRetriever {
return undefined;
}

const attestations = CommitteeAttestation.fromPacked(packedAttestations, this.targetCommitteeSize);
const attestations = CommitteeAttestation.fromPacked(verbatimAttestations, this.targetCommitteeSize);

this.logger.trace(`Validated and decoded propose calldata for checkpoint ${checkpointNumber}`, {
checkpointNumber,
archive: decodedArgs.archive,
header: decodedArgs.header,
l1BlockHash: blockHash,
attestations,
packedAttestations,
verbatimAttestations,
targetCommitteeSize: this.targetCommitteeSize,
});

Expand All @@ -458,6 +464,7 @@ export class CalldataRetriever {
archiveRoot,
header,
attestations,
verbatimAttestations,
blockHash,
feeAssetPriceModifier,
};
Expand All @@ -467,8 +474,8 @@ export class CalldataRetriever {
}

/** Computes the keccak256 hash of ABI-encoded CommitteeAttestations. */
private computeAttestationsHash(packedAttestations: ViemCommitteeAttestations): Hex {
return keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [packedAttestations]));
private computeAttestationsHash(verbatimAttestations: ViemCommitteeAttestations): Hex {
return keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [verbatimAttestations]));
}

/** Computes the keccak256 payload digest from the checkpoint header, archive root, and fee asset price modifier. */
Expand Down
6 changes: 6 additions & 0 deletions yarn-project/archiver/src/l1/data_retrieval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
InboxContract,
MessageSentLog,
RollupContract,
ViemCommitteeAttestations,
ViemHeader,
} from '@aztec/ethereum/contracts';
import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
Expand Down Expand Up @@ -56,6 +57,11 @@ export type RetrievedCheckpointFromCalldata = RetrievedCheckpointBase & {
blobHashes: Buffer[];
/** Parent beacon block root from the L1 block, used for blob fetching. */
parentBeaconBlockRoot: string | undefined;
/**
* The exact packed `CommitteeAttestations` tuple from the propose calldata, carried verbatim so that
* attestation validation can attach byte-faithful invalidation evidence to a negative result.
*/
verbatimAttestations: ViemCommitteeAttestations;
};

export async function retrievedToPublishedCheckpoint({
Expand Down
63 changes: 60 additions & 3 deletions yarn-project/archiver/src/modules/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,48 @@ import { times } from '@aztec/foundation/collection';
import { Secp256k1Signer, flipSignature } from '@aztec/foundation/crypto/secp256k1-signer';
import { Signature } from '@aztec/foundation/eth-signature';
import { type Logger, createLogger } from '@aztec/foundation/log';
import { CommitteeAttestation, EthAddress } from '@aztec/stdlib/block';
import { Checkpoint } from '@aztec/stdlib/checkpoint';
import {
CommitteeAttestation,
CommitteeAttestationsAndSigners,
EthAddress,
type ValidateCheckpointResult,
} from '@aztec/stdlib/block';
import { Checkpoint, type PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
import { ConsensusPayload, type CoordinationSignatureContext } from '@aztec/stdlib/p2p';
import { TEST_COORDINATION_SIGNATURE_CONTEXT } from '@aztec/stdlib/testing';

import { type MockProxy, mock } from 'jest-mock-extended';
import assert from 'node:assert';

import { makeSignedPublishedCheckpoint } from '../test/mock_structs.js';
import { getAttestationInfoFromPublishedCheckpoint, validateCheckpointAttestations } from './validation.js';
import { getAttestationInfoFromPublishedCheckpoint, validateAttestations } from './validation.js';

/**
* Blob-path wrapper over the shared attestation validator, used only by these tests. Production
* invalidation always originates from the calldata path (validateCheckpointAttestationsFromCalldata);
* this repacks a fully decoded checkpoint's attestations to supply the verbatimAttestations field.
*/
function validateCheckpointAttestations(
publishedCheckpoint: PublishedCheckpoint,
epochCache: EpochCache,
constants: Pick<L1RollupConstants, 'epochDuration'>,
signatureContext: CoordinationSignatureContext,
logger?: Logger,
): Promise<ValidateCheckpointResult> {
const { checkpoint, attestations } = publishedCheckpoint;
const payload = ConsensusPayload.fromCheckpoint(checkpoint, signatureContext);
const verbatimAttestations = CommitteeAttestationsAndSigners.packAttestations(attestations);
return validateAttestations(
payload,
attestations,
verbatimAttestations,
checkpoint.toCheckpointInfo(),
epochCache,
constants,
logger,
);
}

describe('validateCheckpointAttestations', () => {
let epochCache: MockProxy<EpochCache>;
Expand Down Expand Up @@ -232,6 +265,30 @@ describe('validateCheckpointAttestations', () => {
expect(result.invalidIndex).toBe(2);
});

it('fails if an attestation is in yParity (v in {0, 1}) form even though it recovers to the right member', async () => {
const checkpoint = await makeCheckpoint(signers.slice(0, 4), committee);

// Rewrite index 2's canonical signature to its yParity form: same (r, s), recovery byte 0/1. It still
// recovers to the same committee member off-chain, but L1 ECDSA.recover rejects v not in {27, 28}.
const original = checkpoint.attestations[2];
const yParity = new Signature(original.signature.r, original.signature.s, original.signature.v - 27);
checkpoint.attestations[2] = new CommitteeAttestation(original.address, yParity);

const attestations = getAttestationInfoFromPublishedCheckpoint(checkpoint, TEST_COORDINATION_SIGNATURE_CONTEXT);
expect(attestations[2].status).toBe('invalid-signature');

const result = await validateCheckpointAttestations(
checkpoint,
epochCache,
constants,
TEST_COORDINATION_SIGNATURE_CONTEXT,
logger,
);
assert(!result.valid);
assert(result.reason === 'invalid-attestation');
expect(result.invalidIndex).toBe(2);
});

it('reports correct index when invalid attestation follows provided address', async () => {
const checkpoint = await makeCheckpoint(signers.slice(0, 3), committee);

Expand Down
33 changes: 15 additions & 18 deletions yarn-project/archiver/src/modules/validation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { EpochCache } from '@aztec/epoch-cache';
import type { ViemCommitteeAttestations } from '@aztec/ethereum/contracts';
import { type CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
import { compactArray } from '@aztec/foundation/collection';
import type { Fr } from '@aztec/foundation/curves/bn254';
Expand Down Expand Up @@ -29,29 +30,15 @@ export function getAttestationInfoFromPublishedCheckpoint(
return getAttestationInfoFromPayload(payload, attestations);
}

/**
* Validates the attestations of a checkpoint already retrieved (with its blocks) from blobs.
* Returns true if the attestations are valid and sufficient, false otherwise.
*/
export function validateCheckpointAttestations(
publishedCheckpoint: PublishedCheckpoint,
epochCache: EpochCache,
constants: Pick<L1RollupConstants, 'epochDuration'>,
signatureContext: CoordinationSignatureContext,
logger?: Logger,
): Promise<ValidateCheckpointResult> {
const { checkpoint, attestations } = publishedCheckpoint;
const payload = ConsensusPayload.fromCheckpoint(checkpoint, signatureContext);
return validateAttestations(payload, attestations, checkpoint.toCheckpointInfo(), epochCache, constants, logger);
}

/** The subset of a calldata-only checkpoint needed to validate its committee attestations. */
export type CalldataCheckpointForAttestations = {
checkpointNumber: CheckpointNumber;
archiveRoot: Fr;
feeAssetPriceModifier: bigint;
header: CheckpointHeader;
attestations: CommitteeAttestation[];
/** The exact packed attestations tuple from L1 calldata, carried verbatim for byte-faithful invalidation. */
verbatimAttestations: ViemCommitteeAttestations;
};

/**
Expand Down Expand Up @@ -80,17 +67,26 @@ export function validateCheckpointAttestationsFromCalldata(
checkpointNumber: checkpoint.checkpointNumber,
timestamp: checkpoint.header.timestamp,
};
return validateAttestations(payload, checkpoint.attestations, checkpointInfo, epochCache, constants, logger);
return validateAttestations(
payload,
checkpoint.attestations,
checkpoint.verbatimAttestations,
checkpointInfo,
epochCache,
constants,
logger,
);
}

/**
* Core attestation validation over a consensus payload, its attestations, and checkpoint metadata --
* independent of whether the checkpoint's blocks have been decoded from blobs. Returns true if the
* attestations are valid and sufficient, false otherwise.
*/
async function validateAttestations(
export async function validateAttestations(
payload: ConsensusPayload,
attestations: CommitteeAttestation[],
verbatimAttestations: ViemCommitteeAttestations,
checkpointInfo: CheckpointInfo,
epochCache: EpochCache,
constants: Pick<L1RollupConstants, 'epochDuration'>,
Expand Down Expand Up @@ -137,6 +133,7 @@ async function validateAttestations(
epoch,
attestors,
attestations,
verbatimAttestations,
});

for (let i = 0; i < attestorInfos.length; i++) {
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/archiver/src/store/block_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,7 @@ describe('BlockStore', () => {
seed: 456n,
attestors: [EthAddress.random()],
attestations: [CommitteeAttestation.random()],
verbatimAttestations: { signatureIndices: '0x', signaturesOrAddresses: '0x' },
reason: 'insufficient-attestations',
};

Expand All @@ -2007,6 +2008,7 @@ describe('BlockStore', () => {
epoch: EpochNumber(789),
seed: 101n,
attestations: [CommitteeAttestation.random()],
verbatimAttestations: { signatureIndices: '0x', signaturesOrAddresses: '0x' },
reason: 'invalid-attestation',
invalidIndex: 5,
};
Expand All @@ -2027,6 +2029,7 @@ describe('BlockStore', () => {
seed: 888n,
attestors: [EthAddress.random()],
attestations: [CommitteeAttestation.random()],
verbatimAttestations: { signatureIndices: '0x', signaturesOrAddresses: '0x' },
reason: 'insufficient-attestations',
};

Expand All @@ -2046,6 +2049,7 @@ describe('BlockStore', () => {
seed: 0n,
attestors: [],
attestations: [],
verbatimAttestations: { signatureIndices: '0x', signaturesOrAddresses: '0x' },
reason: 'insufficient-attestations',
};

Expand Down
6 changes: 3 additions & 3 deletions yarn-project/archiver/src/test/fake_l1_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ export class FakeL1State {
getHashedSignaturePayloadTypedData(attestationsAndSigners),
);

const packedAttestations = attestationsAndSigners.getPackedAttestations();
const verbatimAttestations = attestationsAndSigners.getPackedAttestations();

const rollupInput = encodeFunctionData({
abi: RollupAbi,
Expand All @@ -693,7 +693,7 @@ export class FakeL1State {
archive,
oracleInput: { feeAssetPriceModifier: 0n },
},
packedAttestations,
verbatimAttestations,
attestationsAndSigners.getSigners().map(signer => signer.toString()),
attestationsAndSignersSignature.toViemSignature(),
blobInput,
Expand All @@ -716,7 +716,7 @@ export class FakeL1State {

// Compute attestationsHash (same logic as CalldataRetriever)
const attestationsHash = Buffer32.fromString(
keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [packedAttestations])),
keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [verbatimAttestations])),
);

// Compute payloadDigest (same logic as CalldataRetriever)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ function makeInvalidStatus(firstInvalid: CheckpointNumber): ValidateCheckpointRe
seed: 0n,
attestors: [],
attestations: [],
verbatimAttestations: { signatureIndices: '0x', signaturesOrAddresses: '0x' },
reason: 'insufficient-attestations',
};
}
5 changes: 3 additions & 2 deletions yarn-project/bot/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type ConfigMappingsType,
SecretValue,
booleanConfigHelper,
floatConfigHelper,
getConfigFromMappings,
getDefaultConfig,
numberConfigHelper,
Expand Down Expand Up @@ -103,7 +104,7 @@ export const BotConfigSchema = zodFor<BotConfig>()(
privateTransfersPerTx: z.number().int().nonnegative(),
publicTransfersPerTx: z.number().int().nonnegative(),
feePaymentMethod: z.literal('fee_juice'),
minFeePadding: z.number().int().nonnegative(),
minFeePadding: z.number().nonnegative(),
noStart: z.boolean(),
txMinedWaitSeconds: z.number(),
followChain: z.enum(BotFollowChain),
Expand Down Expand Up @@ -204,7 +205,7 @@ export const botConfigMappings: ConfigMappingsType<BotConfig> = {
minFeePadding: {
env: 'BOT_MIN_FEE_PADDING',
description: 'How much is the bot willing to overpay vs. the current base fee',
...numberConfigHelper(3),
...floatConfigHelper(3),
},
noStart: {
env: 'BOT_NO_START',
Expand Down
7 changes: 5 additions & 2 deletions yarn-project/end-to-end/src/automine/pxe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ describe('automine/pxe', () => {
let contract: TestContract;

beforeAll(async () => {
const test = await AutomineTestContext.setup({ numberOfAccounts: 1 });
({
teardown,
wallet,
accounts: [defaultAccountAddress],
} = (await AutomineTestContext.setup({ numberOfAccounts: 1 })).context);
({ contract } = await TestContract.deploy(wallet).send({ from: defaultAccountAddress }));
} = test.context);
// The test only calls the noinitcheck private `emit_nullifier`, so register the contract instead of
// deploying it — this avoids a deployment tx and its checkpoint cycle.
contract = await test.registerContract(wallet, TestContract);
});

afterAll(() => teardown());
Expand Down
Loading
Loading