Merge pull request #41 from hyperlane-xyz/ism-quorum-check

Implement multisig ISM checker in debugger
pull/44/head
J M Rossy 2 years ago committed by GitHub
commit 9d096b444a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 119
      src/features/debugger/debugMessage.ts
  2. 1
      src/features/debugger/strings.ts
  3. 18
      src/features/debugger/types.ts
  4. 14
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  5. 10
      src/features/deliveryStatus/types.ts
  6. 10
      src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  7. 10
      src/features/messages/MessageDetails.tsx
  8. 64
      src/features/messages/cards/IsmDetailsCard.tsx
  9. 2
      src/features/messages/cards/KeyValueRow.tsx
  10. 17
      src/features/messages/cards/TransactionCard.tsx
  11. 4
      src/images/icons/shield-lock.svg

@ -2,12 +2,19 @@
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/infra/scripts/debug-message.ts
import { BigNumber, utils as ethersUtils, providers } from 'ethers';
import { IMessageRecipient__factory, InterchainGasPaymaster__factory } from '@hyperlane-xyz/core';
import {
IInterchainSecurityModule__factory,
IMailbox__factory,
IMessageRecipient__factory,
IMultisigIsm__factory,
InterchainGasPaymaster__factory,
} from '@hyperlane-xyz/core';
import type { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import { MAILBOX_VERSION } from '../../consts/environments';
import { Message } from '../../types';
import { trimLeading0x } from '../../utils/addresses';
import { isValidAddress, trimLeading0x } from '../../utils/addresses';
import { errorToString } from '../../utils/errors';
import { logger } from '../../utils/logger';
import { trimToLength } from '../../utils/string';
@ -15,37 +22,46 @@ import type { ChainConfig } from '../chains/chainConfig';
import { getContractAddress } from '../chains/utils';
import { isIcaMessage, tryDecodeIcaBody, tryFetchIcaAddress } from '../messages/ica';
import { GasPayment, MessageDebugDetails, MessageDebugStatus } from './types';
import { GasPayment, IsmModuleTypes, MessageDebugResult, MessageDebugStatus } from './types';
type Provider = providers.Provider;
const HANDLE_FUNCTION_SIG = 'handle(uint32,bytes32,bytes)';
export async function debugExplorerMessage(
export async function debugMessage(
multiProvider: MultiProvider,
customChainConfigs: ChainMap<ChainConfig>,
message: Message,
): Promise<MessageDebugDetails> {
const {
{
msgId,
nonce,
sender,
recipient,
originDomainId: originDomain,
destinationDomainId: destDomain,
body,
totalGasAmount,
} = message;
}: Message,
): Promise<MessageDebugResult> {
logger.debug(`Debugging message id: ${msgId}`);
const messageBytes = utils.formatMessage(
MAILBOX_VERSION,
nonce,
originDomain,
sender,
destDomain,
recipient,
body,
);
const destName = multiProvider.tryGetChainName(destDomain)!;
const originProvider = multiProvider.getProvider(originDomain);
const destProvider = multiProvider.getProvider(destDomain);
const senderBytes = utils.addressToBytes32(sender);
const recipInvalid = await isInvalidRecipient(destProvider, recipient);
if (recipInvalid) return recipInvalid;
const destMailbox = getContractAddress(customChainConfigs, destName, 'mailbox');
const senderBytes = utils.addressToBytes32(sender);
const deliveryResult = await debugMessageDelivery(
originDomain,
destMailbox,
@ -55,19 +71,34 @@ export async function debugExplorerMessage(
senderBytes,
body,
);
if (deliveryResult.status && deliveryResult.details) return deliveryResult;
if (deliveryResult.status && deliveryResult.description) return deliveryResult;
const gasEstimate = deliveryResult.gasEstimate;
const ismCheckResult = await checkMultisigIsmEmpty(
recipient,
messageBytes,
destMailbox,
destProvider,
);
if (ismCheckResult.status && ismCheckResult.description) return ismCheckResult;
const ismDetails = ismCheckResult.ismDetails;
const gasCheckResult = await tryCheckIgpGasFunded(
msgId,
originProvider,
gasEstimate,
totalGasAmount,
);
if (gasCheckResult.status && gasCheckResult.details) return gasCheckResult;
if (gasCheckResult?.status && gasCheckResult?.description)
return { ...gasCheckResult, ismDetails };
const gasDetails = gasCheckResult?.gasDetails;
logger.debug(`No errors found debugging message id: ${msgId}`);
return { ...noErrorFound(), gasDetails: gasCheckResult.gasDetails };
return {
...noErrorFound(),
gasDetails,
ismDetails,
};
}
async function isInvalidRecipient(provider: Provider, recipient: Address) {
@ -76,7 +107,7 @@ async function isInvalidRecipient(provider: Provider, recipient: Address) {
logger.info(`Recipient address ${recipient} is not a contract`);
return {
status: MessageDebugStatus.RecipientNotContract,
details: `Recipient address is ${recipient}. Ensure that the bytes32 value is not malformed.`,
description: `Recipient address is ${recipient}. Ensure that the bytes32 value is not malformed.`,
};
}
return false;
@ -123,7 +154,7 @@ async function debugMessageDelivery(
logger.info('Bytecode does not have function matching handle sig');
return {
status: MessageDebugStatus.RecipientNotHandler,
details: `Recipient contract should have handle function of signature: ${HANDLE_FUNCTION_SIG}. Check that recipient is not a proxy. Error: ${errorReason}`,
description: `Recipient contract should have handle function of signature: ${HANDLE_FUNCTION_SIG}. Check that recipient is not a proxy. Error: ${errorReason}`,
};
}
@ -131,15 +162,57 @@ async function debugMessageDelivery(
if (icaCallErr) {
return {
status: MessageDebugStatus.IcaCallFailure,
details: icaCallErr,
description: icaCallErr,
};
}
return {
status: MessageDebugStatus.HandleCallFailure,
details: errorReason,
description: errorReason,
};
}
}
// TODO, this must check recursively for to handle aggregation/routing isms
async function checkMultisigIsmEmpty(
recipientAddr: Address,
messageBytes: string,
destMailbox: Address,
destProvider: Provider,
) {
const mailbox = IMailbox__factory.connect(destMailbox, destProvider);
const ismAddress = await mailbox.recipientIsm(recipientAddr);
if (!isValidAddress(ismAddress)) {
logger.error(
`Recipient ${recipientAddr} on mailbox ${destMailbox} does not have a valid ISM address: ${ismAddress}`,
);
throw new Error('Recipient ISM is not a valid address');
}
const ism = IInterchainSecurityModule__factory.connect(ismAddress, destProvider);
const moduleType = await ism.moduleType();
const ismDetails = { ismAddress, moduleType };
if (moduleType !== IsmModuleTypes.LEGACY_MULTISIG && moduleType !== IsmModuleTypes.MULTISIG) {
return { ismDetails };
}
const multisigIsm = IMultisigIsm__factory.connect(ismAddress, destProvider);
const [validators, threshold] = await multisigIsm.validatorsAndThreshold(messageBytes);
if (!validators?.length) {
return {
status: MessageDebugStatus.MultisigIsmEmpty,
description: 'Validator list is empty, has the ISM been configured correctly?',
ismDetails,
};
} else if (threshold < 1) {
return {
status: MessageDebugStatus.MultisigIsmEmpty,
description: 'Threshold is less than 1, has the ISM been configured correctly?',
ismDetails,
};
}
return { ismDetails };
}
async function tryCheckIgpGasFunded(
@ -150,11 +223,11 @@ async function tryCheckIgpGasFunded(
) {
if (!deliveryGasEstimate) {
logger.warn('No gas estimate provided, skipping IGP check');
return {};
return null;
}
try {
let gasAlreadyFunded = BigNumber.from(0);
let gasDetails: MessageDebugDetails['gasDetails'] = {
let gasDetails: MessageDebugResult['gasDetails'] = {
deliveryGasEstimate,
};
if (totalGasAmount && BigNumber.from(totalGasAmount).gt(0)) {
@ -179,13 +252,13 @@ async function tryCheckIgpGasFunded(
if (gasAlreadyFunded.lte(0)) {
return {
status: MessageDebugStatus.GasUnderfunded,
details: 'Origin IGP has not received any gas payments',
description: 'Origin IGP has not received any gas payments',
gasDetails,
};
} else if (gasAlreadyFunded.lte(deliveryGasEstimate)) {
return {
status: MessageDebugStatus.GasUnderfunded,
details: `Origin IGP gas amount is ${gasAlreadyFunded.toString()} but requires ${deliveryGasEstimate}`,
description: `Origin IGP gas amount is ${gasAlreadyFunded.toString()} but requires ${deliveryGasEstimate}`,
gasDetails,
};
} else {
@ -193,7 +266,7 @@ async function tryCheckIgpGasFunded(
}
} catch (error) {
logger.warn('Error estimating delivery gas cost for message', error);
return {};
return null;
}
}
@ -311,9 +384,9 @@ function extractReasonString(rawError: any) {
}
}
function noErrorFound(): MessageDebugDetails {
function noErrorFound(): MessageDebugResult {
return {
status: MessageDebugStatus.NoErrorsFound,
details: 'Message may just need more time to be processed',
description: 'Message may just need more time to be processed',
};
}

@ -7,5 +7,6 @@ export const debugStatusToDesc: Record<MessageDebugStatus, string> = {
'Recipient bytecode is missing handle function selector',
[MessageDebugStatus.IcaCallFailure]: 'A call from the ICA account failed',
[MessageDebugStatus.HandleCallFailure]: 'Error calling handle on the recipient contract',
[MessageDebugStatus.MultisigIsmEmpty]: 'ISM has no validators and/or no quorum threshold',
[MessageDebugStatus.GasUnderfunded]: 'Insufficient interchain gas has been paid for delivery',
};

@ -4,19 +4,33 @@ export enum MessageDebugStatus {
RecipientNotHandler = 'recipientNotHandler',
IcaCallFailure = 'icaCallFailure',
HandleCallFailure = 'handleCallFailure',
MultisigIsmEmpty = 'multisigIsmEmpty',
GasUnderfunded = 'gasUnderfunded',
}
export interface MessageDebugDetails {
export interface MessageDebugResult {
status: MessageDebugStatus;
details: string;
description: string;
gasDetails?: {
deliveryGasEstimate?: string;
contractToPayments?: AddressTo<GasPayment[]>;
};
ismDetails?: {
ismAddress: Address;
moduleType: IsmModuleTypes;
};
}
export interface GasPayment {
gasAmount: string;
paymentAmount: string;
}
// Must match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/solidity/contracts/interfaces/IInterchainSecurityModule.sol#L5
export enum IsmModuleTypes {
UNUSED,
ROUTING,
AGGREGATION,
LEGACY_MULTISIG,
MULTISIG,
}

@ -8,7 +8,7 @@ import { logger } from '../../utils/logger';
import { toDecimalNumber } from '../../utils/number';
import type { ChainConfig } from '../chains/chainConfig';
import { getContractAddress } from '../chains/utils';
import { debugExplorerMessage } from '../debugger/debugMessage';
import { debugMessage } from '../debugger/debugMessage';
import { MessageDebugStatus } from '../debugger/types';
import {
@ -61,20 +61,14 @@ export async function fetchDeliveryStatus(
};
return result;
} else {
const {
status: debugStatus,
details: debugDetails,
gasDetails,
} = await debugExplorerMessage(multiProvider, customChainConfigs, message);
const debugResult = await debugMessage(multiProvider, customChainConfigs, message);
const messageStatus =
debugStatus === MessageDebugStatus.NoErrorsFound
debugResult.status === MessageDebugStatus.NoErrorsFound
? MessageStatus.Pending
: MessageStatus.Failing;
const result: MessageDeliveryPendingResult | MessageDeliveryFailingResult = {
status: messageStatus,
debugStatus,
debugDetails,
gasDetails,
debugResult,
};
return result;
}

@ -1,5 +1,5 @@
import type { MessageStatus, MessageTx } from '../../types';
import type { MessageDebugDetails, MessageDebugStatus } from '../debugger/types';
import type { MessageDebugResult } from '../debugger/types';
interface MessageDeliveryResult {
status: MessageStatus;
@ -12,16 +12,12 @@ export interface MessageDeliverySuccessResult extends MessageDeliveryResult {
export interface MessageDeliveryFailingResult extends MessageDeliveryResult {
status: MessageStatus.Failing;
debugStatus: MessageDebugStatus;
debugDetails: string;
gasDetails: MessageDebugDetails['gasDetails'];
debugResult: MessageDebugResult;
}
export interface MessageDeliveryPendingResult extends MessageDeliveryResult {
status: MessageStatus.Pending;
debugStatus: MessageDebugStatus;
debugDetails: string;
gasDetails: MessageDebugDetails['gasDetails'];
debugResult: MessageDebugResult;
}
export type MessageDeliveryStatusResponse =

@ -54,7 +54,7 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
}
}, [error]);
const [messageWithDeliveryStatus, debugInfo] = useMemo(() => {
const [messageWithDeliveryStatus, debugResult] = useMemo(() => {
if (data?.status === MessageStatus.Delivered) {
return [
{
@ -69,16 +69,12 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
...message,
status: data.status,
},
{
status: data.debugStatus,
details: data.debugDetails,
gasDetails: data.gasDetails,
},
data.debugResult,
];
} else {
return [message];
}
}, [message, data]);
return { messageWithDeliveryStatus, debugInfo, isDeliveryStatusFetching: isFetching };
return { messageWithDeliveryStatus, debugResult, isDeliveryStatusFetching: isFetching };
}

@ -15,6 +15,7 @@ import { useMultiProvider } from '../providers/multiProvider';
import { ContentDetailsCard } from './cards/ContentDetailsCard';
import { GasDetailsCard } from './cards/GasDetailsCard';
import { IcaDetailsCard } from './cards/IcaDetailsCard';
import { IsmDetailsCard } from './cards/IsmDetailsCard';
import { TimelineCard } from './cards/TimelineCard';
import { DestinationTransactionCard, OriginTransactionCard } from './cards/TransactionCard';
import { useIsIcaMessage } from './ica';
@ -67,7 +68,7 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
// more recent updates and possibly debug info
const {
messageWithDeliveryStatus: message,
debugInfo,
debugResult,
isDeliveryStatusFetching,
} = useMessageDeliveryStatus({
message: _message,
@ -103,7 +104,7 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
chainId={destChainId}
status={status}
transaction={destination}
debugInfo={debugInfo}
debugResult={debugResult}
isStatusFetching={isDeliveryStatusFetching}
isPiMsg={message.isPiMsg}
blur={blur}
@ -112,9 +113,12 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
<ContentDetailsCard message={message} blur={blur} />
<GasDetailsCard
message={message}
igpPayments={debugInfo?.gasDetails?.contractToPayments}
igpPayments={debugResult?.gasDetails?.contractToPayments}
blur={blur}
/>
{debugResult?.ismDetails && (
<IsmDetailsCard ismDetails={debugResult.ismDetails} blur={blur} />
)}
{isIcaMsg && <IcaDetailsCard message={message} blur={blur} />}
</div>
</>

@ -0,0 +1,64 @@
import Image from 'next/image';
import { HelpIcon } from '../../../components/icons/HelpIcon';
import { Card } from '../../../components/layout/Card';
import { links } from '../../../consts/links';
import ShieldLock from '../../../images/icons/shield-lock.svg';
import { isNullish } from '../../../utils/typeof';
import { IsmModuleTypes, MessageDebugResult } from '../../debugger/types';
import { KeyValueRow } from './KeyValueRow';
interface Props {
ismDetails: MessageDebugResult['ismDetails'];
blur: boolean;
}
export function IsmDetailsCard({ ismDetails, blur }: Props) {
return (
<Card classes="w-full space-y-4 relative">
<div className="flex items-center justify-between">
<Image src={ShieldLock} width={24} height={24} alt="" className="opacity-80" />
<div className="flex items-center pb-1">
<h3 className="text-gray-500 font-medium text-md mr-2">Interchain Security Modules</h3>
<HelpIcon
size={16}
text="Details about the Interchain Security Modules (ISM) that must verify this message."
/>
</div>
</div>
<p className="text-sm">
Interchain Security Modules define the rules for verifying messages before delivery.{' '}
<a
href={`${links.docs}/docs/protocol/sovereign-consensus`}
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer text-blue-500 hover:text-blue-400 active:text-blue-300 transition-all"
>
Learn more about ISMs.
</a>
</p>
<KeyValueRow
label="ISM Address:"
labelWidth="w-24"
display={ismDetails?.ismAddress || ''}
showCopy={true}
blurValue={blur}
/>
<KeyValueRow
label="Module Type:"
labelWidth="w-24"
display={!isNullish(ismDetails?.moduleType) ? IsmLabels[ismDetails!.moduleType] : ''}
blurValue={blur}
/>
</Card>
);
}
const IsmLabels: Record<IsmModuleTypes, string> = {
[IsmModuleTypes.UNUSED]: 'Unused',
[IsmModuleTypes.ROUTING]: 'Routing',
[IsmModuleTypes.AGGREGATION]: 'Aggregation',
[IsmModuleTypes.LEGACY_MULTISIG]: 'Legacy Multisig',
[IsmModuleTypes.MULTISIG]: 'Multisig',
};

@ -33,7 +33,7 @@ export function KeyValueRow({
{subDisplay && !useFallbackVal && <span className="text-xs ml-2">{subDisplay}</span>}
</div>
{showCopy && !useFallbackVal && (
<CopyButton copyValue={display} width={13} height={13} classes="ml-1" />
<CopyButton copyValue={display} width={13} height={13} classes="ml-1.5" />
)}
</div>
);

@ -8,7 +8,7 @@ import { MessageStatus, MessageTx } from '../../../types';
import { getDateTimeString, getHumanReadableTimeString } from '../../../utils/time';
import { getChainDisplayName } from '../../chains/utils';
import { debugStatusToDesc } from '../../debugger/strings';
import { MessageDebugStatus } from '../../debugger/types';
import { MessageDebugResult } from '../../debugger/types';
import { useMultiProvider } from '../../providers/multiProvider';
import { KeyValueRow } from './KeyValueRow';
@ -33,7 +33,7 @@ export function DestinationTransactionCard({
chainId,
status,
transaction,
debugInfo,
debugResult,
isStatusFetching,
isPiMsg,
blur,
@ -41,10 +41,7 @@ export function DestinationTransactionCard({
chainId: ChainId;
status: MessageStatus;
transaction?: MessageTx;
debugInfo?: {
status: MessageDebugStatus;
details: string;
};
debugResult?: MessageDebugResult;
isStatusFetching: boolean;
isPiMsg?: boolean;
blur: boolean;
@ -52,7 +49,7 @@ export function DestinationTransactionCard({
let content: ReactNode;
if (transaction) {
content = <TransactionDetails chainId={chainId} transaction={transaction} blur={blur} />;
} else if (!debugInfo && isStatusFetching) {
} else if (!debugResult && isStatusFetching) {
content = (
<DeliveryStatus>
<div>Checking delivery status and inspecting message</div>
@ -63,13 +60,13 @@ export function DestinationTransactionCard({
content = (
<DeliveryStatus>
<div className="text-gray-700">Delivery to destination chain is currently failing</div>
{debugInfo && (
{debugResult && (
<>
<div className="mt-4 text-gray-700 text-center">
{debugStatusToDesc[debugInfo.status]}
{debugStatusToDesc[debugResult.status]}
</div>
<div className="mt-4 text-gray-700 text-sm max-w-sm text-center break-words">
{debugInfo.details}
{debugResult.description}
</div>
</>
)}

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-shield-lock" viewBox="0 0 16 16">
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z"/>
<path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Loading…
Cancel
Save