feat(infra): ensure check-deploy covers ownable ISMs (#4236)
### Description - check-deploy will surface violations when the on-chain ISM state differs from the config for core and warp modules Example of running check-deploy when the warp config includes a new validator Vs on-chain ISM state ``` ┌─────────┬────────────┬────────┬──────────────┬─────────────┬─────────┬──────────────────────────────────────────────┬──────────────────────────────────────────────┐ │ (index) │ chain │ remote │ name │ type │ subType │ actual │ expected │ ├─────────┼────────────┼────────┼──────────────┼─────────────┼─────────┼──────────────────────────────────────────────┼──────────────────────────────────────────────┤ │ 0 │ 'ethereum' │ │ │ 'ClientIsm' │ │ [Object] │ [Object] │ │ 1 │ 'ethereum' │ │ 'collateral' │ 'Owner' │ │ '0x3965AC3D295641E452E0ea896a086A9cD7C6C5b6' │ '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba' │ └─────────┴────────────┴────────┴──────────────┴─────────────┴─────────┴──────────────────────────────────────────────┴──────────────────────────────────────────────┘ Connection client violation ClientIsm details: + Added to config staticAggregationIsm.modules.0.merkleRootMultisigIsm.validators.3: "0xbb5842ae0e05215b53df4787a29144efb7e67551" + Added to config staticAggregationIsm.modules.1.messageIdMultisigIsm.validators.3: "0xbb5842ae0e05215b53df4787a29144efb7e67551" ~ Updated config staticAggregationIsm.modules.0.merkleRootMultisigIsm.validators.0: "0x95c7bf235837cb5a609fe6c95870410b9f68bcff" -> "0x4d966438fe9e2b1e7124c87bbb90cb4f0f6c59a1" ~ Updated config staticAggregationIsm.modules.0.merkleRootMultisigIsm.validators.1: "0xa5a56e97fb46f0ac3a3d261e404acb998d9a6969" -> "0x95c7bf235837cb5a609fe6c95870410b9f68bcff" ~ Updated config staticAggregationIsm.modules.0.merkleRootMultisigIsm.validators.2: "0xbb5842ae0e05215b53df4787a29144efb7e67551" -> "0xa5a56e97fb46f0ac3a3d261e404acb998d9a6969" ~ Updated config staticAggregationIsm.modules.1.messageIdMultisigIsm.validators.0: "0x95c7bf235837cb5a609fe6c95870410b9f68bcff" -> "0x4d966438fe9e2b1e7124c87bbb90cb4f0f6c59a1" ~ Updated config staticAggregationIsm.modules.1.messageIdMultisigIsm.validators.1: "0xa5a56e97fb46f0ac3a3d261e404acb998d9a6969" -> "0x95c7bf235837cb5a609fe6c95870410b9f68bcff" ~ Updated config staticAggregationIsm.modules.1.messageIdMultisigIsm.validators.2: "0xbb5842ae0e05215b53df4787a29144efb7e67551" -> "0xa5a56e97fb46f0ac3a3d261e404acb998d9a6969" ``` ### Backward compatibility Yes ### Testing Manualpull/4246/head
parent
fe9365fdb3
commit
c21a99abf8
@ -0,0 +1,258 @@ |
||||
import chalk from 'chalk'; |
||||
import { addedDiff, deletedDiff, updatedDiff } from 'deep-object-diff'; |
||||
import stringify from 'json-stable-stringify'; |
||||
import { fromError } from 'zod-validation-error'; |
||||
|
||||
import { |
||||
CheckerViolation, |
||||
ConnectionClientViolationType, |
||||
CoreViolationType, |
||||
IsmConfigSchema, |
||||
MailboxViolation, |
||||
MailboxViolationType, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
|
||||
interface AnyObject { |
||||
[key: string]: any; |
||||
} |
||||
|
||||
const enum ChangeType { |
||||
Added = 'Added', |
||||
Deleted = 'Deleted', |
||||
Updated = 'Updated', |
||||
} |
||||
|
||||
const changeTypeMapping: Record<ChangeType, string> = { |
||||
[ChangeType.Added]: '+ Added to config', |
||||
[ChangeType.Deleted]: '- Deleted from config', |
||||
[ChangeType.Updated]: '~ Updated config', |
||||
}; |
||||
|
||||
const ignoreFields = ['address', 'ownerOverrides']; |
||||
|
||||
function updatePath(json: AnyObject, path: string): string { |
||||
const pathParts = path.split('.'); |
||||
const newPathParts: string[] = []; |
||||
let currentObject: AnyObject | undefined = json; |
||||
|
||||
for (let i = 0; i < pathParts.length; i++) { |
||||
const part = pathParts[i]; |
||||
|
||||
if (currentObject && typeof currentObject === 'object') { |
||||
if ('type' in currentObject) { |
||||
newPathParts.push(currentObject.type); |
||||
} |
||||
|
||||
if (part in currentObject) { |
||||
currentObject = currentObject[part]; |
||||
} else { |
||||
currentObject = undefined; |
||||
} |
||||
} |
||||
|
||||
newPathParts.push(part); |
||||
} |
||||
|
||||
return newPathParts.join('.'); |
||||
} |
||||
|
||||
function getElement(config: AnyObject | undefined, path: string) { |
||||
const parts = path.split('.'); |
||||
let currentObject: AnyObject | undefined = config; |
||||
|
||||
for (const part of parts) { |
||||
if (currentObject && typeof currentObject === 'object') { |
||||
if (part in currentObject) { |
||||
currentObject = currentObject[part]; |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
return currentObject; |
||||
} |
||||
|
||||
function toLowerCaseValues(obj: any): any { |
||||
if (typeof obj === 'string') { |
||||
return obj.toLowerCase(); |
||||
} |
||||
|
||||
if (typeof obj !== 'object' || obj === null) { |
||||
return obj; |
||||
} |
||||
|
||||
if (Array.isArray(obj)) { |
||||
return obj.map(toLowerCaseValues); |
||||
} |
||||
|
||||
return Object.keys(obj).reduce((acc: any, key: string) => { |
||||
if (key !== 'type') { |
||||
acc[key] = toLowerCaseValues(obj[key]); |
||||
} else { |
||||
acc[key] = obj[key]; |
||||
} |
||||
return acc; |
||||
}, {}); |
||||
} |
||||
|
||||
function sortArraysByType(obj: AnyObject): AnyObject { |
||||
if (Array.isArray(obj)) { |
||||
return obj |
||||
.sort((a, b) => { |
||||
if (a.type < b.type) return -1; |
||||
if (a.type > b.type) return 1; |
||||
return 0; |
||||
}) |
||||
.map((item) => sortArraysByType(item)); |
||||
} else if (typeof obj === 'object' && obj !== null) { |
||||
const sortedObj: AnyObject = {}; |
||||
Object.keys(obj).forEach((key) => { |
||||
sortedObj[key] = sortArraysByType(obj[key]); |
||||
}); |
||||
return sortedObj; |
||||
} |
||||
return obj; |
||||
} |
||||
|
||||
function removeFields(obj: any): any { |
||||
if (Array.isArray(obj)) { |
||||
return obj.map(removeFields); |
||||
} else if (obj !== null && typeof obj === 'object') { |
||||
return Object.keys(obj).reduce((result: any, key: string) => { |
||||
if (!ignoreFields.includes(key)) { |
||||
result[key] = removeFields(obj[key]); |
||||
} |
||||
return result; |
||||
}, {}); |
||||
} else { |
||||
return obj; |
||||
} |
||||
} |
||||
|
||||
function logDiff(expected: AnyObject, actual: AnyObject): void { |
||||
const sortedExpected = sortArraysByType(expected); |
||||
const sortedActual = sortArraysByType(actual); |
||||
|
||||
const sortedExpectedJson = stringify(sortedExpected, { space: 2 }); |
||||
const sortedActualJson = stringify(sortedActual, { space: 2 }); |
||||
|
||||
const parsedSortedExpected = JSON.parse(sortedExpectedJson); |
||||
const parsedSortedActual = JSON.parse(sortedActualJson); |
||||
|
||||
const added = addedDiff(parsedSortedActual, parsedSortedExpected); |
||||
const deleted = deletedDiff(parsedSortedActual, parsedSortedExpected); |
||||
const updated = updatedDiff(parsedSortedActual, parsedSortedExpected); |
||||
|
||||
const logChanges = ( |
||||
changes: AnyObject, |
||||
changeType: ChangeType, |
||||
color: (text: string) => string, |
||||
config: AnyObject, |
||||
otherConfig?: AnyObject, |
||||
) => { |
||||
const logChange = (obj: AnyObject, path: string = '') => { |
||||
Object.keys(obj).forEach((key) => { |
||||
const currentPath = path ? `${path}.${key}` : key; |
||||
if ( |
||||
typeof obj[key] === 'object' && |
||||
obj[key] !== null && |
||||
!Array.isArray(obj[key]) |
||||
) { |
||||
logChange(obj[key], currentPath); |
||||
} else { |
||||
if (changeType !== ChangeType.Updated) { |
||||
console.log( |
||||
color( |
||||
`${changeTypeMapping[changeType]} ${updatePath( |
||||
config, |
||||
currentPath, |
||||
)}: ${stringify(obj[key], { |
||||
space: 2, |
||||
})}`,
|
||||
), |
||||
); |
||||
} else { |
||||
console.log( |
||||
color( |
||||
`${changeTypeMapping[changeType]} ${updatePath( |
||||
config, |
||||
currentPath, |
||||
)}: ${stringify(getElement(otherConfig, currentPath), { |
||||
space: 2, |
||||
})} -> ${stringify(obj[key], { space: 2 })}`,
|
||||
), |
||||
); |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
logChange(changes); |
||||
}; |
||||
|
||||
logChanges(added, ChangeType.Added, chalk.green, parsedSortedActual); |
||||
logChanges(deleted, ChangeType.Deleted, chalk.red, parsedSortedActual); |
||||
logChanges( |
||||
updated, |
||||
ChangeType.Updated, |
||||
chalk.yellow, |
||||
parsedSortedExpected, |
||||
parsedSortedActual, |
||||
); |
||||
} |
||||
|
||||
function preProcessConfig(config: any) { |
||||
return removeFields(toLowerCaseValues(config)); |
||||
} |
||||
|
||||
function logViolationDetail(violation: CheckerViolation): void { |
||||
const preProcessedExpectedConfig = preProcessConfig(violation.expected); |
||||
const preProcessedActualConfig = preProcessConfig(violation.actual); |
||||
|
||||
const expectedConfigResult = IsmConfigSchema.safeParse( |
||||
preProcessedExpectedConfig, |
||||
); |
||||
|
||||
const actualConfigResult = IsmConfigSchema.safeParse( |
||||
preProcessedActualConfig, |
||||
); |
||||
|
||||
if (!expectedConfigResult.success || !actualConfigResult.success) { |
||||
if (!expectedConfigResult.success) { |
||||
console.error( |
||||
'Failed to parse expected config', |
||||
fromError(expectedConfigResult.error).toString(), |
||||
); |
||||
} |
||||
if (!actualConfigResult.success) { |
||||
console.error( |
||||
'Failed to parse actual config', |
||||
fromError(actualConfigResult.error).toString(), |
||||
); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
logDiff(preProcessedExpectedConfig, preProcessedActualConfig); |
||||
} |
||||
|
||||
export function logViolationDetails(violations: CheckerViolation[]): void { |
||||
for (const violation of violations) { |
||||
if (violation.type === CoreViolationType.Mailbox) { |
||||
const mailboxViolation = violation as MailboxViolation; |
||||
if (mailboxViolation.subType === MailboxViolationType.DefaultIsm) { |
||||
console.log(`Mailbox violation ${mailboxViolation.subType} details:`); |
||||
logViolationDetail(violation); |
||||
} |
||||
} |
||||
|
||||
if ( |
||||
violation.type === ConnectionClientViolationType.InterchainSecurityModule |
||||
) { |
||||
console.log(`Connection client violation ${violation.type} details:`); |
||||
logViolationDetail(violation); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue