diff --git a/l1-contracts/src/core/sequencer_selection/Leonidas.sol b/l1-contracts/src/core/sequencer_selection/Leonidas.sol index 8c646904b22..48cbaef0579 100644 --- a/l1-contracts/src/core/sequencer_selection/Leonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/Leonidas.sol @@ -375,8 +375,7 @@ contract Leonidas is Ownable, ILeonidas { // Validate the attestations uint256 validAttestations = 0; - bytes32 ethSignedDigest = _digest.toEthSignedMessageHash(); - + bytes32 digest = _digest.toEthSignedMessageHash(); for (uint256 i = 0; i < _signatures.length; i++) { SignatureLib.Signature memory signature = _signatures[i]; if (signature.isEmpty) { @@ -384,7 +383,7 @@ contract Leonidas is Ownable, ILeonidas { } // The verification will throw if invalid - signature.verify(committee[i], ethSignedDigest); + signature.verify(committee[i], digest); validAttestations++; } diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index f00adf2ea2d..3454245ffbf 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -7,7 +7,6 @@ import {DecoderBase} from "../decoders/Base.sol"; import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; import {Constants} from "../../src/core/libraries/ConstantsGen.sol"; import {SignatureLib} from "../../src/core/sequencer_selection/SignatureLib.sol"; -import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; import {Registry} from "../../src/core/messagebridge/Registry.sol"; import {Inbox} from "../../src/core/messagebridge/Inbox.sol"; @@ -20,11 +19,12 @@ import {MerkleTestUtil} from "../merkle/TestUtil.sol"; import {PortalERC20} from "../portals/PortalERC20.sol"; import {TxsDecoderHelper} from "../decoders/helpers/TxsDecoderHelper.sol"; import {IFeeJuicePortal} from "../../src/core/interfaces/IFeeJuicePortal.sol"; +import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; + /** * We are using the same blocks as from Rollup.t.sol. * The tests in this file is testing the sequencer selection */ - contract SpartaTest is DecoderBase { using MessageHashUtils for bytes32; @@ -283,8 +283,9 @@ contract SpartaTest is DecoderBase { returns (SignatureLib.Signature memory) { uint256 privateKey = privateKeys[_signer]; - bytes32 digestForSig = _digest.toEthSignedMessageHash(); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digestForSig); + + bytes32 digest = _digest.toEthSignedMessageHash(); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); return SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}); } diff --git a/yarn-project/archiver/src/archiver/eth_log_handlers.ts b/yarn-project/archiver/src/archiver/eth_log_handlers.ts index fa218fb2086..5781f1a3d0f 100644 --- a/yarn-project/archiver/src/archiver/eth_log_handlers.ts +++ b/yarn-project/archiver/src/archiver/eth_log_handlers.ts @@ -1,6 +1,7 @@ -import { Body, InboxLeaf, L2Block, type ViemSignature } from '@aztec/circuit-types'; +import { Body, InboxLeaf, L2Block } from '@aztec/circuit-types'; import { AppendOnlyTreeSnapshot, Header, Proof } from '@aztec/circuits.js'; import { type EthAddress } from '@aztec/foundation/eth-address'; +import { type ViemSignature } from '@aztec/foundation/eth-signature'; import { Fr } from '@aztec/foundation/fields'; import { numToUInt32BE } from '@aztec/foundation/serialize'; import { InboxAbi, RollupAbi } from '@aztec/l1-artifacts'; 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 1a0ba523548..9fe3789454b 100644 --- a/yarn-project/circuit-types/src/p2p/block_attestation.test.ts +++ b/yarn-project/circuit-types/src/p2p/block_attestation.test.ts @@ -3,8 +3,8 @@ import { BlockAttestation } from './block_attestation.js'; import { makeBlockAttestation, randomSigner } from './mocks.js'; describe('Block Attestation serialization / deserialization', () => { - it('Should serialize / deserialize', async () => { - const attestation = await makeBlockAttestation(); + it('Should serialize / deserialize', () => { + const attestation = makeBlockAttestation(); const serialized = attestation.toBuffer(); const deserialized = BlockAttestation.fromBuffer(serialized); @@ -12,17 +12,17 @@ describe('Block Attestation serialization / deserialization', () => { expect(deserialized).toEqual(attestation); }); - it('Should serialize / deserialize + recover sender', async () => { + it('Should serialize / deserialize + recover sender', () => { const account = randomSigner(); - const proposal = await makeBlockAttestation(account); + const proposal = 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); + const sender = deserialized.getSender(); + expect(sender).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 e38effdb368..ec554f45203 100644 --- a/yarn-project/circuit-types/src/p2p/block_attestation.ts +++ b/yarn-project/circuit-types/src/p2p/block_attestation.ts @@ -1,14 +1,13 @@ -import { EthAddress, Header } from '@aztec/circuits.js'; +import { type EthAddress, Header } from '@aztec/circuits.js'; import { Buffer32 } from '@aztec/foundation/buffer'; +import { recoverAddress } from '@aztec/foundation/crypto'; +import { Signature } from '@aztec/foundation/eth-signature'; import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; -import { recoverMessageAddress } from 'viem'; - import { TxHash } from '../tx/tx_hash.js'; -import { get0xStringHashedSignaturePayload, getSignaturePayload } from './block_utils.js'; +import { getHashedSignaturePayloadEthSignedMessage, getSignaturePayload } from './block_utils.js'; import { Gossipable } from './gossipable.js'; -import { Signature } from './signature.js'; import { TopicType, createTopicString } from './topic_type.js'; export class BlockAttestationHash extends Buffer32 { @@ -53,16 +52,12 @@ export class BlockAttestation extends Gossipable { * Lazily evaluate and cache the sender of the attestation * @returns The sender of the attestation */ - async getSender() { + getSender() { if (!this.sender) { // Recover the sender from the attestation - const hashed = get0xStringHashedSignaturePayload(this.archive, this.txHashes); - const address = await recoverMessageAddress({ - message: { raw: hashed }, - signature: this.signature.to0xString(), - }); + const hashed = getHashedSignaturePayloadEthSignedMessage(this.archive, this.txHashes); // Cache the sender for later use - this.sender = EthAddress.fromString(address); + this.sender = recoverAddress(hashed, this.signature); } return this.sender; 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 6eb1c427f4f..d6c91f14b20 100644 --- a/yarn-project/circuit-types/src/p2p/block_proposal.test.ts +++ b/yarn-project/circuit-types/src/p2p/block_proposal.test.ts @@ -3,8 +3,8 @@ import { BlockProposal } from './block_proposal.js'; import { makeBlockProposal, randomSigner } from './mocks.js'; describe('Block Proposal serialization / deserialization', () => { - it('Should serialize / deserialize', async () => { - const proposal = await makeBlockProposal(); + it('Should serialize / deserialize', () => { + const proposal = makeBlockProposal(); const serialized = proposal.toBuffer(); const deserialized = BlockProposal.fromBuffer(serialized); @@ -12,17 +12,17 @@ describe('Block Proposal serialization / deserialization', () => { expect(deserialized).toEqual(proposal); }); - it('Should serialize / deserialize + recover sender', async () => { + it('Should serialize / deserialize + recover sender', () => { const account = randomSigner(); - const proposal = await makeBlockProposal(account); + const proposal = 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); + const sender = deserialized.getSender(); + expect(sender).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 8164e755117..7da744c377a 100644 --- a/yarn-project/circuit-types/src/p2p/block_proposal.ts +++ b/yarn-project/circuit-types/src/p2p/block_proposal.ts @@ -1,14 +1,17 @@ -import { EthAddress, Header } from '@aztec/circuits.js'; +import { type EthAddress, Header } from '@aztec/circuits.js'; import { Buffer32 } from '@aztec/foundation/buffer'; +import { recoverAddress } from '@aztec/foundation/crypto'; +import { Signature } from '@aztec/foundation/eth-signature'; import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; -import { recoverMessageAddress } from 'viem'; - import { TxHash } from '../tx/tx_hash.js'; -import { get0xStringHashedSignaturePayload, getHashedSignaturePayload, getSignaturePayload } from './block_utils.js'; +import { + getHashedSignaturePayload, + getHashedSignaturePayloadEthSignedMessage, + getSignaturePayload, +} from './block_utils.js'; import { Gossipable } from './gossipable.js'; -import { Signature } from './signature.js'; import { TopicType, createTopicString } from './topic_type.js'; export class BlockProposalHash extends Buffer32 { @@ -54,7 +57,7 @@ export class BlockProposal extends Gossipable { header: Header, archive: Fr, txs: TxHash[], - payloadSigner: (payload: Buffer) => Promise, + payloadSigner: (payload: Buffer32) => Promise, ) { const hashed = getHashedSignaturePayload(archive, txs); const sig = await payloadSigner(hashed); @@ -65,16 +68,11 @@ export class BlockProposal extends Gossipable { /**Get Sender * Lazily evaluate the sender of the proposal; result is cached */ - async getSender() { + getSender() { if (!this.sender) { - // performance note(): this signature method requires another hash behind the scenes - const hashed = get0xStringHashedSignaturePayload(this.archive, this.txs); - const address = await recoverMessageAddress({ - message: { raw: hashed }, - signature: this.signature.to0xString(), - }); + const hashed = getHashedSignaturePayloadEthSignedMessage(this.archive, this.txs); // Cache the sender for later use - this.sender = EthAddress.fromString(address); + this.sender = recoverAddress(hashed, this.signature); } return this.sender; diff --git a/yarn-project/circuit-types/src/p2p/block_utils.ts b/yarn-project/circuit-types/src/p2p/block_utils.ts index cd1c5a48842..d4f4cfc34b7 100644 --- a/yarn-project/circuit-types/src/p2p/block_utils.ts +++ b/yarn-project/circuit-types/src/p2p/block_utils.ts @@ -1,7 +1,8 @@ -import { keccak256 as keccak256Buffer } from '@aztec/foundation/crypto'; +import { Buffer32 } from '@aztec/foundation/buffer'; +import { keccak256, makeEthSignDigest } from '@aztec/foundation/crypto'; import { type Fr } from '@aztec/foundation/fields'; -import { encodeAbiParameters, keccak256 as keccak2560xString, parseAbiParameters } from 'viem'; +import { encodeAbiParameters, parseAbiParameters } from 'viem'; import { type TxHash } from '../tx/tx_hash.js'; @@ -25,10 +26,17 @@ export function getSignaturePayload(archive: Fr, txs: TxHash[]) { * @param txs - The transactions in the block * @returns The hashed payload for the signature of the block proposal */ -export function getHashedSignaturePayload(archive: Fr, txs: TxHash[]): Buffer { - return keccak256Buffer(getSignaturePayload(archive, txs)); +export function getHashedSignaturePayload(archive: Fr, txs: TxHash[]): Buffer32 { + return Buffer32.fromBuffer(keccak256(getSignaturePayload(archive, txs))); } -export function get0xStringHashedSignaturePayload(archive: Fr, txs: TxHash[]): `0x${string}` { - return keccak2560xString(getSignaturePayload(archive, txs)); +/** + * Get the hashed payload for the signature of the block proposal as an Ethereum signed message EIP-712 + * @param archive - The archive of the block + * @param txs - The transactions in the block + * @returns The hashed payload for the signature of the block proposal as an Ethereum signed message + */ +export function getHashedSignaturePayloadEthSignedMessage(archive: Fr, txs: TxHash[]): Buffer32 { + const payload = getHashedSignaturePayload(archive, txs); + return makeEthSignDigest(payload); } diff --git a/yarn-project/circuit-types/src/p2p/index.ts b/yarn-project/circuit-types/src/p2p/index.ts index e6c268523c1..8afa13bb6c5 100644 --- a/yarn-project/circuit-types/src/p2p/index.ts +++ b/yarn-project/circuit-types/src/p2p/index.ts @@ -3,5 +3,4 @@ export * from './block_proposal.js'; export * from './interface.js'; export * from './gossipable.js'; export * from './topic_type.js'; -export * from './signature.js'; export * from './block_utils.js'; diff --git a/yarn-project/circuit-types/src/p2p/mocks.ts b/yarn-project/circuit-types/src/p2p/mocks.ts index 2e4b9495d66..f211a6fc523 100644 --- a/yarn-project/circuit-types/src/p2p/mocks.ts +++ b/yarn-project/circuit-types/src/p2p/mocks.ts @@ -1,41 +1,39 @@ import { makeHeader } from '@aztec/circuits.js/testing'; +import { Buffer32 } from '@aztec/foundation/buffer'; +import { Secp256k1Signer } from '@aztec/foundation/crypto'; 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 { get0xStringHashedSignaturePayload } from './block_utils.js'; -import { Signature } from './signature.js'; +import { getHashedSignaturePayloadEthSignedMessage } from './block_utils.js'; -export const makeBlockProposal = async (signer?: PrivateKeyAccount): Promise => { +export const makeBlockProposal = (signer?: Secp256k1Signer): BlockProposal => { signer = signer || randomSigner(); const blockHeader = makeHeader(1); const archive = Fr.random(); const txs = [0, 1, 2, 3, 4, 5].map(() => TxHash.random()); - const hash = get0xStringHashedSignaturePayload(archive, txs); - const signature = Signature.from0xString(await signer.signMessage({ message: { raw: hash } })); + const hash = getHashedSignaturePayloadEthSignedMessage(archive, txs); + const signature = signer.sign(hash); return new BlockProposal(blockHeader, archive, txs, signature); }; // TODO(https://github.com/AztecProtocol/aztec-packages/issues/8028) -export const makeBlockAttestation = async (signer?: PrivateKeyAccount): Promise => { +export const makeBlockAttestation = (signer?: Secp256k1Signer): BlockAttestation => { signer = signer || randomSigner(); const blockHeader = makeHeader(1); const archive = Fr.random(); const txs = [0, 1, 2, 3, 4, 5].map(() => TxHash.random()); - const hash = get0xStringHashedSignaturePayload(archive, txs); - const signature = Signature.from0xString(await signer.signMessage({ message: { raw: hash } })); + const hash = getHashedSignaturePayloadEthSignedMessage(archive, txs); + const signature = signer.sign(hash); return new BlockAttestation(blockHeader, archive, txs, signature); }; -export const randomSigner = (): PrivateKeyAccount => { - const privateKey = generatePrivateKey(); - return privateKeyToAccount(privateKey); +export const randomSigner = (): Secp256k1Signer => { + const privateKey = Buffer32.random(); + return new Secp256k1Signer(privateKey); }; diff --git a/yarn-project/circuit-types/src/p2p/signature.test.ts b/yarn-project/circuit-types/src/p2p/signature.test.ts deleted file mode 100644 index 7e443a4f105..00000000000 --- a/yarn-project/circuit-types/src/p2p/signature.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/foundation/package.json b/yarn-project/foundation/package.json index f14bc84c6e1..2af937f8c18 100644 --- a/yarn-project/foundation/package.json +++ b/yarn-project/foundation/package.json @@ -17,6 +17,7 @@ "./crypto": "./dest/crypto/index.js", "./error": "./dest/error/index.js", "./eth-address": "./dest/eth-address/index.js", + "./eth-signature": "./dest/eth-signature/index.js", "./queue": "./dest/queue/index.js", "./fs": "./dest/fs/index.js", "./buffer": "./dest/buffer/index.js", @@ -144,7 +145,8 @@ "prettier": "^2.7.1", "supertest": "^6.3.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/foundation/src/buffer/buffer32.ts b/yarn-project/foundation/src/buffer/buffer32.ts index 736c0ada457..a3a1d6b6702 100644 --- a/yarn-project/foundation/src/buffer/buffer32.ts +++ b/yarn-project/foundation/src/buffer/buffer32.ts @@ -112,7 +112,25 @@ export class Buffer32 { * @param str - The TX hash in string format. * @returns A new Buffer32 object. */ + public static fromStringUnchecked(str: string): Buffer32 { + return new Buffer32(Buffer.from(str, 'hex')); + } + + /** + * Converts a string into a Buffer32 object. + * NOTE: this method includes checks for the 0x prefix and the length of the string. + * if you dont need this checks, use fromStringUnchecked instead. + * + * @param str - The TX hash in string format. + * @returns A new Buffer32 object. + */ public static fromString(str: string): Buffer32 { + if (str.startsWith('0x')) { + str = str.slice(2); + } + if (str.length !== 64) { + throw new Error(`Expected string to be 64 characters long, but was ${str.length}`); + } return new Buffer32(Buffer.from(str, 'hex')); } diff --git a/yarn-project/foundation/src/crypto/index.ts b/yarn-project/foundation/src/crypto/index.ts index 96365958e92..4a93708f498 100644 --- a/yarn-project/foundation/src/crypto/index.ts +++ b/yarn-project/foundation/src/crypto/index.ts @@ -6,6 +6,7 @@ export * from './sha256/index.js'; export * from './sha512/index.js'; export * from './pedersen/index.js'; export * from './poseidon/index.js'; +export * from './secp256k1-signer/index.js'; /** * Init the bb singleton. This constructs (if not already) the barretenberg sync api within bb.js itself. diff --git a/yarn-project/foundation/src/crypto/secp256k1-signer/index.ts b/yarn-project/foundation/src/crypto/secp256k1-signer/index.ts new file mode 100644 index 00000000000..b64273685d2 --- /dev/null +++ b/yarn-project/foundation/src/crypto/secp256k1-signer/index.ts @@ -0,0 +1,2 @@ +export * from './secp256k1_signer.js'; +export * from './utils.js'; diff --git a/yarn-project/foundation/src/crypto/secp256k1-signer/secp256k1_signer.test.ts b/yarn-project/foundation/src/crypto/secp256k1-signer/secp256k1_signer.test.ts new file mode 100644 index 00000000000..707227208f2 --- /dev/null +++ b/yarn-project/foundation/src/crypto/secp256k1-signer/secp256k1_signer.test.ts @@ -0,0 +1,63 @@ +import { Buffer32 } from '@aztec/foundation/buffer'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Signature } from '@aztec/foundation/eth-signature'; + +import { hashMessage, recoverAddress as viemRecoverAddress, recoverPublicKey as viemRecoverPublicKey } from 'viem'; +import { type PrivateKeyAccount, generatePrivateKey, privateKeyToAccount, publicKeyToAddress } from 'viem/accounts'; + +import { Secp256k1Signer } from './secp256k1_signer.js'; +import { recoverAddress as lightRecoverAddress, recoverPublicKey as lightRecoverPublicKey } from './utils.js'; + +/** + * Differential fuzzing implementation of viem's signer and the secp256k1 signer + */ +describe('Secp256k1Signer', () => { + let viemSigner: PrivateKeyAccount; + let lightSigner: Secp256k1Signer; + + beforeEach(() => { + const privateKey = generatePrivateKey(); + viemSigner = privateKeyToAccount(privateKey); + + lightSigner = new Secp256k1Signer(Buffer32.fromBuffer(Buffer.from(privateKey.slice(2), 'hex'))); + }); + + it('Compare implementation against viem', async () => { + const message = Buffer.from('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex'); + // Use to compare addresses at the end + const accountAddress = viemSigner.address; + + // We use eth hashed message as viem will automatically do this with signMessage + const ethHashedMessage = hashMessage({ raw: message }); + const ethHashedMessageBuffer = Buffer32.fromBuffer(Buffer.from(ethHashedMessage.slice(2), 'hex')); + + const viemSignature = Signature.from0xString(await viemSigner.signMessage({ message: { raw: message } })); + const lightSignature = lightSigner.sign(ethHashedMessageBuffer); + + // Check signatures match + expect(viemSignature.equals(lightSignature)).toBe(true); + + const viemPublicKey = await viemRecoverPublicKey({ hash: ethHashedMessage, signature: viemSignature.to0xString() }); + const lightPublicKey = lightRecoverPublicKey(ethHashedMessageBuffer, lightSignature); + + // Check recovered public keys match + expect(Buffer.from(viemPublicKey.slice(2), 'hex')).toEqual(lightPublicKey); + + // Get the eth address can be recovered from the message and signature + const viemPublicKeyToAddress = publicKeyToAddress(viemPublicKey); + const viemAddress = EthAddress.fromString( + await viemRecoverAddress({ hash: ethHashedMessage, signature: viemSignature.to0xString() }), + ); + const lightAddress = lightRecoverAddress( + Buffer32.fromBuffer(Buffer.from(ethHashedMessage.slice(2), 'hex')), + lightSignature, + ); + + // Check viem signer matches + expect(viemAddress.toString()).toEqual(accountAddress.toString().toLowerCase()); + expect(accountAddress.toString()).toEqual(viemPublicKeyToAddress.toString()); + + // Check light signer matches + expect(viemAddress.toString()).toEqual(lightAddress.toString()); + }); +}); diff --git a/yarn-project/foundation/src/crypto/secp256k1-signer/secp256k1_signer.ts b/yarn-project/foundation/src/crypto/secp256k1-signer/secp256k1_signer.ts new file mode 100644 index 00000000000..1af00bc4026 --- /dev/null +++ b/yarn-project/foundation/src/crypto/secp256k1-signer/secp256k1_signer.ts @@ -0,0 +1,34 @@ +import { type Buffer32 } from '@aztec/foundation/buffer'; +import { type EthAddress } from '@aztec/foundation/eth-address'; +import { type Signature } from '@aztec/foundation/eth-signature'; + +import { addressFromPrivateKey, makeEthSignDigest, signMessage } from './utils.js'; + +/** + * Secp256k1Signer + * + * A class for signing messages using a secp256k1 private key. + * - This is a slim drop in replacement for an Ethereum signer, so it can be used in the same way. + * - See `utils.ts` for functions that enable recovering addresses and public keys from signatures. + */ +export class Secp256k1Signer { + public readonly address: EthAddress; + + constructor(private privateKey: Buffer32) { + this.address = addressFromPrivateKey(privateKey.buffer); + } + + sign(message: Buffer32): Signature { + return signMessage(message, this.privateKey.buffer); + } + + /** + * Sign a message using the same method as eth_sign + * @param message - The message to sign. + * @returns The signature. + */ + signMessage(message: Buffer32): Signature { + const digest = makeEthSignDigest(message); + return this.sign(digest); + } +} diff --git a/yarn-project/foundation/src/crypto/secp256k1-signer/utils.ts b/yarn-project/foundation/src/crypto/secp256k1-signer/utils.ts new file mode 100644 index 00000000000..c3be5e04b3a --- /dev/null +++ b/yarn-project/foundation/src/crypto/secp256k1-signer/utils.ts @@ -0,0 +1,99 @@ +import { secp256k1 } from '@noble/curves/secp256k1'; + +import { Buffer32 } from '../../buffer/buffer32.js'; +import { EthAddress } from '../../eth-address/index.js'; +import { Signature } from '../../eth-signature/eth_signature.js'; +import { keccak256 } from '../keccak/index.js'; + +const ETH_SIGN_PREFIX = '\x19Ethereum Signed Message:\n32'; + +// We just hash the message to make it easier to work with in the smart contract. +export function makeEthSignDigest(message: Buffer32): Buffer32 { + const prefix = Buffer.from(ETH_SIGN_PREFIX); + return Buffer32.fromBuffer(keccak256(Buffer.concat([prefix, message.buffer]))); +} + +/** + * Converts a public key to an address. + * @param publicKey - The public key to convert. + * @returns The address. + */ +function publicKeyToAddress(publicKey: Buffer): EthAddress { + const hash = keccak256(publicKey.subarray(1)); + return new EthAddress(hash.subarray(12)); +} + +/** + * Converts a private key to a public key. + * @param privateKey - The private key to convert. + * @returns The public key. + */ +export function publicKeyFromPrivateKey(privateKey: Buffer): Buffer { + return Buffer.from(secp256k1.getPublicKey(privateKey, false)); +} + +/** + * Converts a private key to an address. + * @param privateKey - The private key to convert. + * @returns The address. + */ +export function addressFromPrivateKey(privateKey: Buffer): EthAddress { + const publicKey = publicKeyFromPrivateKey(privateKey); + return publicKeyToAddress(publicKey); +} + +/** + * Recovers an address from a hash and a signature. + * @param hash - The hash to recover the address from. + * @param signature - The signature to recover the address from. + * @returns The address. + */ +export function recoverAddress(hash: Buffer32, signature: Signature): EthAddress { + const publicKey = recoverPublicKey(hash, signature); + return publicKeyToAddress(publicKey); +} + +/** + * @attribution - viem + * Converts a yParityOrV value to a recovery bit. + * @param yParityOrV - The yParityOrV value to convert. + * @returns The recovery bit. + */ +function toRecoveryBit(yParityOrV: number) { + if (yParityOrV === 0 || yParityOrV === 1) { + return yParityOrV; + } + if (yParityOrV === 27) { + return 0; + } + if (yParityOrV === 28) { + return 1; + } + throw new Error('Invalid yParityOrV value'); +} + +/** + * Signs a message using ecdsa over the secp256k1 curve. + * @param message - The message to sign. + * @param privateKey - The private key to sign the message with. + * @returns The signature. + */ +export function signMessage(message: Buffer32, privateKey: Buffer) { + const { r, s, recovery } = secp256k1.sign(message.buffer, privateKey); + return new Signature(Buffer32.fromBigInt(r), Buffer32.fromBigInt(s), recovery ? 28 : 27); +} + +/** + * Recovers a public key from a hash and a signature. + * @param hash - The hash to recover the public key from. + * @param signature - The signature to recover the public key from. + * @returns The public key. + */ +export function recoverPublicKey(hash: Buffer32, signature: Signature): Buffer { + const { r, s, v } = signature; + const recoveryBit = toRecoveryBit(v); + const sig = new secp256k1.Signature(r.toBigInt(), s.toBigInt()).addRecoveryBit(recoveryBit); + + const publicKey = sig.recoverPublicKey(hash.buffer).toHex(false); + return Buffer.from(publicKey, 'hex'); +} diff --git a/yarn-project/foundation/src/eth-signature/eth_signature.test.ts b/yarn-project/foundation/src/eth-signature/eth_signature.test.ts new file mode 100644 index 00000000000..d8fc489a675 --- /dev/null +++ b/yarn-project/foundation/src/eth-signature/eth_signature.test.ts @@ -0,0 +1,30 @@ +import { Buffer32 } from '@aztec/foundation/buffer'; +import { Secp256k1Signer, recoverAddress } from '@aztec/foundation/crypto'; +import { Fr } from '@aztec/foundation/fields'; + +import { Signature } from './eth_signature.js'; + +const randomSigner = () => { + const pk = Buffer32.random(); + return new Secp256k1Signer(pk); +}; + +describe('Signature serialization / deserialization', () => { + it('Should serialize / deserialize', () => { + const signer = randomSigner(); + + const originalMessage = Fr.random(); + const message = Buffer32.fromField(originalMessage); + + const signature = signer.sign(message); + + // Serde + const serialized = signature.toBuffer(); + const deserialized = Signature.fromBuffer(serialized); + expect(deserialized).toEqual(signature); + + // Recover signature + const sender = recoverAddress(message, signature); + expect(sender).toEqual(signer.address); + }); +}); diff --git a/yarn-project/circuit-types/src/p2p/signature.ts b/yarn-project/foundation/src/eth-signature/eth_signature.ts similarity index 93% rename from yarn-project/circuit-types/src/p2p/signature.ts rename to yarn-project/foundation/src/eth-signature/eth_signature.ts index 41870e59c62..2808ea63ce3 100644 --- a/yarn-project/circuit-types/src/p2p/signature.ts +++ b/yarn-project/foundation/src/eth-signature/eth_signature.ts @@ -32,6 +32,7 @@ export class Signature { 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(); @@ -41,18 +42,6 @@ export class Signature { 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 @@ -71,6 +60,22 @@ export class Signature { return new Signature(r, s, v, isEmpty); } + static empty(): Signature { + return new Signature(Buffer32.ZERO, Buffer32.ZERO, 0, true); + } + + equals(other: Signature): boolean { + return this.r.equals(other.r) && this.s.equals(other.s) && this.v === other.v && this.isEmpty === other.isEmpty; + } + + toBuffer(): Buffer { + return serializeToBuffer([this.r, this.s, this.v]); + } + + to0xString(): `0x${string}` { + return `0x${this.r.toString()}${this.s.toString()}${this.v.toString(16)}`; + } + /** * Return the signature with `0x${string}` encodings for r and s */ diff --git a/yarn-project/foundation/src/eth-signature/index.ts b/yarn-project/foundation/src/eth-signature/index.ts new file mode 100644 index 00000000000..73bc59dbe1a --- /dev/null +++ b/yarn-project/foundation/src/eth-signature/index.ts @@ -0,0 +1 @@ +export * from './eth_signature.js'; diff --git a/yarn-project/foundation/src/fields/point.ts b/yarn-project/foundation/src/fields/point.ts index 458252bd87a..3f3620f9348 100644 --- a/yarn-project/foundation/src/fields/point.ts +++ b/yarn-project/foundation/src/fields/point.ts @@ -1,5 +1,6 @@ import { toBigIntBE } from '../bigint-buffer/index.js'; -import { poseidon2Hash, randomBoolean } from '../crypto/index.js'; +import { poseidon2Hash } from '../crypto/poseidon/index.js'; +import { randomBoolean } from '../crypto/random/index.js'; import { BufferReader, FieldReader, serializeToBuffer } from '../serialize/index.js'; import { Fr } from './fields.js'; diff --git a/yarn-project/foundation/src/index.ts b/yarn-project/foundation/src/index.ts index 7a899eba520..78694f262f8 100644 --- a/yarn-project/foundation/src/index.ts +++ b/yarn-project/foundation/src/index.ts @@ -30,3 +30,4 @@ export * as worker from './worker/index.js'; export * as testing from './testing/index.js'; export * as config from './config/index.js'; export * as buffer from './buffer/index.js'; +export * as ethSignature from './eth-signature/index.js'; diff --git a/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.ts b/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.ts index 524a399aece..967429825da 100644 --- a/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.ts +++ b/yarn-project/p2p/src/attestation_pool/memory_attestation_pool.ts @@ -19,18 +19,19 @@ export class InMemoryAttestationPool implements AttestationPool { } } - public async addAttestations(attestations: BlockAttestation[]): Promise { + public 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 address = attestation.getSender(); const slotAttestationMap = getSlotOrDefault(this.attestations, slotNumber.toBigInt()); slotAttestationMap.set(address.toString(), attestation); this.log.verbose(`Added attestation for slot ${slotNumber} from ${address}`); } + return Promise.resolve(); } public deleteAttestationsForSlot(slot: bigint): Promise { @@ -40,12 +41,12 @@ export class InMemoryAttestationPool implements AttestationPool { return Promise.resolve(); } - public async deleteAttestations(attestations: BlockAttestation[]): Promise { + public 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(); + const address = attestation.getSender(); slotAttestationMap.delete(address.toString()); this.log.verbose(`Deleted attestation for slot ${slotNumber} from ${address}`); } diff --git a/yarn-project/p2p/src/attestation_pool/mocks.ts b/yarn-project/p2p/src/attestation_pool/mocks.ts index 00194d52b96..7b2e55a7af6 100644 --- a/yarn-project/p2p/src/attestation_pool/mocks.ts +++ b/yarn-project/p2p/src/attestation_pool/mocks.ts @@ -1,5 +1,6 @@ -import { BlockAttestation, Signature, TxHash } from '@aztec/circuit-types'; +import { BlockAttestation, TxHash } from '@aztec/circuit-types'; import { makeHeader } from '@aztec/circuits.js/testing'; +import { Signature } from '@aztec/foundation/eth-signature'; import { Fr } from '@aztec/foundation/fields'; import { serializeToBuffer } from '@aztec/foundation/serialize'; diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index a6c95b16a7d..4ab745fc920 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -1,4 +1,5 @@ import { type L1ToL2MessageSource, type L2BlockSource, type WorldStateSynchronizer } from '@aztec/circuit-types'; +import { type EthAddress } from '@aztec/foundation/eth-address'; import { type P2P } from '@aztec/p2p'; import { PublicProcessorFactory, type SimulationProvider } from '@aztec/simulator'; import { type TelemetryClient } from '@aztec/telemetry-client'; @@ -99,7 +100,7 @@ export class SequencerClient { this.sequencer.restart(); } - get coinbase() { + get coinbase(): EthAddress { return this.sequencer.coinbase; } diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts index 0d09e221cd0..a3e6d007c42 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts @@ -1,5 +1,6 @@ -import { L2Block, type ViemSignature } from '@aztec/circuit-types'; +import { L2Block } from '@aztec/circuit-types'; import { EthAddress } from '@aztec/circuits.js'; +import { type ViemSignature } from '@aztec/foundation/eth-signature'; import { sleep } from '@aztec/foundation/sleep'; import { RollupAbi } from '@aztec/l1-artifacts'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 0fbeb91f881..d7d5fe5156d 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -1,8 +1,9 @@ -import { type L2Block, type Signature, type TxHash } from '@aztec/circuit-types'; +import { type L2Block, type TxHash } from '@aztec/circuit-types'; import { getHashedSignaturePayload } from '@aztec/circuit-types'; import { type L1PublishBlockStats, type L1PublishProofStats } from '@aztec/circuit-types/stats'; import { ETHEREUM_SLOT_DURATION, EthAddress, type Header, type Proof } from '@aztec/circuits.js'; import { createEthereumChain } from '@aztec/ethereum'; +import { type Signature } from '@aztec/foundation/eth-signature'; import { type Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { serializeToBuffer } from '@aztec/foundation/serialize'; @@ -255,7 +256,7 @@ export class L1Publisher { // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which // make time consistency checks break. await this.validateBlockForSubmission(block.header, { - digest, + digest: digest.toBuffer(), signatures: attestations ?? [], }); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 0d990c56298..b7581454862 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -10,7 +10,6 @@ import { PROVING_STATUS, type ProvingSuccess, type ProvingTicket, - Signature, type Tx, TxHash, type UnencryptedL2Log, @@ -31,6 +30,7 @@ import { import { Buffer32 } from '@aztec/foundation/buffer'; import { times } from '@aztec/foundation/collection'; import { randomBytes } from '@aztec/foundation/crypto'; +import { Signature } from '@aztec/foundation/eth-signature'; import { type Writeable } from '@aztec/foundation/types'; import { type P2P, P2PClientState } from '@aztec/p2p'; import { type PublicProcessor, type PublicProcessorFactory } from '@aztec/simulator'; diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 6e4f3c157e9..69b8398d152 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -4,7 +4,6 @@ import { type L2Block, type L2BlockSource, type ProcessedTx, - Signature, Tx, type TxHash, type TxValidator, @@ -15,13 +14,14 @@ import { type AllowedElement, BlockProofError, PROVING_STATUS } from '@aztec/cir import { type L2BlockBuiltStats } from '@aztec/circuit-types/stats'; import { AppendOnlyTreeSnapshot, - AztecAddress, ContentCommitment, - EthAddress, GENESIS_ARCHIVE_ROOT, Header, StateReference, } from '@aztec/circuits.js'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Signature } from '@aztec/foundation/eth-signature'; import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { RunningPromise } from '@aztec/foundation/running-promise'; @@ -537,7 +537,7 @@ export class Sequencer { this.log.verbose(`Collected attestations from validators, number of attestations: ${attestations.length}`); // note: the smart contract requires that the signatures are provided in the order of the committee - return await orderAttestations(attestations, committee); + return orderAttestations(attestations, committee); } /** @@ -666,12 +666,12 @@ export enum SequencerState { * * @todo: perform this logic within the memory attestation store instead? */ -async function orderAttestations(attestations: BlockAttestation[], orderAddresses: EthAddress[]): Promise { +function orderAttestations(attestations: BlockAttestation[], orderAddresses: EthAddress[]): Signature[] { // Create a map of sender addresses to BlockAttestations const attestationMap = new Map(); for (const attestation of attestations) { - const sender = await attestation.getSender(); + const sender = attestation.getSender(); if (sender) { attestationMap.set(sender.toString(), attestation); } diff --git a/yarn-project/validator-client/src/duties/validation_service.ts b/yarn-project/validator-client/src/duties/validation_service.ts index 3a4ec7e8c6d..6df10436870 100644 --- a/yarn-project/validator-client/src/duties/validation_service.ts +++ b/yarn-project/validator-client/src/duties/validation_service.ts @@ -1,5 +1,6 @@ import { BlockAttestation, BlockProposal, type TxHash } from '@aztec/circuit-types'; import { type Header } from '@aztec/circuits.js'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { keccak256 } from '@aztec/foundation/crypto'; import { type Fr } from '@aztec/foundation/fields'; @@ -18,7 +19,7 @@ export class ValidationService { * @returns A block proposal signing the above information (not the current implementation!!!) */ createBlockProposal(header: Header, archive: Fr, txs: TxHash[]): Promise { - const payloadSigner = (payload: Buffer) => this.keyStore.sign(payload); + const payloadSigner = (payload: Buffer32) => this.keyStore.signMessage(payload); return BlockProposal.createProposalFromSigner(header, archive, txs, payloadSigner); } @@ -35,8 +36,8 @@ export class ValidationService { async attestToProposal(proposal: BlockProposal): Promise { // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7961): check that the current validator is correct - const buf = keccak256(proposal.getPayload()); - const sig = await this.keyStore.sign(buf); + const buf = Buffer32.fromBuffer(keccak256(proposal.getPayload())); + const sig = await this.keyStore.signMessage(buf); return new BlockAttestation(proposal.header, proposal.archive, proposal.txs, sig); } } diff --git a/yarn-project/validator-client/src/errors/validator.error.ts b/yarn-project/validator-client/src/errors/validator.error.ts index 5cc3929962e..73c3eaf2729 100644 --- a/yarn-project/validator-client/src/errors/validator.error.ts +++ b/yarn-project/validator-client/src/errors/validator.error.ts @@ -2,7 +2,13 @@ import { type TxHash } from '@aztec/circuit-types/tx_hash'; export class ValidatorError extends Error { constructor(message: string) { - super(message); + super(`Validator Error: ${message}`); + } +} + +export class InvalidValidatorPrivateKeyError extends ValidatorError { + constructor() { + super('Invalid validator private key provided'); } } diff --git a/yarn-project/validator-client/src/key_store/interface.ts b/yarn-project/validator-client/src/key_store/interface.ts index d55921b6dca..2b96c90377b 100644 --- a/yarn-project/validator-client/src/key_store/interface.ts +++ b/yarn-project/validator-client/src/key_store/interface.ts @@ -1,9 +1,18 @@ -import { type Signature } from '@aztec/circuit-types'; +import { type Buffer32 } from '@aztec/foundation/buffer'; +import { type Signature } from '@aztec/foundation/eth-signature'; /** Key Store * * A keystore interface that can be replaced with a local keystore / remote signer service */ export interface ValidatorKeyStore { - sign(message: Buffer): Promise; + sign(message: Buffer32): Promise; + /** + * Flavor of sign message that followed EIP-712 eth signed message prefix + * Note: this is only required when we are using ecdsa signatures over secp256k1 + * + * @param message - The message to sign. + * @returns The signature. + */ + signMessage(message: Buffer32): 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 index 220b62fe409..736ee361a45 100644 --- a/yarn-project/validator-client/src/key_store/local_key_store.ts +++ b/yarn-project/validator-client/src/key_store/local_key_store.ts @@ -1,6 +1,6 @@ -import { Signature } from '@aztec/circuit-types'; - -import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'; +import { type Buffer32 } from '@aztec/foundation/buffer'; +import { Secp256k1Signer } from '@aztec/foundation/crypto'; +import { type Signature } from '@aztec/foundation/eth-signature'; import { type ValidatorKeyStore } from './interface.js'; @@ -10,10 +10,10 @@ import { type ValidatorKeyStore } from './interface.js'; * An implementation of the Key store using an in memory private key. */ export class LocalKeyStore implements ValidatorKeyStore { - private signer: PrivateKeyAccount; + private signer: Secp256k1Signer; - constructor(privateKey: string) { - this.signer = privateKeyToAccount(privateKey as `0x{string}`); + constructor(privateKey: Buffer32) { + this.signer = new Secp256k1Signer(privateKey); } /** @@ -22,10 +22,15 @@ export class LocalKeyStore implements ValidatorKeyStore { * @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 } }); + public sign(digest: Buffer32): Promise { + const signature = this.signer.sign(digest); + + return Promise.resolve(signature); + } - return Signature.from0xString(signature); + public signMessage(message: Buffer32): Promise { + // Sign message adds eth sign prefix and hashes before signing + const signature = this.signer.signMessage(message); + return Promise.resolve(signature); } } diff --git a/yarn-project/validator-client/src/validator.test.ts b/yarn-project/validator-client/src/validator.test.ts index 4802656e5ce..fc870184fec 100644 --- a/yarn-project/validator-client/src/validator.test.ts +++ b/yarn-project/validator-client/src/validator.test.ts @@ -12,10 +12,16 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import { type PrivateKeyAccount, generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { makeBlockProposal } from '../../circuit-types/src/p2p/mocks.js'; -import { AttestationTimeoutError, TransactionsNotAvailableError } from './errors/validator.error.js'; +import { type ValidatorClientConfig } from './config.js'; +import { + AttestationTimeoutError, + InvalidValidatorPrivateKeyError, + TransactionsNotAvailableError, +} from './errors/validator.error.js'; import { ValidatorClient } from './validator.js'; describe('ValidationService', () => { + let config: ValidatorClientConfig; let validatorClient: ValidatorClient; let p2pClient: MockProxy; let validatorAccount: PrivateKeyAccount; @@ -27,7 +33,7 @@ describe('ValidationService', () => { const validatorPrivateKey = generatePrivateKey(); validatorAccount = privateKeyToAccount(validatorPrivateKey); - const config = { + config = { validatorPrivateKey: validatorPrivateKey, attestationPoolingIntervalMs: 1000, attestationWaitTimeoutMs: 1000, @@ -36,6 +42,11 @@ describe('ValidationService', () => { validatorClient = ValidatorClient.new(config, p2pClient); }); + it('Should throw error if an invalid private key is provided', () => { + config.validatorPrivateKey = '0x1234567890123456789'; + expect(() => ValidatorClient.new(config, p2pClient)).toThrow(InvalidValidatorPrivateKeyError); + }); + it('Should create a valid block proposal', async () => { const header = makeHeader(); const archive = Fr.random(); @@ -46,17 +57,17 @@ describe('ValidationService', () => { expect(blockProposal).toBeDefined(); const validatorAddress = EthAddress.fromString(validatorAccount.address); - expect(await blockProposal.getSender()).toEqual(validatorAddress); + expect(blockProposal.getSender()).toEqual(validatorAddress); }); it('Should a timeout if we do not collect enough attestations in time', async () => { - const proposal = await makeBlockProposal(); + const proposal = makeBlockProposal(); await expect(validatorClient.collectAttestations(proposal, 2)).rejects.toThrow(AttestationTimeoutError); }); it('Should throw an error if the transactions are not available', async () => { - const proposal = await makeBlockProposal(); + const proposal = makeBlockProposal(); // mock the p2pClient.getTxStatus to return undefined for all transactions p2pClient.getTxStatus.mockImplementation(() => undefined); diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts index 1c8442ef367..890e413ffff 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -1,5 +1,6 @@ import { type BlockAttestation, type BlockProposal, type TxHash } from '@aztec/circuit-types'; import { type Header } from '@aztec/circuits.js'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { type Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; @@ -7,7 +8,11 @@ import { type P2P } from '@aztec/p2p'; import { type ValidatorClientConfig } from './config.js'; import { ValidationService } from './duties/validation_service.js'; -import { AttestationTimeoutError, TransactionsNotAvailableError } from './errors/validator.error.js'; +import { + AttestationTimeoutError, + InvalidValidatorPrivateKeyError, + TransactionsNotAvailableError, +} from './errors/validator.error.js'; import { type ValidatorKeyStore } from './key_store/interface.js'; import { LocalKeyStore } from './key_store/local_key_store.js'; @@ -42,7 +47,12 @@ export class ValidatorClient implements Validator { } static new(config: ValidatorClientConfig, p2pClient: P2P) { - const localKeyStore = new LocalKeyStore(config.validatorPrivateKey); + if (!config.validatorPrivateKey) { + throw new InvalidValidatorPrivateKeyError(); + } + + const privateKey = validatePrivateKey(config.validatorPrivateKey); + const localKeyStore = new LocalKeyStore(privateKey); const validator = new ValidatorClient( localKeyStore, @@ -154,3 +164,11 @@ export class ValidatorClient implements Validator { } } } + +function validatePrivateKey(privateKey: string): Buffer32 { + try { + return Buffer32.fromString(privateKey); + } catch (error) { + throw new InvalidValidatorPrivateKeyError(); + } +} diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 0a64d09d724..159c6eecb4a 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -677,6 +677,7 @@ __metadata: supertest: ^6.3.3 ts-node: ^10.9.1 typescript: ^5.0.4 + viem: ^2.7.15 zod: ^3.22.4 languageName: unknown linkType: soft