Skip to content

Commit 33aaffd

Browse files
authored
feat: Enable the use of multiple EOAs (#16272)
This PR integrates the node key store and enables the use of multiple EOAs for publishing to L1. The primary changes are: 1. `SequencerPublisher` is now stateless. Slashing state has been moved into the `Sequencer`. A new publisher is created every block using an appropriate publisher account. 2. Publisher accounts and values of `coinbase` and `feeRecipient` are selected with every new block and come from the key store. 3. L1TxUtils now tracks some state to determine where the account can/should be used. 4. Signing of all transactions is no longer performed by viem's wallet client, instead it is deferred to the key store allowing abstractions such as remote signers.
2 parents a4850d4 + 8fc62bb commit 33aaffd

File tree

105 files changed

+3854
-845
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+3854
-845
lines changed

yarn-project/aztec-node/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"@aztec/kv-store": "workspace:^",
7777
"@aztec/l1-artifacts": "workspace:^",
7878
"@aztec/merkle-tree": "workspace:^",
79+
"@aztec/node-keystore": "workspace:^",
7980
"@aztec/node-lib": "workspace:^",
8081
"@aztec/noir-protocol-circuits-types": "workspace:^",
8182
"@aztec/p2p": "workspace:^",
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { EthAddress } from '@aztec/foundation/eth-address';
2+
import type { AztecAddressHex, EthAddressHex, EthPrivateKey } from '@aztec/node-keystore';
3+
import type { SharedNodeConfig } from '@aztec/node-lib/config';
4+
import type { SequencerClientConfig, TxSenderConfig } from '@aztec/sequencer-client/config';
5+
import { AztecAddress } from '@aztec/stdlib/aztec-address';
6+
import type { ValidatorClientConfig } from '@aztec/validator-client/config';
7+
8+
import { generatePrivateKey, privateKeyToAddress } from 'viem/accounts';
9+
10+
import { createKeyStoreForValidator } from './config.js';
11+
12+
describe('createKeyStoreForValidator', () => {
13+
const mockValidatorKey1 = generatePrivateKey() as EthPrivateKey;
14+
const mockValidatorKey2 = generatePrivateKey() as EthPrivateKey;
15+
const mockPublisherKey1 = generatePrivateKey() as EthPrivateKey;
16+
const mockPublisherKey2 = generatePrivateKey() as EthPrivateKey;
17+
const mockCoinbase = EthAddress.random().toString() as EthAddressHex;
18+
const web3SignerUrl = 'http://web3signer:1000';
19+
const mockValidatorAddresses = [mockValidatorKey1, mockValidatorKey2].map(privateKeyToAddress);
20+
const mockPublisherAddresses = [mockPublisherKey1, mockPublisherKey2].map(privateKeyToAddress);
21+
let mockFeeRecipient: AztecAddressHex;
22+
23+
const createMockConfig = (
24+
validatorKeys: string[] = [],
25+
publisherKeys: string[] = [],
26+
coinbase?: string,
27+
feeRecipient?: string,
28+
web3SignerUrl?: string,
29+
validatorAddresses: string[] = [],
30+
publisherAddresses: string[] = [],
31+
): TxSenderConfig & ValidatorClientConfig & SequencerClientConfig & SharedNodeConfig => {
32+
const mockValidatorPrivateKeys =
33+
validatorKeys.length > 0
34+
? {
35+
getValue: () => validatorKeys,
36+
}
37+
: undefined;
38+
39+
const mockPublisherPrivateKeys =
40+
publisherKeys.length > 0 ? publisherKeys.map(key => ({ getValue: () => key })) : undefined;
41+
42+
return {
43+
validatorPrivateKeys: mockValidatorPrivateKeys,
44+
publisherPrivateKeys: mockPublisherPrivateKeys,
45+
coinbase: coinbase ? { toString: () => coinbase } : undefined,
46+
feeRecipient: feeRecipient ? { toString: () => feeRecipient } : undefined,
47+
web3SignerUrl,
48+
validatorAddresses: validatorAddresses.map(addr => EthAddress.fromString(addr)),
49+
publisherAddresses: publisherAddresses.map(addr => EthAddress.fromString(addr)),
50+
} as TxSenderConfig & ValidatorClientConfig & SequencerClientConfig & SharedNodeConfig;
51+
};
52+
53+
beforeAll(async () => {
54+
mockFeeRecipient = (await AztecAddress.random()).toString() as AztecAddressHex;
55+
});
56+
57+
it('should return undefined when no validator keys are provided', () => {
58+
const config = createMockConfig([]);
59+
const result = createKeyStoreForValidator(config);
60+
expect(result).toBeUndefined();
61+
});
62+
63+
it('should return undefined when validatorPrivateKeys is undefined', () => {
64+
const config = {
65+
validatorPrivateKeys: undefined,
66+
publisherPrivateKeys: undefined,
67+
coinbase: undefined,
68+
feeRecipient: undefined,
69+
} as unknown as TxSenderConfig & ValidatorClientConfig & SequencerClientConfig & SharedNodeConfig;
70+
const result = createKeyStoreForValidator(config);
71+
expect(result).toBeUndefined();
72+
});
73+
74+
it('should create keystore with single validator key and default coinbase/feeRecipient', () => {
75+
const config = createMockConfig([mockValidatorKey1]);
76+
const result = createKeyStoreForValidator(config);
77+
78+
const expectedCoinbase = privateKeyToAddress(mockValidatorKey1);
79+
const expectedFeeRecipient = AztecAddress.ZERO.toString();
80+
81+
expect(result).toEqual({
82+
schemaVersion: 1,
83+
slasher: undefined,
84+
prover: undefined,
85+
remoteSigner: undefined,
86+
validators: [
87+
{
88+
attester: [mockValidatorKey1],
89+
feeRecipient: expectedFeeRecipient,
90+
coinbase: expectedCoinbase,
91+
remoteSigner: undefined,
92+
publisher: [],
93+
},
94+
],
95+
});
96+
});
97+
98+
it('should create keystore with multiple validator keys', () => {
99+
const config = createMockConfig([mockValidatorKey1, mockValidatorKey2]);
100+
const result = createKeyStoreForValidator(config);
101+
102+
const expectedCoinbase = privateKeyToAddress(mockValidatorKey1);
103+
104+
expect(result).toEqual({
105+
schemaVersion: 1,
106+
slasher: undefined,
107+
prover: undefined,
108+
remoteSigner: undefined,
109+
validators: [
110+
{
111+
attester: [mockValidatorKey1, mockValidatorKey2],
112+
feeRecipient: AztecAddress.ZERO.toString(),
113+
coinbase: expectedCoinbase,
114+
remoteSigner: undefined,
115+
publisher: [],
116+
},
117+
],
118+
});
119+
});
120+
121+
it('should create keystore with custom coinbase and feeRecipient', () => {
122+
const config = createMockConfig([mockValidatorKey1], [], mockCoinbase, mockFeeRecipient);
123+
const result = createKeyStoreForValidator(config);
124+
125+
expect(result).toEqual({
126+
schemaVersion: 1,
127+
slasher: undefined,
128+
prover: undefined,
129+
remoteSigner: undefined,
130+
validators: [
131+
{
132+
attester: [mockValidatorKey1],
133+
feeRecipient: mockFeeRecipient,
134+
coinbase: mockCoinbase,
135+
remoteSigner: undefined,
136+
publisher: [],
137+
},
138+
],
139+
});
140+
});
141+
142+
it('should create keystore with publisher keys', () => {
143+
const config = createMockConfig([mockValidatorKey1], [mockPublisherKey1, mockPublisherKey2]);
144+
const result = createKeyStoreForValidator(config);
145+
146+
const expectedCoinbase = privateKeyToAddress(mockValidatorKey1);
147+
148+
expect(result).toEqual({
149+
schemaVersion: 1,
150+
slasher: undefined,
151+
prover: undefined,
152+
remoteSigner: undefined,
153+
validators: [
154+
{
155+
attester: [mockValidatorKey1],
156+
feeRecipient: AztecAddress.ZERO.toString(),
157+
coinbase: expectedCoinbase,
158+
remoteSigner: undefined,
159+
publisher: [mockPublisherKey1, mockPublisherKey2],
160+
},
161+
],
162+
});
163+
});
164+
165+
it('should create keystore with all fields populated', () => {
166+
const config = createMockConfig(
167+
[mockValidatorKey1, mockValidatorKey2],
168+
[mockPublisherKey1],
169+
mockCoinbase,
170+
mockFeeRecipient,
171+
);
172+
const result = createKeyStoreForValidator(config);
173+
174+
expect(result).toEqual({
175+
schemaVersion: 1,
176+
slasher: undefined,
177+
prover: undefined,
178+
remoteSigner: undefined,
179+
validators: [
180+
{
181+
attester: [mockValidatorKey1, mockValidatorKey2],
182+
feeRecipient: mockFeeRecipient,
183+
coinbase: mockCoinbase,
184+
remoteSigner: undefined,
185+
publisher: [mockPublisherKey1],
186+
},
187+
],
188+
});
189+
});
190+
191+
it('should handle empty publisher keys array', () => {
192+
const config = createMockConfig([mockValidatorKey1], []);
193+
const result = createKeyStoreForValidator(config);
194+
195+
expect(result?.validators?.[0]?.publisher).toEqual([]);
196+
});
197+
198+
it('should use first validator key for coinbase when no coinbase provided', () => {
199+
const config = createMockConfig([mockValidatorKey1, mockValidatorKey2]);
200+
const result = createKeyStoreForValidator(config);
201+
202+
const expectedCoinbase = privateKeyToAddress(mockValidatorKey1);
203+
expect(result?.validators?.[0]?.coinbase).toBe(expectedCoinbase);
204+
});
205+
206+
it('should use AztecAddress.ZERO for feeRecipient when not provided', () => {
207+
const config = createMockConfig([mockValidatorKey1]);
208+
const result = createKeyStoreForValidator(config);
209+
210+
expect(result?.validators?.[0]?.feeRecipient).toBe(AztecAddress.ZERO.toString());
211+
});
212+
213+
it('should create keystore with remote signer details', () => {
214+
const config = createMockConfig(
215+
[],
216+
[],
217+
mockCoinbase,
218+
mockFeeRecipient,
219+
web3SignerUrl,
220+
mockValidatorAddresses,
221+
mockPublisherAddresses,
222+
);
223+
const result = createKeyStoreForValidator(config);
224+
225+
expect(result).toEqual({
226+
schemaVersion: 1,
227+
slasher: undefined,
228+
prover: undefined,
229+
remoteSigner: undefined,
230+
validators: [
231+
{
232+
attester: mockValidatorAddresses,
233+
feeRecipient: mockFeeRecipient,
234+
coinbase: mockCoinbase,
235+
remoteSigner: web3SignerUrl,
236+
publisher: mockPublisherAddresses,
237+
},
238+
],
239+
});
240+
});
241+
});

yarn-project/aztec-node/src/aztec-node/config.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,34 @@ import {
77
} from '@aztec/ethereum';
88
import { type ConfigMappingsType, booleanConfigHelper, getConfigFromMappings } from '@aztec/foundation/config';
99
import { type DataStoreConfig, dataConfigMappings } from '@aztec/kv-store/config';
10+
import {
11+
type AztecAddressHex,
12+
type EthAddressHex,
13+
type EthPrivateKey,
14+
type EthRemoteSignerAccount,
15+
type Hex,
16+
type KeyStore,
17+
type KeyStoreConfig,
18+
type ValidatorKeyStore,
19+
keyStoreConfigMappings,
20+
} from '@aztec/node-keystore';
1021
import { type SharedNodeConfig, sharedNodeConfigMappings } from '@aztec/node-lib/config';
1122
import { type P2PConfig, p2pConfigMappings } from '@aztec/p2p/config';
1223
import { type ProverClientUserConfig, proverClientConfigMappings } from '@aztec/prover-client/config';
13-
import { type SequencerClientConfig, sequencerClientConfigMappings } from '@aztec/sequencer-client/config';
24+
import {
25+
type SequencerClientConfig,
26+
type TxSenderConfig,
27+
sequencerClientConfigMappings,
28+
} from '@aztec/sequencer-client/config';
1429
import { slasherConfigMappings } from '@aztec/slasher';
30+
import { AztecAddress } from '@aztec/stdlib/aztec-address';
1531
import { type NodeRPCConfig, nodeRpcConfigMappings } from '@aztec/stdlib/config';
1632
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
1733
import { type ValidatorClientConfig, validatorClientConfigMappings } from '@aztec/validator-client/config';
1834
import { type WorldStateConfig, worldStateConfigMappings } from '@aztec/world-state/config';
1935

36+
import { privateKeyToAddress } from 'viem/accounts';
37+
2038
import { type SentinelConfig, sentinelConfigMappings } from '../sentinel/config.js';
2139

2240
export { sequencerClientConfigMappings, type SequencerClientConfig };
@@ -32,6 +50,7 @@ export type AztecNodeConfig = ArchiverConfig &
3250
Pick<ProverClientUserConfig, 'bbBinaryPath' | 'bbWorkingDirectory' | 'realProofs'> &
3351
P2PConfig &
3452
DataStoreConfig &
53+
KeyStoreConfig &
3554
SentinelConfig &
3655
SharedNodeConfig &
3756
GenesisStateConfig &
@@ -45,6 +64,7 @@ export type AztecNodeConfig = ArchiverConfig &
4564

4665
export const aztecNodeConfigMappings: ConfigMappingsType<AztecNodeConfig> = {
4766
...dataConfigMappings,
67+
...keyStoreConfigMappings,
4868
...archiverConfigMappings,
4969
...sequencerClientConfigMappings,
5070
...validatorClientConfigMappings,
@@ -74,3 +94,90 @@ export const aztecNodeConfigMappings: ConfigMappingsType<AztecNodeConfig> = {
7494
export function getConfigEnvVars(): AztecNodeConfig {
7595
return getConfigFromMappings<AztecNodeConfig>(aztecNodeConfigMappings);
7696
}
97+
98+
type ConfigRequiredToBuildKeyStore = TxSenderConfig & SequencerClientConfig & SharedNodeConfig & ValidatorClientConfig;
99+
100+
function createKeyStoreFromWeb3Signer(config: ConfigRequiredToBuildKeyStore) {
101+
const validatorKeyStores: ValidatorKeyStore[] = [];
102+
103+
if (
104+
config.web3SignerUrl === undefined ||
105+
config.web3SignerUrl.length === 0 ||
106+
config.validatorAddresses === undefined ||
107+
config.validatorAddresses.length === 0
108+
) {
109+
return undefined;
110+
}
111+
const coinbase = config.coinbase ? config.coinbase.toString() : config.validatorAddresses[0].toString();
112+
const feeRecipient = config.feeRecipient ? config.feeRecipient.toString() : AztecAddress.ZERO.toString();
113+
114+
const publisherAddresses =
115+
config.publisherAddresses && config.publisherAddresses.length > 0
116+
? config.publisherAddresses.map(k => k.toChecksumString() as EthRemoteSignerAccount)
117+
: [];
118+
119+
const attestors = config.validatorAddresses.map(k => k.toChecksumString() as EthRemoteSignerAccount);
120+
121+
validatorKeyStores.push({
122+
attester: attestors,
123+
feeRecipient: feeRecipient as AztecAddressHex,
124+
coinbase: coinbase as EthAddressHex,
125+
remoteSigner: config.web3SignerUrl,
126+
publisher: publisherAddresses,
127+
});
128+
129+
const keyStore: KeyStore = {
130+
schemaVersion: 1,
131+
slasher: undefined,
132+
prover: undefined,
133+
remoteSigner: undefined,
134+
validators: validatorKeyStores,
135+
};
136+
return keyStore;
137+
}
138+
139+
function createKeyStoreFromPrivateKeys(config: ConfigRequiredToBuildKeyStore) {
140+
const validatorKeyStores: ValidatorKeyStore[] = [];
141+
const ethPrivateKeys: EthPrivateKey[] = [];
142+
const validatorKeys = config.validatorPrivateKeys ? config.validatorPrivateKeys.getValue() : [];
143+
for (let i = 0; i < validatorKeys.length; i++) {
144+
const key = validatorKeys[i];
145+
const ethPrivateKey: EthPrivateKey = key as Hex<32>;
146+
ethPrivateKeys.push(ethPrivateKey);
147+
}
148+
149+
if (!ethPrivateKeys.length) {
150+
return undefined;
151+
}
152+
const coinbase = config.coinbase ? config.coinbase.toString() : privateKeyToAddress(ethPrivateKeys[0]);
153+
const feeRecipient = config.feeRecipient ? config.feeRecipient.toString() : AztecAddress.ZERO.toString();
154+
155+
const publisherKeys = config.publisherPrivateKeys
156+
? config.publisherPrivateKeys.map(k => k.getValue() as EthPrivateKey)
157+
: [];
158+
159+
validatorKeyStores.push({
160+
attester: ethPrivateKeys,
161+
feeRecipient: feeRecipient as AztecAddressHex,
162+
coinbase: coinbase as EthAddressHex,
163+
remoteSigner: undefined,
164+
publisher: publisherKeys,
165+
});
166+
167+
const keyStore: KeyStore = {
168+
schemaVersion: 1,
169+
slasher: undefined,
170+
prover: undefined,
171+
remoteSigner: undefined,
172+
validators: validatorKeyStores,
173+
};
174+
return keyStore;
175+
}
176+
177+
export function createKeyStoreForValidator(config: TxSenderConfig & SequencerClientConfig & SharedNodeConfig) {
178+
if (config.web3SignerUrl !== undefined && config.web3SignerUrl.length > 0) {
179+
return createKeyStoreFromWeb3Signer(config);
180+
}
181+
182+
return createKeyStoreFromPrivateKeys(config);
183+
}

0 commit comments

Comments
 (0)