Skip to content

Commit

Permalink
fix: support empty epochs (#9341)
Browse files Browse the repository at this point in the history
Change the rollup contract to report the claimable epoch; this returns
when there is an epoch to prove and it is not the epoch presently
claimed.

Also improve debugging in the native test.
  • Loading branch information
just-mitch authored Oct 23, 2024
1 parent ebf5d2e commit 9dda91e
Show file tree
Hide file tree
Showing 14 changed files with 97 additions and 47 deletions.
18 changes: 12 additions & 6 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,18 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
_validateHeader(header, _signatures, _digest, _currentTime, _txsEffectsHash, _flags);
}

function nextEpochToClaim() external view override(IRollup) returns (Epoch) {
Epoch epochClaimed = proofClaim.epochToProve;
if (proofClaim.proposerClaimant == address(0) && epochClaimed == Epoch.wrap(0)) {
return Epoch.wrap(0);
}
return Epoch.wrap(1) + epochClaimed;
/**
* @notice Get the next epoch that can be claimed
* @dev Will revert if the epoch has already been claimed or if there is no epoch to prove
*/
function getClaimableEpoch() external view override(IRollup) returns (Epoch) {
Epoch epochToProve = getEpochToProve();
require(
proofClaim.epochToProve != epochToProve
|| (proofClaim.proposerClaimant == address(0) && proofClaim.epochToProve == Epoch.wrap(0)),
Errors.Rollup__ProofRightAlreadyClaimed()
);
return epochToProve;
}

function computeTxsEffectsHash(bytes calldata _body)
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ interface IRollup {
function getProvenBlockNumber() external view returns (uint256);
function getPendingBlockNumber() external view returns (uint256);
function getEpochToProve() external view returns (Epoch);
function nextEpochToClaim() external view returns (Epoch);
function getClaimableEpoch() external view returns (Epoch);
function getEpochForBlock(uint256 blockNumber) external view returns (Epoch);
function validateEpochProofRightClaim(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote)
external
Expand Down
17 changes: 17 additions & 0 deletions l1-contracts/test/Rollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,23 @@ contract RollupTest is DecoderBase {
vm.warp(Timestamp.unwrap(rollup.getTimestampForSlot(Slot.wrap(_slot))));
}

function testClaimableEpoch(uint256 epochForMixedBlock) public setUpFor("mixed_block_1") {
epochForMixedBlock = bound(epochForMixedBlock, 1, 10);
vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NoEpochToProve.selector));
assertEq(rollup.getClaimableEpoch(), 0, "Invalid claimable epoch");

quote.epochToProve = Epoch.wrap(epochForMixedBlock);
quote.validUntilSlot = Slot.wrap(epochForMixedBlock * Constants.AZTEC_EPOCH_DURATION + 1);
signedQuote = _quoteToSignedQuote(quote);

_testBlock("mixed_block_1", false, epochForMixedBlock * Constants.AZTEC_EPOCH_DURATION);
assertEq(rollup.getClaimableEpoch(), Epoch.wrap(epochForMixedBlock), "Invalid claimable epoch");

rollup.claimEpochProofRight(signedQuote);
vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector));
rollup.getClaimableEpoch();
}

function testClaimWithNothingToProve() public setUpFor("mixed_block_1") {
assertEq(rollup.getCurrentSlot(), 0, "genesis slot should be zero");

Expand Down
17 changes: 15 additions & 2 deletions yarn-project/aztec.js/src/utils/cheat_codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,20 @@ export class RollupCheatCodes {
return await this.rollup.read.getEpochAtSlot([slotNumber]);
}

/**
* Returns the pending and proven chain tips
* @returns The pending and proven chain tips
*/
public async getTips(): Promise<{
/** The pending chain tip */
pending: bigint;
/** The proven chain tip */
proven: bigint;
}> {
const [pending, proven] = await this.rollup.read.tips();
return { pending, proven };
}

/** Warps time in L1 until the next epoch */
public async advanceToNextEpoch() {
const slot = await this.getSlot();
Expand Down Expand Up @@ -373,8 +387,7 @@ export class RollupCheatCodes {
: await this.rollup.read.tips().then(([pending]) => pending);

await this.asOwner(async account => {
// TODO: Figure out proper typing for viem
await this.rollup.write.setAssumeProvenThroughBlockNumber([blockNumber], { account } as any);
await this.rollup.write.setAssumeProvenThroughBlockNumber([blockNumber], { account, chain: this.client.chain });
this.logger.verbose(`Marked ${blockNumber} as proven`);
});
}
Expand Down
9 changes: 6 additions & 3 deletions yarn-project/end-to-end/Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ kind-network-smoke:
kind-network-transfer:
ARG values_file
LOCALLY
# TODO(https://github.com/AztecProtocol/aztec-packages/issues/9166):
# This has || true so it does NOT report failure in github actions job. To be reenabled once stable
RUN NAMESPACE=transfer FRESH_INSTALL=true VALUES_FILE=${values_file:-default.yaml} ./scripts/network_test.sh ./src/spartan/transfer.test.ts || true
RUN NAMESPACE=transfer FRESH_INSTALL=true VALUES_FILE=${values_file:-default.yaml} ./scripts/network_test.sh ./src/spartan/transfer.test.ts

kind-network-4epochs:
ARG values_file
LOCALLY
RUN NAMESPACE=transfer FRESH_INSTALL=true VALUES_FILE=${values_file:-default.yaml} ./scripts/network_test.sh ./src/spartan/4epochs.test.ts

Check failure on line 305 in yarn-project/end-to-end/Earthfile

View workflow job for this annotation

GitHub Actions / e2e (kind-network-4epochs)

Error

The command RUN NAMESPACE=transfer FRESH_INSTALL=true VALUES_FILE=${values_file:-default.yaml} ./scripts/network_test.sh ./src/spartan/4epochs.test.ts did not complete successfully. Exit code 1
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname

# Set environment variables
export PORT=${PORT:-"8080"}
export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator:*"}
export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"}
export LOG_LEVEL=${LOG_LEVEL:-"debug"}
export ETHEREUM_HOST="http://127.0.0.1:8545"
export P2P_ENABLED="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export BOOTSTRAP_NODES=$(echo "$output" | grep -oP 'Node ENR: \K.*')

# Set environment variables
export LOG_LEVEL=${LOG_LEVEL:-"debug"}
export DEBUG=${DEBUG:-"aztec:*"}
export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"}
export ETHEREUM_HOST="http://127.0.0.1:8545"
export PROVER_AGENT_ENABLED="true"
export PROVER_PUBLISHER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export ETHEREUM_HOST="http://127.0.0.1:8545"
export AZTEC_NODE_URL="http://127.0.0.1:8080"
export LOG_JSON="1"
export LOG_LEVEL=${LOG_LEVEL:-"debug"}
export DEBUG="aztec:*"
export DEBUG="aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"
export BOT_PRIVATE_KEY="0xcafe"
export BOT_TX_INTERVAL_SECONDS="5"
export BOT_PRIVATE_TRANSFERS_PER_TX="1"
Expand Down
9 changes: 7 additions & 2 deletions yarn-project/end-to-end/scripts/native-network/validator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export LOG_LEVEL=${LOG_LEVEL:-"debug"}
export VALIDATOR_PRIVATE_KEY=$(echo $json_account | jq -r '.privateKey')
export L1_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY
export SEQ_PUBLISHER_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY
export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator:*"}
export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"}
export ETHEREUM_HOST="http://127.0.0.1:8545"
export P2P_ENABLED="true"
export VALIDATOR_DISABLED="false"
Expand All @@ -56,7 +56,12 @@ export P2P_TCP_LISTEN_ADDR="0.0.0.0:$P2P_PORT"
export P2P_UDP_LISTEN_ADDR="0.0.0.0:$P2P_PORT"

# Add L1 validator
node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js add-l1-validator --validator $ADDRESS --rollup $ROLLUP_CONTRACT_ADDRESS
# this may fail, so try 3 times
for i in {1..3}; do
node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js add-l1-validator --validator $ADDRESS --rollup $ROLLUP_CONTRACT_ADDRESS && break
sleep 1
done

# Fast forward epochs
node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js fast-forward-epochs --rollup $ROLLUP_CONTRACT_ADDRESS --count 1
# Start the Validator Node with the sequencer and archiver
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/end-to-end/scripts/network_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@ kubectl wait pod -l app==pxe --for=condition=Ready -n "$NAMESPACE" --timeout=10m

# tunnel in to get access directly to our PXE service in k8s
(kubectl port-forward --namespace $NAMESPACE svc/spartan-aztec-network-pxe 9082:8080 2>/dev/null >/dev/null || true) &
(kubectl port-forward --namespace $NAMESPACE svc/spartan-aztec-network-anvil 9545:8545 2>/dev/null >/dev/null || true) &

docker run --rm --network=host \
-e PXE_URL=http://localhost:9082 \
-e PXE_URL=http://127.0.0.1:9082 \
-e DEBUG="aztec:*" \
-e LOG_LEVEL=debug \
-e ETHEREUM_HOST=http://127.0.0.1:9545 \
-e LOG_JSON=1 \
aztecprotocol/end-to-end:$AZTEC_DOCKER_TAG $TEST
6 changes: 3 additions & 3 deletions yarn-project/end-to-end/src/spartan/4epochs.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EthCheatCodes, readFieldCompressedString } from '@aztec/aztec.js';
import { AZTEC_SLOT_DURATION } from '@aztec/circuits.js';
import { AZTEC_EPOCH_DURATION } from '@aztec/circuits.js';
import { createDebugLogger } from '@aztec/foundation/log';
import { TokenContract } from '@aztec/noir-contracts.js';

Expand All @@ -17,13 +17,13 @@ if (!ETHEREUM_HOST) {
}

describe('token transfer test', () => {
jest.setTimeout(10 * 60 * 2000); // 20 minutes
jest.setTimeout(10 * 60 * 4000); // 40 minutes

const logger = createDebugLogger(`aztec:spartan:4epochs`);
// We want plenty of minted tokens for a lot of slots that fill up multiple epochs
const MINT_AMOUNT = 2000000n;
const TEST_EPOCHS = 4;
const ROUNDS = BigInt(AZTEC_SLOT_DURATION * TEST_EPOCHS);
const ROUNDS = BigInt(AZTEC_EPOCH_DURATION * TEST_EPOCHS);

let testWallets: TestWallets;

Expand Down
24 changes: 19 additions & 5 deletions yarn-project/sequencer-client/src/publisher/l1-publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { areArraysEqual, compactArray, times } from '@aztec/foundation/collectio
import { type Signature } from '@aztec/foundation/eth-signature';
import { Fr } from '@aztec/foundation/fields';
import { createDebugLogger } from '@aztec/foundation/log';
import { type Tuple, serializeToBuffer, toFriendlyJSON } from '@aztec/foundation/serialize';
import { type Tuple, serializeToBuffer } from '@aztec/foundation/serialize';
import { InterruptibleSleep } from '@aztec/foundation/sleep';
import { Timer } from '@aztec/foundation/timer';
import { RollupAbi } from '@aztec/l1-artifacts';
Expand Down Expand Up @@ -221,8 +221,23 @@ export class L1Publisher {
return [slot, blockNumber];
}

public async nextEpochToClaim(): Promise<bigint> {
return await this.rollupContract.read.nextEpochToClaim();
public async getClaimableEpoch(): Promise<bigint | undefined> {
try {
return await this.rollupContract.read.getClaimableEpoch();
} catch (err: any) {
const errorName = tryGetCustomErrorName(err);
// getting the error name from the abi is redundant,
// but it enforces that the error name is correct.
// That is, if the error name is not found, this will not compile.
const acceptedErrors = (['Rollup__NoEpochToProve', 'Rollup__ProofRightAlreadyClaimed'] as const).map(
name => getAbiItem({ abi: RollupAbi, name }).name,
);

if (errorName && acceptedErrors.includes(errorName as any)) {
return undefined;
}
throw err;
}
}

public async getEpochForSlotNumber(slotNumber: bigint): Promise<bigint> {
Expand Down Expand Up @@ -268,7 +283,6 @@ export class L1Publisher {
try {
await this.rollupContract.read.validateEpochProofRightClaim(args, { account: this.account });
} catch (err) {
this.log.verbose(toFriendlyJSON(err as object));
const errorName = tryGetCustomErrorName(err);
this.log.warn(`Proof quote validation failed: ${errorName}`);
return undefined;
Expand Down Expand Up @@ -767,7 +781,7 @@ function getCalldataGasUsage(data: Uint8Array) {
function tryGetCustomErrorName(err: any) {
try {
// See https://viem.sh/docs/contract/simulateContract#handling-custom-errors
if (err.name === 'ViemError') {
if (err.name === 'ViemError' || err.name === 'ContractFunctionExecutionError') {
const baseError = err as BaseError;
const revertError = baseError.walk(err => (err as Error).name === 'ContractFunctionRevertedError');
if (revertError) {
Expand Down
16 changes: 7 additions & 9 deletions yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ describe('sequencer', () => {
publisher.validateProofQuote.mockImplementation((x: EpochProofQuote) => Promise.resolve(x));

// The previous epoch can be claimed
publisher.nextEpochToClaim.mockImplementation(() => Promise.resolve(currentEpoch - 1n));
publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n));

await sequencer.work();
expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], proofQuote);
Expand All @@ -599,8 +599,7 @@ describe('sequencer', () => {
publisher.proposeL2Block.mockResolvedValueOnce(true);
publisher.validateProofQuote.mockImplementation((x: EpochProofQuote) => Promise.resolve(x));

// The previous epoch can be claimed
publisher.nextEpochToClaim.mockImplementation(() => Promise.resolve(0n));
publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(undefined));

await sequencer.work();
expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined);
Expand All @@ -624,7 +623,7 @@ describe('sequencer', () => {
publisher.validateProofQuote.mockImplementation((x: EpochProofQuote) => Promise.resolve(x));

// The previous epoch can be claimed
publisher.nextEpochToClaim.mockImplementation(() => Promise.resolve(currentEpoch - 1n));
publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n));

await sequencer.work();
expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined);
Expand All @@ -646,8 +645,7 @@ describe('sequencer', () => {
publisher.proposeL2Block.mockResolvedValueOnce(true);
publisher.validateProofQuote.mockImplementation((x: EpochProofQuote) => Promise.resolve(x));

// The previous epoch can be claimed
publisher.nextEpochToClaim.mockResolvedValue(currentEpoch);
publisher.getClaimableEpoch.mockResolvedValue(undefined);

await sequencer.work();
expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined);
Expand All @@ -672,7 +670,7 @@ describe('sequencer', () => {
publisher.validateProofQuote.mockImplementation(_ => Promise.resolve(undefined));

// The previous epoch can be claimed
publisher.nextEpochToClaim.mockImplementation(() => Promise.resolve(currentEpoch - 1n));
publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n));

await sequencer.work();
expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined);
Expand Down Expand Up @@ -727,7 +725,7 @@ describe('sequencer', () => {
);

// The previous epoch can be claimed
publisher.nextEpochToClaim.mockImplementation(() => Promise.resolve(currentEpoch - 1n));
publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n));

await sequencer.work();
expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], validProofQuote);
Expand Down Expand Up @@ -786,7 +784,7 @@ describe('sequencer', () => {
);

// The previous epoch can be claimed
publisher.nextEpochToClaim.mockImplementation(() => Promise.resolve(currentEpoch - 1n));
publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n));

await sequencer.work();
expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], validQuotes[0]);
Expand Down
16 changes: 4 additions & 12 deletions yarn-project/sequencer-client/src/sequencer/sequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,20 +548,12 @@ export class Sequencer {
protected async createProofClaimForPreviousEpoch(slotNumber: bigint): Promise<EpochProofQuote | undefined> {
try {
// Find out which epoch we are currently in
const epochForBlock = await this.publisher.getEpochForSlotNumber(slotNumber);
if (epochForBlock < 1n) {
// It's the 0th epoch, nothing to be proven yet
this.log.verbose(`First epoch has no claim`);
return undefined;
}
const epochToProve = epochForBlock - 1n;
// Find out the next epoch that can be claimed
const canClaim = await this.publisher.nextEpochToClaim();
if (canClaim != epochToProve) {
// It's not the one we are looking to claim
this.log.verbose(`Unable to claim previous epoch (${canClaim} != ${epochToProve})`);
const epochToProve = await this.publisher.getClaimableEpoch();
if (epochToProve === undefined) {
this.log.verbose(`No epoch to prove`);
return undefined;
}

// Get quotes for the epoch to be proven
const quotes = await this.p2pClient.getEpochProofQuotes(epochToProve);
this.log.verbose(`Retrieved ${quotes.length} quotes, slot: ${slotNumber}, epoch to prove: ${epochToProve}`);
Expand Down

0 comments on commit 9dda91e

Please sign in to comment.