diff --git a/l1-contracts/bootstrap.sh b/l1-contracts/bootstrap.sh index cae5c957e58..9d8e2ead7ee 100755 --- a/l1-contracts/bootstrap.sh +++ b/l1-contracts/bootstrap.sh @@ -33,6 +33,9 @@ function build { # Step 1: Build everything in src. forge build $(find src test -name '*.sol') + # Step 1.5: Output storage information for the rollup contract. + forge inspect src/core/Rollup.sol:Rollup storage > ./out/Rollup.sol/storage.json + # Step 2: Build the the generated verifier contract with optimization. forge build $(find generated -name '*.sol') \ --optimize \ diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index ea820587138..37545b91411 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -434,7 +434,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo } /** - * @notice Validate blob transactions against given inputs. + * @notice Validate blob transactions against given inputs * @dev Only exists here for gas estimation. */ function validateBlobs(bytes calldata _blobsInput) diff --git a/spartan/aztec-network/templates/eth/eth-execution.yaml b/spartan/aztec-network/templates/eth/eth-execution.yaml index daa1baab44a..35b7ba243ab 100644 --- a/spartan/aztec-network/templates/eth/eth-execution.yaml +++ b/spartan/aztec-network/templates/eth/eth-execution.yaml @@ -50,7 +50,7 @@ spec: - name: entrypoint-scripts mountPath: /entrypoints resources: - {{- toYaml .Values.ethereum.resources | nindent 12 }} + {{- toYaml .Values.ethereum.execution.resources | nindent 12 }} volumes: - name: shared-volume persistentVolumeClaim: diff --git a/spartan/aztec-network/values.yaml b/spartan/aztec-network/values.yaml index e770bd6f0ac..60ee0e0485f 100644 --- a/spartan/aztec-network/values.yaml +++ b/spartan/aztec-network/values.yaml @@ -270,11 +270,7 @@ ethereum: timeoutSeconds: 5 successThreshold: 1 failureThreshold: 3 - resources: - requests: - memory: "4Gi" - cpu: "1.5" - storageSize: "80Gi" + deployL1ContractsPrivateKey: proverAgent: service: diff --git a/spartan/aztec-network/values/3-validators-with-metrics.yaml b/spartan/aztec-network/values/3-validators-with-metrics.yaml index fe8c8cde79f..6471976cb42 100644 --- a/spartan/aztec-network/values/3-validators-with-metrics.yaml +++ b/spartan/aztec-network/values/3-validators-with-metrics.yaml @@ -15,3 +15,17 @@ validator: bootNode: validator: disabled: true + +ethereum: + execution: + resources: + requests: + memory: "1Gi" + beacon: + resources: + requests: + memory: "1Gi" + validator: + resources: + requests: + memory: "1Gi" diff --git a/spartan/scripts/test_kind.sh b/spartan/scripts/test_kind.sh index 4f2d2610547..96c5b3b15da 100755 --- a/spartan/scripts/test_kind.sh +++ b/spartan/scripts/test_kind.sh @@ -82,13 +82,14 @@ if [ "$fresh_install" != "no-deploy" ]; then ./deploy_kind.sh $namespace $values_file fi -# Find 3 free ports between 9000 and 10000 -free_ports=$(find_ports 3) +# Find 4 free ports between 9000 and 10000 +free_ports=$(find_ports 4) # Extract the free ports from the list -pxe_port=$(echo $free_ports | awk '{print $1}') -anvil_port=$(echo $free_ports | awk '{print $2}') -metrics_port=$(echo $free_ports | awk '{print $3}') +forwarded_pxe_port=$(echo $free_ports | awk '{print $1}') +forwarded_anvil_port=$(echo $free_ports | awk '{print $2}') +forwarded_metrics_port=$(echo $free_ports | awk '{print $3}') +forwarded_node_port=$(echo $free_ports | awk '{print $4}') if [ "$install_metrics" = "true" ]; then grafana_password=$(kubectl get secrets -n metrics metrics-grafana -o jsonpath='{.data.admin-password}' | base64 --decode) @@ -114,11 +115,13 @@ if [ "$use_docker" = "true" ]; then -e INSTANCE_NAME="spartan" \ -e SPARTAN_DIR="/usr/src/spartan" \ -e NAMESPACE="$namespace" \ - -e HOST_PXE_PORT=$pxe_port \ + -e HOST_PXE_PORT=$forwarded_pxe_port \ -e CONTAINER_PXE_PORT=8081 \ - -e HOST_ETHEREUM_PORT=$anvil_port \ + -e HOST_ETHEREUM_PORT=$forwarded_anvil_port \ -e CONTAINER_ETHEREUM_PORT=8545 \ - -e HOST_METRICS_PORT=$metrics_port \ + -e HOST_NODE_PORT=$forwarded_node_port \ + -e CONTAINER_NODE_PORT=8080 \ + -e HOST_METRICS_PORT=$forwarded_metrics_port \ -e CONTAINER_METRICS_PORT=80 \ -e GRAFANA_PASSWORD=$grafana_password \ -e DEBUG=${DEBUG:-""} \ @@ -136,11 +139,13 @@ else export INSTANCE_NAME="spartan" export SPARTAN_DIR="$(pwd)/.." export NAMESPACE="$namespace" - export HOST_PXE_PORT="$pxe_port" + export HOST_PXE_PORT="$forwarded_pxe_port" export CONTAINER_PXE_PORT="8081" - export HOST_ETHEREUM_PORT="$anvil_port" + export HOST_ETHEREUM_PORT="$forwarded_anvil_port" export CONTAINER_ETHEREUM_PORT="8545" - export HOST_METRICS_PORT="$metrics_port" + export HOST_NODE_PORT="$forwarded_node_port" + export CONTAINER_NODE_PORT="8080" + export HOST_METRICS_PORT="$forwarded_metrics_port" export CONTAINER_METRICS_PORT="80" export GRAFANA_PASSWORD="$grafana_password" export DEBUG="${DEBUG:-""}" diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 6e215c11454..f21e3859243 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -863,7 +863,7 @@ export class AztecNodeService implements AztecNode, Traceable { ); const fork = await this.worldStateSynchronizer.fork(); - this.log.verbose(`Simulating public calls for tx ${tx.getTxHash()}`, { + this.log.verbose(`Simulating public calls for tx ${txHash}`, { globalVariables: newGlobalVariables.toInspect(), txHash, blockNumber, @@ -876,7 +876,7 @@ export class AztecNodeService implements AztecNode, Traceable { const [processedTxs, failedTxs, returns] = await processor.process([tx]); // REFACTOR: Consider returning the error rather than throwing if (failedTxs.length) { - this.log.warn(`Simulated tx ${tx.getTxHash()} fails: ${failedTxs[0].error}`, { txHash }); + this.log.warn(`Simulated tx ${txHash} fails: ${failedTxs[0].error}`, { txHash }); throw failedTxs[0].error; } diff --git a/yarn-project/end-to-end/scripts/native-network/test-transfer.sh b/yarn-project/end-to-end/scripts/native-network/test-transfer.sh index a58483c3fc0..1b91130664b 100755 --- a/yarn-project/end-to-end/scripts/native-network/test-transfer.sh +++ b/yarn-project/end-to-end/scripts/native-network/test-transfer.sh @@ -9,6 +9,7 @@ SCRIPT_NAME=$(basename "$0" .sh) exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log" >&2) export BOOTNODE_URL=${BOOTNODE_URL:-http://127.0.0.1:8080} +export NODE_URL=${NODE_URL:-${BOOTNODE_URL:-http://127.0.0.1:8080}} export PXE_URL=${PXE_URL:-http://127.0.0.1:8079} export ETHEREUM_HOST=${ETHEREUM_HOST:-http://127.0.0.1:8545} export K8S=${K8S:-false} diff --git a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts index 1982b5c8269..1045c573781 100644 --- a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts @@ -17,9 +17,11 @@ import { BlockBlobPublicInputs } from '@aztec/circuits.js/blobs'; import { fr } from '@aztec/circuits.js/testing'; import { EpochCache } from '@aztec/epoch-cache'; import { + GovernanceProposerContract, type L1ContractAddresses, L1TxUtilsWithBlobs, RollupContract, + SlashingProposerContract, createEthereumChain, createL1Clients, } from '@aztec/ethereum'; @@ -189,7 +191,20 @@ describe('L1Publisher integration', () => { ); const l1TxUtils = new L1TxUtilsWithBlobs(sequencerPublicClient, sequencerWalletClient, logger, config); const rollupContract = new RollupContract(sequencerPublicClient, l1ContractAddresses.rollupAddress.toString()); - const forwarderContract = await createForwarderContract(config, sequencerPK); + const forwarderContract = await createForwarderContract( + config, + sequencerPK, + l1ContractAddresses.rollupAddress.toString(), + ); + const slashingProposerAddress = await rollupContract.getSlashingProposerAddress(); + const slashingProposerContract = new SlashingProposerContract( + sequencerPublicClient, + slashingProposerAddress.toString(), + ); + const governanceProposerContract = new GovernanceProposerContract( + sequencerPublicClient, + l1ContractAddresses.governanceProposerAddress.toString(), + ); const epochCache = await EpochCache.create(l1ContractAddresses.rollupAddress, config, { dateProvider: new TestDateProvider(), }); @@ -211,6 +226,8 @@ describe('L1Publisher integration', () => { rollupContract, forwarderContract, epochCache, + governanceProposerContract, + slashingProposerContract, }, ); @@ -566,26 +583,44 @@ describe('L1Publisher integration', () => { await expect(publisher.enqueueProposeL2Block(block)).resolves.toEqual(true); await expect(publisher.sendRequests()).resolves.toMatchObject({ - errorMsg: expect.stringContaining('Rollup__InvalidBlobHash'), + errorMsg: expect.stringContaining('Rollup__InvalidInHash'), }); // Test for both calls // NOTE: First error is from the simulate fn, which isn't supported by anvil - expect(loggerErrorSpy).toHaveBeenCalledTimes(2); - - expect(loggerErrorSpy).toHaveBeenNthCalledWith(1, 'Bundled [propose] transaction [failed]'); + expect(loggerErrorSpy).toHaveBeenCalledTimes(3); + expect(loggerErrorSpy).toHaveBeenNthCalledWith( + 1, + 'Forwarder transaction failed', + undefined, + expect.objectContaining({ + receipt: expect.objectContaining({ + type: 'eip4844', + blockHash: expect.any(String), + blockNumber: expect.any(BigInt), + transactionHash: expect.any(String), + }), + }), + ); expect(loggerErrorSpy).toHaveBeenNthCalledWith( 2, + expect.stringContaining('Bundled [propose] transaction [failed]'), + ); + + expect(loggerErrorSpy).toHaveBeenNthCalledWith( + 3, expect.stringMatching( - /^Rollup process tx reverted\. The contract function "forward" reverted\. Error: Rollup__InvalidBlobHash/i, + /^Rollup process tx reverted\. The contract function "forward" reverted\. Error: Rollup__InvalidInHash/i, ), undefined, expect.objectContaining({ - blockHash: expect.any(String), + blockHash: expect.any(Fr), blockNumber: expect.any(Number), slotNumber: expect.any(BigInt), txHash: expect.any(String), + txCount: expect.any(Number), + blockTimestamp: expect.any(Number), }), ); }); diff --git a/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts b/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts index 5960f25a374..fa325743565 100644 --- a/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts @@ -1,5 +1,4 @@ -import { getSchnorrAccount } from '@aztec/accounts/schnorr'; -import { Fr, GrumpkinScalar, type Logger, type PXE, TxStatus } from '@aztec/aztec.js'; +import { type Logger, type PXE } from '@aztec/aztec.js'; import { EthAddress } from '@aztec/circuits.js'; import { getL1ContractsConfigEnvVars } from '@aztec/ethereum'; import { type PXEService } from '@aztec/pxe'; @@ -8,6 +7,7 @@ import { jest } from '@jest/globals'; import { privateKeyToAccount } from 'viem/accounts'; import { getPrivateKeyFromIndex, setup } from './fixtures/utils.js'; +import { submitTxsTo } from './shared/submit-transactions.js'; jest.setTimeout(1000 * 60 * 10); @@ -32,7 +32,7 @@ describe('e2e_l1_with_wall_time', () => { it('should produce blocks with a bunch of transactions', async () => { for (let i = 0; i < 4; i++) { - const txs = await submitTxsTo(pxe as PXEService, 8); + const txs = await submitTxsTo(pxe as PXEService, 8, logger); await Promise.all( txs.map(async (tx, j) => { logger.info(`Waiting for tx ${i}-${j}: ${await tx.getTxHash()} to be mined`); @@ -41,38 +41,4 @@ describe('e2e_l1_with_wall_time', () => { ); } }); - - // submits a set of transactions to the provided Private eXecution Environment (PXE) - const submitTxsTo = async (pxe: PXEService, numTxs: number) => { - const provenTxs = []; - for (let i = 0; i < numTxs; i++) { - const accountManager = await getSchnorrAccount(pxe, Fr.random(), GrumpkinScalar.random(), Fr.random()); - const deployMethod = await accountManager.getDeployMethod(); - const tx = await deployMethod.prove({ - contractAddressSalt: new Fr(accountManager.salt), - skipClassRegistration: true, - skipPublicDeployment: true, - universalDeploy: true, - }); - provenTxs.push(tx); - } - const sentTxs = await Promise.all( - provenTxs.map(async provenTx => { - const tx = provenTx.send(); - const txHash = await tx.getTxHash(); - - logger.info(`Tx sent with hash ${txHash}`); - const receipt = await tx.getReceipt(); - expect(receipt).toEqual( - expect.objectContaining({ - status: TxStatus.PENDING, - error: '', - }), - ); - logger.info(`Receipt received for ${txHash}`); - return tx; - }), - ); - return sentTxs; - }; }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/shared.ts b/yarn-project/end-to-end/src/e2e_p2p/shared.ts index 4829070d1f5..6c8603f4ab2 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/shared.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/shared.ts @@ -1,12 +1,11 @@ -import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AztecNodeService } from '@aztec/aztec-node'; -import { type Logger, type SentTx } from '@aztec/aztec.js'; -import { CompleteAddress, TxStatus } from '@aztec/aztec.js'; -import { Fr, GrumpkinScalar } from '@aztec/foundation/fields'; +import { CompleteAddress, type Logger, type SentTx, TxStatus } from '@aztec/aztec.js'; +import { Fr } from '@aztec/foundation/fields'; import { type SpamContract } from '@aztec/noir-contracts.js/Spam'; -import { type PXEService, createPXEService, getPXEServiceConfig as getRpcConfig } from '@aztec/pxe'; +import { createPXEService, getPXEServiceConfig as getRpcConfig } from '@aztec/pxe'; import { type NodeContext } from '../fixtures/setup_p2p_test.js'; +import { submitTxsTo } from '../shared/submit-transactions.js'; // submits a set of transactions to the provided Private eXecution Environment (PXE) export const submitComplexTxsTo = async ( @@ -52,7 +51,7 @@ export const createPXEServiceAndSubmitTransactions = async ( const completeAddress = await CompleteAddress.fromSecretKeyAndPartialAddress(secretKey, Fr.random()); await pxeService.registerAccount(secretKey, completeAddress.partialAddress); - const txs = await submitTxsTo(logger, pxeService, numTxs); + const txs = await submitTxsTo(pxeService, numTxs, logger); return { txs, account: completeAddress.address, @@ -60,37 +59,3 @@ export const createPXEServiceAndSubmitTransactions = async ( node, }; }; - -// submits a set of transactions to the provided Private eXecution Environment (PXE) -const submitTxsTo = async (logger: Logger, pxe: PXEService, numTxs: number) => { - const provenTxs = []; - for (let i = 0; i < numTxs; i++) { - const accountManager = await getSchnorrAccount(pxe, Fr.random(), GrumpkinScalar.random(), Fr.random()); - const deployMethod = await accountManager.getDeployMethod(); - const tx = await deployMethod.prove({ - contractAddressSalt: new Fr(accountManager.salt), - skipClassRegistration: true, - skipPublicDeployment: true, - universalDeploy: true, - }); - provenTxs.push(tx); - } - const sentTxs = await Promise.all( - provenTxs.map(async provenTx => { - const tx = provenTx.send(); - const txHash = await tx.getTxHash(); - - logger.info(`Tx sent with hash ${txHash}`); - const receipt = await tx.getReceipt(); - expect(receipt).toEqual( - expect.objectContaining({ - status: TxStatus.PENDING, - error: '', - }), - ); - logger.info(`Receipt received for ${txHash}`); - return tx; - }), - ); - return sentTxs; -}; diff --git a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts index 5712a7b443f..66832a8b32f 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts @@ -191,9 +191,9 @@ describe('e2e_p2p_governance_proposer', () => { const proposal = await governance.read.getProposal([0n]); const timeToActive = proposal.creation + proposal.config.votingDelay; - t.logger.info(`Warpping to ${timeToActive + 1n}`); + t.logger.info(`Warping to ${timeToActive + 1n}`); await t.ctx.cheatCodes.eth.warp(Number(timeToActive + 1n)); - t.logger.info(`Warpped to ${timeToActive + 1n}`); + t.logger.info(`Warped to ${timeToActive + 1n}`); await waitL1Block(); t.logger.info(`Voting`); @@ -202,9 +202,9 @@ describe('e2e_p2p_governance_proposer', () => { t.logger.info(`Voted`); const timeToExecutable = timeToActive + proposal.config.votingDuration + proposal.config.executionDelay + 1n; - t.logger.info(`Warpping to ${timeToExecutable}`); + t.logger.info(`Warping to ${timeToExecutable}`); await t.ctx.cheatCodes.eth.warp(Number(timeToExecutable)); - t.logger.info(`Warpped to ${timeToExecutable}`); + t.logger.info(`Warped to ${timeToExecutable}`); await waitL1Block(); t.logger.info(`Checking governance proposer`); diff --git a/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.test.ts b/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.test.ts new file mode 100644 index 00000000000..5240ae926aa --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.test.ts @@ -0,0 +1,99 @@ +import { type AztecNode, type CheatCodes, type Logger, type PXE } from '@aztec/aztec.js'; +import { EthAddress } from '@aztec/circuits.js'; +import { + type DeployL1Contracts, + GovernanceProposerContract, + RollupContract, + deployL1Contract, + getL1ContractsConfigEnvVars, +} from '@aztec/ethereum'; +import { NewGovernanceProposerPayloadAbi } from '@aztec/l1-artifacts/NewGovernanceProposerPayloadAbi'; +import { NewGovernanceProposerPayloadBytecode } from '@aztec/l1-artifacts/NewGovernanceProposerPayloadBytecode'; +import { type PXEService } from '@aztec/pxe'; + +import { privateKeyToAccount } from 'viem/accounts'; + +import { getPrivateKeyFromIndex, setup } from '../fixtures/utils.js'; +import { submitTxsTo } from '../shared/submit-transactions.js'; + +describe('e2e_gov_proposal', () => { + let logger: Logger; + let teardown: () => Promise; + let pxe: PXE; + let aztecNode: AztecNode; + let deployL1ContractsValues: DeployL1Contracts; + let aztecSlotDuration: number; + let cheatCodes: CheatCodes; + beforeEach(async () => { + const account = privateKeyToAccount(`0x${getPrivateKeyFromIndex(0)!.toString('hex')}`); + const initialValidators = [EthAddress.fromString(account.address)]; + const { ethereumSlotDuration, aztecSlotDuration: _aztecSlotDuration } = getL1ContractsConfigEnvVars(); + aztecSlotDuration = _aztecSlotDuration; + + ({ teardown, logger, pxe, aztecNode, deployL1ContractsValues, cheatCodes } = await setup(0, { + initialValidators, + ethereumSlotDuration, + salt: 420, + minTxsPerBlock: 8, + enforceTimeTable: true, + })); + }, 3 * 60000); + + afterEach(() => teardown()); + + it( + 'should build/propose blocks while voting', + async () => { + const { address: newGovernanceProposerAddress } = await deployL1Contract( + deployL1ContractsValues.walletClient, + deployL1ContractsValues.publicClient, + NewGovernanceProposerPayloadAbi, + NewGovernanceProposerPayloadBytecode, + [deployL1ContractsValues.l1ContractAddresses.registryAddress.toString()], + '0x2a', // salt + ); + await aztecNode.setConfig({ + governanceProposerPayload: newGovernanceProposerAddress, + }); + const rollup = new RollupContract( + deployL1ContractsValues.publicClient, + deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), + ); + const governanceProposer = new GovernanceProposerContract( + deployL1ContractsValues.publicClient, + deployL1ContractsValues.l1ContractAddresses.governanceProposerAddress.toString(), + ); + + const roundDuration = await governanceProposer.getRoundSize(); + const slot = await rollup.getSlotNumber(); + const round = await governanceProposer.computeRound(slot); + const nextRoundBeginsAtSlot = (slot / roundDuration) * roundDuration + roundDuration; + const nextRoundBeginsAtTimestamp = await rollup.getTimestampForSlot(nextRoundBeginsAtSlot); + logger.info(`Warping to round ${round + 1n} at slot ${nextRoundBeginsAtSlot}`); + await cheatCodes.eth.warp(Number(nextRoundBeginsAtTimestamp)); + + // Now we submit a bunch of transactions to the PXE. + // We know that this will last at least as long as the round duration, + // since we wait for the txs to be mined, and do so `roundDuration` times. + // Simultaneously, we should be voting for the proposal in every slot. + + for (let i = 0; i < roundDuration; i++) { + const txs = await submitTxsTo(pxe as PXEService, 8, logger); + await Promise.all( + txs.map(async (tx, j) => { + logger.info(`Waiting for tx ${i}-${j}: ${await tx.getTxHash()} to be mined`); + return tx.wait({ timeout: 2 * aztecSlotDuration + 2 }); + }), + ); + } + + const votes = await governanceProposer.getProposalVotes( + deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), + round + 1n, + newGovernanceProposerAddress.toString(), + ); + expect(votes).toEqual(roundDuration); + }, + 1000 * 60 * 5, + ); +}); diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index a61fca34477..26a5c1df279 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -50,7 +50,13 @@ import { createBlobSinkClient } from '@aztec/blob-sink/client'; import { L2Block, tryStop } from '@aztec/circuit-types'; import { type AztecAddress, EthAddress } from '@aztec/circuits.js'; import { EpochCache } from '@aztec/epoch-cache'; -import { L1TxUtilsWithBlobs, RollupContract, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; +import { + GovernanceProposerContract, + L1TxUtilsWithBlobs, + RollupContract, + SlashingProposerContract, + getL1ContractsConfigEnvVars, +} from '@aztec/ethereum'; import { TestDateProvider, Timer } from '@aztec/foundation/timer'; import { RollupAbi } from '@aztec/l1-artifacts'; import { SchnorrHardcodedAccountContract } from '@aztec/noir-contracts.js/SchnorrHardcodedAccount'; @@ -402,11 +408,18 @@ describe('e2e_synching', () => { logger, config, ); - const rollupContract = new RollupContract( + const rollupAddress = deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(); + const rollupContract = new RollupContract(deployL1ContractsValues.publicClient, rollupAddress); + const governanceProposerContract = new GovernanceProposerContract( + deployL1ContractsValues.publicClient, + config.l1Contracts.governanceProposerAddress.toString(), + ); + const slashingProposerAddress = await rollupContract.getSlashingProposerAddress(); + const slashingProposerContract = new SlashingProposerContract( deployL1ContractsValues.publicClient, - deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), + slashingProposerAddress.toString(), ); - const forwarderContract = await createForwarderContract(config, sequencerPK); + const forwarderContract = await createForwarderContract(config, sequencerPK, rollupAddress); const epochCache = await EpochCache.create(config.l1Contracts.rollupAddress, config, { dateProvider: new TestDateProvider(), }); @@ -428,6 +441,8 @@ describe('e2e_synching', () => { l1TxUtils, rollupContract, forwarderContract, + governanceProposerContract, + slashingProposerContract, epochCache, }, ); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 70ff827ad6c..c097c040546 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -767,13 +767,18 @@ function createDelayedL1TxUtils(aztecNodeConfig: AztecNodeConfig, privateKey: `0 return l1TxUtils; } -export async function createForwarderContract(aztecNodeConfig: AztecNodeConfig, privateKey: `0x${string}`) { +export async function createForwarderContract( + aztecNodeConfig: AztecNodeConfig, + privateKey: `0x${string}`, + rollupAddress: Hex, +) { const { walletClient, publicClient } = createL1Clients(aztecNodeConfig.l1RpcUrl, privateKey, foundry); const forwarderContract = await ForwarderContract.create( walletClient.account.address, walletClient, publicClient, createLogger('forwarder'), + rollupAddress, ); return forwarderContract; } diff --git a/yarn-project/end-to-end/src/shared/submit-transactions.ts b/yarn-project/end-to-end/src/shared/submit-transactions.ts new file mode 100644 index 00000000000..d4899185c60 --- /dev/null +++ b/yarn-project/end-to-end/src/shared/submit-transactions.ts @@ -0,0 +1,37 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { type Logger, TxStatus } from '@aztec/aztec.js'; +import { Fr, GrumpkinScalar, type PXEService } from '@aztec/pxe'; + +// submits a set of transactions to the provided Private eXecution Environment (PXE) +export const submitTxsTo = async (pxe: PXEService, numTxs: number, logger: Logger) => { + const provenTxs = []; + for (let i = 0; i < numTxs; i++) { + const accountManager = await getSchnorrAccount(pxe, Fr.random(), GrumpkinScalar.random(), Fr.random()); + const deployMethod = await accountManager.getDeployMethod(); + const tx = await deployMethod.prove({ + contractAddressSalt: new Fr(accountManager.salt), + skipClassRegistration: true, + skipPublicDeployment: true, + universalDeploy: true, + }); + provenTxs.push(tx); + } + const sentTxs = await Promise.all( + provenTxs.map(async provenTx => { + const tx = provenTx.send(); + const txHash = await tx.getTxHash(); + + logger.info(`Tx sent with hash ${txHash}`); + const receipt = await tx.getReceipt(); + expect(receipt).toEqual( + expect.objectContaining({ + status: TxStatus.PENDING, + error: '', + }), + ); + logger.info(`Receipt received for ${txHash}`); + return tx; + }), + ); + return sentTxs; +}; diff --git a/yarn-project/end-to-end/src/spartan/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/spartan/upgrade_governance_proposer.test.ts new file mode 100644 index 00000000000..7fed811cb21 --- /dev/null +++ b/yarn-project/end-to-end/src/spartan/upgrade_governance_proposer.test.ts @@ -0,0 +1,168 @@ +import { EthAddress, type NodeInfo, type PXE, createCompatibleClient, sleep } from '@aztec/aztec.js'; +import { + GovernanceProposerContract, + L1TxUtils, + RollupContract, + createEthereumChain, + createL1Clients, + deployL1Contract, +} from '@aztec/ethereum'; +import { createLogger } from '@aztec/foundation/log'; +import { NewGovernanceProposerPayloadAbi } from '@aztec/l1-artifacts/NewGovernanceProposerPayloadAbi'; +import { NewGovernanceProposerPayloadBytecode } from '@aztec/l1-artifacts/NewGovernanceProposerPayloadBytecode'; + +import { privateKeyToAccount } from 'viem/accounts'; +import { parseEther, stringify } from 'viem/utils'; + +import { MNEMONIC } from '../fixtures/fixtures.js'; +import { isK8sConfig, setupEnvironment, startPortForward, updateSequencersConfig } from './utils.js'; + +// random private key +const deployerPrivateKey = '0x23206a40226aad90d5673b8adbbcfe94a617e7a6f9e59fc68615fe1bd4bc72f1'; + +const config = setupEnvironment(process.env); +if (!isK8sConfig(config)) { + throw new Error('This test must be run in a k8s environment'); +} + +const debugLogger = createLogger('e2e:spartan-test:upgrade_governance_proposer'); + +describe('spartan_upgrade_governance_proposer', () => { + let pxe: PXE; + let nodeInfo: NodeInfo; + let ETHEREUM_HOST: string; + beforeAll(async () => { + await startPortForward({ + resource: `svc/${config.INSTANCE_NAME}-aztec-network-pxe`, + namespace: config.NAMESPACE, + containerPort: config.CONTAINER_PXE_PORT, + hostPort: config.HOST_PXE_PORT, + }); + await startPortForward({ + resource: `svc/${config.INSTANCE_NAME}-aztec-network-eth-execution`, + namespace: config.NAMESPACE, + containerPort: config.CONTAINER_ETHEREUM_PORT, + hostPort: config.HOST_ETHEREUM_PORT, + }); + ETHEREUM_HOST = `http://127.0.0.1:${config.HOST_ETHEREUM_PORT}`; + + const PXE_URL = `http://127.0.0.1:${config.HOST_PXE_PORT}`; + pxe = await createCompatibleClient(PXE_URL, debugLogger); + nodeInfo = await pxe.getNodeInfo(); + }); + + // We need a separate account to deploy the new governance proposer + // because the underlying validators are currently producing blob transactions + // and you can't submit blob and non-blob transactions from the same account + const setupDeployerAccount = async () => { + const chain = createEthereumChain(ETHEREUM_HOST, 1337); + const { walletClient: validatorWalletClient } = createL1Clients(ETHEREUM_HOST, MNEMONIC, chain.chainInfo); + // const privateKey = generatePrivateKey(); + const privateKey = deployerPrivateKey; + debugLogger.info(`deployer privateKey: ${privateKey}`); + const account = privateKeyToAccount(privateKey); + // check the balance of the account + const balance = await validatorWalletClient.getBalance({ address: account.address }); + debugLogger.info(`deployer balance: ${balance}`); + if (balance <= parseEther('5')) { + debugLogger.info('sending some eth to the deployer account'); + // send some eth to the account + const tx = await validatorWalletClient.sendTransaction({ + to: account.address, + value: parseEther('10'), + }); + const receipt = await validatorWalletClient.waitForTransactionReceipt({ hash: tx }); + debugLogger.info(`receipt: ${stringify(receipt)}`); + } + return createL1Clients(ETHEREUM_HOST, account, chain.chainInfo); + }; + + it( + 'should deploy new governance proposer', + async () => { + /** Helpers */ + const govInfo = async () => { + const bn = await l1PublicClient.getBlockNumber(); + const slot = await rollup.getSlotNumber(); + const round = await governanceProposer.computeRound(slot); + const info = await governanceProposer.getRoundInfo( + nodeInfo.l1ContractAddresses.rollupAddress.toString(), + round, + ); + const leaderVotes = await governanceProposer.getProposalVotes( + nodeInfo.l1ContractAddresses.rollupAddress.toString(), + round, + info.leader, + ); + return { bn, slot, round, info, leaderVotes }; + }; + + /** Setup */ + + const { walletClient: l1WalletClient, publicClient: l1PublicClient } = await setupDeployerAccount(); + + const { address: newGovernanceProposerAddress } = await deployL1Contract( + l1WalletClient, + l1PublicClient, + NewGovernanceProposerPayloadAbi, + NewGovernanceProposerPayloadBytecode, + [nodeInfo.l1ContractAddresses.registryAddress.toString()], + '0x2a', // salt + ); + expect(newGovernanceProposerAddress).toBeDefined(); + expect(newGovernanceProposerAddress.equals(EthAddress.ZERO)).toBeFalsy(); + debugLogger.info(`newGovernanceProposerAddress: ${newGovernanceProposerAddress.toString()}`); + await updateSequencersConfig(config, { + governanceProposerPayload: newGovernanceProposerAddress, + }); + + const rollup = new RollupContract(l1PublicClient, nodeInfo.l1ContractAddresses.rollupAddress.toString()); + const governanceProposer = new GovernanceProposerContract( + l1PublicClient, + nodeInfo.l1ContractAddresses.governanceProposerAddress.toString(), + ); + + let info = await govInfo(); + expect(info.bn).toBeDefined(); + expect(info.slot).toBeDefined(); + debugLogger.info(`info: ${stringify(info)}`); + + const quorumSize = await governanceProposer.getQuorumSize(); + debugLogger.info(`quorumSize: ${quorumSize}`); + expect(quorumSize).toBeGreaterThan(0); + + /** GovernanceProposer Voting */ + + // Wait until we have enough votes to execute the proposal. + while (true) { + info = await govInfo(); + debugLogger.info(`Leader votes: ${info.leaderVotes}`); + if (info.leaderVotes >= quorumSize) { + debugLogger.info(`Leader votes have reached quorum size`); + break; + } + await sleep(12000); + } + + const executableRound = info.round; + debugLogger.info(`Waiting for round ${executableRound + 1n}`); + + while (info.round === executableRound) { + await sleep(12500); + info = await govInfo(); + debugLogger.info(`slot: ${info.slot}`); + } + + expect(info.round).toBeGreaterThan(executableRound); + + debugLogger.info(`Executing proposal ${info.round}`); + + const l1TxUtils = new L1TxUtils(l1PublicClient, l1WalletClient, debugLogger); + const { receipt } = await governanceProposer.executeProposal(executableRound, l1TxUtils); + expect(receipt).toBeDefined(); + expect(receipt.status).toEqual('success'); + debugLogger.info(`Executed proposal ${info.round}`); + }, + 1000 * 60 * 10, + ); +}); diff --git a/yarn-project/end-to-end/src/spartan/utils.ts b/yarn-project/end-to-end/src/spartan/utils.ts index 547497a6d85..facd41b7202 100644 --- a/yarn-project/end-to-end/src/spartan/utils.ts +++ b/yarn-project/end-to-end/src/spartan/utils.ts @@ -1,5 +1,6 @@ -import { createLogger, sleep } from '@aztec/aztec.js'; +import { createAztecNodeClient, createLogger, sleep } from '@aztec/aztec.js'; import type { Logger } from '@aztec/foundation/log'; +import type { SequencerConfig } from '@aztec/sequencer-client'; import { exec, execSync, spawn } from 'child_process'; import path from 'path'; @@ -16,6 +17,8 @@ const logger = createLogger('e2e:k8s-utils'); const k8sLocalConfigSchema = z.object({ INSTANCE_NAME: z.string().min(1, 'INSTANCE_NAME env variable must be set'), NAMESPACE: z.string().min(1, 'NAMESPACE env variable must be set'), + HOST_NODE_PORT: z.coerce.number().min(1, 'HOST_NODE_PORT env variable must be set'), + CONTAINER_NODE_PORT: z.coerce.number().default(8080), HOST_PXE_PORT: z.coerce.number().min(1, 'HOST_PXE_PORT env variable must be set'), CONTAINER_PXE_PORT: z.coerce.number().default(8080), HOST_ETHEREUM_PORT: z.coerce.number().min(1, 'HOST_ETHEREUM_PORT env variable must be set'), @@ -36,6 +39,7 @@ const k8sGCloudConfigSchema = k8sLocalConfigSchema.extend({ const directConfigSchema = z.object({ PXE_URL: z.string().url('PXE_URL must be a valid URL'), + NODE_URL: z.string().url('NODE_URL must be a valid URL'), ETHEREUM_HOST: z.string().url('ETHEREUM_HOST must be a valid URL'), K8S: z.literal('false'), }); @@ -424,3 +428,48 @@ export async function runAlertCheck(config: EnvConfig, alerts: AlertConfig[], lo logger.info('Not running alert check in non-k8s environment'); } } + +export async function updateSequencerConfig(url: string, config: Partial) { + const node = createAztecNodeClient(url); + await node.setConfig(config); +} + +export async function getSequencers(namespace: string) { + const command = `kubectl get pods -l app=validator -n ${namespace} -o jsonpath='{.items[*].metadata.name}'`; + const { stdout } = await execAsync(command); + return stdout.split(' '); +} + +export async function updateK8sSequencersConfig(args: { + containerPort: number; + hostPort: number; + namespace: string; + config: Partial; +}) { + const { containerPort, hostPort, namespace, config } = args; + const sequencers = await getSequencers(namespace); + for (const sequencer of sequencers) { + await startPortForward({ + resource: `pod/${sequencer}`, + namespace, + containerPort, + hostPort, + }); + + const url = `http://localhost:${hostPort}`; + await updateSequencerConfig(url, config); + } +} + +export async function updateSequencersConfig(env: EnvConfig, config: Partial) { + if (isK8sConfig(env)) { + await updateK8sSequencersConfig({ + containerPort: env.CONTAINER_NODE_PORT, + hostPort: env.HOST_NODE_PORT, + namespace: env.NAMESPACE, + config, + }); + } else { + await updateSequencerConfig(env.NODE_URL, config); + } +} diff --git a/yarn-project/ethereum/src/contracts/empire_base.ts b/yarn-project/ethereum/src/contracts/empire_base.ts new file mode 100644 index 00000000000..6fa4bc447c8 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/empire_base.ts @@ -0,0 +1,19 @@ +import { EmpireBaseAbi } from '@aztec/l1-artifacts/EmpireBaseAbi'; + +import { type Hex, encodeFunctionData } from 'viem'; + +import { type L1TxRequest } from '../l1_tx_utils.js'; + +export interface IEmpireBase { + getRoundInfo(rollupAddress: Hex, round: bigint): Promise<{ lastVote: bigint; leader: Hex; executed: boolean }>; + computeRound(slot: bigint): Promise; + createVoteRequest(payload: Hex): L1TxRequest; +} + +export function encodeVote(payload: Hex): Hex { + return encodeFunctionData({ + abi: EmpireBaseAbi, + functionName: 'vote', + args: [payload], + }); +} diff --git a/yarn-project/ethereum/src/contracts/forwarder.test.ts b/yarn-project/ethereum/src/contracts/forwarder.test.ts new file mode 100644 index 00000000000..8cb79d28450 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/forwarder.test.ts @@ -0,0 +1,142 @@ +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Fr } from '@aztec/foundation/fields'; +import { type Logger, createLogger } from '@aztec/foundation/log'; +import { GovernanceProposerAbi } from '@aztec/l1-artifacts/GovernanceProposerAbi'; +import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi'; +import { TestERC20Bytecode } from '@aztec/l1-artifacts/TestERC20Bytecode'; + +import { type Anvil } from '@viem/anvil'; +import { + type Chain, + type GetContractReturnType, + type HttpTransport, + type PublicClient, + encodeFunctionData, + getContract, +} from 'viem'; +import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +import { DefaultL1ContractsConfig } from '../config.js'; +import { type L1Clients, createL1Clients, deployL1Contract, deployL1Contracts } from '../deploy_l1_contracts.js'; +import { L1TxUtils } from '../l1_tx_utils.js'; +import { startAnvil } from '../test/start_anvil.js'; +import { FormattedViemError } from '../utils.js'; +import { ForwarderContract } from './forwarder.js'; + +describe('Forwarder', () => { + let anvil: Anvil; + let rpcUrl: string; + let privateKey: PrivateKeyAccount; + let logger: Logger; + + let vkTreeRoot: Fr; + let protocolContractTreeRoot: Fr; + let l2FeeJuiceAddress: AztecAddress; + let walletClient: L1Clients['walletClient']; + let publicClient: L1Clients['publicClient']; + let forwarder: ForwarderContract; + let l1TxUtils: L1TxUtils; + let govProposerAddress: EthAddress; + let tokenAddress: EthAddress; + let tokenContract: GetContractReturnType>; + beforeAll(async () => { + logger = createLogger('ethereum:test:forwarder'); + // this is the 6th address that gets funded by the junk mnemonic + privateKey = privateKeyToAccount('0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba'); + vkTreeRoot = Fr.random(); + protocolContractTreeRoot = Fr.random(); + l2FeeJuiceAddress = await AztecAddress.random(); + + ({ anvil, rpcUrl } = await startAnvil()); + + ({ walletClient, publicClient } = createL1Clients(rpcUrl, privateKey)); + + const deployed = await deployL1Contracts(rpcUrl, privateKey, foundry, logger, { + ...DefaultL1ContractsConfig, + salt: undefined, + vkTreeRoot, + protocolContractTreeRoot, + l2FeeJuiceAddress, + }); + + govProposerAddress = deployed.l1ContractAddresses.governanceProposerAddress; + + forwarder = await ForwarderContract.create( + privateKey.address, + walletClient, + publicClient, + logger, + deployed.l1ContractAddresses.rollupAddress.toString(), + ); + + l1TxUtils = new L1TxUtils(publicClient, walletClient, logger); + + const { address: erc20Address, txHash: erc20TxHash } = await deployL1Contract( + walletClient, + publicClient, + TestERC20Abi, + TestERC20Bytecode, + ['test', 'TST', privateKey.address], + '0x42', + undefined, + logger, + ); + expect(erc20TxHash).toBeDefined(); + await publicClient.waitForTransactionReceipt({ hash: erc20TxHash! }); + tokenAddress = erc20Address; + tokenContract = getContract({ + address: tokenAddress.toString(), + abi: TestERC20Abi, + client: publicClient, + }); + + const freeForAllHash = await tokenContract.write.setFreeForAll([true], { account: privateKey }); + await publicClient.waitForTransactionReceipt({ hash: freeForAllHash }); + + logger.info(`Token address: ${tokenAddress}`); + }); + + afterAll(async () => { + await anvil.stop(); + }); + + it('gets good error messages', async () => { + expect(forwarder).toBeDefined(); + const initialBalance = await tokenContract.read.balanceOf([privateKey.address]); + expect(initialBalance).toBe(0n); + const err = await forwarder + .forward( + [ + // This one passes + { + to: tokenAddress.toString(), + data: encodeFunctionData({ + abi: TestERC20Abi, + functionName: 'mint', + args: [privateKey.address, 100n], + }), + }, + + // This one fails + { + to: govProposerAddress.toString(), + data: encodeFunctionData({ + abi: GovernanceProposerAbi, + functionName: 'vote', + args: [EthAddress.random().toString()], + }), + }, + ], + l1TxUtils, + undefined, + undefined, + logger, + ) + .catch(err => err); + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(FormattedViemError); + expect(err.message).toMatch(/GovernanceProposer__OnlyProposerCanVote/); + }); +}); diff --git a/yarn-project/ethereum/src/contracts/forwarder.ts b/yarn-project/ethereum/src/contracts/forwarder.ts index 7d1602aa9e7..6008e8a645c 100644 --- a/yarn-project/ethereum/src/contracts/forwarder.ts +++ b/yarn-project/ethereum/src/contracts/forwarder.ts @@ -1,9 +1,11 @@ +import { toHex } from '@aztec/foundation/bigint-buffer'; import { type Logger } from '@aztec/foundation/log'; import { ForwarderAbi, ForwarderBytecode } from '@aztec/l1-artifacts'; import { type Account, type Chain, + type EncodeFunctionDataParameters, type GetContractReturnType, type Hex, type HttpTransport, @@ -15,11 +17,12 @@ import { import { type L1Clients, deployL1Contract } from '../deploy_l1_contracts.js'; import { type L1BlobInputs, type L1GasConfig, type L1TxRequest, type L1TxUtils } from '../l1_tx_utils.js'; +import { RollupContract } from './rollup.js'; export class ForwarderContract { private readonly forwarder: GetContractReturnType>; - constructor(public readonly client: L1Clients['publicClient'], address: Hex) { + constructor(public readonly client: L1Clients['publicClient'], address: Hex, public readonly rollupAddress: Hex) { this.forwarder = getContract({ address, abi: ForwarderAbi, client }); } @@ -28,6 +31,7 @@ export class ForwarderContract { walletClient: WalletClient, publicClient: PublicClient, logger: Logger, + rollupAddress: Hex, ) { logger.info('Deploying forwarder contract'); @@ -48,7 +52,7 @@ export class ForwarderContract { logger.info(`Forwarder contract deployed at ${address} with owner ${owner}`); - return new ForwarderContract(publicClient, address.toString()); + return new ForwarderContract(publicClient, address.toString(), rollupAddress); } public getAddress() { @@ -60,20 +64,22 @@ export class ForwarderContract { l1TxUtils: L1TxUtils, gasConfig: L1GasConfig | undefined, blobConfig: L1BlobInputs | undefined, + logger: Logger, ) { requests = requests.filter(request => request.to !== null); const toArgs = requests.map(request => request.to!); const dataArgs = requests.map(request => request.data!); - const data = encodeFunctionData({ + const forwarderFunctionData: EncodeFunctionDataParameters = { abi: ForwarderAbi, functionName: 'forward', args: [toArgs, dataArgs], - }); + }; + const encodedForwarderData = encodeFunctionData(forwarderFunctionData); const { receipt, gasPrice } = await l1TxUtils.sendAndMonitorTransaction( { to: this.forwarder.address, - data, + data: encodedForwarderData, }, gasConfig, blobConfig, @@ -83,10 +89,10 @@ export class ForwarderContract { const stats = await l1TxUtils.getTransactionStats(receipt.transactionHash); return { receipt, gasPrice, stats }; } else { + logger.error('Forwarder transaction failed', undefined, { receipt }); + const args = { - args: [toArgs, dataArgs], - functionName: 'forward', - abi: ForwarderAbi, + ...forwarderFunctionData, address: this.forwarder.address, }; @@ -97,14 +103,31 @@ export class ForwarderContract { if (maxFeePerBlobGas === undefined) { errorMsg = 'maxFeePerBlobGas is required to get the error message'; } else { - errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(data, args, { - blobs: blobConfig.blobs, - kzg: blobConfig.kzg, - maxFeePerBlobGas, - }); + logger.debug('Trying to get error from reverted tx with blob config'); + errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx( + encodedForwarderData, + args, + { + blobs: blobConfig.blobs, + kzg: blobConfig.kzg, + maxFeePerBlobGas, + }, + [ + { + address: this.rollupAddress, + stateDiff: [ + { + slot: toHex(RollupContract.checkBlobStorageSlot, true), + value: toHex(0n, true), + }, + ], + }, + ], + ); } } else { - errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(data, args); + logger.debug('Trying to get error from reverted tx without blob config'); + errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(encodedForwarderData, args, undefined, []); } return { receipt, gasPrice, errorMsg }; diff --git a/yarn-project/ethereum/src/contracts/governance_proposer.ts b/yarn-project/ethereum/src/contracts/governance_proposer.ts index 5f802709e27..31d39cd669c 100644 --- a/yarn-project/ethereum/src/contracts/governance_proposer.ts +++ b/yarn-project/ethereum/src/contracts/governance_proposer.ts @@ -8,13 +8,19 @@ import { type Hex, type HttpTransport, type PublicClient, + type TransactionReceipt, + encodeFunctionData, getContract, } from 'viem'; -export class GovernanceProposerContract { +import type { L1Clients } from '../deploy_l1_contracts.js'; +import type { GasPrice, L1TxRequest, L1TxUtils } from '../l1_tx_utils.js'; +import { type IEmpireBase, encodeVote } from './empire_base.js'; + +export class GovernanceProposerContract implements IEmpireBase { private readonly proposer: GetContractReturnType>; - constructor(public readonly client: PublicClient, address: Hex) { + constructor(public readonly client: L1Clients['publicClient'], address: Hex) { this.proposer = getContract({ address, abi: GovernanceProposerAbi, client }); } @@ -31,11 +37,55 @@ export class GovernanceProposerContract { return EthAddress.fromString(await this.proposer.read.REGISTRY()); } - public getQuorumSize() { + public getQuorumSize(): Promise { return this.proposer.read.N(); } - public getRoundSize() { + public getRoundSize(): Promise { return this.proposer.read.M(); } + + public computeRound(slot: bigint): Promise { + return this.proposer.read.computeRound([slot]); + } + + public async getRoundInfo( + rollupAddress: Hex, + round: bigint, + ): Promise<{ lastVote: bigint; leader: Hex; executed: boolean }> { + const roundInfo = await this.proposer.read.rounds([rollupAddress, round]); + return { + lastVote: roundInfo[0], + leader: roundInfo[1], + executed: roundInfo[2], + }; + } + + public getProposalVotes(rollupAddress: Hex, round: bigint, proposal: Hex): Promise { + return this.proposer.read.yeaCount([rollupAddress, round, proposal]); + } + + public createVoteRequest(payload: Hex): L1TxRequest { + return { + to: this.address.toString(), + data: encodeVote(payload), + }; + } + + public executeProposal( + round: bigint, + l1TxUtils: L1TxUtils, + ): Promise<{ + receipt: TransactionReceipt; + gasPrice: GasPrice; + }> { + return l1TxUtils.sendAndMonitorTransaction({ + to: this.address.toString(), + data: encodeFunctionData({ + abi: this.proposer.abi, + functionName: 'executeProposal', + args: [round], + }), + }); + } } diff --git a/yarn-project/ethereum/src/contracts/index.ts b/yarn-project/ethereum/src/contracts/index.ts index f26c95ede98..9745bced574 100644 --- a/yarn-project/ethereum/src/contracts/index.ts +++ b/yarn-project/ethereum/src/contracts/index.ts @@ -1,5 +1,6 @@ +export * from './empire_base.js'; export * from './forwarder.js'; -export * from './rollup.js'; export * from './governance.js'; export * from './governance_proposer.js'; +export * from './rollup.js'; export * from './slashing_proposer.js'; diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index d92b3680c93..82ef9fbd240 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -1,7 +1,7 @@ import { memoize } from '@aztec/foundation/decorators'; import { EthAddress } from '@aztec/foundation/eth-address'; import type { ViemSignature } from '@aztec/foundation/eth-signature'; -import { RollupAbi, SlasherAbi } from '@aztec/l1-artifacts'; +import { RollupAbi, RollupStorage, SlasherAbi } from '@aztec/l1-artifacts'; import { type Account, @@ -45,6 +45,14 @@ export type EpochProofQuoteViemArgs = { export class RollupContract { private readonly rollup: GetContractReturnType>; + static get checkBlobStorageSlot(): bigint { + const asString = RollupStorage.find(storage => storage.label === 'checkBlob')?.slot; + if (asString === undefined) { + throw new Error('checkBlobStorageSlot not found'); + } + return BigInt(asString); + } + static getFromL1ContractsValues(deployL1ContractsValues: DeployL1Contracts) { const { publicClient, @@ -171,6 +179,10 @@ export class RollupContract { return this.rollup.read.getTips(); } + getTimestampForSlot(slot: bigint) { + return this.rollup.read.getTimestampForSlot([slot]); + } + async getEpochNumber(blockNumber?: bigint) { blockNumber ??= await this.getBlockNumber(); return this.rollup.read.getEpochForBlock([BigInt(blockNumber)]); diff --git a/yarn-project/ethereum/src/contracts/slashing_proposer.ts b/yarn-project/ethereum/src/contracts/slashing_proposer.ts index a765622f341..6e0a5493aa5 100644 --- a/yarn-project/ethereum/src/contracts/slashing_proposer.ts +++ b/yarn-project/ethereum/src/contracts/slashing_proposer.ts @@ -10,10 +10,14 @@ import { getContract, } from 'viem'; -export class SlashingProposerContract { +import type { L1Clients } from '../deploy_l1_contracts.js'; +import type { L1TxRequest } from '../l1_tx_utils.js'; +import { type IEmpireBase, encodeVote } from './empire_base.js'; + +export class SlashingProposerContract implements IEmpireBase { private readonly proposer: GetContractReturnType>; - constructor(public readonly client: PublicClient, address: Hex) { + constructor(public readonly client: L1Clients['publicClient'], address: Hex) { this.proposer = getContract({ address, abi: SlashingProposerAbi, client }); } @@ -28,4 +32,27 @@ export class SlashingProposerContract { public getRoundSize() { return this.proposer.read.M(); } + + public computeRound(slot: bigint): Promise { + return this.proposer.read.computeRound([slot]); + } + + public async getRoundInfo( + rollupAddress: Hex, + round: bigint, + ): Promise<{ lastVote: bigint; leader: Hex; executed: boolean }> { + const roundInfo = await this.proposer.read.rounds([rollupAddress, round]); + return { + lastVote: roundInfo[0], + leader: roundInfo[1], + executed: roundInfo[2], + }; + } + + public createVoteRequest(payload: Hex): L1TxRequest { + return { + to: this.address.toString(), + data: encodeVote(payload), + }; + } } diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index e53789c17c9..ca7db205900 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -1,4 +1,3 @@ -import { toHex } from '@aztec/foundation/bigint-buffer'; import { compactArray, times } from '@aztec/foundation/collection'; import { type ConfigMappingsType, @@ -23,7 +22,6 @@ import { type HttpTransport, MethodNotFoundRpcError, MethodNotSupportedRpcError, - type PublicClient, type StateOverride, type TransactionReceipt, type WalletClient, @@ -32,6 +30,7 @@ import { hexToBytes, } from 'viem'; +import { type L1Clients } from './deploy_l1_contracts.js'; import { formatViemError } from './utils.js'; // 1_000_000_000 Gwei = 1 ETH @@ -207,7 +206,7 @@ export class L1TxUtils { private interrupted = false; constructor( - public publicClient: PublicClient, + public publicClient: L1Clients['publicClient'], public walletClient: WalletClient, protected readonly logger?: Logger, config?: Partial, @@ -658,32 +657,21 @@ export class L1TxUtils { public async tryGetErrorFromRevertedTx( data: Hex, args: { - args: any[]; + args: readonly any[]; functionName: string; abi: Abi; address: Hex; }, - blobInputs?: L1BlobInputs & { maxFeePerBlobGas: bigint }, + blobInputs: (L1BlobInputs & { maxFeePerBlobGas: bigint }) | undefined, + stateOverride: StateOverride = [], ) { try { - // NB: If this fn starts unexpectedly giving incorrect blob hash errors, it may be because the checkBlob - // bool is no longer at the slot below. To find the slot, run: forge inspect src/core/Rollup.sol:Rollup storage - const checkBlobSlot = 9n; await this.publicClient.simulateContract({ ...args, account: this.walletClient.account, - stateOverride: [ - { - address: args.address, - stateDiff: [ - { - slot: toHex(checkBlobSlot, true), - value: toHex(0n, true), - }, - ], - }, - ], + stateOverride, }); + this.logger?.trace('Simulated blob tx', { blobInputs }); // If the above passes, we have a blob error. We cannot simulate blob txs, and failed txs no longer throw errors. // Strangely, the only way to throw the revert reason as an error and provide blobs is prepareTransactionRequest. // See: https://github.com/wevm/viem/issues/2075 @@ -702,7 +690,9 @@ export class L1TxUtils { to: args.address, data, }; + this.logger?.trace('Preparing tx', { request }); await this.walletClient.prepareTransactionRequest(request); + this.logger?.trace('Prepared tx'); return undefined; } catch (simulationErr: any) { // If we don't have a ContractFunctionExecutionError, we have a blob related error => use getContractError to get the error msg. diff --git a/yarn-project/ethereum/src/utils.ts b/yarn-project/ethereum/src/utils.ts index e614e0afbaa..8c408e4a7fb 100644 --- a/yarn-project/ethereum/src/utils.ts +++ b/yarn-project/ethereum/src/utils.ts @@ -92,6 +92,30 @@ export function prettyLogViemErrorMsg(err: any) { return err?.message ?? err; } +function getNestedErrorData(error: unknown): string | undefined { + // If nothing, bail + if (!error) { + return undefined; + } + + // If it's an object with a `data` property, return it + // (Remember to check TS type-safely or cast as needed) + if (typeof error === 'object' && error !== null && 'data' in error) { + const possibleData = (error as any).data; + if (typeof possibleData === 'string' && possibleData.startsWith('0x')) { + return possibleData; + } + } + + // If it has a `cause`, recurse + if (typeof error === 'object' && error !== null && 'cause' in error) { + return getNestedErrorData((error as any).cause); + } + + // Not found + return undefined; +} + /** * Formats a Viem error into a FormattedViemError instance. * @param error - The error to format. @@ -106,11 +130,12 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem // First try to decode as a custom error using the ABI try { - if (error?.data) { + const data = getNestedErrorData(error); + if (data) { // Try to decode the error data using the ABI const decoded = decodeErrorResult({ abi, - data: error.data as Hex, + data: data as Hex, }); if (decoded) { return new FormattedViemError(`${decoded.errorName}(${decoded.args?.join(', ') ?? ''})`, error?.metaMessages); @@ -120,6 +145,7 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem // If it's a BaseError, try to get the custom error through ContractFunctionRevertedError if (error instanceof BaseError) { const revertError = error.walk(err => err instanceof ContractFunctionRevertedError); + if (revertError instanceof ContractFunctionRevertedError) { let errorName = revertError.data?.errorName; if (!errorName) { @@ -138,7 +164,7 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem // If it's a regular Error instance, return it with its message if (error instanceof Error) { - return new FormattedViemError(error.message); + return error; } // Original formatting logic for non-custom errors diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index 08a0b12cfeb..0e105e252e7 100755 --- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh +++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh @@ -108,4 +108,17 @@ for contract_name in "${contracts[@]}"; do echo "export * from './${contract_name}Bytecode.js';" >>"generated/index.ts" done +# Generate RollupStorage.ts +( + echo "/**" + echo " * Rollup storage." + echo " */" + echo -n "export const RollupStorage = " + jq -j '.storage' "../../l1-contracts/out/Rollup.sol/storage.json" + echo " as const;" +) >"generated/RollupStorage.ts" + +# Update index.ts exports +echo "export * from './RollupStorage.js';" >>"generated/index.ts" + echo "Successfully generated TS artifacts!" diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index f722d7c691c..b30c5760328 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -584,7 +584,7 @@ export class LibP2PService extends WithTracer implement } private async processTxFromPeer(tx: Tx): Promise { - const txHash = tx.getTxHash(); + const txHash = await tx.getTxHash(); const txHashString = txHash.toString(); this.logger.verbose(`Received tx ${txHashString} from external peer.`); await this.mempools.txPool.addTxs([tx]); diff --git a/yarn-project/prover-node/src/prover-node-publisher.ts b/yarn-project/prover-node/src/prover-node-publisher.ts index 8e96b709ebd..ed3be11af28 100644 --- a/yarn-project/prover-node/src/prover-node-publisher.ts +++ b/yarn-project/prover-node/src/prover-node-publisher.ts @@ -229,12 +229,17 @@ export class ProverNodePublisher { return receipt; } catch (err) { this.log.error(`Rollup submit epoch proof failed`, err); - const errorMsg = await this.l1TxUtils.tryGetErrorFromRevertedTx(data, { - args: [...txArgs], - functionName: 'submitEpochRootProof', - abi: RollupAbi, - address: this.rollupContract.address, - }); + const errorMsg = await this.l1TxUtils.tryGetErrorFromRevertedTx( + data, + { + args: [...txArgs], + functionName: 'submitEpochRootProof', + abi: RollupAbi, + address: this.rollupContract.address, + }, + /*blobInputs*/ undefined, + /*stateOverride*/ [], + ); this.log.error(`Rollup submit epoch proof tx reverted. ${errorMsg}`); return undefined; } diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index 6999904110c..54dda5232ab 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -4,8 +4,10 @@ import { type AztecAddress, type ContractDataSource } from '@aztec/circuits.js'; import { EpochCache } from '@aztec/epoch-cache'; import { ForwarderContract, + GovernanceProposerContract, L1TxUtilsWithBlobs, RollupContract, + SlashingProposerContract, createEthereumChain, createL1Clients, isAnvilTestChain, @@ -84,8 +86,25 @@ export class SequencerClient { ] as const); const forwarderContract = config.customForwarderContractAddress && config.customForwarderContractAddress !== EthAddress.ZERO - ? new ForwarderContract(publicClient, config.customForwarderContractAddress.toString()) - : await ForwarderContract.create(walletClient.account.address, walletClient, publicClient, log); + ? new ForwarderContract( + publicClient, + config.customForwarderContractAddress.toString(), + config.l1Contracts.rollupAddress.toString(), + ) + : await ForwarderContract.create( + walletClient.account.address, + walletClient, + publicClient, + log, + config.l1Contracts.rollupAddress.toString(), + ); + + const governanceProposerContract = new GovernanceProposerContract( + publicClient, + config.l1Contracts.governanceProposerAddress.toString(), + ); + const slashingProposerAddress = await rollupContract.getSlashingProposerAddress(); + const slashingProposerContract = new SlashingProposerContract(publicClient, slashingProposerAddress.toString()); const epochCache = deps.epochCache ?? (await EpochCache.create( @@ -110,6 +129,8 @@ export class SequencerClient { rollupContract, epochCache, forwarderContract, + governanceProposerContract, + slashingProposerContract, }); const globalsBuilder = new GlobalVariableBuilder(config); diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts index 9964e83e76c..8dc52cb2b44 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts @@ -6,10 +6,12 @@ import { type EpochCache } from '@aztec/epoch-cache'; import { type ForwarderContract, type GasPrice, + type GovernanceProposerContract, type L1ContractsConfig, type L1TxUtilsConfig, type L1TxUtilsWithBlobs, type RollupContract, + type SlashingProposerContract, defaultL1TxUtilsConfig, getL1ContractsConfigEnvVars, } from '@aztec/ethereum'; @@ -23,7 +25,7 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import { type GetTransactionReceiptReturnType, type TransactionReceipt, encodeFunctionData } from 'viem'; import { type PublisherConfig, type TxSenderConfig } from './config.js'; -import { SequencerPublisher } from './sequencer-publisher.js'; +import { SequencerPublisher, VoteType } from './sequencer-publisher.js'; const mockRollupAddress = EthAddress.random().toString(); const mockGovernanceProposerAddress = EthAddress.random().toString(); @@ -34,6 +36,8 @@ const BLOB_SINK_URL = `http://localhost:${BLOB_SINK_PORT}`; describe('SequencerPublisher', () => { let rollup: MockProxy; let forwarder: MockProxy; + let slashingProposerContract: MockProxy; + let governanceProposerContract: MockProxy; let l1TxUtils: MockProxy; let proposeTxHash: `0x${string}`; @@ -67,6 +71,7 @@ describe('SequencerPublisher', () => { proposeTxHash = `0x${Buffer.from('txHashPropose').toString('hex')}`; // random tx hash proposeTxReceipt = { + blockNumber: 1n, transactionHash: proposeTxHash, status: 'success', logs: [], @@ -105,6 +110,9 @@ describe('SequencerPublisher', () => { errorMsg: undefined, }); + slashingProposerContract = mock(); + governanceProposerContract = mock(); + const epochCache = mock(); epochCache.getEpochAndSlotNow.mockReturnValue({ epoch: 1n, slot: 2n, ts: 3n }); @@ -114,6 +122,8 @@ describe('SequencerPublisher', () => { l1TxUtils, forwarderContract: forwarder, epochCache, + slashingProposerContract, + governanceProposerContract, }); (publisher as any)['l1TxUtils'] = l1TxUtils; @@ -187,15 +197,23 @@ describe('SequencerPublisher', () => { await runBlobSinkServer(expectedBlobs); expect(await publisher.enqueueProposeL2Block(l2Block)).toEqual(true); - // TODO - // const govPayload = EthAddress.random(); - // publisher.setGovernancePayload(govPayload); - // rollup.getProposerAt.mockResolvedValueOnce(mockForwarderAddress); - // expect(await publisher.enqueueCastVote(1n, 1n, VoteType.GOVERNANCE)).toEqual(true); + const govPayload = EthAddress.random(); + publisher.setGovernancePayload(govPayload); + governanceProposerContract.getRoundInfo.mockResolvedValue({ + lastVote: 1n, + leader: govPayload.toString(), + executed: false, + }); + governanceProposerContract.createVoteRequest.mockReturnValue({ + to: mockGovernanceProposerAddress, + data: encodeFunctionData({ abi: EmpireBaseAbi, functionName: 'vote', args: [govPayload.toString()] }), + }); + rollup.getProposerAt.mockResolvedValueOnce(mockForwarderAddress); + expect(await publisher.enqueueCastVote(2n, 1n, VoteType.GOVERNANCE)).toEqual(true); // expect(await publisher.enqueueCastVote(0n, 0n, VoteType.SLASHING)).toEqual(true); await publisher.sendRequests(); - + expect(forwarder.forward).toHaveBeenCalledTimes(1); const blobInput = Blob.getEthBlobEvaluationInputs(expectedBlobs); const args = [ @@ -219,15 +237,16 @@ describe('SequencerPublisher', () => { to: mockRollupAddress, data: encodeFunctionData({ abi: RollupAbi, functionName: 'propose', args }), }, - // { - // to: mockGovernanceProposerAddress, - // data: encodeFunctionData({ abi: EmpireBaseAbi, functionName: 'vote', args: [govPayload.toString()] }), - // }, + { + to: mockGovernanceProposerAddress, + data: encodeFunctionData({ abi: EmpireBaseAbi, functionName: 'vote', args: [govPayload.toString()] }), + }, ], l1TxUtils, // val + (val * 20n) / 100n { gasLimit: 1_000_000n + GAS_GUESS + ((1_000_000n + GAS_GUESS) * 20n) / 100n }, { blobs: expectedBlobs.map(b => b.data), kzg }, + expect.anything(), // the logger ); }); diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index 56b7b84f1b2..a806d0db032 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -14,25 +14,28 @@ import { FormattedViemError, type ForwarderContract, type GasPrice, + type GovernanceProposerContract, + type IEmpireBase, type L1BlobInputs, type L1ContractsConfig, type L1GasConfig, type L1TxRequest, type L1TxUtilsWithBlobs, - type RollupContract, + RollupContract, + type SlashingProposerContract, type TransactionStats, formatViemError, } from '@aztec/ethereum'; import { toHex } from '@aztec/foundation/bigint-buffer'; import { Blob } from '@aztec/foundation/blob'; import { type Signature } from '@aztec/foundation/eth-signature'; -import { type Logger, createLogger } from '@aztec/foundation/log'; +import { createLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; -import { EmpireBaseAbi, ForwarderAbi, RollupAbi } from '@aztec/l1-artifacts'; +import { ForwarderAbi, RollupAbi } from '@aztec/l1-artifacts'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; import pick from 'lodash.pick'; -import { type TransactionReceipt, encodeFunctionData, getAddress, getContract } from 'viem'; +import { type TransactionReceipt, encodeFunctionData } from 'viem'; import { type PublisherConfig, type TxSenderConfig } from './config.js'; import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js'; @@ -106,6 +109,8 @@ export class SequencerPublisher { public l1TxUtils: L1TxUtilsWithBlobs; public rollupContract: RollupContract; + public govProposerContract: GovernanceProposerContract; + public slashingProposerContract: SlashingProposerContract; protected requests: RequestWithExpiry[] = []; @@ -117,15 +122,14 @@ export class SequencerPublisher { forwarderContract: ForwarderContract; l1TxUtils: L1TxUtilsWithBlobs; rollupContract: RollupContract; + slashingProposerContract: SlashingProposerContract; + governanceProposerContract: GovernanceProposerContract; epochCache: EpochCache; }, ) { this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration); this.epochCache = deps.epochCache; - if (config.l1Contracts.governanceProposerAddress) { - this.governanceProposerAddress = EthAddress.fromString(config.l1Contracts.governanceProposerAddress.toString()); - } this.blobSinkClient = deps.blobSinkClient ?? createBlobSinkClient(config); const telemetry = deps.telemetry ?? getTelemetryClient(); @@ -134,6 +138,9 @@ export class SequencerPublisher { this.rollupContract = deps.rollupContract; this.forwarderContract = deps.forwarderContract; + + this.govProposerContract = deps.governanceProposerContract; + this.slashingProposerContract = deps.slashingProposerContract; } public registerSlashPayloadGetter(callback: GetSlashPayloadCallBack) { @@ -221,12 +228,13 @@ export class SequencerPublisher { this.l1TxUtils, gasConfig, blobConfig, + this.log, ); this.callbackBundledTransactions(validRequests, result); return result; } catch (err) { - const { message, metaMessages } = formatViemError(err); - this.log.error(`Failed to publish bundled transactions`, message, { metaMessages }); + const viemError = formatViemError(err); + this.log.error(`Failed to publish bundled transactions`, viemError); return undefined; } } @@ -335,114 +343,85 @@ export class SequencerPublisher { return committee.map(EthAddress.fromString); } - /** - * Enqueues a castVote transaction to cast a vote for a given slot number. - * @param slotNumber - The slot number to cast a vote for. - * @param timestamp - The timestamp of the slot to cast a vote for. - * @param voteType - The type of vote to cast. - * @returns True if the vote was successfully enqueued, false otherwise. - */ - public async enqueueCastVote(slotNumber: bigint, timestamp: bigint, voteType: VoteType): Promise { - // @todo This function can be optimized by doing some of the computations locally instead of calling the L1 contracts + private async enqueueCastVoteHelper( + slotNumber: bigint, + timestamp: bigint, + voteType: VoteType, + payload: EthAddress, + base: IEmpireBase, + ): Promise { if (this.myLastVotes[voteType] >= slotNumber) { return false; } - - const voteConfig = async (): Promise< - { payload: EthAddress; voteContractAddress: EthAddress; logger: Logger } | undefined - > => { - if (voteType === VoteType.GOVERNANCE) { - if (this.governancePayload.equals(EthAddress.ZERO)) { - return undefined; - } - if (!this.governanceProposerAddress) { - return undefined; - } - return { - payload: this.governancePayload, - voteContractAddress: this.governanceProposerAddress, - logger: this.governanceLog, - }; - } else if (voteType === VoteType.SLASHING) { - if (!this.getSlashPayload) { - return undefined; - } - const slashingProposerAddress = await this.rollupContract.getSlashingProposerAddress(); - if (!slashingProposerAddress) { - return undefined; - } - - const slashPayload = await this.getSlashPayload(slotNumber); - - if (!slashPayload) { - return undefined; - } - - return { - payload: slashPayload, - voteContractAddress: slashingProposerAddress, - logger: this.slashingLog, - }; - } else { - throw new Error('Invalid vote type'); - } - }; - - const vConfig = await voteConfig(); - - if (!vConfig) { + if (payload.equals(EthAddress.ZERO)) { return false; } - - const { payload, voteContractAddress } = vConfig; - - const voteContract = getContract({ - address: getAddress(voteContractAddress.toString()), - abi: EmpireBaseAbi, - client: this.l1TxUtils.walletClient, - }); - - const [proposer, roundNumber] = await Promise.all([ + const round = await base.computeRound(slotNumber); + const [proposer, roundInfo] = await Promise.all([ this.rollupContract.getProposerAt(timestamp), - voteContract.read.computeRound([slotNumber]), + base.getRoundInfo(this.rollupContract.address, round), ]); if (proposer.toLowerCase() !== this.getForwarderAddress().toString().toLowerCase()) { return false; } - - const [slotForLastVote] = await voteContract.read.rounds([this.rollupContract.address, roundNumber]); - - if (slotForLastVote >= slotNumber) { + if (roundInfo.lastVote >= slotNumber) { return false; } const cachedLastVote = this.myLastVotes[voteType]; - this.myLastVotes[voteType] = slotNumber; this.addRequest({ action: voteType === VoteType.GOVERNANCE ? 'governance-vote' : 'slashing-vote', - request: { - to: voteContractAddress.toString(), - data: encodeFunctionData({ - abi: EmpireBaseAbi, - functionName: 'vote', - args: [payload.toString()], - }), - }, + request: base.createVoteRequest(payload.toString()), lastValidL2Slot: slotNumber, onResult: (_request, result) => { if (!result || result.receipt.status !== 'success') { this.myLastVotes[voteType] = cachedLastVote; } else { - this.log.info(`Cast ${voteType} vote for slot ${slotNumber}`); + this.log.info(`Cast [${voteType}] vote for slot ${slotNumber}`); } }, }); return true; } + private async getVoteConfig( + slotNumber: bigint, + voteType: VoteType, + ): Promise<{ payload: EthAddress; base: IEmpireBase } | undefined> { + if (voteType === VoteType.GOVERNANCE) { + return { payload: this.governancePayload, base: this.govProposerContract }; + } else if (voteType === VoteType.SLASHING) { + if (!this.getSlashPayload) { + return undefined; + } + const slashPayload = await this.getSlashPayload(slotNumber); + if (!slashPayload) { + return undefined; + } + return { payload: slashPayload, base: this.slashingProposerContract }; + } + throw new Error('Unreachable: Invalid vote type'); + } + + /** + * Enqueues a castVote transaction to cast a vote for a given slot number. + * @param slotNumber - The slot number to cast a vote for. + * @param timestamp - The timestamp of the slot to cast a vote for. + * @param voteType - The type of vote to cast. + * @returns True if the vote was successfully enqueued, false otherwise. + */ + public async enqueueCastVote(slotNumber: bigint, timestamp: bigint, voteType: VoteType): Promise { + const voteConfig = await this.getVoteConfig(slotNumber, voteType); + if (!voteConfig) { + return false; + } + const { payload, base } = voteConfig; + return this.enqueueCastVoteHelper(slotNumber, timestamp, voteType, payload, base); + } + /** * Proposes a L2 block on L1. * @@ -627,7 +606,7 @@ export class SequencerPublisher { // @note we override checkBlob to false since blobs are not part simulate() stateDiff: [ { - slot: toHex(9n, true), + slot: toHex(RollupContract.checkBlobStorageSlot, true), value: toHex(0n, true), }, ], @@ -657,6 +636,7 @@ export class SequencerPublisher { const kzg = Blob.getViemKzgInstance(); const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp); const startBlock = await this.l1TxUtils.getBlockNumber(); + const blockHash = await block.hash(); return this.addRequest({ action: 'propose', @@ -708,7 +688,7 @@ export class SequencerPublisher { this.log.error(`Rollup process tx reverted. ${errorMsg ?? 'No error message'}`, undefined, { ...block.getStats(), txHash: receipt.transactionHash, - blockHash: block.hash().toString(), + blockHash, slotNumber: block.header.globalVariables.slotNumber.toBigInt(), }); }