xdapp-book/src/technical/evm/composableVerification.md

135 lines
5.2 KiB
Markdown
Raw Normal View History

2023-02-16 12:29:03 -08:00
# Composable Verification
Wormhole offers flexible [Consistency Levels](/wormhole/3_coreLayerContracts.md#consistency-levels), but more advanced integrators may want additional layers of verification.
Working examples of the following are available in [this example repo](https://github.com/wormhole-foundation/example-composable-verification).
## Additional Signers
If your project has some additional off-chain processes or checks to perform _after_ a message was emitted but _before_ it could be consumed on the receiving chain, you may want to consider adding an additional signer requirement to your integration. There are plenty of ways to achieve this concept, but an approach like the following keeps the emission and verification in your integrating contract, entirely separate from Wormhole.
### 1. Emit a unique message hash
```solidity
event LogMessageHash(bytes32 hash);
...
bytes32 messageHash = keccak256(
abi.encodePacked(encodedMessage, messageSequence)
);
emit LogMessageHash(messageHash);
```
After calling `publishMessage`, emit a hash for the message and sequence number. This way, the signature will be unique for two different instances of the same message contents. You could also make it more unique across implementations by including the sending chain id and contract address.
### 2. Sign the hash
```solidity
function getSigningHash(bytes32 _messageHash) public view returns (bytes32) {
return
keccak256(abi.encodePacked(_messageHash, block.chainid, address(this)));
}
```
It is helpful to have a utility function on the receiving side so you can generate an even more unique hash which ensures the signature you generate is intended for this receiving chain and contract address.
```typescript
const {
args: { hash },
} = sender.interface.parseLog(log);
const signingHash = await receiver.getSigningHash(hash);
const additionalSignature = await signer.signMessage(
ethers.utils.arrayify(signingHash)
);
```
Have your off-chain process pick up logs via your preferred method (like `finalized` block polling for `eth_getLogs`), perform its duties, and produce a signature.
### 3. Verify the signature
```solidity
function receiveMessage(
bytes memory _vaa,
bytes memory _additionalSignature
) public {
...
require(
verify(
keccak256(
abi.encodePacked(wormholeMessage.payload, wormholeMessage.sequence)
),
_additionalSignature
),
"invalid additional signature"
);
...
}
function verify(
bytes32 _messageHash,
bytes memory _signature
) public view returns (bool) {
bytes32 signingHash = getSigningHash(_messageHash);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(signingHash);
return recoverSigner(ethSignedMessageHash, _signature) == signerAddress;
}
```
Add another parameter to your `receiveMessage` function and after calling `parseAndVerifyVM`, verify that the additional signature checks out!
## Two-Bridge Rule
This example sends a message from Ethereum to Optimism via Wormhole _and_ sends the hash of that message via the [native bridge](https://community.optimism.io/docs/developers/bridge/messaging/). The receiver then requires both messages to agree, like requiring two keys to open a safe.
### 1. Send a unique hash natively
```solidity
/// Optimism L1-L2 bridge from https://community.optimism.io/docs/useful-tools/networks/#optimism-goerli
address public crossDomainMessengerAddr =
0x5086d1eEF304eb5284A0f6720f79403b4e9bE294;
/// Optimism bridge requires a recipient address so the message can be relayed
address public receiverL2Addr;
...
// Send the expected message hash and sequence via the native bridge
bytes32 messageHash = keccak256(
abi.encodePacked(encodedMessage, messageSequence)
);
ICrossDomainMessenger(crossDomainMessengerAddr).sendMessage(
receiverL2Addr,
abi.encodeWithSignature("expectPayload(bytes32)", messageHash),
1000000 // within the free gas limit amount
);
```
Similar to the previous example, after calling `publishMessage`, send a hash for the message and sequence number over the native bridge.
### 2. Receive the expected hash
```solidity
/// Sender contract address for confirming validity of native bridge messages
address public immutable l1SenderAddress;
/// Stores the expected payload hash
bytes32 public expectedPayloadHash;
/// Used by the native bridge to set the expected payload hash
/// This signature must match the ICrossDomainMessenger.sendMessage call in the Sender
/// @param _expectedPayloadHash The hash of the expected payload for the corresponding Wormhole message
function expectPayload(bytes32 _expectedPayloadHash) public {
require(getXorig() == l1SenderAddress, "invalid sender");
expectedPayloadHash = _expectedPayloadHash;
}
```
Again similar to a basic Wormhole integration where you [verify the emitter](./bestPractices.md#receiving-messages), verify that this message came from the expected L1 contract. This example only “expects” one message at a time, but you could just as easily make this a map like `processedMessages`.
### 3. Verify the hashes match
```solidity
require(
keccak256(
abi.encodePacked(wormholeMessage.payload, wormholeMessage.sequence)
) == expectedPayloadHash,
"unexpected payload"
);
```
After calling `parseAndVerifyVM`, verify that the hash checks out!