diff --git a/cspell.json b/cspell.json index c4ae01a32e54..c84e2e3e1ff5 100644 --- a/cspell.json +++ b/cspell.json @@ -105,6 +105,7 @@ "fuzzers", "gitmodules", "gitrepo", + "Gossipable", "gossipsub", "grumpkin", "gtest", diff --git a/docker-compose.provernet.yml b/docker-compose.provernet.yml index f889e7860e72..6b45561f364d 100644 --- a/docker-compose.provernet.yml +++ b/docker-compose.provernet.yml @@ -30,6 +30,7 @@ services: SEQ_MIN_SECONDS_BETWEEN_BLOCKS: 0 SEQ_RETRY_INTERVAL: 10000 SEQ_PUBLISHER_PRIVATE_KEY: "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a" + VALIDATOR_PRIVATE_KEY: "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a" PROVER_REAL_PROOFS: "${PROVER_REAL_PROOFS:-false}" ASSUME_PROVEN_UNTIL_BLOCK_NUMBER: "${ASSUME_PROVEN_UNTIL_BLOCK_NUMBER:-4}" P2P_ENABLED: false diff --git a/docs/docs/reference/developer_references/sandbox_reference/sandbox-reference.md b/docs/docs/reference/developer_references/sandbox_reference/sandbox-reference.md index 999bd18c7c49..079de8cff0b3 100644 --- a/docs/docs/reference/developer_references/sandbox_reference/sandbox-reference.md +++ b/docs/docs/reference/developer_references/sandbox_reference/sandbox-reference.md @@ -76,6 +76,9 @@ SEQ_MAX_TX_PER_BLOCK=32 # Maximum txs to go on a block. (default: 32) SEQ_MIN_TX_PER_BLOCK=1 # Minimum txs to go on a block. (default: 1) SEQ_MAX_SECONDS_BETWEEN_BLOCKS=0 # Sequencer will produce a block with less than the min number of txs once this threshold is reached. (default: 0, means disabled) SEQ_MIN_SECONDS_BETWEEN_BLOCKS=0 # Minimum seconds to wait between consecutive blocks. (default: 0) + +## Validator variables ## +VALIDATOR_PRIVATE_KEY=0x01234567890abcde01234567890abcde # Private key of the ethereum account that will be used to perform validator duties ``` **PXE** diff --git a/l1-contracts/src/core/sequencer_selection/ILeonidas.sol b/l1-contracts/src/core/sequencer_selection/ILeonidas.sol index 24501446da30..a7de4ac1b3c0 100644 --- a/l1-contracts/src/core/sequencer_selection/ILeonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/ILeonidas.sol @@ -22,6 +22,8 @@ interface ILeonidas { function getTimestampForSlot(uint256 _slotNumber) external view returns (uint256); // Likely removal of these to replace with a size and indiviual getter + // Get the current epoch committee + function getCurrentEpochCommittee() external view returns (address[] memory); function getEpochCommittee(uint256 _epoch) external view returns (address[] memory); function getValidators() external view returns (address[] memory); } diff --git a/l1-contracts/src/core/sequencer_selection/Leonidas.sol b/l1-contracts/src/core/sequencer_selection/Leonidas.sol index 4427115fe572..4efdc8ff3cd8 100644 --- a/l1-contracts/src/core/sequencer_selection/Leonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/Leonidas.sol @@ -132,6 +132,43 @@ contract Leonidas is Ownable, ILeonidas { return epochs[_epoch].committee; } + function getCommitteeAt(uint256 _ts) internal view returns (address[] memory) { + uint256 epochNumber = getEpochAt(_ts); + if (epochNumber == 0) { + return new address[](0); + } + + Epoch storage epoch = epochs[epochNumber]; + + if (epoch.sampleSeed != 0) { + uint256 committeeSize = epoch.committee.length; + if (committeeSize == 0) { + return new address[](0); + } + return epoch.committee; + } + + // Allow anyone if there is no validator set + if (validatorSet.length() == 0) { + return new address[](0); + } + + // Emulate a sampling of the validators + uint256 sampleSeed = _getSampleSeed(epochNumber); + return _sampleValidators(epochNumber, sampleSeed); + } + + /** + * @notice Get the validator set for the current epoch + * + * @dev Makes a call to setupEpoch under the hood, this should ONLY be called as a view function, and not from within + * this contract. + * @return The validator set for the current epoch + */ + function getCurrentEpochCommittee() external view override(ILeonidas) returns (address[] memory) { + return getCommitteeAt(block.timestamp); + } + /** * @notice Get the validator set * diff --git a/yarn-project/aztec-node/package.json b/yarn-project/aztec-node/package.json index 6189671d5290..e6227b6f0480 100644 --- a/yarn-project/aztec-node/package.json +++ b/yarn-project/aztec-node/package.json @@ -72,6 +72,7 @@ "@aztec/simulator": "workspace:^", "@aztec/telemetry-client": "workspace:^", "@aztec/types": "workspace:^", + "@aztec/validator-client": "workspace:^", "@aztec/world-state": "workspace:^", "koa": "^2.14.2", "koa-router": "^12.0.0", diff --git a/yarn-project/aztec-node/src/aztec-node/config.ts b/yarn-project/aztec-node/src/aztec-node/config.ts index ce8af0e112b6..df9e0bd7309f 100644 --- a/yarn-project/aztec-node/src/aztec-node/config.ts +++ b/yarn-project/aztec-node/src/aztec-node/config.ts @@ -3,6 +3,7 @@ import { type ConfigMappingsType, booleanConfigHelper, getConfigFromMappings } f import { type P2PConfig, p2pConfigMappings } from '@aztec/p2p'; import { type ProverClientConfig, proverClientConfigMappings } from '@aztec/prover-client'; import { type SequencerClientConfig, sequencerClientConfigMappings } from '@aztec/sequencer-client'; +import { type ValidatorClientConfig, validatorClientConfigMappings } from '@aztec/validator-client'; import { type WorldStateConfig, worldStateConfigMappings } from '@aztec/world-state'; import { readFileSync } from 'fs'; @@ -16,17 +17,22 @@ export { sequencerClientConfigMappings, SequencerClientConfig } from '@aztec/seq */ export type AztecNodeConfig = ArchiverConfig & SequencerClientConfig & + ValidatorClientConfig & ProverClientConfig & WorldStateConfig & Pick & P2PConfig & { /** Whether the sequencer is disabled for this node. */ disableSequencer: boolean; + + /** Whether the validator is disabled for this node */ + disableValidator: boolean; }; export const aztecNodeConfigMappings: ConfigMappingsType = { ...archiverConfigMappings, ...sequencerClientConfigMappings, + ...validatorClientConfigMappings, ...proverClientConfigMappings, ...worldStateConfigMappings, ...p2pConfigMappings, @@ -35,6 +41,11 @@ export const aztecNodeConfigMappings: ConfigMappingsType = { description: 'Whether the sequencer is disabled for this node.', ...booleanConfigHelper(), }, + disableValidator: { + env: 'VALIDATOR_DISABLED', + description: 'Whether the validator is disabled for this node.', + ...booleanConfigHelper(), + }, }; /** diff --git a/yarn-project/aztec-node/src/aztec-node/http_rpc_server.ts b/yarn-project/aztec-node/src/aztec-node/http_rpc_server.ts index d209b5957d97..7409d95e1341 100644 --- a/yarn-project/aztec-node/src/aztec-node/http_rpc_server.ts +++ b/yarn-project/aztec-node/src/aztec-node/http_rpc_server.ts @@ -17,9 +17,9 @@ import { import { FunctionSelector, Header } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; -import { BaseHashType } from '@aztec/foundation/hash'; import { JsonRpcServer } from '@aztec/foundation/json-rpc/server'; /** @@ -41,7 +41,7 @@ export function createAztecNodeRpcServer(node: AztecNode) { TxEffect, LogId, TxHash, - BaseHashType, + Buffer32, PublicDataWitness, SiblingPath, }, diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 1ce2b13d582c..9d2dfd36cb99 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -52,7 +52,7 @@ import { Timer } from '@aztec/foundation/timer'; import { type AztecKVStore } from '@aztec/kv-store'; import { createStore, openTmpStore } from '@aztec/kv-store/utils'; import { SHA256Trunc, StandardTree, UnbalancedTree } from '@aztec/merkle-tree'; -import { AztecKVTxPool, type P2P, createP2PClient } from '@aztec/p2p'; +import { AztecKVTxPool, InMemoryAttestationPool, type P2P, createP2PClient } from '@aztec/p2p'; import { getCanonicalClassRegisterer } from '@aztec/protocol-contracts/class-registerer'; import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; import { getCanonicalInstanceDeployer } from '@aztec/protocol-contracts/instance-deployer'; @@ -74,6 +74,7 @@ import { type ContractInstanceWithAddress, type ProtocolContractAddresses, } from '@aztec/types/contracts'; +import { createValidatorClient } from '@aztec/validator-client'; import { MerkleTrees, type WorldStateSynchronizer, createWorldStateSynchronizer } from '@aztec/world-state'; import { type AztecNodeConfig, getPackageInfo } from './config.js'; @@ -149,7 +150,13 @@ export class AztecNodeService implements AztecNode { config.transactionProtocol = `/aztec/tx/${config.l1Contracts.rollupAddress.toString()}`; // create the tx pool and the p2p client, which will need the l2 block source - const p2pClient = await createP2PClient(config, store, new AztecKVTxPool(store, telemetry), archiver); + const p2pClient = await createP2PClient( + config, + store, + new AztecKVTxPool(store, telemetry), + new InMemoryAttestationPool(), + archiver, + ); // now create the merkle trees and the world state synchronizer const worldStateSynchronizer = await createWorldStateSynchronizer(config, store, archiver); @@ -166,11 +173,14 @@ export class AztecNodeService implements AztecNode { const simulationProvider = await createSimulationProvider(config, log); + const validatorClient = createValidatorClient(config, p2pClient); + // now create the sequencer const sequencer = config.disableSequencer ? undefined : await SequencerClient.new( config, + validatorClient, p2pClient, worldStateSynchronizer, archiver, diff --git a/yarn-project/aztec-node/tsconfig.json b/yarn-project/aztec-node/tsconfig.json index 5a6637a7baca..d0ffd81234ba 100644 --- a/yarn-project/aztec-node/tsconfig.json +++ b/yarn-project/aztec-node/tsconfig.json @@ -54,6 +54,9 @@ { "path": "../types" }, + { + "path": "../validator-client" + }, { "path": "../world-state" } diff --git a/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts b/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts index 4e8424bf5eaf..7a0b896660fc 100644 --- a/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts +++ b/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts @@ -27,7 +27,7 @@ import { Point, } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; -import { BaseHashType } from '@aztec/foundation/hash'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { createJsonRpcClient, makeFetch } from '@aztec/foundation/json-rpc/client'; /** @@ -57,7 +57,7 @@ export const createPXEClient = (url: string, fetch = makeFetch([1, 2, 3], false) Point, TxExecutionRequest, TxHash, - BaseHashType, + Buffer32, }, { EncryptedNoteL2BlockL2Logs, diff --git a/yarn-project/aztec/src/sandbox.ts b/yarn-project/aztec/src/sandbox.ts index d1342e4cde36..c5fa92eec3a8 100644 --- a/yarn-project/aztec/src/sandbox.ts +++ b/yarn-project/aztec/src/sandbox.ts @@ -160,6 +160,10 @@ export async function createSandbox(config: Partial = {}) { const privKey = hdAccount.getHdKey().privateKey; aztecNodeConfig.publisherPrivateKey = `0x${Buffer.from(privKey!).toString('hex')}`; } + if (!aztecNodeConfig.validatorPrivateKey || aztecNodeConfig.validatorPrivateKey === NULL_KEY) { + const privKey = hdAccount.getHdKey().privateKey; + aztecNodeConfig.validatorPrivateKey = `0x${Buffer.from(privKey!).toString('hex')}`; + } if (!aztecNodeConfig.p2pEnabled) { await deployContractsToL1(aztecNodeConfig, hdAccount); diff --git a/yarn-project/aztec/terraform/node/main.tf b/yarn-project/aztec/terraform/node/main.tf index e7f7b040d7de..9df0305751d3 100644 --- a/yarn-project/aztec/terraform/node/main.tf +++ b/yarn-project/aztec/terraform/node/main.tf @@ -240,6 +240,10 @@ resource "aws_ecs_task_definition" "aztec-node" { name = "SEQ_PUBLISHER_PRIVATE_KEY" value = local.sequencer_private_keys[count.index] }, + { + name = "VALIDATOR_PRIVATE_KEY" + value = local.sequencer_private_keys[count.index] + }, { name = "ROLLUP_CONTRACT_ADDRESS" value = data.terraform_remote_state.l1_contracts.outputs.rollup_contract_address diff --git a/yarn-project/circuit-types/package.json b/yarn-project/circuit-types/package.json index 09e852caff31..d72351c6ecbc 100644 --- a/yarn-project/circuit-types/package.json +++ b/yarn-project/circuit-types/package.json @@ -80,7 +80,8 @@ "jest": "^29.5.0", "jest-mock-extended": "^3.0.3", "ts-node": "^10.9.1", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "viem": "^2.7.15" }, "files": [ "dest", diff --git a/yarn-project/circuit-types/src/aztec_node/rpc/aztec_node_client.ts b/yarn-project/circuit-types/src/aztec_node/rpc/aztec_node_client.ts index 85056113aec4..73c2da7cab06 100644 --- a/yarn-project/circuit-types/src/aztec_node/rpc/aztec_node_client.ts +++ b/yarn-project/circuit-types/src/aztec_node/rpc/aztec_node_client.ts @@ -1,9 +1,9 @@ import { FunctionSelector, Header } from '@aztec/circuits.js'; import { EventSelector, NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; -import { BaseHashType } from '@aztec/foundation/hash'; import { createJsonRpcClient, defaultFetch } from '@aztec/foundation/json-rpc/client'; import { type AztecNode } from '../../interfaces/aztec-node.js'; @@ -41,7 +41,7 @@ export function createAztecNodeClient(url: string, fetch = defaultFetch): AztecN TxEffect, LogId, TxHash, - BaseHashType, + Buffer32, PublicDataWitness, SiblingPath, }, diff --git a/yarn-project/circuit-types/src/p2p/block_attestation.test.ts b/yarn-project/circuit-types/src/p2p/block_attestation.test.ts index 75628308ae7e..1a0ba5235483 100644 --- a/yarn-project/circuit-types/src/p2p/block_attestation.test.ts +++ b/yarn-project/circuit-types/src/p2p/block_attestation.test.ts @@ -1,22 +1,28 @@ // Serde test for the block attestation type -import { makeHeader } from '@aztec/circuits.js/testing'; - import { BlockAttestation } from './block_attestation.js'; - -const makeBlockAttestation = (): BlockAttestation => { - const blockHeader = makeHeader(1); - const signature = Buffer.alloc(64, 1); - - return new BlockAttestation(blockHeader, signature); -}; +import { makeBlockAttestation, randomSigner } from './mocks.js'; describe('Block Attestation serialization / deserialization', () => { - it('Should serialize / deserialize', () => { - const attestation = makeBlockAttestation(); + it('Should serialize / deserialize', async () => { + const attestation = await makeBlockAttestation(); const serialized = attestation.toBuffer(); const deserialized = BlockAttestation.fromBuffer(serialized); expect(deserialized).toEqual(attestation); }); + + it('Should serialize / deserialize + recover sender', async () => { + const account = randomSigner(); + + const proposal = await makeBlockAttestation(account); + const serialized = proposal.toBuffer(); + const deserialized = BlockAttestation.fromBuffer(serialized); + + expect(deserialized).toEqual(proposal); + + // Recover signature + const sender = await deserialized.getSender(); + expect(sender.toChecksumString()).toEqual(account.address); + }); }); diff --git a/yarn-project/circuit-types/src/p2p/block_attestation.ts b/yarn-project/circuit-types/src/p2p/block_attestation.ts index d1d34a1570e1..fd38d6911721 100644 --- a/yarn-project/circuit-types/src/p2p/block_attestation.ts +++ b/yarn-project/circuit-types/src/p2p/block_attestation.ts @@ -1,11 +1,15 @@ -import { Header } from '@aztec/circuits.js'; -import { BaseHashType } from '@aztec/foundation/hash'; +import { EthAddress, Header } from '@aztec/circuits.js'; +import { Buffer32 } from '@aztec/foundation/buffer'; +import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { recoverMessageAddress } from 'viem'; + import { Gossipable } from './gossipable.js'; +import { Signature } from './signature.js'; import { TopicType, createTopicString } from './topic_type.js'; -export class BlockAttestationHash extends BaseHashType { +export class BlockAttestationHash extends Buffer32 { constructor(hash: Buffer) { super(hash); } @@ -20,11 +24,15 @@ export class BlockAttestationHash extends BaseHashType { export class BlockAttestation extends Gossipable { static override p2pTopic: string; + private sender: EthAddress | undefined; + constructor( /** The block header the attestation is made over */ public readonly header: Header, + // TODO(https://github.com/AztecProtocol/aztec-packages/pull/7727#discussion_r1713670830): temporary + public readonly archive: Fr, /** The signature of the block attester */ - public readonly signature: Buffer, + public readonly signature: Signature, ) { super(); } @@ -33,16 +41,39 @@ export class BlockAttestation extends Gossipable { this.p2pTopic = createTopicString(TopicType.block_attestation); } - override p2pMessageIdentifier(): BaseHashType { - return BlockAttestationHash.fromField(this.header.hash()); + override p2pMessageIdentifier(): Buffer32 { + return BlockAttestationHash.fromField(this.archive); + } + + /**Get sender + * + * Lazily evaluate and cache the sender of the attestation + * @returns The sender of the attestation + */ + async getSender() { + if (!this.sender) { + // Recover the sender from the attestation + const address = await recoverMessageAddress({ + message: { raw: this.p2pMessageIdentifier().to0xString() }, + signature: this.signature.to0xString(), + }); + // Cache the sender for later use + this.sender = EthAddress.fromString(address); + } + + return this.sender; } toBuffer(): Buffer { - return serializeToBuffer([this.header, this.signature.length, this.signature]); + return serializeToBuffer([this.header, this.archive, this.signature]); } static fromBuffer(buf: Buffer | BufferReader): BlockAttestation { const reader = BufferReader.asReader(buf); - return new BlockAttestation(reader.readObject(Header), reader.readBuffer()); + return new BlockAttestation(reader.readObject(Header), reader.readObject(Fr), reader.readObject(Signature)); + } + + static empty(): BlockAttestation { + return new BlockAttestation(Header.empty(), Fr.ZERO, Signature.empty()); } } diff --git a/yarn-project/circuit-types/src/p2p/block_proposal.test.ts b/yarn-project/circuit-types/src/p2p/block_proposal.test.ts index 995dfef9cb98..6eb1c427f4f5 100644 --- a/yarn-project/circuit-types/src/p2p/block_proposal.test.ts +++ b/yarn-project/circuit-types/src/p2p/block_proposal.test.ts @@ -1,24 +1,28 @@ // Serde test for the block proposal type -import { makeHeader } from '@aztec/circuits.js/testing'; - -import { TxHash } from '../index.js'; import { BlockProposal } from './block_proposal.js'; +import { makeBlockProposal, randomSigner } from './mocks.js'; describe('Block Proposal serialization / deserialization', () => { - const makeBlockProposal = (): BlockProposal => { - const blockHeader = makeHeader(1); - const txs = [0, 1, 2, 3, 4, 5].map(() => TxHash.random()); - const signature = Buffer.alloc(64, 1); + it('Should serialize / deserialize', async () => { + const proposal = await makeBlockProposal(); - return new BlockProposal(blockHeader, txs, signature); - }; + const serialized = proposal.toBuffer(); + const deserialized = BlockProposal.fromBuffer(serialized); - it('Should serialize / deserialize', () => { - const proposal = makeBlockProposal(); + expect(deserialized).toEqual(proposal); + }); + + it('Should serialize / deserialize + recover sender', async () => { + const account = randomSigner(); + const proposal = await makeBlockProposal(account); const serialized = proposal.toBuffer(); const deserialized = BlockProposal.fromBuffer(serialized); expect(deserialized).toEqual(proposal); + + // Recover signature + const sender = await deserialized.getSender(); + expect(sender.toChecksumString()).toEqual(account.address); }); }); diff --git a/yarn-project/circuit-types/src/p2p/block_proposal.ts b/yarn-project/circuit-types/src/p2p/block_proposal.ts index 49aae6a5a679..5c77de9b5e96 100644 --- a/yarn-project/circuit-types/src/p2p/block_proposal.ts +++ b/yarn-project/circuit-types/src/p2p/block_proposal.ts @@ -1,12 +1,16 @@ -import { Header } from '@aztec/circuits.js'; -import { BaseHashType } from '@aztec/foundation/hash'; +import { EthAddress, Header } from '@aztec/circuits.js'; +import { Buffer32 } from '@aztec/foundation/buffer'; +import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; -import { TxHash } from '../index.js'; +import { recoverMessageAddress } from 'viem'; + +import { TxHash } from '../tx/tx_hash.js'; import { Gossipable } from './gossipable.js'; +import { Signature } from './signature.js'; import { TopicType, createTopicString } from './topic_type.js'; -export class BlockProposalHash extends BaseHashType { +export class BlockProposalHash extends Buffer32 { constructor(hash: Buffer) { super(hash); } @@ -21,13 +25,18 @@ export class BlockProposalHash extends BaseHashType { export class BlockProposal extends Gossipable { static override p2pTopic: string; + private sender: EthAddress | undefined; + constructor( /** The block header, after execution of the below sequence of transactions */ public readonly header: Header, + + // TODO(https://github.com/AztecProtocol/aztec-packages/pull/7727#discussion_r1713670830): temporary + public readonly archive: Fr, /** The sequence of transactions in the block */ public readonly txs: TxHash[], /** The signer of the BlockProposal over the header of the new block*/ - public readonly signature: Buffer, + public readonly signature: Signature, ) { super(); } @@ -36,20 +45,38 @@ export class BlockProposal extends Gossipable { this.p2pTopic = createTopicString(TopicType.block_proposal); } - override p2pMessageIdentifier(): BaseHashType { - return BlockProposalHash.fromField(this.header.hash()); + override p2pMessageIdentifier(): Buffer32 { + return BlockProposalHash.fromField(this.archive); + } + + /**Get Sender + * Lazily evaluate the sender of the proposal; result is cached + */ + async getSender() { + if (!this.sender) { + // performance note(): this signature method requires another hash behind the scenes + const address = await recoverMessageAddress({ + message: { raw: this.p2pMessageIdentifier().to0xString() }, + signature: this.signature.to0xString(), + }); + // Cache the sender for later use + this.sender = EthAddress.fromString(address); + } + + return this.sender; } toBuffer(): Buffer { - return serializeToBuffer([this.header, this.txs.length, this.txs, this.signature.length, this.signature]); + return serializeToBuffer([this.header, this.archive, this.txs.length, this.txs, this.signature]); } static fromBuffer(buf: Buffer | BufferReader): BlockProposal { const reader = BufferReader.asReader(buf); return new BlockProposal( reader.readObject(Header), + reader.readObject(Fr), reader.readArray(reader.readNumber(), TxHash), - reader.readBuffer(), + reader.readObject(Signature), ); } } diff --git a/yarn-project/circuit-types/src/p2p/gossipable.ts b/yarn-project/circuit-types/src/p2p/gossipable.ts index 10cfa506ce95..4b9c407cb03a 100644 --- a/yarn-project/circuit-types/src/p2p/gossipable.ts +++ b/yarn-project/circuit-types/src/p2p/gossipable.ts @@ -1,4 +1,4 @@ -import { type BaseHashType } from '@aztec/foundation/hash'; +import { type Buffer32 } from '@aztec/foundation/buffer'; /** * Gossipable @@ -16,7 +16,7 @@ export abstract class Gossipable { * * - A digest of the message information, this key is used for deduplication */ - abstract p2pMessageIdentifier(): BaseHashType; + abstract p2pMessageIdentifier(): Buffer32; /** To Buffer * diff --git a/yarn-project/circuit-types/src/p2p/index.ts b/yarn-project/circuit-types/src/p2p/index.ts index 31f8d8c56e39..a8a7f011fd04 100644 --- a/yarn-project/circuit-types/src/p2p/index.ts +++ b/yarn-project/circuit-types/src/p2p/index.ts @@ -3,3 +3,4 @@ export * from './block_proposal.js'; export * from './interface.js'; export * from './gossipable.js'; export * from './topic_type.js'; +export * from './signature.js'; diff --git a/yarn-project/circuit-types/src/p2p/mocks.ts b/yarn-project/circuit-types/src/p2p/mocks.ts new file mode 100644 index 000000000000..f85ba76a6aed --- /dev/null +++ b/yarn-project/circuit-types/src/p2p/mocks.ts @@ -0,0 +1,37 @@ +import { makeHeader } from '@aztec/circuits.js/testing'; +import { Fr } from '@aztec/foundation/fields'; + +import { type PrivateKeyAccount } from 'viem'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; + +import { TxHash } from '../tx/tx_hash.js'; +import { BlockAttestation } from './block_attestation.js'; +import { BlockProposal } from './block_proposal.js'; +import { Signature } from './signature.js'; + +export const makeBlockProposal = async (signer?: PrivateKeyAccount): Promise => { + signer = signer || randomSigner(); + + const blockHeader = makeHeader(1); + const archive = Fr.random(); + const txs = [0, 1, 2, 3, 4, 5].map(() => TxHash.random()); + const signature = Signature.from0xString(await signer.signMessage({ message: { raw: archive.toString() } })); + + return new BlockProposal(blockHeader, archive, txs, signature); +}; + +// TODO(https://github.com/AztecProtocol/aztec-packages/issues/8028) +export const makeBlockAttestation = async (signer?: PrivateKeyAccount): Promise => { + signer = signer || randomSigner(); + + const blockHeader = makeHeader(1); + const archive = Fr.random(); + const signature = Signature.from0xString(await signer.signMessage({ message: { raw: archive.toString() } })); + + return new BlockAttestation(blockHeader, archive, signature); +}; + +export const randomSigner = (): PrivateKeyAccount => { + const privateKey = generatePrivateKey(); + return privateKeyToAccount(privateKey); +}; diff --git a/yarn-project/circuit-types/src/p2p/signature.test.ts b/yarn-project/circuit-types/src/p2p/signature.test.ts new file mode 100644 index 000000000000..7e443a4f105f --- /dev/null +++ b/yarn-project/circuit-types/src/p2p/signature.test.ts @@ -0,0 +1,31 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { recoverMessageAddress } from 'viem'; + +import { randomSigner } from './mocks.js'; +import { Signature } from './signature.js'; + +describe('Signature serialization / deserialization', () => { + it('Should serialize / deserialize', async () => { + const signer = randomSigner(); + + const originalMessage = Fr.random(); + const m = `0x${originalMessage.toBuffer().toString('hex')}`; + + const signature = await signer.signMessage({ message: m }); + + const signatureObj = Signature.from0xString(signature); + + // Serde + const serialized = signatureObj.toBuffer(); + const deserialized = Signature.fromBuffer(serialized); + expect(deserialized).toEqual(signatureObj); + + const as0x = deserialized.to0xString(); + expect(as0x).toEqual(signature); + + // Recover signature + const sender = await recoverMessageAddress({ message: originalMessage.toString(), signature: as0x }); + expect(sender).toEqual(signer.address); + }); +}); diff --git a/yarn-project/circuit-types/src/p2p/signature.ts b/yarn-project/circuit-types/src/p2p/signature.ts new file mode 100644 index 000000000000..41870e59c625 --- /dev/null +++ b/yarn-project/circuit-types/src/p2p/signature.ts @@ -0,0 +1,85 @@ +import { Buffer32 } from '@aztec/foundation/buffer'; +import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; + +/**Viem Signature + * + * A version of the Signature class that uses `0x${string}` values for r and s rather than + * Buffer32s + */ +export type ViemSignature = { + r: `0x${string}`; + s: `0x${string}`; + v: number; + isEmpty: boolean; +}; + +/** + * Signature + * + * Contains a signature split into it's primary components (r,s,v) + */ +export class Signature { + constructor( + /** The r value of the signature */ + public readonly r: Buffer32, + /** The s value of the signature */ + public readonly s: Buffer32, + /** The v value of the signature */ + public readonly v: number, + /** Does this struct store an empty signature */ + public readonly isEmpty: boolean = false, + ) {} + + static fromBuffer(buf: Buffer | BufferReader): Signature { + const reader = BufferReader.asReader(buf); + const r = reader.readObject(Buffer32); + const s = reader.readObject(Buffer32); + const v = reader.readNumber(); + + const isEmpty = r.isZero() && s.isZero(); + + return new Signature(r, s, v, isEmpty); + } + + toBuffer(): Buffer { + return serializeToBuffer([this.r, this.s, this.v]); + } + + static empty(): Signature { + return new Signature(Buffer32.ZERO, Buffer32.ZERO, 0, true); + } + + to0xString(): `0x${string}` { + return `0x${this.r.toString()}${this.s.toString()}${this.v.toString(16)}`; + } + + /** + * A seperate method exists for this as when signing locally with viem, as when + * parsing from viem, we can expect the v value to be a u8, rather than our + * default serialization of u32 + */ + static from0xString(sig: `0x${string}`): Signature { + const buf = Buffer.from(sig.slice(2), 'hex'); + const reader = BufferReader.asReader(buf); + + const r = reader.readObject(Buffer32); + const s = reader.readObject(Buffer32); + const v = reader.readUInt8(); + + const isEmpty = r.isZero() && s.isZero(); + + return new Signature(r, s, v, isEmpty); + } + + /** + * Return the signature with `0x${string}` encodings for r and s + */ + toViemSignature(): ViemSignature { + return { + r: this.r.to0xString(), + s: this.s.to0xString(), + v: this.v, + isEmpty: this.isEmpty, + }; + } +} diff --git a/yarn-project/circuit-types/src/tx/tx.ts b/yarn-project/circuit-types/src/tx/tx.ts index d401036956da..c15335287dfd 100644 --- a/yarn-project/circuit-types/src/tx/tx.ts +++ b/yarn-project/circuit-types/src/tx/tx.ts @@ -4,8 +4,8 @@ import { PrivateKernelTailCircuitPublicInputs, type PublicKernelCircuitPublicInputs, } from '@aztec/circuits.js'; +import { type Buffer32 } from '@aztec/foundation/buffer'; import { arraySerializedSizeOfNonEmpty } from '@aztec/foundation/collection'; -import { type BaseHashType } from '@aztec/foundation/hash'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { type GetUnencryptedLogsResponse } from '../logs/get_unencrypted_logs_response.js'; @@ -64,7 +64,7 @@ export class Tx extends Gossipable { } // Gossipable method - override p2pMessageIdentifier(): BaseHashType { + override p2pMessageIdentifier(): Buffer32 { return this.getTxHash(); } diff --git a/yarn-project/circuit-types/src/tx/tx_hash.ts b/yarn-project/circuit-types/src/tx/tx_hash.ts index 2530ee5ccf9f..921903d75a00 100644 --- a/yarn-project/circuit-types/src/tx/tx_hash.ts +++ b/yarn-project/circuit-types/src/tx/tx_hash.ts @@ -1,9 +1,9 @@ -import { BaseHashType } from '@aztec/foundation/hash'; +import { Buffer32 } from '@aztec/foundation/buffer'; /** * A class representing hash of Aztec transaction. */ -export class TxHash extends BaseHashType { +export class TxHash extends Buffer32 { constructor( /** * The buffer containing the hash. diff --git a/yarn-project/end-to-end/src/e2e_p2p_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p_network.test.ts index fb626830295b..fed7d9ce6c19 100644 --- a/yarn-project/end-to-end/src/e2e_p2p_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p_network.test.ts @@ -1,7 +1,6 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; -import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; +import { type AztecNodeConfig, type AztecNodeService } from '@aztec/aztec-node'; import { - type AztecAddress, CompleteAddress, type DebugLogger, type DeployL1Contracts, @@ -10,20 +9,25 @@ import { GrumpkinScalar, type SentTx, TxStatus, - createDebugLogger, sleep, } from '@aztec/aztec.js'; import { IS_DEV_NET } from '@aztec/circuits.js'; import { RollupAbi } from '@aztec/l1-artifacts'; -import { type BootnodeConfig, BootstrapNode, createLibP2PPeerId } from '@aztec/p2p'; +import { type BootstrapNode } from '@aztec/p2p'; import { type PXEService, createPXEService, getPXEServiceConfig as getRpcConfig } from '@aztec/pxe'; -import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; import fs from 'fs'; import { getContract } from 'viem'; import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { MNEMONIC } from './fixtures/fixtures.js'; +import { + type NodeContext, + createBootstrapNode, + createNode, + createNodes, + generatePeerIdPrivateKeys, +} from './fixtures/setup_p2p_test.js'; import { setup } from './fixtures/utils.js'; // Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds @@ -32,19 +36,7 @@ const NUM_TXS_PER_BLOCK = 4; const NUM_TXS_PER_NODE = 2; const BOOT_NODE_UDP_PORT = 40400; -interface NodeContext { - node: AztecNodeService; - pxeService: PXEService; - txs: SentTx[]; - account: AztecAddress; -} - -const PEER_ID_PRIVATE_KEYS = [ - '0802122002f651fd8653925529e3baccb8489b3af4d7d9db440cbf5df4a63ff04ea69683', - '08021220c3bd886df5fe5b33376096ad0dab3d2dc86ed2a361d5fde70f24d979dc73da41', - '080212206b6567ac759db5434e79495ec7458e5e93fe479a5b80713446e0bce5439a5655', - '08021220366453668099bdacdf08fab476ee1fced6bf00ddc1223d6c2ee626e7236fb526', -]; +const PEER_ID_PRIVATE_KEYS = generatePeerIdPrivateKeys(NUM_NODES); describe('e2e_p2p_network', () => { let config: AztecNodeConfig; @@ -74,12 +66,8 @@ describe('e2e_p2p_network', () => { await rollup.write.addValidator([account.address]); logger.info(`Adding sequencer ${account.address}`); } else { - // @todo Should be updated when we have attestations to add all the sequencers - // Since it is currently a mess because sequencer selection needs attestations for - // validity, but we currently have no way to collect them. - // When attestations works, add all 4, and lets ROLL! - - for (let i = 0; i < 1; i++) { + // Add all nodes as validators - they will all sign attestations of each other's proposals + for (let i = 0; i < NUM_NODES; i++) { const hdAccount = mnemonicToAccount(MNEMONIC, { addressIndex: i + 1 }); const publisherPrivKey = Buffer.from(hdAccount.getHdKey().privateKey!); const account = privateKeyToAccount(`0x${publisherPrivKey!.toString('hex')}`); @@ -95,8 +83,11 @@ describe('e2e_p2p_network', () => { const timestamp = (await cheatCodes.timestamp()) + Number(timeToJump); await cheatCodes.warp(timestamp); - bootstrapNode = await createBootstrapNode(); + bootstrapNode = await createBootstrapNode(BOOT_NODE_UDP_PORT); bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt(); + + config.minTxsPerBlock = NUM_TXS_PER_BLOCK; + config.maxTxsPerBlock = NUM_TXS_PER_BLOCK; }); afterEach(() => teardown()); @@ -117,11 +108,14 @@ describe('e2e_p2p_network', () => { // should be set so that the only way for rollups to be built // is if the txs are successfully gossiped around the nodes. const contexts: NodeContext[] = []; - const nodes: AztecNodeService[] = []; - for (let i = 0; i < NUM_NODES; i++) { - const node = await createNode(i + 1 + BOOT_NODE_UDP_PORT, bootstrapNodeEnr, i); - nodes.push(node); - } + const nodes: AztecNodeService[] = await createNodes( + config, + PEER_ID_PRIVATE_KEYS, + bootstrapNodeEnr, + NUM_NODES, + BOOT_NODE_UDP_PORT, + /*activate validators=*/ !IS_DEV_NET, + ); // wait a bit for peers to discover each other await sleep(2000); @@ -151,11 +145,14 @@ describe('e2e_p2p_network', () => { it('should re-discover stored peers without bootstrap node', async () => { const contexts: NodeContext[] = []; - const nodes: AztecNodeService[] = []; - for (let i = 0; i < NUM_NODES; i++) { - const node = await createNode(i + 1 + BOOT_NODE_UDP_PORT, bootstrapNodeEnr, i, `./data-${i}`); - nodes.push(node); - } + const nodes: AztecNodeService[] = await createNodes( + config, + PEER_ID_PRIVATE_KEYS, + bootstrapNodeEnr, + NUM_NODES, + BOOT_NODE_UDP_PORT, + ); + // wait a bit for peers to discover each other await sleep(3000); @@ -171,11 +168,18 @@ describe('e2e_p2p_network', () => { await node.stop(); logger.info(`Node ${i} stopped`); await sleep(1200); - const newNode = await createNode(i + 1 + BOOT_NODE_UDP_PORT, undefined, i, `./data-${i}`); + // TODO: make a restart nodes function + const newNode = await createNode( + config, + PEER_ID_PRIVATE_KEYS[i], + i + 1 + BOOT_NODE_UDP_PORT, + undefined, + i, + /*validators*/ false, + `./data-${i}`, + ); logger.info(`Node ${i} restarted`); newNodes.push(newNode); - // const context = await createPXEServiceAndSubmitTransactions(node, NUM_TXS_PER_NODE); - // contexts.push(context); } // wait a bit for peers to discover each other @@ -204,57 +208,6 @@ describe('e2e_p2p_network', () => { } }); - const createBootstrapNode = async () => { - const peerId = await createLibP2PPeerId(); - const bootstrapNode = new BootstrapNode(); - const config: BootnodeConfig = { - udpListenAddress: `0.0.0.0:${BOOT_NODE_UDP_PORT}`, - udpAnnounceAddress: `127.0.0.1:${BOOT_NODE_UDP_PORT}`, - peerIdPrivateKey: Buffer.from(peerId.privateKey!).toString('hex'), - minPeerCount: 10, - maxPeerCount: 100, - }; - await bootstrapNode.start(config); - - return bootstrapNode; - }; - - // creates a P2P enabled instance of Aztec Node Service - const createNode = async ( - tcpListenPort: number, - bootstrapNode: string | undefined, - publisherAddressIndex: number, - dataDirectory?: string, - ) => { - // We use different L1 publisher accounts in order to avoid duplicate tx nonces. We start from - // publisherAddressIndex + 1 because index 0 was already used during test environment setup. - const hdAccount = mnemonicToAccount(MNEMONIC, { addressIndex: publisherAddressIndex + 1 }); - const publisherPrivKey = Buffer.from(hdAccount.getHdKey().privateKey!); - config.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; - - const newConfig: AztecNodeConfig = { - ...config, - peerIdPrivateKey: PEER_ID_PRIVATE_KEYS[publisherAddressIndex], - udpListenAddress: `0.0.0.0:${tcpListenPort}`, - tcpListenAddress: `0.0.0.0:${tcpListenPort}`, - tcpAnnounceAddress: `127.0.0.1:${tcpListenPort}`, - udpAnnounceAddress: `127.0.0.1:${tcpListenPort}`, - minTxsPerBlock: NUM_TXS_PER_BLOCK, - maxTxsPerBlock: NUM_TXS_PER_BLOCK, - p2pEnabled: true, - blockCheckIntervalMS: 1000, - l2QueueSize: 1, - transactionProtocol: '', - dataDirectory, - bootstrapNodes: bootstrapNode ? [bootstrapNode] : [], - }; - return await AztecNodeService.createAndSync( - newConfig, - new NoopTelemetryClient(), - createDebugLogger(`aztec:node-${tcpListenPort}`), - ); - }; - // creates an instance of the PXE and submit a given number of transactions to it. const createPXEServiceAndSubmitTransactions = async ( node: AztecNodeService, diff --git a/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts b/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts new file mode 100644 index 000000000000..57fb87f171fd --- /dev/null +++ b/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts @@ -0,0 +1,111 @@ +/** + * Test fixtures and utilities to set up and run a test using multiple validators + */ +import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; +import { type SentTx, createDebugLogger } from '@aztec/aztec.js'; +import { type AztecAddress } from '@aztec/circuits.js'; +import { type BootnodeConfig, BootstrapNode, createLibP2PPeerId } from '@aztec/p2p'; +import { type PXEService } from '@aztec/pxe'; +import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; + +import { generatePrivateKey } from 'viem/accounts'; + +import { getPrivateKeyFromIndex } from './utils.js'; + +export interface NodeContext { + node: AztecNodeService; + pxeService: PXEService; + txs: SentTx[]; + account: AztecAddress; +} + +export function generatePeerIdPrivateKeys(numberOfPeers: number): string[] { + const peerIdPrivateKeys = []; + for (let i = 0; i < numberOfPeers; i++) { + // magic number is multiaddr prefix: https://multiformats.io/multiaddr/ + peerIdPrivateKeys.push('08021220' + generatePrivateKey().substr(2, 66)); + } + return peerIdPrivateKeys; +} + +export async function createNodes( + config: AztecNodeConfig, + peerIdPrivateKeys: string[], + bootstrapNodeEnr: string, + numNodes: number, + bootNodePort: number, + activateValidators: boolean = false, +): Promise { + const nodes = []; + for (let i = 0; i < numNodes; i++) { + const node = await createNode( + config, + peerIdPrivateKeys[i], + i + 1 + bootNodePort, + bootstrapNodeEnr, + i, + activateValidators, + ); + nodes.push(node); + } + return nodes; +} + +// creates a P2P enabled instance of Aztec Node Service +export async function createNode( + config: AztecNodeConfig, + peerIdPrivateKey: string, + tcpListenPort: number, + bootstrapNode: string | undefined, + publisherAddressIndex: number, + activateValidators: boolean = false, + dataDirectory?: string, +) { + // We use different L1 publisher accounts in order to avoid duplicate tx nonces. We start from + // publisherAddressIndex + 1 because index 0 was already used during test environment setup. + const publisherPrivKey = getPrivateKeyFromIndex(publisherAddressIndex + 1); + config.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; + + if (activateValidators) { + const validatorPrivKey = getPrivateKeyFromIndex(1 + publisherAddressIndex); + config.validatorPrivateKey = `0x${validatorPrivKey!.toString('hex')}`; + config.disableValidator = false; + } + + const newConfig: AztecNodeConfig = { + ...config, + peerIdPrivateKey: peerIdPrivateKey, + udpListenAddress: `0.0.0.0:${tcpListenPort}`, + tcpListenAddress: `0.0.0.0:${tcpListenPort}`, + tcpAnnounceAddress: `127.0.0.1:${tcpListenPort}`, + udpAnnounceAddress: `127.0.0.1:${tcpListenPort}`, + minTxsPerBlock: config.minTxsPerBlock, + maxTxsPerBlock: config.maxTxsPerBlock, + p2pEnabled: true, + blockCheckIntervalMS: 1000, + l2QueueSize: 1, + transactionProtocol: '', + dataDirectory, + bootstrapNodes: bootstrapNode ? [bootstrapNode] : [], + }; + return await AztecNodeService.createAndSync( + newConfig, + new NoopTelemetryClient(), + createDebugLogger(`aztec:node-${tcpListenPort}`), + ); +} + +export async function createBootstrapNode(port: number) { + const peerId = await createLibP2PPeerId(); + const bootstrapNode = new BootstrapNode(); + const config: BootnodeConfig = { + udpListenAddress: `0.0.0.0:${port}`, + udpAnnounceAddress: `127.0.0.1:${port}`, + peerIdPrivateKey: Buffer.from(peerId.privateKey!).toString('hex'), + minPeerCount: 10, + maxPeerCount: 100, + }; + await bootstrapNode.start(config); + + return bootstrapNode; +} diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 9c9e6ac7cb3d..8f4a9f65acaf 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -33,7 +33,7 @@ import { MNEMONIC } from './fixtures.js'; import { getACVMConfig } from './get_acvm_config.js'; import { getBBConfig } from './get_bb_config.js'; import { setupL1Contracts } from './setup_l1_contracts.js'; -import { deployCanonicalAuthRegistry, deployCanonicalKeyRegistry } from './utils.js'; +import { deployCanonicalAuthRegistry, deployCanonicalKeyRegistry, getPrivateKeyFromIndex } from './utils.js'; export type SubsystemsContext = { anvil: Anvil; @@ -255,11 +255,16 @@ async function setupFromFresh( // Deploy our L1 contracts. logger.verbose('Deploying L1 contracts...'); - const hdAccount = mnemonicToAccount(MNEMONIC); - const privKeyRaw = hdAccount.getHdKey().privateKey; - const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); - const deployL1ContractsValues = await setupL1Contracts(aztecNodeConfig.l1RpcUrl, hdAccount, logger); + const hdAccount = mnemonicToAccount(MNEMONIC, { accountIndex: 0 }); + const publisherPrivKeyRaw = hdAccount.getHdKey().privateKey; + const publisherPrivKey = publisherPrivKeyRaw === null ? null : Buffer.from(publisherPrivKeyRaw); + + const validatorPrivKey = getPrivateKeyFromIndex(1); + aztecNodeConfig.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; + aztecNodeConfig.validatorPrivateKey = `0x${validatorPrivKey!.toString('hex')}`; + + const deployL1ContractsValues = await setupL1Contracts(aztecNodeConfig.l1RpcUrl, hdAccount, logger); aztecNodeConfig.l1Contracts = deployL1ContractsValues.l1ContractAddresses; aztecNodeConfig.l1PublishRetryIntervalMS = 100; diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 015222b0d3d0..0161ff2c2aa9 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -101,6 +101,12 @@ const getAztecUrl = () => { return PXE_URL; }; +export const getPrivateKeyFromIndex = (index: number): Buffer | null => { + const hdAccount = mnemonicToAccount(MNEMONIC, { addressIndex: index }); + const privKeyRaw = hdAccount.getHdKey().privateKey; + return privKeyRaw === null ? null : Buffer.from(privKeyRaw); +}; + export const setupL1Contracts = async ( l1RpcUrl: string, account: HDAccount | PrivateKeyAccount, @@ -319,6 +325,7 @@ export async function setup( opts: SetupOptions = {}, pxeOpts: Partial = {}, enableGas = false, + enableValidators = false, ): Promise { const config = { ...getConfigEnvVars(), ...opts }; const logger = getLogger(); @@ -349,21 +356,28 @@ export async function setup( await ethCheatCodes.loadChainState(opts.stateLoad); } - const hdAccount = mnemonicToAccount(MNEMONIC); - const privKeyRaw = hdAccount.getHdKey().privateKey; - const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); + const publisherHdAccount = mnemonicToAccount(MNEMONIC, { addressIndex: 0 }); + const publisherPrivKeyRaw = publisherHdAccount.getHdKey().privateKey; + const publisherPrivKey = publisherPrivKeyRaw === null ? null : Buffer.from(publisherPrivKeyRaw); if (PXE_URL) { // we are setting up against a remote environment, l1 contracts are assumed to already be deployed - return await setupWithRemoteEnvironment(hdAccount, config, logger, numberOfAccounts, enableGas); + return await setupWithRemoteEnvironment(publisherHdAccount, config, logger, numberOfAccounts, enableGas); } const deployL1ContractsValues = - opts.deployL1ContractsValues ?? (await setupL1Contracts(config.l1RpcUrl, hdAccount, logger)); + opts.deployL1ContractsValues ?? (await setupL1Contracts(config.l1RpcUrl, publisherHdAccount, logger)); config.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; config.l1Contracts = deployL1ContractsValues.l1ContractAddresses; + // Run the test with validators enabled + if (enableValidators) { + const validatorPrivKey = getPrivateKeyFromIndex(1); + config.validatorPrivateKey = `0x${validatorPrivKey!.toString('hex')}`; + } + config.disableValidator = !enableValidators; + logger.verbose('Creating and synching an aztec node...'); const acvmConfig = await getACVMConfig(logger); @@ -378,6 +392,7 @@ export async function setup( config.bbWorkingDirectory = bbConfig.bbWorkingDirectory; } config.l1PublishRetryIntervalMS = 100; + const aztecNode = await AztecNodeService.createAndSync(config, telemetry); const sequencer = aztecNode.getSequencer(); diff --git a/yarn-project/foundation/package.json b/yarn-project/foundation/package.json index cc7b6512aeff..f14bc84c6e1a 100644 --- a/yarn-project/foundation/package.json +++ b/yarn-project/foundation/package.json @@ -19,7 +19,7 @@ "./eth-address": "./dest/eth-address/index.js", "./queue": "./dest/queue/index.js", "./fs": "./dest/fs/index.js", - "./hash": "./dest/hash/index.js", + "./buffer": "./dest/buffer/index.js", "./json-rpc": "./dest/json-rpc/index.js", "./json-rpc/server": "./dest/json-rpc/server/index.js", "./json-rpc/client": "./dest/json-rpc/client/index.js", diff --git a/yarn-project/foundation/src/hash/index.ts b/yarn-project/foundation/src/buffer/buffer32.ts similarity index 57% rename from yarn-project/foundation/src/hash/index.ts rename to yarn-project/foundation/src/buffer/buffer32.ts index e6071e82cf53..399cdb009e36 100644 --- a/yarn-project/foundation/src/hash/index.ts +++ b/yarn-project/foundation/src/buffer/buffer32.ts @@ -1,21 +1,20 @@ import { randomBytes } from '@aztec/foundation/crypto'; +import { type Fr } from '@aztec/foundation/fields'; import { BufferReader, deserializeBigInt, serializeBigInt } from '@aztec/foundation/serialize'; -import { type Fr } from '../fields/fields.js'; - /** - * A class representing a hash. + * A class representing a 32 byte Buffer. */ -export class BaseHashType { +export class Buffer32 { /** * The size of the hash in bytes. */ public static SIZE = 32; /** - * HashType with value zero. + * Buffer32 with value zero. */ - public static ZERO = new BaseHashType(Buffer.alloc(BaseHashType.SIZE)); + public static ZERO = new Buffer32(Buffer.alloc(Buffer32.SIZE)); constructor( /** @@ -23,8 +22,8 @@ export class BaseHashType { */ public buffer: Buffer, ) { - if (buffer.length !== BaseHashType.SIZE) { - throw new Error(`Expected buffer to have length ${BaseHashType.SIZE} but was ${buffer.length}`); + if (buffer.length !== Buffer32.SIZE) { + throw new Error(`Expected buffer to have length ${Buffer32.SIZE} but was ${buffer.length}`); } } @@ -37,13 +36,13 @@ export class BaseHashType { } /** - * Creates a HashType from a buffer. + * Creates a Buffer32 from a buffer. * @param buffer - The buffer to create from. - * @returns A new HashType object. + * @returns A new Buffer32 object. */ public static fromBuffer(buffer: Buffer | BufferReader) { const reader = BufferReader.asReader(buffer); - return new BaseHashType(reader.readBytes(BaseHashType.SIZE)); + return new Buffer32(reader.readBytes(Buffer32.SIZE)); } /** @@ -51,7 +50,7 @@ export class BaseHashType { * @param hash - A hash to compare with. * @returns True if the hashes are equal, false otherwise. */ - public equals(hash: BaseHashType): boolean { + public equals(hash: Buffer32): boolean { return this.buffer.equals(hash.buffer); } @@ -71,53 +70,57 @@ export class BaseHashType { return this.buffer.toString('hex'); } + public to0xString(): `0x${string}` { + return `0x${this.buffer.toString('hex')}`; + } + /** * Convert this hash to a big int. * @returns The big int. */ public toBigInt() { - return deserializeBigInt(this.buffer, 0, BaseHashType.SIZE).elem; + return deserializeBigInt(this.buffer, 0, Buffer32.SIZE).elem; } /** - * Creates a tx hash from a bigint. + * Creates a Buffer32 from a bigint. * @param hash - The tx hash as a big int. - * @returns The HashType. + * @returns The Buffer32. */ public static fromBigInt(hash: bigint) { - return new BaseHashType(serializeBigInt(hash, BaseHashType.SIZE)); + return new Buffer32(serializeBigInt(hash, Buffer32.SIZE)); } public static fromField(hash: Fr) { - return new BaseHashType(serializeBigInt(hash.toBigInt())); + return new Buffer32(serializeBigInt(hash.toBigInt())); } /** * Converts this hash from a buffer of 28 bytes. * Verifies the input is 28 bytes. * @param buffer - The 28 byte buffer to construct from. - * @returns A HashType created from the input buffer with 4 bytes 0 padding at the front. + * @returns A Buffer32 created from the input buffer with 4 bytes 0 padding at the front. */ public static fromBuffer28(buffer: Buffer) { if (buffer.length != 28) { - throw new Error(`Expected HashType input buffer to be 28 bytes`); + throw new Error(`Expected Buffer32 input buffer to be 28 bytes`); } const padded = Buffer.concat([Buffer.alloc(this.SIZE - 28), buffer]); - return new BaseHashType(padded); + return new Buffer32(padded); } /** - * Converts a string into a HashType object. + * Converts a string into a Buffer32 object. * @param str - The TX hash in string format. - * @returns A new HashType object. + * @returns A new Buffer32 object. */ - public static fromString(str: string): BaseHashType { - return new BaseHashType(Buffer.from(str, 'hex')); + public static fromString(str: string): Buffer32 { + return new Buffer32(Buffer.from(str, 'hex')); } /** - * Generates a random HashType. - * @returns A new HashType object. + * Generates a random Buffer32. + * @returns A new Buffer32 object. */ - public static random(): BaseHashType { - return new BaseHashType(Buffer.from(randomBytes(BaseHashType.SIZE))); + public static random(): Buffer32 { + return new Buffer32(Buffer.from(randomBytes(Buffer32.SIZE))); } } diff --git a/yarn-project/foundation/src/buffer/index.ts b/yarn-project/foundation/src/buffer/index.ts new file mode 100644 index 000000000000..f4181b7b8e46 --- /dev/null +++ b/yarn-project/foundation/src/buffer/index.ts @@ -0,0 +1 @@ +export * from './buffer32.js'; diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index d4383b4b64b1..cc45692f3e4e 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -95,4 +95,6 @@ export type EnvVar = | 'BB_BINARY_PATH' | 'BB_WORKING_DIRECTORY' | 'BB_SKIP_CLEANUP' - | 'PXE_PROVER_ENABLED'; + | 'PXE_PROVER_ENABLED' + | 'VALIDATOR_PRIVATE_KEY' + | 'VALIDATOR_DISABLED'; diff --git a/yarn-project/foundation/src/index.ts b/yarn-project/foundation/src/index.ts index a422558dd666..7a899eba5200 100644 --- a/yarn-project/foundation/src/index.ts +++ b/yarn-project/foundation/src/index.ts @@ -29,4 +29,4 @@ export * as wasm from './wasm/index.js'; export * as worker from './worker/index.js'; export * as testing from './testing/index.js'; export * as config from './config/index.js'; -export * as hash from './hash/index.js'; +export * as buffer from './buffer/index.js'; diff --git a/yarn-project/ivc-integration/tsconfig.json b/yarn-project/ivc-integration/tsconfig.json index b06f6579555e..622822f4e169 100644 --- a/yarn-project/ivc-integration/tsconfig.json +++ b/yarn-project/ivc-integration/tsconfig.json @@ -18,7 +18,7 @@ }, { "path": "../bb-prover" - }, + } ], "include": ["src", "artifacts/*.d.json.ts", "artifacts/**/*.d.json.ts"] } diff --git a/yarn-project/p2p/package.json b/yarn-project/p2p/package.json index b1f146e1356c..5d754eea5522 100644 --- a/yarn-project/p2p/package.json +++ b/yarn-project/p2p/package.json @@ -95,7 +95,8 @@ "jest-mock-extended": "^3.0.4", "ts-node": "^10.9.1", "typescript": "^5.0.4", - "uint8arrays": "^5.0.3" + "uint8arrays": "^5.0.3", + "viem": "^2.7.15" }, "files": [ "dest", diff --git a/yarn-project/p2p/src/attestation_pool/attestation_pool.ts b/yarn-project/p2p/src/attestation_pool/attestation_pool.ts new file mode 100644 index 000000000000..a4fcb993c6ec --- /dev/null +++ b/yarn-project/p2p/src/attestation_pool/attestation_pool.ts @@ -0,0 +1,42 @@ +import { type BlockAttestation } from '@aztec/circuit-types'; + +/** + * An Attestation Pool contains attestations collected by a validator + * + * Attestations that are observed via the p2p network are stored for requests + * from the validator to produce a block, or to serve to other peers. + */ +export interface AttestationPool { + /** + * AddAttestation + * + * @param attestations - Attestations to add into the pool + */ + addAttestations(attestations: BlockAttestation[]): Promise; + + /** + * DeleteAttestation + * + * @param attestations - Attestations to remove from the pool + */ + deleteAttestations(attestations: BlockAttestation[]): Promise; + + /** + * Delete Attestations for slot + * + * Removes all attestations associated with a slot + * + * @param slot - The slot to delete. + */ + deleteAttestationsForSlot(slot: bigint): Promise; + + /** + * Get Attestations for slot + * + * Retrieve all of the attestations observed pertaining to a given slot + * + * @param slot - The slot to query + * @return BlockAttestations + */ + getAttestationsForSlot(slot: bigint): Promise; +} diff --git a/yarn-project/p2p/src/attestation_pool/index.ts b/yarn-project/p2p/src/attestation_pool/index.ts new file mode 100644 index 000000000000..f4878b9ca0e8 --- /dev/null +++ b/yarn-project/p2p/src/attestation_pool/index.ts @@ -0,0 +1,2 @@ +export * from './attestation_pool.js'; +export * from './memory_attestation_pool.js'; diff --git a/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.test.ts b/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.test.ts new file mode 100644 index 000000000000..37e3195c3099 --- /dev/null +++ b/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.test.ts @@ -0,0 +1,66 @@ +import { type PrivateKeyAccount } from 'viem'; + +import { InMemoryAttestationPool } from './memory_attestation_pool.js'; +import { generateAccount, mockAttestation } from './mocks.js'; + +const NUMBER_OF_SIGNERS_PER_TEST = 4; + +describe('MemoryAttestationPool', () => { + let ap: InMemoryAttestationPool; + let signers: PrivateKeyAccount[]; + + beforeEach(() => { + ap = new InMemoryAttestationPool(); + signers = Array.from({ length: NUMBER_OF_SIGNERS_PER_TEST }, generateAccount); + }); + + it('should add attestation to pool', async () => { + const slotNumber = 420; + const attestations = await Promise.all(signers.map(signer => mockAttestation(signer, slotNumber))); + + await ap.addAttestations(attestations); + + const retreivedAttestations = await ap.getAttestationsForSlot(BigInt(slotNumber)); + + expect(retreivedAttestations.length).toBe(NUMBER_OF_SIGNERS_PER_TEST); + expect(retreivedAttestations).toEqual(attestations); + + // Delete by slot + await ap.deleteAttestationsForSlot(BigInt(slotNumber)); + + const retreivedAttestationsAfterDelete = await ap.getAttestationsForSlot(BigInt(slotNumber)); + expect(retreivedAttestationsAfterDelete.length).toBe(0); + }); + + it('Should store attestations by differing slot', async () => { + const slotNumbers = [1, 2, 3, 4]; + const attestations = await Promise.all(signers.map((signer, i) => mockAttestation(signer, slotNumbers[i]))); + + await ap.addAttestations(attestations); + + for (const attestation of attestations) { + const slot = attestation.header.globalVariables.slotNumber; + + const retreivedAttestations = await ap.getAttestationsForSlot(slot.toBigInt()); + expect(retreivedAttestations.length).toBe(1); + expect(retreivedAttestations[0]).toEqual(attestation); + expect(retreivedAttestations[0].header.globalVariables.slotNumber).toEqual(slot); + } + }); + + it('Should delete attestations', async () => { + const slotNumber = 420; + const attestations = await Promise.all(signers.map(signer => mockAttestation(signer, slotNumber))); + + await ap.addAttestations(attestations); + + const retreivedAttestations = await ap.getAttestationsForSlot(BigInt(slotNumber)); + expect(retreivedAttestations.length).toBe(NUMBER_OF_SIGNERS_PER_TEST); + expect(retreivedAttestations).toEqual(attestations); + + await ap.deleteAttestations(attestations); + + const gottenAfterDelete = await ap.getAttestationsForSlot(BigInt(slotNumber)); + expect(gottenAfterDelete.length).toBe(0); + }); +}); diff --git a/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.ts b/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.ts new file mode 100644 index 000000000000..524a399aece4 --- /dev/null +++ b/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.ts @@ -0,0 +1,70 @@ +import { type BlockAttestation } from '@aztec/circuit-types'; +import { createDebugLogger } from '@aztec/foundation/log'; + +import { type AttestationPool } from './attestation_pool.js'; + +export class InMemoryAttestationPool implements AttestationPool { + private attestations: Map>; + + constructor(private log = createDebugLogger('aztec:attestation_pool')) { + this.attestations = new Map(); + } + + public getAttestationsForSlot(slot: bigint): Promise { + const slotAttestationMap = this.attestations.get(slot); + if (slotAttestationMap) { + return Promise.resolve(Array.from(slotAttestationMap.values())); + } else { + return Promise.resolve([]); + } + } + + public async addAttestations(attestations: BlockAttestation[]): Promise { + for (const attestation of attestations) { + // Perf: order and group by slot before insertion + const slotNumber = attestation.header.globalVariables.slotNumber; + + const address = await attestation.getSender(); + + const slotAttestationMap = getSlotOrDefault(this.attestations, slotNumber.toBigInt()); + slotAttestationMap.set(address.toString(), attestation); + + this.log.verbose(`Added attestation for slot ${slotNumber} from ${address}`); + } + } + + public deleteAttestationsForSlot(slot: bigint): Promise { + // TODO(md): check if this will free the memory of the inner hash map + this.attestations.delete(slot); + this.log.verbose(`Removed attestation for slot ${slot}`); + return Promise.resolve(); + } + + public async deleteAttestations(attestations: BlockAttestation[]): Promise { + for (const attestation of attestations) { + const slotNumber = attestation.header.globalVariables.slotNumber; + const slotAttestationMap = this.attestations.get(slotNumber.toBigInt()); + if (slotAttestationMap) { + const address = await attestation.getSender(); + slotAttestationMap.delete(address.toString()); + this.log.verbose(`Deleted attestation for slot ${slotNumber} from ${address}`); + } + } + return Promise.resolve(); + } +} + +/** + * Get Slot or Default + * + * Fetch the slot mapping, if it does not exist, then create a mapping and return it + */ +function getSlotOrDefault( + map: Map>, + slot: bigint, +): Map { + if (!map.has(slot)) { + map.set(slot, new Map()); + } + return map.get(slot)!; +} diff --git a/yarn-project/p2p/src/attestation_pool/mocks.ts b/yarn-project/p2p/src/attestation_pool/mocks.ts new file mode 100644 index 000000000000..22e5da70a940 --- /dev/null +++ b/yarn-project/p2p/src/attestation_pool/mocks.ts @@ -0,0 +1,33 @@ +import { BlockAttestation, Signature } from '@aztec/circuit-types'; +import { makeHeader } from '@aztec/circuits.js/testing'; +import { Fr } from '@aztec/foundation/fields'; + +import { type PrivateKeyAccount } from 'viem'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; + +/** Generate Account + * + * Create a random signer + * @returns A random viem signer + */ +export const generateAccount = () => { + const privateKey = generatePrivateKey(); + return privateKeyToAccount(privateKey); +}; + +/** Mock Attestation + * + * @param signer A viem signer to create a signature + * @param slot The slot number the attestation is for + * @returns A Block Attestation + */ +export const mockAttestation = async (signer: PrivateKeyAccount, slot: number = 0): Promise => { + // Use arbitrary numbers for all other than slot + const header = makeHeader(1, 2, slot); + const archive = Fr.random(); + const message = archive.toString(); + const sigString = await signer.signMessage({ message }); + + const signature = Signature.from0xString(sigString); + return new BlockAttestation(header, archive, signature); +}; diff --git a/yarn-project/p2p/src/client/index.ts b/yarn-project/p2p/src/client/index.ts index 86b5e2a86727..56f6618a7623 100644 --- a/yarn-project/p2p/src/client/index.ts +++ b/yarn-project/p2p/src/client/index.ts @@ -1,6 +1,7 @@ import { type L2BlockSource } from '@aztec/circuit-types'; import { type AztecKVStore } from '@aztec/kv-store'; +import { type AttestationPool } from '../attestation_pool/attestation_pool.js'; import { P2PClient } from '../client/p2p_client.js'; import { type P2PConfig } from '../config.js'; import { DiscV5Service } from '../service/discV5_service.js'; @@ -15,6 +16,7 @@ export const createP2PClient = async ( config: P2PConfig, store: AztecKVStore, txPool: TxPool, + attestationsPool: AttestationPool, l2BlockSource: L2BlockSource, ) => { let p2pService; @@ -59,9 +61,9 @@ export const createP2PClient = async ( // Create peer discovery service const peerId = await createLibP2PPeerId(config.peerIdPrivateKey); const discoveryService = new DiscV5Service(peerId, config); - p2pService = await LibP2PService.new(config, discoveryService, peerId, txPool, store); + p2pService = await LibP2PService.new(config, discoveryService, peerId, txPool, attestationsPool, store); } else { p2pService = new DummyP2PService(); } - return new P2PClient(store, l2BlockSource, txPool, p2pService, config.keepProvenTxsInPoolFor); + return new P2PClient(store, l2BlockSource, txPool, attestationsPool, p2pService, config.keepProvenTxsInPoolFor); }; diff --git a/yarn-project/p2p/src/client/p2p_client.test.ts b/yarn-project/p2p/src/client/p2p_client.test.ts index d064264d4c33..7f58fa9734fd 100644 --- a/yarn-project/p2p/src/client/p2p_client.test.ts +++ b/yarn-project/p2p/src/client/p2p_client.test.ts @@ -5,6 +5,7 @@ import { openTmpStore } from '@aztec/kv-store/utils'; import { expect, jest } from '@jest/globals'; +import { type AttestationPool } from '../attestation_pool/attestation_pool.js'; import { type P2PService } from '../index.js'; import { type TxPool } from '../tx_pool/index.js'; import { MockBlockSource } from './mocks.js'; @@ -19,6 +20,7 @@ type Mockify = { describe('In-Memory P2P Client', () => { let txPool: Mockify; + let attestationPool: Mockify; let blockSource: MockBlockSource; let p2pService: Mockify; let kvStore: AztecKVStore; @@ -40,13 +42,21 @@ describe('In-Memory P2P Client', () => { p2pService = { start: jest.fn(), stop: jest.fn(), - propagateTx: jest.fn(), + propagate: jest.fn(), + registerBlockReceivedCallback: jest.fn(), + }; + + attestationPool = { + addAttestations: jest.fn(), + deleteAttestations: jest.fn(), + deleteAttestationsForSlot: jest.fn(), + getAttestationsForSlot: jest.fn().mockReturnValue(undefined), }; blockSource = new MockBlockSource(); kvStore = openTmpStore(); - client = new P2PClient(kvStore, blockSource, txPool, p2pService, 0); + client = new P2PClient(kvStore, blockSource, txPool, attestationPool, p2pService, 0); }); const advanceToProvenBlock = async (provenBlockNum: number) => { @@ -95,16 +105,16 @@ describe('In-Memory P2P Client', () => { txPool.getAllTxs.mockReturnValue([tx1, tx2]); await client.start(); - expect(p2pService.propagateTx).toHaveBeenCalledTimes(2); - expect(p2pService.propagateTx).toHaveBeenCalledWith(tx1); - expect(p2pService.propagateTx).toHaveBeenCalledWith(tx2); + expect(p2pService.propagate).toHaveBeenCalledTimes(2); + expect(p2pService.propagate).toHaveBeenCalledWith(tx1); + expect(p2pService.propagate).toHaveBeenCalledWith(tx2); }); it('restores the previous block number it was at', async () => { await client.start(); await client.stop(); - const client2 = new P2PClient(kvStore, blockSource, txPool, p2pService, 0); + const client2 = new P2PClient(kvStore, blockSource, txPool, attestationPool, p2pService, 0); expect(client2.getSyncedLatestBlockNum()).toEqual(client.getSyncedLatestBlockNum()); }); @@ -119,7 +129,7 @@ describe('In-Memory P2P Client', () => { }); it('deletes txs after waiting the set number of blocks', async () => { - client = new P2PClient(kvStore, blockSource, txPool, p2pService, 10); + client = new P2PClient(kvStore, blockSource, txPool, attestationPool, p2pService, 10); blockSource.setProvenBlockNumber(0); await client.start(); expect(txPool.deleteTxs).not.toHaveBeenCalled(); @@ -134,4 +144,6 @@ describe('In-Memory P2P Client', () => { expect(txPool.deleteTxs).toHaveBeenCalledTimes(10); await client.stop(); }); + + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7971): tests for attestation pool pruning }); diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index d7eab6941f55..11bc75b06e4c 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -1,8 +1,17 @@ -import { type L2Block, L2BlockDownloader, type L2BlockSource, type Tx, type TxHash } from '@aztec/circuit-types'; +import { + type BlockAttestation, + type BlockProposal, + type L2Block, + L2BlockDownloader, + type L2BlockSource, + type Tx, + type TxHash, +} from '@aztec/circuit-types'; import { INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js/constants'; import { createDebugLogger } from '@aztec/foundation/log'; import { type AztecKVStore, type AztecSingleton } from '@aztec/kv-store'; +import { type AttestationPool } from '../attestation_pool/attestation_pool.js'; import { getP2PConfigEnvVars } from '../config.js'; import type { P2PService } from '../service/service.js'; import { type TxPool } from '../tx_pool/index.js'; @@ -35,6 +44,31 @@ export interface P2PSyncState { * Interface of a P2P client. **/ export interface P2P { + /** + * Broadcasts a block proposal to other peers. + * + * @param proposal - the block proposal + */ + broadcastProposal(proposal: BlockProposal): void; + + /** + * Queries the Attestation pool for attestations for the given slot + * + * @param slot - the slot to query + * @returns BlockAttestations + */ + getAttestationsForSlot(slot: bigint): Promise; + + /** + * Registers a callback from the validator client that determines how to behave when + * foreign block proposals are received + * + * @param handler - A function taking a received block proposal and producing an attestation + */ + // REVIEW: https://github.com/AztecProtocol/aztec-packages/issues/7963 + // ^ This pattern is not my favorite (md) + registerBlockProposalHandler(handler: (block: BlockProposal) => Promise): void; + /** * Verifies the 'tx' and, if valid, adds it to local tx pool and forwards it to other peers. * @param tx - The transaction. @@ -130,6 +164,7 @@ export class P2PClient implements P2P { store: AztecKVStore, private l2BlockSource: L2BlockSource, private txPool: TxPool, + private attestationPool: AttestationPool, private p2pService: P2PService, private keepProvenTxsFor: number, private log = createDebugLogger('aztec:p2p'), @@ -220,6 +255,20 @@ export class P2PClient implements P2P { this.log.info('P2P client stopped.'); } + public broadcastProposal(proposal: BlockProposal): void { + return this.p2pService.propagate(proposal); + } + + public getAttestationsForSlot(slot: bigint): Promise { + return Promise.resolve(this.attestationPool.getAttestationsForSlot(slot)); + } + + // REVIEW: https://github.com/AztecProtocol/aztec-packages/issues/7963 + // ^ This pattern is not my favorite (md) + public registerBlockProposalHandler(handler: (block: BlockProposal) => Promise): void { + this.p2pService.registerBlockReceivedCallback(handler); + } + /** * Returns all transactions in the transaction pool. * @returns An array of Txs. @@ -263,7 +312,7 @@ export class P2PClient implements P2P { throw new Error('P2P client not ready'); } await this.txPool.addTxs([tx]); - this.p2pService.propagateTx(tx); + this.p2pService.propagate(tx); } /** @@ -425,7 +474,7 @@ export class P2PClient implements P2P { const txs = this.txPool.getAllTxs(); if (txs.length > 0) { this.log.debug(`Publishing ${txs.length} previously stored txs`); - await Promise.all(txs.map(tx => this.p2pService.propagateTx(tx))); + await Promise.all(txs.map(tx => this.p2pService.propagate(tx))); } } } diff --git a/yarn-project/p2p/src/index.ts b/yarn-project/p2p/src/index.ts index 4a04a398da3e..cf9356eef937 100644 --- a/yarn-project/p2p/src/index.ts +++ b/yarn-project/p2p/src/index.ts @@ -1,5 +1,6 @@ export * from './client/index.js'; export * from './config.js'; export * from './tx_pool/index.js'; +export * from './attestation_pool/index.js'; export * from './service/index.js'; export * from './bootstrap/bootstrap.js'; diff --git a/yarn-project/p2p/src/service/dummy_service.ts b/yarn-project/p2p/src/service/dummy_service.ts index aeeedb1f03d1..7f322c908b1e 100644 --- a/yarn-project/p2p/src/service/dummy_service.ts +++ b/yarn-project/p2p/src/service/dummy_service.ts @@ -1,4 +1,4 @@ -import type { Tx, TxHash } from '@aztec/circuit-types'; +import type { BlockAttestation, BlockProposal, Gossipable, TxHash } from '@aztec/circuit-types'; import type { PeerId } from '@libp2p/interface'; import EventEmitter from 'events'; @@ -26,16 +26,21 @@ export class DummyP2PService implements P2PService { } /** - * Called to have the given transaction propagated through the P2P network. - * @param _ - The transaction to be propagated. + * Called to have the given message propagated through the P2P network. + * @param _ - The message to be propagated. */ - public propagateTx(_: Tx) {} + public propagate(_: T) {} /** * Called upon receipt of settled transactions. * @param _ - The hashes of the settled transactions. */ public settledTxs(_: TxHash[]) {} + + /** + * Register a callback into the validator client for when a block proposal is received + */ + public registerBlockReceivedCallback(_: (block: BlockProposal) => Promise) {} } /** diff --git a/yarn-project/p2p/src/service/libp2p_service.ts b/yarn-project/p2p/src/service/libp2p_service.ts index 4570c8291f5b..5c153beaec0d 100644 --- a/yarn-project/p2p/src/service/libp2p_service.ts +++ b/yarn-project/p2p/src/service/libp2p_service.ts @@ -1,4 +1,12 @@ -import { type Gossipable, type RawGossipMessage, TopicType, TopicTypeMap, Tx } from '@aztec/circuit-types'; +import { + BlockAttestation, + BlockProposal, + type Gossipable, + type RawGossipMessage, + TopicType, + TopicTypeMap, + Tx, +} from '@aztec/circuit-types'; import { createDebugLogger } from '@aztec/foundation/log'; import { SerialQueue } from '@aztec/foundation/queue'; import { RunningPromise } from '@aztec/foundation/running-promise'; @@ -15,6 +23,7 @@ import { createFromJSON, createSecp256k1PeerId } from '@libp2p/peer-id-factory'; import { tcp } from '@libp2p/tcp'; import { type Libp2p, createLibp2p } from 'libp2p'; +import { type AttestationPool } from '../attestation_pool/attestation_pool.js'; import { type P2PConfig } from '../config.js'; import { type TxPool } from '../tx_pool/index.js'; import { convertToMultiaddr } from '../util.js'; @@ -50,14 +59,25 @@ export class LibP2PService implements P2PService { private jobQueue: SerialQueue = new SerialQueue(); private peerManager: PeerManager; private discoveryRunningPromise?: RunningPromise; + + private blockReceivedCallback: (block: BlockProposal) => Promise; + constructor( private config: P2PConfig, private node: PubSubLibp2p, private peerDiscoveryService: PeerDiscoveryService, private txPool: TxPool, + private attestationPool: AttestationPool, private logger = createDebugLogger('aztec:libp2p_service'), ) { this.peerManager = new PeerManager(node, peerDiscoveryService, config, logger); + + this.blockReceivedCallback = (block: BlockProposal): Promise => { + this.logger.verbose( + `[WARNING] handler not yet registered: Block received callback not set. Received block ${block.p2pMessageIdentifier()} from peer.`, + ); + return Promise.resolve(undefined); + }; } /** @@ -132,6 +152,7 @@ export class LibP2PService implements P2PService { peerDiscoveryService: PeerDiscoveryService, peerId: PeerId, txPool: TxPool, + attestationPool: AttestationPool, store: AztecKVStore, ) { const { tcpListenAddress, tcpAnnounceAddress, minPeerCount, maxPeerCount } = config; @@ -184,7 +205,12 @@ export class LibP2PService implements P2PService { }, }); - return new LibP2PService(config, node, peerDiscoveryService, txPool); + return new LibP2PService(config, node, peerDiscoveryService, txPool, attestationPool); + } + + public registerBlockReceivedCallback(callback: (block: BlockProposal) => Promise) { + this.blockReceivedCallback = callback; + this.logger.verbose('Block received callback registered'); } /** @@ -223,16 +249,52 @@ export class LibP2PService implements P2PService { const tx = Tx.fromBuffer(Buffer.from(message.data)); await this.processTxFromPeer(tx); } + if (message.topic === BlockAttestation.p2pTopic) { + const attestation = BlockAttestation.fromBuffer(Buffer.from(message.data)); + await this.processAttestationFromPeer(attestation); + } + if (message.topic == BlockProposal.p2pTopic) { + const block = BlockProposal.fromBuffer(Buffer.from(message.data)); + await this.processBlockFromPeer(block); + } return; } + /**Process Attestation From Peer + * When a proposal is received from a peer, we add it to the attestation pool, so it can be accessed by other services. + * + * @param attestation - The attestation to process. + */ + private async processAttestationFromPeer(attestation: BlockAttestation): Promise { + this.logger.verbose(`Received attestation ${attestation.p2pMessageIdentifier()} from external peer.`); + await this.attestationPool.addAttestations([attestation]); + } + + /**Process block from peer + * + * Pass the received block to the validator client + * + * @param block - The block to process. + */ + // REVIEW: callback pattern https://github.com/AztecProtocol/aztec-packages/issues/7963 + private async processBlockFromPeer(block: BlockProposal): Promise { + this.logger.verbose(`Received block ${block.p2pMessageIdentifier()} from external peer.`); + const attestation = await this.blockReceivedCallback(block); + + // TODO: fix up this pattern - the abstraction is not nice + // The attestation can be undefined if no handler is registered / the validator deems the block invalid + if (attestation != undefined) { + this.propagate(attestation); + } + } + /** - * Propagates the provided transaction to peers. - * @param tx - The transaction to propagate. + * Propagates provided message to peers. + * @param message - The message to propagate. */ - public propagateTx(tx: Tx): void { - void this.jobQueue.put(() => Promise.resolve(this.sendToPeers(tx))); + public propagate(message: T): void { + void this.jobQueue.put(() => Promise.resolve(this.sendToPeers(message))); } private async processTxFromPeer(tx: Tx): Promise { @@ -246,7 +308,7 @@ export class LibP2PService implements P2PService { const parent = message.constructor as typeof Gossipable; const identifier = message.p2pMessageIdentifier().toString(); - this.logger.verbose(`Sending tx ${identifier} to peers`); + this.logger.verbose(`Sending message ${identifier} to peers`); const recipientsNum = await this.publishToTopic(parent.p2pTopic, message.toBuffer()); this.logger.verbose(`Sent tx ${identifier} to ${recipientsNum} peers`); diff --git a/yarn-project/p2p/src/service/service.ts b/yarn-project/p2p/src/service/service.ts index f9933dd3b346..0980515f768d 100644 --- a/yarn-project/p2p/src/service/service.ts +++ b/yarn-project/p2p/src/service/service.ts @@ -1,4 +1,4 @@ -import type { Tx } from '@aztec/circuit-types'; +import type { BlockAttestation, BlockProposal, Gossipable } from '@aztec/circuit-types'; import type { ENR } from '@chainsafe/enr'; import type { PeerId } from '@libp2p/interface'; @@ -27,9 +27,12 @@ export interface P2PService { /** * Called to have the given transaction propagated through the P2P network. - * @param tx - The transaction to be propagated. + * @param message - The message to be propagated. */ - propagateTx(tx: Tx): void; + propagate(message: T): void; + + // Leaky abstraction: fix https://github.com/AztecProtocol/aztec-packages/issues/7963 + registerBlockReceivedCallback(callback: (block: BlockProposal) => Promise): void; } /** diff --git a/yarn-project/package.json b/yarn-project/package.json index a2dc51410b7c..f75791d97d40 100644 --- a/yarn-project/package.json +++ b/yarn-project/package.json @@ -24,6 +24,7 @@ "archiver", "aztec-faucet", "aztec-node", + "validator-client", "bb-prover", "bot", "builder", diff --git a/yarn-project/pxe/src/pxe_http/pxe_http_server.ts b/yarn-project/pxe/src/pxe_http/pxe_http_server.ts index 3a99d2b572a0..13239d9ba30b 100644 --- a/yarn-project/pxe/src/pxe_http/pxe_http_server.ts +++ b/yarn-project/pxe/src/pxe_http/pxe_http_server.ts @@ -21,9 +21,9 @@ import { import { FunctionSelector } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr, GrumpkinScalar, Point } from '@aztec/foundation/fields'; -import { BaseHashType } from '@aztec/foundation/hash'; import { JsonRpcServer, createNamespacedJsonRpcServer } from '@aztec/foundation/json-rpc/server'; import http from 'http'; @@ -42,7 +42,7 @@ export function createPXERpcServer(pxeService: PXE): JsonRpcServer { ExtendedUnencryptedL2Log, FunctionSelector, TxHash, - BaseHashType, + Buffer32, EthAddress, Point, Fr, diff --git a/yarn-project/sequencer-client/package.json b/yarn-project/sequencer-client/package.json index 0445a6ed59d8..a6d7d7f0fec1 100644 --- a/yarn-project/sequencer-client/package.json +++ b/yarn-project/sequencer-client/package.json @@ -39,6 +39,7 @@ "@aztec/simulator": "workspace:^", "@aztec/telemetry-client": "workspace:^", "@aztec/types": "workspace:^", + "@aztec/validator-client": "workspace:^", "@aztec/world-state": "workspace:^", "@noir-lang/acvm_js": "portal:../../noir/packages/acvm_js", "@noir-lang/types": "portal:../../noir/packages/types", diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index a3cc6060d56b..4852518d47bf 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -3,6 +3,7 @@ import { type P2P } from '@aztec/p2p'; import { PublicProcessorFactory, type SimulationProvider } from '@aztec/simulator'; import { type TelemetryClient } from '@aztec/telemetry-client'; import { type ContractDataSource } from '@aztec/types/contracts'; +import { type ValidatorClient } from '@aztec/validator-client'; import { type WorldStateSynchronizer } from '@aztec/world-state'; import { BlockBuilderFactory } from '../block_builder/index.js'; @@ -22,6 +23,7 @@ export class SequencerClient { * Initializes and starts a new instance. * @param config - Configuration for the sequencer, publisher, and L1 tx sender. * @param p2pClient - P2P client that provides the txs to be sequenced. + * @param validatorClient - Validator client performs attestation duties when rotating proposers. * @param worldStateSynchronizer - Provides access to world state. * @param contractDataSource - Provides access to contract bytecode for public executions. * @param l2BlockSource - Provides information about the previously published blocks. @@ -32,6 +34,7 @@ export class SequencerClient { */ public static async new( config: SequencerClientConfig, + validatorClient: ValidatorClient | undefined, // allowed to be undefined while we migrate p2pClient: P2P, worldStateSynchronizer: WorldStateSynchronizer, contractDataSource: ContractDataSource, @@ -53,6 +56,7 @@ export class SequencerClient { const sequencer = new Sequencer( publisher, + validatorClient, globalsBuilder, p2pClient, worldStateSynchronizer, diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 0efedfde0f06..58b71d9ea7c8 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -1,4 +1,4 @@ -import { type L2Block } from '@aztec/circuit-types'; +import { type L2Block, type Signature } from '@aztec/circuit-types'; import { type L1PublishBlockStats, type L1PublishProofStats } from '@aztec/circuit-types/stats'; import { type EthAddress, type Header, type Proof } from '@aztec/circuits.js'; import { type Fr } from '@aztec/foundation/fields'; @@ -42,26 +42,19 @@ export type MinimalTransactionReceipt = { logs: any[]; }; -/** - * @notice An attestation for the sequencing model. - * @todo This is not where it belongs. But I think we should do a bigger rewrite of some of - * this spaghetti. - */ -export type Attestation = { isEmpty: boolean; v: number; r: `0x${string}`; s: `0x${string}` }; - /** * Pushes txs to the L1 chain and waits for their completion. */ export interface L1PublisherTxSender { - /** Attests to the given archive root. */ - attest(archive: `0x${string}`): Promise; - /** Returns the EOA used for sending txs to L1. */ getSenderAddress(): Promise; /** Returns the address of the L2 proposer at the NEXT Ethereum block zero if anyone can submit. */ getProposerAtNextEthBlock(): Promise; + /** Returns the current epoch committee */ + getCurrentEpochCommittee(): Promise; + /** * Publishes tx effects to Availability Oracle. * @param encodedBody - Encoded block body. @@ -126,7 +119,7 @@ export type L1ProcessArgs = { /** L2 block body. */ body: Buffer; /** Attestations */ - attestations?: Attestation[]; + attestations?: Signature[]; }; /** Arguments to the submitProof method of the rollup contract */ @@ -163,10 +156,6 @@ export class L1Publisher implements L2BlockReceiver { this.metrics = new L1PublisherMetrics(client, 'L1Publisher'); } - public async attest(archive: `0x${string}`): Promise { - return await this.txSender.attest(archive); - } - public async senderAddress(): Promise { return await this.txSender.getSenderAddress(); } @@ -177,12 +166,16 @@ export class L1Publisher implements L2BlockReceiver { return submitter.isZero() || submitter.equals(sender); } + public getCurrentEpochCommittee(): Promise { + return this.txSender.getCurrentEpochCommittee(); + } + /** * Publishes L2 block on L1. * @param block - L2 block to publish. * @returns True once the tx has been confirmed and is successful, false on revert or interrupt, blocks otherwise. */ - public async processL2Block(block: L2Block, attestations?: Attestation[]): Promise { + public async processL2Block(block: L2Block, attestations?: Signature[]): Promise { const ctx = { blockNumber: block.number, blockHash: block.hash().toString() }; // TODO(#4148) Remove this block number check, it's here because we don't currently have proper genesis state on the contract const lastArchive = block.header.lastArchive.root.toBuffer(); diff --git a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts index 1928b38c6b34..e33aafbe79f7 100644 --- a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts +++ b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts @@ -16,14 +16,12 @@ import { getContract, hexToBytes, http, - parseSignature, } from 'viem'; import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'; import * as chains from 'viem/chains'; import { type TxSenderConfig } from './config.js'; import { - type Attestation, type L1PublisherTxSender, type L1SubmitProofArgs, type MinimalTransactionReceipt, @@ -75,20 +73,6 @@ export class ViemTxSender implements L1PublisherTxSender { }); } - async attest(archive: `0x{string}`): Promise { - // @note Something seems slightly off in viem, think it should be Hex instead of Hash - // but as they both are just `0x${string}` it should be fine anyways. - const signature = await this.account.signMessage({ message: { raw: archive } }); - const { r, s, v } = parseSignature(signature as `0x${string}`); - - return { - isEmpty: false, - v: v ? Number(v) : 0, - r: r, - s: s, - }; - } - getSenderAddress(): Promise { return Promise.resolve(EthAddress.fromString(this.account.address)); } @@ -107,6 +91,11 @@ export class ViemTxSender implements L1PublisherTxSender { } } + async getCurrentEpochCommittee(): Promise { + const committee = await this.rollupContract.read.getCurrentEpochCommittee(); + return committee.map(address => EthAddress.fromString(address)); + } + async getCurrentArchive(): Promise { const archive = await this.rollupContract.read.archive(); return Buffer.from(archive.replace('0x', ''), 'hex'); @@ -179,10 +168,13 @@ export class ViemTxSender implements L1PublisherTxSender { */ async sendProcessTx(encodedData: ProcessTxArgs): Promise { if (encodedData.attestations) { + // Get `0x${string}` encodings + const attestations = encodedData.attestations.map(attest => attest.toViemSignature()); + const args = [ `0x${encodedData.header.toString('hex')}`, `0x${encodedData.archive.toString('hex')}`, - encodedData.attestations, + attestations, ] as const; const gas = await this.rollupContract.estimateGas.process(args, { @@ -213,10 +205,11 @@ export class ViemTxSender implements L1PublisherTxSender { async sendPublishAndProcessTx(encodedData: ProcessTxArgs): Promise { // @note This is quite a sin, but I'm committing war crimes in this code already. if (encodedData.attestations) { + const attestations = encodedData.attestations.map(attest => attest.toViemSignature()); const args = [ `0x${encodedData.header.toString('hex')}`, `0x${encodedData.archive.toString('hex')}`, - encodedData.attestations, + attestations, `0x${encodedData.body.toString('hex')}`, ] as const; diff --git a/yarn-project/sequencer-client/src/receiver.ts b/yarn-project/sequencer-client/src/receiver.ts index f6defd2fafba..77ae6ee05a4d 100644 --- a/yarn-project/sequencer-client/src/receiver.ts +++ b/yarn-project/sequencer-client/src/receiver.ts @@ -1,11 +1,9 @@ -import { type L2Block } from '@aztec/circuit-types'; - -import { type Attestation } from './publisher/l1-publisher.js'; +import { type L2Block, type Signature } from '@aztec/circuit-types'; /** * Given the necessary rollup data, verifies it, and updates the underlying state accordingly to advance the state of the system. * See https://hackmd.io/ouVCnacHQRq2o1oRc5ksNA#RollupReceiver. */ export interface L2BlockReceiver { - processL2Block(block: L2Block, attestations?: Attestation[]): Promise; + processL2Block(block: L2Block, attestations?: Signature[]): Promise; } diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 1d8e9e79b849..23df5037b10a 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -28,6 +28,7 @@ import { type P2P, P2PClientState } from '@aztec/p2p'; import { type PublicProcessor, type PublicProcessorFactory } from '@aztec/simulator'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; import { type ContractDataSource } from '@aztec/types/contracts'; +import { type ValidatorClient } from '@aztec/validator-client'; import { type MerkleTreeOperations, WorldStateRunningState, type WorldStateSynchronizer } from '@aztec/world-state'; import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; @@ -40,6 +41,7 @@ import { Sequencer } from './sequencer.js'; describe('sequencer', () => { let publisher: MockProxy; + let validatorClient: MockProxy; let globalVariableBuilder: MockProxy; let p2p: MockProxy; let worldState: MockProxy; @@ -75,10 +77,6 @@ describe('sequencer', () => { publisher = mock(); - publisher.attest.mockImplementation(_archive => { - return Promise.resolve(mockedAttestation); - }); - publisher.isItMyTurnToSubmit.mockResolvedValue(true); globalVariableBuilder = mock(); @@ -127,6 +125,8 @@ describe('sequencer', () => { sequencer = new TestSubject( publisher, + // TDOO(md): add the relevant methods to the validator client that will prevent it stalling when waiting for attestations + validatorClient, globalVariableBuilder, p2p, worldState, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index bdd247645955..d493e03e0a66 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -1,8 +1,10 @@ import { + type BlockAttestation, type L1ToL2MessageSource, type L2Block, type L2BlockSource, type ProcessedTx, + Signature, Tx, type TxValidator, } from '@aztec/circuit-types'; @@ -16,11 +18,12 @@ import { Timer, elapsed } from '@aztec/foundation/timer'; import { type P2P } from '@aztec/p2p'; import { type PublicProcessorFactory } from '@aztec/simulator'; import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec/telemetry-client'; +import { type ValidatorClient } from '@aztec/validator-client'; import { type WorldStateStatus, type WorldStateSynchronizer } from '@aztec/world-state'; import { type BlockBuilderFactory } from '../block_builder/index.js'; import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js'; -import { type Attestation, type L1Publisher } from '../publisher/l1-publisher.js'; +import { type L1Publisher } from '../publisher/l1-publisher.js'; import { type TxValidatorFactory } from '../tx_validator/tx_validator_factory.js'; import { type SequencerConfig } from './config.js'; import { SequencerMetrics } from './metrics.js'; @@ -53,6 +56,7 @@ export class Sequencer { constructor( private publisher: L1Publisher, + private validatorClient: ValidatorClient | undefined, // During migration the validator client can be inactive private globalsBuilder: GlobalVariableBuilder, private p2pClient: P2P, private worldState: WorldStateSynchronizer, @@ -385,7 +389,7 @@ export class Sequencer { } } - protected async collectAttestations(block: L2Block): Promise { + protected async collectAttestations(block: L2Block): Promise { // @todo This should collect attestations properly and fix the ordering of them to make sense // the current implementation is a PLACEHOLDER and should be nuked from orbit. // It is assuming that there will only be ONE (1) validator, so only one attestation @@ -399,12 +403,30 @@ export class Sequencer { // ; ; // / \ // _____________/_ __ \_____________ - if (IS_DEV_NET) { + if (IS_DEV_NET || !this.validatorClient) { return undefined; } - const myAttestation = await this.publisher.attest(block.archive.root.toString()); - return [myAttestation]; + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962): inefficient to have a round trip in here - this should be cached + const committee = await this.publisher.getCurrentEpochCommittee(); + const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1; + + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7974): we do not have transaction[] lists in the block for now + // Dont do anything with the proposals for now - just collect them + + const proposal = await this.validatorClient.createBlockProposal(block.header, block.archive.root, []); + + this.state = SequencerState.PUBLISHING_BLOCK_TO_PEERS; + this.validatorClient.broadcastBlockProposal(proposal); + + this.state = SequencerState.WAITING_FOR_ATTESTATIONS; + const attestations = await this.validatorClient.collectAttestations( + proposal.header.globalVariables.slotNumber.toBigInt(), + numberOfRequiredAttestations, + ); + + // note: the smart contract requires that the signatures are provided in the order of the committee + return await orderAttestations(attestations, committee); } /** @@ -414,9 +436,10 @@ export class Sequencer { @trackSpan('Sequencer.publishL2Block', block => ({ [Attributes.BLOCK_NUMBER]: block.number, })) - protected async publishL2Block(block: L2Block, attestations?: Attestation[]) { + protected async publishL2Block(block: L2Block, attestations?: Signature[]) { // Publishes new block to the network and awaits the tx to be mined this.state = SequencerState.PUBLISHING_BLOCK; + const publishedL2Block = await this.publisher.processL2Block(block, attestations); if (publishedL2Block) { this.lastPublishedBlock = block.number; @@ -503,6 +526,14 @@ export enum SequencerState { * Creating a new L2 block. Includes processing public function calls and running rollup circuits. Will move to PUBLISHING_CONTRACT_DATA. */ CREATING_BLOCK, + /** + * Publishing blocks to validator peers. Will move to WAITING_FOR_ATTESTATIONS. + */ + PUBLISHING_BLOCK_TO_PEERS, + /** + * The block has been published to peers, and we are waiting for attestations. Will move to PUBLISHING_CONTRACT_DATA. + */ + WAITING_FOR_ATTESTATIONS, /** * Sending the tx to L1 with encrypted logs and awaiting it to be mined. Will move back to PUBLISHING_BLOCK once finished. */ @@ -516,3 +547,30 @@ export enum SequencerState { */ STOPPED, } + +/** Order Attestations + * + * Returns attestation signatures in the order of a series of provided ethereum addresses + * The rollup smart contract expects attestations to appear in the order of the committee + * + * @todo: perform this logic within the memory attestation store instead? + */ +async function orderAttestations(attestations: BlockAttestation[], orderAddresses: EthAddress[]): Promise { + // Create a map of sender addresses to BlockAttestations + const attestationMap = new Map(); + + for (const attestation of attestations) { + const sender = await attestation.getSender(); + if (sender) { + attestationMap.set(sender.toString(), attestation); + } + } + + // Create the ordered array based on the orderAddresses, else return an empty signature + const orderedAttestations = orderAddresses.map(address => { + const addressString = address.toString(); + return attestationMap.get(addressString)?.signature || Signature.empty(); + }); + + return orderedAttestations; +} diff --git a/yarn-project/sequencer-client/tsconfig.json b/yarn-project/sequencer-client/tsconfig.json index cff0c4789de5..54c62d4427fc 100644 --- a/yarn-project/sequencer-client/tsconfig.json +++ b/yarn-project/sequencer-client/tsconfig.json @@ -51,6 +51,9 @@ { "path": "../types" }, + { + "path": "../validator-client" + }, { "path": "../world-state" }, diff --git a/yarn-project/tsconfig.json b/yarn-project/tsconfig.json index e0c0f0aca1a8..52f60af50fb9 100644 --- a/yarn-project/tsconfig.json +++ b/yarn-project/tsconfig.json @@ -26,6 +26,7 @@ { "path": "aztec-faucet/tsconfig.json" }, { "path": "aztec.js/tsconfig.json" }, { "path": "aztec-node/tsconfig.json" }, + { "path": "validator-client/tsconfig.json" }, { "path": "bb-prover/tsconfig.json" }, { "path": "bot/tsconfig.json" }, { "path": "pxe/tsconfig.json" }, diff --git a/yarn-project/validator-client/.eslintrc.cjs b/yarn-project/validator-client/.eslintrc.cjs new file mode 100644 index 000000000000..e659927475c0 --- /dev/null +++ b/yarn-project/validator-client/.eslintrc.cjs @@ -0,0 +1 @@ +module.exports = require('@aztec/foundation/eslint'); diff --git a/yarn-project/validator-client/package.json b/yarn-project/validator-client/package.json new file mode 100644 index 000000000000..e89785cf9dc9 --- /dev/null +++ b/yarn-project/validator-client/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aztec/validator-client", + "version": "0.1.0", + "main": "dest/index.js", + "type": "module", + "exports": "./dest/index.js", + "bin": "./dest/bin/index.js", + "typedocOptions": { + "entryPoints": [ + "./src/index.ts" + ], + "name": "Aztec validator", + "tsconfig": "./tsconfig.json" + }, + "scripts": { + "start": "node --no-warnings ./dest/bin", + "build": "yarn clean && tsc -b", + "build:dev": "tsc -b --watch", + "clean": "rm -rf ./dest .tsbuildinfo", + "formatting": "run -T prettier --check ./src && run -T eslint ./src", + "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src", + "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests" + }, + "inherits": [ + "../package.common.json" + ], + "jest": { + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.[cm]?js$": "$1" + }, + "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$", + "rootDir": "./src", + "transform": { + "^.+\\.tsx?$": [ + "@swc/jest", + { + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true + } + } + } + ] + }, + "extensionsToTreatAsEsm": [ + ".ts" + ], + "reporters": [ + [ + "default", + { + "summaryThreshold": 9999 + } + ] + ] + }, + "dependencies": { + "@aztec/circuit-types": "workspace:^", + "@aztec/circuits.js": "workspace:^", + "@aztec/ethereum": "workspace:^", + "@aztec/foundation": "workspace:^", + "@aztec/p2p": "workspace:^", + "@aztec/types": "workspace:^", + "koa": "^2.14.2", + "koa-router": "^12.0.0", + "tslib": "^2.4.0", + "viem": "^2.7.15" + }, + "devDependencies": { + "@jest/globals": "^29.5.0", + "@types/jest": "^29.5.0", + "@types/node": "^18.7.23", + "jest": "^29.5.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "files": [ + "dest", + "src", + "!*.test.*" + ], + "types": "./dest/index.d.ts", + "engines": { + "node": ">=18" + } +} diff --git a/yarn-project/validator-client/src/config.ts b/yarn-project/validator-client/src/config.ts new file mode 100644 index 000000000000..083beead6c18 --- /dev/null +++ b/yarn-project/validator-client/src/config.ts @@ -0,0 +1,35 @@ +import { NULL_KEY } from '@aztec/ethereum'; +import { type ConfigMappingsType, booleanConfigHelper, getConfigFromMappings } from '@aztec/foundation/config'; + +/** + * The Validator Configuration + */ +export interface ValidatorClientConfig { + /** The private key of the validator participating in attestation duties */ + validatorPrivateKey: string; + + /** Do not run the validator */ + disableValidator: boolean; +} + +export const validatorClientConfigMappings: ConfigMappingsType = { + validatorPrivateKey: { + env: 'VALIDATOR_PRIVATE_KEY', + parseEnv: (val: string) => (val ? `0x${val.replace('0x', '')}` : NULL_KEY), + description: 'The private key of the validator participating in attestation duties', + }, + disableValidator: { + env: 'VALIDATOR_DISABLED', + description: 'Do not run the validator', + ...booleanConfigHelper(), + }, +}; + +/** + * Returns the prover configuration from the environment variables. + * Note: If an environment variable is not set, the default value is used. + * @returns The validator configuration. + */ +export function getProverEnvVars(): ValidatorClientConfig { + return getConfigFromMappings(validatorClientConfigMappings); +} diff --git a/yarn-project/validator-client/src/duties/validation_service.ts b/yarn-project/validator-client/src/duties/validation_service.ts new file mode 100644 index 000000000000..870f82d95206 --- /dev/null +++ b/yarn-project/validator-client/src/duties/validation_service.ts @@ -0,0 +1,40 @@ +import { BlockAttestation, BlockProposal, type TxHash } from '@aztec/circuit-types'; +import { type Header } from '@aztec/circuits.js'; +import { type Fr } from '@aztec/foundation/fields'; + +import { type ValidatorKeyStore } from '../key_store/interface.js'; + +export class ValidationService { + constructor(private keyStore: ValidatorKeyStore) {} + + /** + * Create a block proposal with the given header, archive, and transactions + * + * @param header - The block header + * @param archive - The archive of the current block + * @param txs - TxHash[] ordered list of transactions + * + * @returns A block proposal signing the above information (not the current implementation!!!) + */ + async createBlockProposal(header: Header, archive: Fr, txs: TxHash[]): Promise { + // Note: just signing the archive for now + const archiveBuf = archive.toBuffer(); + const sig = await this.keyStore.sign(archiveBuf); + + return new BlockProposal(header, archive, txs, sig); + } + + /** + * Attest to the given block proposal constructed by the current sequencer + * + * @param proposal - The proposal to attest to + * @returns attestation + */ + async attestToProposal(proposal: BlockProposal): Promise { + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7961): check that the current validator is correct + + const buf = proposal.archive.toBuffer(); + const sig = await this.keyStore.sign(buf); + return new BlockAttestation(proposal.header, proposal.archive, sig); + } +} diff --git a/yarn-project/validator-client/src/factory.ts b/yarn-project/validator-client/src/factory.ts new file mode 100644 index 000000000000..adca375b2b27 --- /dev/null +++ b/yarn-project/validator-client/src/factory.ts @@ -0,0 +1,8 @@ +import { type P2P } from '@aztec/p2p'; + +import { type ValidatorClientConfig } from './config.js'; +import { ValidatorClient } from './validator.js'; + +export function createValidatorClient(config: ValidatorClientConfig, p2pClient: P2P) { + return config.disableValidator ? undefined : ValidatorClient.new(config, p2pClient); +} diff --git a/yarn-project/validator-client/src/index.ts b/yarn-project/validator-client/src/index.ts new file mode 100644 index 000000000000..fb5aec613872 --- /dev/null +++ b/yarn-project/validator-client/src/index.ts @@ -0,0 +1,3 @@ +export * from './config.js'; +export * from './validator.js'; +export * from './factory.js'; diff --git a/yarn-project/validator-client/src/key_store/index.ts b/yarn-project/validator-client/src/key_store/index.ts new file mode 100644 index 000000000000..11b2828c367f --- /dev/null +++ b/yarn-project/validator-client/src/key_store/index.ts @@ -0,0 +1,2 @@ +export * from './interface.js'; +export * from './local_key_store.js'; diff --git a/yarn-project/validator-client/src/key_store/interface.ts b/yarn-project/validator-client/src/key_store/interface.ts new file mode 100644 index 000000000000..d55921b6dca9 --- /dev/null +++ b/yarn-project/validator-client/src/key_store/interface.ts @@ -0,0 +1,9 @@ +import { type Signature } from '@aztec/circuit-types'; + +/** Key Store + * + * A keystore interface that can be replaced with a local keystore / remote signer service + */ +export interface ValidatorKeyStore { + sign(message: Buffer): Promise; +} diff --git a/yarn-project/validator-client/src/key_store/local_key_store.ts b/yarn-project/validator-client/src/key_store/local_key_store.ts new file mode 100644 index 000000000000..220b62fe4095 --- /dev/null +++ b/yarn-project/validator-client/src/key_store/local_key_store.ts @@ -0,0 +1,31 @@ +import { Signature } from '@aztec/circuit-types'; + +import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'; + +import { type ValidatorKeyStore } from './interface.js'; + +/** + * Local Key Store + * + * An implementation of the Key store using an in memory private key. + */ +export class LocalKeyStore implements ValidatorKeyStore { + private signer: PrivateKeyAccount; + + constructor(privateKey: string) { + this.signer = privateKeyToAccount(privateKey as `0x{string}`); + } + + /** + * Sign a message with the keystore private key + * + * @param messageBuffer - The message buffer to sign + * @return signature + */ + public async sign(digestBuffer: Buffer): Promise { + const digest: `0x${string}` = `0x${digestBuffer.toString('hex')}`; + const signature = await this.signer.signMessage({ message: { raw: digest } }); + + return Signature.from0xString(signature); + } +} diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts new file mode 100644 index 000000000000..a0338ddadb0f --- /dev/null +++ b/yarn-project/validator-client/src/validator.ts @@ -0,0 +1,97 @@ +import { type BlockAttestation, type BlockProposal, type TxHash } from '@aztec/circuit-types'; +import { type Header } from '@aztec/circuits.js'; +import { type Fr } from '@aztec/foundation/fields'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { sleep } from '@aztec/foundation/sleep'; +import { type P2P } from '@aztec/p2p'; + +import { type ValidatorClientConfig } from './config.js'; +import { ValidationService } from './duties/validation_service.js'; +import { type ValidatorKeyStore } from './key_store/interface.js'; +import { LocalKeyStore } from './key_store/local_key_store.js'; + +export interface Validator { + start(): Promise; + registerBlockProposalHandler(): void; + + // Block validation responsiblities + createBlockProposal(header: Header, archive: Fr, txs: TxHash[]): Promise; + attestToProposal(proposal: BlockProposal): void; + + // TODO(md): possible abstraction leak + broadcastBlockProposal(proposal: BlockProposal): void; + collectAttestations(slot: bigint, numberOfRequiredAttestations: number): Promise; +} + +/** Validator Client + */ +export class ValidatorClient implements Validator { + private attestationPoolingIntervalMs: number = 1000; + + private validationService: ValidationService; + + constructor(keyStore: ValidatorKeyStore, private p2pClient: P2P, private log = createDebugLogger('aztec:validator')) { + //TODO: We need to setup and store all of the currently active validators https://github.com/AztecProtocol/aztec-packages/issues/7962 + + this.validationService = new ValidationService(keyStore); + this.log.verbose('Initialized validator'); + } + + static new(config: ValidatorClientConfig, p2pClient: P2P) { + const localKeyStore = new LocalKeyStore(config.validatorPrivateKey); + + const validator = new ValidatorClient(localKeyStore, p2pClient); + validator.registerBlockProposalHandler(); + return validator; + } + + public start() { + // Sync the committee from the smart contract + // https://github.com/AztecProtocol/aztec-packages/issues/7962 + + this.log.info('Validator started'); + return Promise.resolve(); + } + + public registerBlockProposalHandler() { + const handler = (block: BlockProposal): Promise => { + return this.validationService.attestToProposal(block); + }; + this.p2pClient.registerBlockProposalHandler(handler); + } + + attestToProposal(proposal: BlockProposal) { + return this.validationService.attestToProposal(proposal); + } + + createBlockProposal(header: Header, archive: Fr, txs: TxHash[]): Promise { + return this.validationService.createBlockProposal(header, archive, txs); + } + + broadcastBlockProposal(proposal: BlockProposal): void { + this.p2pClient.broadcastProposal(proposal); + } + + // Target is temporarily hardcoded, for a test, but will be calculated from smart contract + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962) + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7976): require suitable timeouts + async collectAttestations(slot: bigint, numberOfRequiredAttestations: number): Promise { + // Wait and poll the p2pClients attestation pool for this block + // until we have enough attestations + + this.log.info(`Waiting for attestations for slot, ${slot}`); + + let attestations: BlockAttestation[] = []; + while (attestations.length < numberOfRequiredAttestations) { + attestations = await this.p2pClient.getAttestationsForSlot(slot); + + if (attestations.length < numberOfRequiredAttestations) { + this.log.verbose(`Waiting ${this.attestationPoolingIntervalMs}ms for more attestations...`); + await sleep(this.attestationPoolingIntervalMs); + } + } + this.log.info(`Collected all attestations for slot, ${slot}`); + + return attestations; + } +} diff --git a/yarn-project/validator-client/tsconfig.json b/yarn-project/validator-client/tsconfig.json new file mode 100644 index 000000000000..a4198a7f0c2f --- /dev/null +++ b/yarn-project/validator-client/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "..", + "compilerOptions": { + "outDir": "dest", + "rootDir": "src", + "tsBuildInfoFile": ".tsbuildinfo" + }, + "references": [ + { + "path": "../circuit-types" + }, + { + "path": "../circuits.js" + }, + { + "path": "../ethereum" + }, + { + "path": "../foundation" + }, + { + "path": "../p2p" + }, + { + "path": "../types" + } + ], + "include": ["src"] +} diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 4793d815660b..760f8200f69c 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -161,6 +161,7 @@ __metadata: "@aztec/simulator": "workspace:^" "@aztec/telemetry-client": "workspace:^" "@aztec/types": "workspace:^" + "@aztec/validator-client": "workspace:^" "@aztec/world-state": "workspace:^" "@jest/globals": ^29.5.0 "@types/jest": ^29.5.0 @@ -387,6 +388,7 @@ __metadata: ts-node: ^10.9.1 tslib: ^2.5.0 typescript: ^5.0.4 + viem: ^2.7.15 languageName: unknown linkType: soft @@ -864,6 +866,7 @@ __metadata: tslib: ^2.4.0 typescript: ^5.0.4 uint8arrays: ^5.0.3 + viem: ^2.7.15 languageName: unknown linkType: soft @@ -1030,6 +1033,7 @@ __metadata: "@aztec/simulator": "workspace:^" "@aztec/telemetry-client": "workspace:^" "@aztec/types": "workspace:^" + "@aztec/validator-client": "workspace:^" "@aztec/world-state": "workspace:^" "@jest/globals": ^29.5.0 "@noir-lang/acvm_js": "portal:../../noir/packages/acvm_js" @@ -1169,6 +1173,31 @@ __metadata: languageName: unknown linkType: soft +"@aztec/validator-client@workspace:^, @aztec/validator-client@workspace:validator-client": + version: 0.0.0-use.local + resolution: "@aztec/validator-client@workspace:validator-client" + dependencies: + "@aztec/circuit-types": "workspace:^" + "@aztec/circuits.js": "workspace:^" + "@aztec/ethereum": "workspace:^" + "@aztec/foundation": "workspace:^" + "@aztec/p2p": "workspace:^" + "@aztec/types": "workspace:^" + "@jest/globals": ^29.5.0 + "@types/jest": ^29.5.0 + "@types/node": ^18.7.23 + jest: ^29.5.0 + koa: ^2.14.2 + koa-router: ^12.0.0 + ts-node: ^10.9.1 + tslib: ^2.4.0 + typescript: ^5.0.4 + viem: ^2.7.15 + bin: + validator-client: ./dest/bin/index.js + languageName: unknown + linkType: soft + "@aztec/world-state@workspace:^, @aztec/world-state@workspace:world-state": version: 0.0.0-use.local resolution: "@aztec/world-state@workspace:world-state"