Skip to content

Commit 93a48a9

Browse files
committed
feat: add support for overriding ethereum session key
1 parent f849da7 commit 93a48a9

File tree

10 files changed

+208
-27
lines changed

10 files changed

+208
-27
lines changed

docs/src/network-definition-spec.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ The network config can be provided both in `json` or `toml` format and each sect
112112
- name: (String) name of the `env` var.
113113
- value: (String| number) Value of the env var.
114114
- `keystore_key_types`: Defines which keystore keys should be created, for more details checkout details below.
115+
- `override_eth_key`: (String) Overrides the auto-generated ethereum session key for EVM-based paras. When set, the provided key is used and Zombienet omits the random seed from the resulting `zombie.json`.
115116

116117
- `collator_groups`:
117118

@@ -126,6 +127,7 @@ The network config can be provided both in `json` or `toml` format and each sect
126127
- name: (String) name of the `env` var.
127128
- value: (String| number) Value of the env var.
128129
- `substrate_cli_args_version`: (0|1|2) By default zombienet will evaluate your binary and set the correct version, but that produces a small overhead that could be skipped if you set directly with this key.
130+
- `override_eth_key`: (String) Same as above, lets you provide the ethereum session key.
129131

130132
- `onboard_as_parachain`: (Boolean, default true) flag to specify whether the para should be onboarded as a parachain or stay a parathread
131133
- `register_para`: (Boolean, default true) flag to specify whether the para should be registered. The `add_to_genesis` flag **must** be set to false for this flag to have any effect.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[settings]
2+
timeout = 1000
3+
4+
[relaychain]
5+
default_image = "docker.io/paritypr/polkadot-debug:master"
6+
default_command = "polkadot"
7+
default_args = ["-lparachain=debug"]
8+
9+
chain = "rococo-local"
10+
11+
[[relaychain.nodes]]
12+
name = "alice"
13+
14+
[[relaychain.nodes]]
15+
name = "bob"
16+
17+
[[parachains]]
18+
id = 2000
19+
chain = "generic-evm"
20+
21+
[parachains.collator]
22+
name = "evm-collator"
23+
image = "docker.io/parity/polkadot-parachain:latest"
24+
command = "polkadot-parachain"
25+
args = ["-lparachain=debug"]
26+
override_eth_key = "calm dirt remove satisfy web real danger swap ship enroll youth prize"

javascript/packages/orchestrator/src/chain-decorators/generic-evm.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Keyring } from "@polkadot/api";
2+
import type { KeyringPair } from "@polkadot/keyring/types";
23
import { u8aToHex } from "@polkadot/util";
34
import { cryptoWaitReady } from "@polkadot/util-crypto";
45
import { CreateLogTable, decorators } from "@zombienet/utils";
@@ -8,24 +9,46 @@ import {
89
readAndParseChainSpec,
910
writeChainSpec,
1011
} from "../chainSpec";
11-
import { generateKeyForNode as _generateKeyForNode } from "../keys";
12+
import {
13+
type NodeAccounts,
14+
generateKeyForNode as _generateKeyForNode,
15+
resolveEthereumDerivationUri,
16+
} from "../keys";
1217
import { Node } from "../sharedTypes";
1318

14-
async function generateKeyForNode(nodeName?: string): Promise<any> {
15-
const keys = await _generateKeyForNode(nodeName);
19+
async function generateKeyForNode(
20+
nodeName?: string,
21+
overrideEthKey?: string,
22+
): Promise<NodeAccounts> {
23+
const keys = await _generateKeyForNode(nodeName, overrideEthKey);
1624

1725
await cryptoWaitReady();
1826

1927
const eth_keyring = new Keyring({ type: "ethereum" });
20-
const eth_account = eth_keyring.createFromUri(
21-
`${keys.mnemonic}/m/44'/60'/0'/0/0`,
28+
const uri = resolveEthereumDerivationUri(
29+
keys.mnemonic ?? "",
30+
overrideEthKey,
2231
);
2332

33+
let eth_account: KeyringPair;
34+
try {
35+
eth_account = eth_keyring.createFromUri(uri);
36+
} catch (error) {
37+
throw new Error(
38+
`Failed to create ethereum session key for ${nodeName}: ${error}`,
39+
);
40+
}
41+
2442
keys.eth_account = {
2543
address: eth_account.address,
2644
publicKey: u8aToHex(eth_account.publicKey),
2745
};
2846

47+
if (overrideEthKey) {
48+
keys.ethKeyOverrideUsed = true;
49+
delete keys.mnemonic;
50+
}
51+
2952
return keys;
3053
}
3154

javascript/packages/orchestrator/src/chain-decorators/local-v.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Keyring } from "@polkadot/api";
2+
import type { KeyringPair } from "@polkadot/keyring/types";
23
import { u8aToHex } from "@polkadot/util";
34
import { cryptoWaitReady } from "@polkadot/util-crypto";
45
import { CreateLogTable, decorators } from "@zombienet/utils";
@@ -8,24 +9,46 @@ import {
89
readAndParseChainSpec,
910
writeChainSpec,
1011
} from "../chainSpec";
11-
import { generateKeyForNode as _generateKeyForNode } from "../keys";
12+
import {
13+
type NodeAccounts,
14+
generateKeyForNode as _generateKeyForNode,
15+
resolveEthereumDerivationUri,
16+
} from "../keys";
1217
import { Node } from "../sharedTypes";
1318

14-
async function generateKeyForNode(nodeName?: string): Promise<any> {
15-
const keys = await _generateKeyForNode(nodeName);
19+
async function generateKeyForNode(
20+
nodeName?: string,
21+
overrideEthKey?: string,
22+
): Promise<NodeAccounts> {
23+
const keys = await _generateKeyForNode(nodeName, overrideEthKey);
1624

1725
await cryptoWaitReady();
1826

1927
const eth_keyring = new Keyring({ type: "ethereum" });
20-
const eth_account = eth_keyring.createFromUri(
21-
`${keys.mnemonic}/m/44'/60'/0'/0/0`,
28+
const uri = resolveEthereumDerivationUri(
29+
keys.mnemonic ?? "",
30+
overrideEthKey,
2231
);
2332

33+
let eth_account: KeyringPair;
34+
try {
35+
eth_account = eth_keyring.createFromUri(uri);
36+
} catch (error) {
37+
throw new Error(
38+
`Failed to create ethereum session key for ${nodeName ?? "collator"}: ${error}`,
39+
);
40+
}
41+
2442
keys.eth_account = {
2543
address: eth_account.address,
2644
publicKey: u8aToHex(eth_account.publicKey),
2745
};
2846

47+
if (overrideEthKey) {
48+
keys.ethKeyOverrideUsed = true;
49+
delete keys.mnemonic;
50+
}
51+
2952
return keys;
3053
}
3154

javascript/packages/orchestrator/src/chain-decorators/mainnet-local-v.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Keyring } from "@polkadot/api";
2+
import type { KeyringPair } from "@polkadot/keyring/types";
23
import { u8aToHex } from "@polkadot/util";
34
import { cryptoWaitReady } from "@polkadot/util-crypto";
45
import { CreateLogTable, decorators } from "@zombienet/utils";
@@ -8,24 +9,46 @@ import {
89
readAndParseChainSpec,
910
writeChainSpec,
1011
} from "../chainSpec";
11-
import { generateKeyForNode as _generateKeyForNode } from "../keys";
12+
import {
13+
type NodeAccounts,
14+
generateKeyForNode as _generateKeyForNode,
15+
resolveEthereumDerivationUri,
16+
} from "../keys";
1217
import { Node } from "../sharedTypes";
1318

14-
async function generateKeyForNode(nodeName?: string): Promise<any> {
15-
const keys = await _generateKeyForNode(nodeName);
19+
async function generateKeyForNode(
20+
nodeName?: string,
21+
overrideEthKey?: string,
22+
): Promise<NodeAccounts> {
23+
const keys = await _generateKeyForNode(nodeName, overrideEthKey);
1624

1725
await cryptoWaitReady();
1826

1927
const eth_keyring = new Keyring({ type: "ethereum" });
20-
const eth_account = eth_keyring.createFromUri(
21-
`${keys.mnemonic}/m/44'/60'/0'/0/0`,
28+
const uri = resolveEthereumDerivationUri(
29+
keys.mnemonic ?? "",
30+
overrideEthKey,
2231
);
2332

33+
let eth_account: KeyringPair;
34+
try {
35+
eth_account = eth_keyring.createFromUri(uri);
36+
} catch (error) {
37+
throw new Error(
38+
`Failed to create ethereum session key for ${nodeName ?? "collator"}: ${error}`,
39+
);
40+
}
41+
2442
keys.eth_account = {
2543
address: eth_account.address,
2644
publicKey: u8aToHex(eth_account.publicKey),
2745
};
2846

47+
if (overrideEthKey) {
48+
keys.ethKeyOverrideUsed = true;
49+
delete keys.mnemonic;
50+
}
51+
2952
return keys;
3053
}
3154

javascript/packages/orchestrator/src/chain-decorators/moonbeam.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Keyring } from "@polkadot/api";
2+
import type { KeyringPair } from "@polkadot/keyring/types";
23
import { u8aToHex } from "@polkadot/util";
34
import { cryptoWaitReady } from "@polkadot/util-crypto";
45
import { CreateLogTable, decorators } from "@zombienet/utils";
@@ -9,7 +10,11 @@ import {
910
readAndParseChainSpec,
1011
writeChainSpec,
1112
} from "../chainSpec";
12-
import { generateKeyForNode as _generateKeyForNode } from "../keys";
13+
import {
14+
type NodeAccounts,
15+
generateKeyForNode as _generateKeyForNode,
16+
resolveEthereumDerivationUri,
17+
} from "../keys";
1318
import { ChainSpec } from "../types";
1419
import { Node } from "../sharedTypes";
1520

@@ -96,23 +101,40 @@ async function clearAuthorities(specPath: string) {
96101
writeChainSpec(specPath, chainSpec);
97102
}
98103

99-
async function generateKeyForNode(nodeName?: string): Promise<any> {
100-
const keys = await _generateKeyForNode(nodeName);
104+
async function generateKeyForNode(
105+
nodeName?: string,
106+
overrideEthKey?: string,
107+
): Promise<NodeAccounts> {
108+
const keys = await _generateKeyForNode(nodeName, overrideEthKey);
101109

102110
await cryptoWaitReady();
103111

104112
const eth_keyring = new Keyring({ type: "ethereum" });
105-
const eth_account = eth_keyring.createFromUri(
106-
nodeName && nodeName.toLocaleLowerCase() in KNOWN_MOONBEAM_KEYS
107-
? KNOWN_MOONBEAM_KEYS[nodeName.toLocaleLowerCase()]
108-
: `${keys.mnemonic}/m/44'/60'/0'/0/0`,
109-
);
113+
const lowerName = nodeName?.toLocaleLowerCase();
114+
const knownKey = lowerName ? KNOWN_MOONBEAM_KEYS[lowerName] : undefined;
115+
const uri = knownKey
116+
? knownKey
117+
: resolveEthereumDerivationUri(keys.mnemonic ?? "", overrideEthKey);
118+
119+
let eth_account: KeyringPair;
120+
try {
121+
eth_account = eth_keyring.createFromUri(uri);
122+
} catch (error) {
123+
throw new Error(
124+
`Failed to create ethereum session key for ${nodeName ?? "collator"}: ${error}`,
125+
);
126+
}
110127

111128
keys.eth_account = {
112129
address: eth_account.address,
113130
publicKey: u8aToHex(eth_account.publicKey),
114131
};
115132

133+
if (overrideEthKey) {
134+
keys.ethKeyOverrideUsed = true;
135+
delete keys.mnemonic;
136+
}
137+
116138
return keys;
117139
}
118140

javascript/packages/orchestrator/src/configGenerator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,10 @@ async function getCollatorNodeFromConfig(
616616

617617
const collatorName = getUniqueName(collatorConfig.name || "collator");
618618
const [decoratedKeysGenerator] = decorate(para, [generateKeyForNode]);
619-
const accountsForNode = await decoratedKeysGenerator(collatorName);
619+
const accountsForNode = await decoratedKeysGenerator(
620+
collatorName,
621+
collatorConfig.override_eth_key,
622+
);
620623

621624
const provider = networkSpec.settings.provider;
622625
const ports = await getPorts(provider, collatorConfig);

javascript/packages/orchestrator/src/keys.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Keyring } from "@polkadot/api";
2+
import type { KeyringPair } from "@polkadot/keyring/types";
23
import { u8aToHex } from "@polkadot/util";
34
import {
45
cryptoWaitReady,
@@ -13,14 +14,64 @@ function nameCase(string: string) {
1314
return string.charAt(0).toUpperCase() + string.slice(1);
1415
}
1516

16-
export async function generateKeyFromSeed(seed: string): Promise<any> {
17+
export interface SigningAccount {
18+
address: string;
19+
publicKey: string;
20+
}
21+
22+
export interface NodeAccounts {
23+
seed?: string;
24+
mnemonic?: string;
25+
sr_account: SigningAccount;
26+
sr_stash: SigningAccount;
27+
ed_account: SigningAccount;
28+
ec_account: {
29+
publicKey: string;
30+
};
31+
eth_account?: SigningAccount;
32+
ethKeyOverrideUsed?: boolean;
33+
}
34+
35+
const BIP39_WORD_LENGTHS = new Set([12, 15, 18, 21, 24]);
36+
37+
export function resolveEthereumDerivationUri(
38+
fallbackMnemonic: string,
39+
override?: string,
40+
): string {
41+
if (!override || override.trim().length === 0) {
42+
return `${fallbackMnemonic}/m/44'/60'/0'/0/0`;
43+
}
44+
45+
const trimmed = override.trim();
46+
47+
if (trimmed.startsWith("//")) {
48+
throw new Error(
49+
"override_eth_key for ethereum nodes must be a mnemonic, a mnemonic with an explicit /m/ path, or a 0x-prefixed private key.",
50+
);
51+
}
52+
53+
if (trimmed.startsWith("0x")) return trimmed;
54+
if (trimmed.includes("/m/")) return trimmed;
55+
56+
const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
57+
if (BIP39_WORD_LENGTHS.has(wordCount)) {
58+
return `${trimmed}/m/44'/60'/0'/0/0`;
59+
}
60+
61+
return trimmed;
62+
}
63+
64+
export async function generateKeyFromSeed(seed: string): Promise<KeyringPair> {
1765
await cryptoWaitReady();
1866

1967
const sr_keyring = new Keyring({ type: "sr25519" });
2068
return sr_keyring.createFromUri(`//${seed}`);
2169
}
2270

23-
export async function generateKeyForNode(nodeName?: string): Promise<any> {
71+
export async function generateKeyForNode(
72+
nodeName?: string,
73+
_overrideEthKey?: string,
74+
): Promise<NodeAccounts> {
2475
await cryptoWaitReady();
2576

2677
const mnemonic = mnemonicGenerate();
@@ -38,7 +89,6 @@ export async function generateKeyForNode(nodeName?: string): Promise<any> {
3889
const ec_keyring = new Keyring({ type: "ecdsa" });
3990
const ec_account = ec_keyring.createFromUri(`${seed}`);
4091

41-
// return the needed info
4292
return {
4393
seed,
4494
mnemonic,

javascript/packages/orchestrator/src/sharedTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export interface NodeConfig extends NodeCommonTypes {
115115
prometheus_port?: number;
116116
p2p_cert_hash?: string; // libp2p certhash to use with webrtc transport.
117117
delay_network_settings?: DelayNetworkSettings;
118+
override_eth_key?: string;
118119
}
119120

120121
export interface NodeCommonTypes {

javascript/packages/orchestrator/src/spawner.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ export const spawnNode = async (
9696
isAssetHubPolkadot,
9797
);
9898
keystoreLocalDir = path.dirname(keystoreFiles[0]);
99+
100+
// When we have `override_eth_key` option
101+
// seeds/mnemonics that should not leak into `zombie.json`;
102+
if (node.accounts.ethKeyOverrideUsed) {
103+
delete node.accounts.seed;
104+
delete node.accounts.mnemonic;
105+
delete node.accounts.ethKeyOverrideUsed;
106+
}
99107
}
100108

101109
// replace all network references in command

0 commit comments

Comments
 (0)