feat:allow full IsmConfig for CLI as an advanced config feature (#2939)
### Description - added `ism-advanced` config which allows you flexibility to define your own IsmConfig shape - configure through `hyperlane config create ism-advanced` - core deployments checks for both `ism-advanced.yaml` and then `ism.yaml` (old multisig config way) ie. precedence is given to the advanced version - support for `Aggregation`, `Routing`, `MerkleRoot`, `MessageId`, and `TestIsm` (with a warning) - instead of routing over just messageId, the simpler `ism` config way is now routing over aggregation of merkle and message matching hyperlane V3 core deployments - also renamed multisig option to ism to not introduce multisig user-side and stick with consistent ism or ism- options. ### Drive-by changes - `ism.yaml` doesn't take in a type since we provide both `merkleRoot` and `messageId` variants by default. ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/737 ### Backward compatibility Yes ### Testing Manual --------- Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz>pull/2990/head
parent
50aed86517
commit
df693708b6
@ -0,0 +1,6 @@ |
||||
--- |
||||
'@hyperlane-xyz/cli': minor |
||||
'@hyperlane-xyz/sdk': minor |
||||
--- |
||||
|
||||
Add support for all ISM types in CLI interactive config creation |
@ -0,0 +1,33 @@ |
||||
anvil1: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil2: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 1 |
||||
|
||||
anvil2: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil1: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 1 |
@ -1,21 +1,13 @@ |
||||
# A config for a multisig Interchain Security Module (ISM) |
||||
# Schema: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/ism/types.ts |
||||
# |
||||
# Valid module types: |
||||
# routing |
||||
# aggregation |
||||
# merkleRootMultisig |
||||
# messageIdMultisigIsm |
||||
|
||||
# ism type don't work currently (sets to messageIdMultisigIsm for all) |
||||
--- |
||||
anvil1: |
||||
type: 'messageIdMultisigIsm' |
||||
threshold: 1 # Number: Signatures required to approve a message |
||||
validators: # Array: List of validator addresses |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
anvil2: |
||||
type: 'messageIdMultisigIsm' |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
@ -0,0 +1,268 @@ |
||||
import { confirm, input, select } from '@inquirer/prompts'; |
||||
import { z } from 'zod'; |
||||
|
||||
import { ChainMap, ChainName, IsmType } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { errorRed, log, logBlue, logGreen } from '../../logger.js'; |
||||
import { runMultiChainSelectionStep } from '../utils/chains.js'; |
||||
import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js'; |
||||
|
||||
import { readChainConfigsIfExists } from './chain.js'; |
||||
|
||||
const MultisigIsmConfigSchema = z.object({ |
||||
type: z.union([ |
||||
z.literal(IsmType.MERKLE_ROOT_MULTISIG), |
||||
z.literal(IsmType.MESSAGE_ID_MULTISIG), |
||||
]), |
||||
threshold: z.number(), |
||||
validators: z.array(z.string()), |
||||
}); |
||||
|
||||
const RoutingIsmConfigSchema: z.ZodSchema<any> = z.lazy(() => |
||||
z.object({ |
||||
type: z.literal(IsmType.ROUTING), |
||||
owner: z.string(), |
||||
domains: z.record(IsmConfigSchema), |
||||
}), |
||||
); |
||||
|
||||
const AggregationIsmConfigSchema: z.ZodSchema<any> = z |
||||
.lazy(() => |
||||
z.object({ |
||||
type: z.literal(IsmType.AGGREGATION), |
||||
modules: z.array(IsmConfigSchema), |
||||
threshold: z.number(), |
||||
}), |
||||
) |
||||
.refine( |
||||
// check ig modules.length >= threshold
|
||||
(ismConfig) => { |
||||
return ismConfig.modules.length >= ismConfig.threshold; |
||||
}, |
||||
{ |
||||
message: 'Threshold cannot be greater than number of modules', |
||||
}, |
||||
); |
||||
|
||||
const TestIsmConfigSchema = z.object({ |
||||
type: z.literal(IsmType.TEST_ISM), |
||||
}); |
||||
|
||||
const IsmConfigSchema = z.union([ |
||||
MultisigIsmConfigSchema, |
||||
RoutingIsmConfigSchema, |
||||
AggregationIsmConfigSchema, |
||||
TestIsmConfigSchema, |
||||
]); |
||||
const IsmConfigMapSchema = z.record(IsmConfigSchema).refine( |
||||
(ismConfigMap) => { |
||||
// check if any key in IsmConfigMap is found in its own RoutingIsmConfigSchema.domains
|
||||
for (const [key, config] of Object.entries(ismConfigMap)) { |
||||
if (config.type === IsmType.ROUTING) { |
||||
if (config.domains && key in config.domains) { |
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
return true; |
||||
}, |
||||
{ |
||||
message: |
||||
'Cannot set RoutingIsm.domain to the same chain you are configuring', |
||||
}, |
||||
); |
||||
export type ZodIsmConfig = z.infer<typeof IsmConfigSchema>; |
||||
export type ZodIsmConfigMap = z.infer<typeof IsmConfigMapSchema>; |
||||
|
||||
export function parseIsmConfig(filePath: string) { |
||||
const config = readYamlOrJson(filePath); |
||||
if (!config) throw new Error(`No ISM config found at ${filePath}`); |
||||
return IsmConfigMapSchema.safeParse(config); |
||||
} |
||||
|
||||
export function readIsmConfig(filePath: string) { |
||||
const result = parseIsmConfig(filePath); |
||||
if (!result.success) { |
||||
const firstIssue = result.error.issues[0]; |
||||
throw new Error( |
||||
`Invalid ISM config: ${firstIssue.path} => ${firstIssue.message}`, |
||||
); |
||||
} |
||||
const parsedConfig = result.data; |
||||
return parsedConfig; |
||||
} |
||||
|
||||
export function isValildIsmConfig(config: any) { |
||||
return IsmConfigMapSchema.safeParse(config).success; |
||||
} |
||||
|
||||
export async function createIsmConfigMap({ |
||||
format, |
||||
outPath, |
||||
chainConfigPath, |
||||
}: { |
||||
format: FileFormat; |
||||
outPath: string; |
||||
chainConfigPath: string; |
||||
}) { |
||||
logBlue('Creating a new ISM config'); |
||||
const customChains = readChainConfigsIfExists(chainConfigPath); |
||||
const chains = await runMultiChainSelectionStep(customChains); |
||||
|
||||
const result: ZodIsmConfigMap = {}; |
||||
for (const chain of chains) { |
||||
log(`Setting values for chain ${chain}`); |
||||
result[chain] = await createIsmConfig(chain, chainConfigPath); |
||||
|
||||
// TODO consider re-enabling. Disabling based on feedback from @nambrot for now.
|
||||
// repeat = await confirm({
|
||||
// message: 'Use this same config for remaining chains?',
|
||||
// });
|
||||
} |
||||
|
||||
if (isValildIsmConfig(result)) { |
||||
logGreen(`ISM config is valid, writing to file ${outPath}`); |
||||
mergeYamlOrJson(outPath, result, format); |
||||
} else { |
||||
errorRed( |
||||
`ISM config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/ism.yaml for an example`, |
||||
); |
||||
throw new Error('Invalid ISM config'); |
||||
} |
||||
} |
||||
|
||||
export async function createIsmConfig( |
||||
chain: ChainName, |
||||
chainConfigPath: string, |
||||
): Promise<ZodIsmConfig> { |
||||
let lastConfig: ZodIsmConfig; |
||||
const moduleType = await select({ |
||||
message: 'Select ISM type', |
||||
choices: [ |
||||
{ |
||||
value: IsmType.MESSAGE_ID_MULTISIG, |
||||
name: IsmType.MESSAGE_ID_MULTISIG, |
||||
description: 'Validators need to sign just this messageId', |
||||
}, |
||||
{ |
||||
value: IsmType.MERKLE_ROOT_MULTISIG, |
||||
name: IsmType.MERKLE_ROOT_MULTISIG, |
||||
description: |
||||
'Validators need to sign the root of the merkle tree of all messages from origin chain', |
||||
}, |
||||
{ |
||||
value: IsmType.ROUTING, |
||||
name: IsmType.ROUTING, |
||||
description: |
||||
'Each origin chain can be verified by the specified ISM type via RoutingISM', |
||||
}, |
||||
{ |
||||
value: IsmType.AGGREGATION, |
||||
name: IsmType.AGGREGATION, |
||||
description: |
||||
'You can aggregate multiple ISMs into one ISM via AggregationISM', |
||||
}, |
||||
{ |
||||
value: IsmType.TEST_ISM, |
||||
name: IsmType.TEST_ISM, |
||||
description: |
||||
'ISM where you can deliver messages without any validation (WARNING: only for testing, do not use in production)', |
||||
}, |
||||
], |
||||
pageSize: 10, |
||||
}); |
||||
if ( |
||||
moduleType === IsmType.MESSAGE_ID_MULTISIG || |
||||
moduleType === IsmType.MERKLE_ROOT_MULTISIG |
||||
) { |
||||
lastConfig = await createMultisigConfig(moduleType); |
||||
} else if (moduleType === IsmType.ROUTING) { |
||||
lastConfig = await createRoutingConfig(chain, chainConfigPath); |
||||
} else if (moduleType === IsmType.AGGREGATION) { |
||||
lastConfig = await createAggregationConfig(chain, chainConfigPath); |
||||
} else if (moduleType === IsmType.TEST_ISM) { |
||||
lastConfig = { type: IsmType.TEST_ISM }; |
||||
} else { |
||||
throw new Error(`Invalid ISM type: ${moduleType}}`); |
||||
} |
||||
return lastConfig; |
||||
} |
||||
|
||||
export async function createMultisigConfig( |
||||
type: IsmType.MERKLE_ROOT_MULTISIG | IsmType.MESSAGE_ID_MULTISIG, |
||||
): Promise<ZodIsmConfig> { |
||||
const thresholdInput = await input({ |
||||
message: 'Enter threshold of signers (number)', |
||||
}); |
||||
const threshold = parseInt(thresholdInput, 10); |
||||
|
||||
const validatorsInput = await input({ |
||||
message: 'Enter validator addresses (comma separated list)', |
||||
}); |
||||
const validators = validatorsInput.split(',').map((v) => v.trim()); |
||||
return { |
||||
type, |
||||
threshold, |
||||
validators, |
||||
}; |
||||
} |
||||
|
||||
export async function createAggregationConfig( |
||||
chain: ChainName, |
||||
chainConfigPath: string, |
||||
): Promise<ZodIsmConfig> { |
||||
const isms = parseInt( |
||||
await input({ |
||||
message: 'Enter the number of ISMs to aggregate (number)', |
||||
}), |
||||
10, |
||||
); |
||||
|
||||
const threshold = parseInt( |
||||
await input({ |
||||
message: 'Enter the threshold of ISMs to for verification (number)', |
||||
}), |
||||
10, |
||||
); |
||||
|
||||
const modules: Array<ZodIsmConfig> = []; |
||||
for (let i = 0; i < isms; i++) { |
||||
modules.push(await createIsmConfig(chain, chainConfigPath)); |
||||
} |
||||
return { |
||||
type: IsmType.AGGREGATION, |
||||
modules, |
||||
threshold, |
||||
}; |
||||
} |
||||
|
||||
export async function createRoutingConfig( |
||||
chain: ChainName, |
||||
chainConfigPath: string, |
||||
): Promise<ZodIsmConfig> { |
||||
const owner = await input({ |
||||
message: 'Enter owner address', |
||||
}); |
||||
const ownerAddress = owner; |
||||
const customChains = readChainConfigsIfExists(chainConfigPath); |
||||
delete customChains[chain]; |
||||
const chains = await runMultiChainSelectionStep( |
||||
customChains, |
||||
`Select origin chains to be verified on ${chain}`, |
||||
[chain], |
||||
); |
||||
|
||||
const domainsMap: ChainMap<ZodIsmConfig> = {}; |
||||
for (const chain of chains) { |
||||
await confirm({ |
||||
message: `You are about to configure ISM from source chain ${chain}. Continue?`, |
||||
}); |
||||
const config = await createIsmConfig(chain, chainConfigPath); |
||||
domainsMap[chain] = config; |
||||
} |
||||
return { |
||||
type: IsmType.ROUTING, |
||||
owner: ownerAddress, |
||||
domains: domainsMap, |
||||
}; |
||||
} |
@ -0,0 +1,85 @@ |
||||
import { expect } from 'chai'; |
||||
|
||||
import { ChainMap, IsmConfig, IsmType } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { readIsmConfig } from '../config/ism.js'; |
||||
|
||||
describe('readIsmConfig', () => { |
||||
it('parses and validates example correctly', () => { |
||||
const ism = readIsmConfig('examples/ism-advanced.yaml'); |
||||
|
||||
const exampleIsmConfig: ChainMap<IsmConfig> = { |
||||
anvil1: { |
||||
type: IsmType.ROUTING, |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720', |
||||
domains: { |
||||
anvil2: { |
||||
type: IsmType.AGGREGATION, |
||||
modules: [ |
||||
{ |
||||
type: IsmType.MESSAGE_ID_MULTISIG, |
||||
threshold: 1, |
||||
validators: ['0xa0ee7a142d267c1f36714e4a8f75612f20a79720'], |
||||
}, |
||||
{ |
||||
type: IsmType.MERKLE_ROOT_MULTISIG, |
||||
threshold: 1, |
||||
validators: ['0xa0ee7a142d267c1f36714e4a8f75612f20a79720'], |
||||
}, |
||||
], |
||||
threshold: 1, |
||||
}, |
||||
}, |
||||
}, |
||||
anvil2: { |
||||
type: IsmType.ROUTING, |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720', |
||||
domains: { |
||||
anvil1: { |
||||
type: IsmType.AGGREGATION, |
||||
modules: [ |
||||
{ |
||||
type: IsmType.MESSAGE_ID_MULTISIG, |
||||
threshold: 1, |
||||
validators: ['0xa0ee7a142d267c1f36714e4a8f75612f20a79720'], |
||||
}, |
||||
{ |
||||
type: IsmType.MERKLE_ROOT_MULTISIG, |
||||
threshold: 1, |
||||
validators: ['0xa0ee7a142d267c1f36714e4a8f75612f20a79720'], |
||||
}, |
||||
], |
||||
threshold: 1, |
||||
}, |
||||
}, |
||||
}, |
||||
}; |
||||
expect(ism).to.deep.equal(exampleIsmConfig); |
||||
}); |
||||
|
||||
it('parsing failure, missing internal key "threshold"', () => { |
||||
expect(function () { |
||||
readIsmConfig('src/tests/ism/safe-parse-fail.yaml'); |
||||
}).to.throw(); |
||||
}); |
||||
|
||||
it('parsing failure, routingIsm.domains includes destination domain', () => { |
||||
expect(function () { |
||||
readIsmConfig('src/tests/ism/routing-same-chain-fail.yaml'); |
||||
}).to.throw( |
||||
'Cannot set RoutingIsm.domain to the same chain you are configuring', |
||||
); |
||||
}); |
||||
|
||||
it('parsing failure, wrong ism type', () => { |
||||
expect(function () { |
||||
readIsmConfig('src/tests/ism/wrong-ism-type-fail.yaml'); |
||||
}).to.throw('Invalid ISM config: anvil2 => Invalid input'); |
||||
}); |
||||
|
||||
it('parsing failure, threshold > modules.length', () => { |
||||
expect(function () { |
||||
readIsmConfig('src/tests/ism/threshold-gt-modules-length-fail.yaml'); |
||||
}).to.throw('Threshold cannot be greater than number of modules'); |
||||
}); |
||||
}); |
@ -0,0 +1,33 @@ |
||||
anvil1: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil1: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 1 |
||||
|
||||
anvil2: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil2: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 1 |
@ -0,0 +1,32 @@ |
||||
anvil1: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil2: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 1 |
||||
|
||||
anvil2: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil1: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
@ -0,0 +1,33 @@ |
||||
anvil1: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil2: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 1 |
||||
|
||||
anvil2: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil1: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 3 |
@ -0,0 +1,33 @@ |
||||
anvil1: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil2: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: merkleRootMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 1 |
||||
|
||||
anvil2: |
||||
type: domainRoutingIsm |
||||
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
domains: |
||||
anvil1: |
||||
type: staticAggregationIsm |
||||
modules: |
||||
- type: messageIdMultisigIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
- type: domainRoutingIsm |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
threshold: 1 |
@ -0,0 +1,41 @@ |
||||
import { expect } from 'chai'; |
||||
|
||||
import { ChainMap, MultisigConfig } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { readMultisigConfig } from '../config/multisig.js'; |
||||
|
||||
describe('readMultisigConfig', () => { |
||||
it('parses and validates example correctly', () => { |
||||
const multisig = readMultisigConfig('examples/ism.yaml'); |
||||
|
||||
const exampleMultisigConfig: ChainMap<MultisigConfig> = { |
||||
anvil1: { |
||||
threshold: 1, |
||||
validators: ['0xa0Ee7A142d267C1f36714E4a8F75612F20a79720'], |
||||
}, |
||||
anvil2: { |
||||
threshold: 1, |
||||
validators: ['0xa0Ee7A142d267C1f36714E4a8F75612F20a79720'], |
||||
}, |
||||
}; |
||||
expect(multisig).to.deep.equal(exampleMultisigConfig); |
||||
}); |
||||
|
||||
it('parsing failure', () => { |
||||
expect(function () { |
||||
readMultisigConfig('src/tests/multisig/safe-parse-fail.yaml'); |
||||
}).to.throw('Invalid multisig config: anvil2,validators => Required'); |
||||
}); |
||||
|
||||
it('threshold cannot be greater than the # of validators', () => { |
||||
expect(function () { |
||||
readMultisigConfig('src/tests/multisig/threshold-gt-fail.yaml'); |
||||
}).to.throw('Threshold cannot be greater than number of validators'); |
||||
}); |
||||
|
||||
it('invalid address', () => { |
||||
expect(function () { |
||||
readMultisigConfig('src/tests/multisig/invalid-address-fail.yaml'); |
||||
}).to.throw('Invalid address 0xa0ee7a142d267c1n36714e4a8f7561f20a79720'); |
||||
}); |
||||
}); |
@ -0,0 +1,8 @@ |
||||
anvil1: |
||||
threshold: 1 # Number: Signatures required to approve a message |
||||
validators: # Array: List of validator addresses |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
anvil2: |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1n36714e4a8f7561f20a79720' |
@ -0,0 +1,6 @@ |
||||
anvil1: |
||||
threshold: 1 # Number: Signatures required to approve a message |
||||
validators: # Array: List of validator addresses |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
anvil2: |
||||
threshold: 1 |
@ -0,0 +1,8 @@ |
||||
anvil1: |
||||
threshold: 1 # Number: Signatures required to approve a message |
||||
validators: # Array: List of validator addresses |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
anvil2: |
||||
threshold: 3 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
Loading…
Reference in new issue