-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Globally Unique Message IDs #209
Conversation
Co-authored-by: cam-schultz <[email protected]> Signed-off-by: Michael Kaplan <[email protected]>
Co-authored-by: Geoff Stuart <[email protected]> Signed-off-by: Michael Kaplan <[email protected]>
|
||
/** | ||
* @dev Emitted when a Teleporter message is being delivered on the destination chain to an address, | ||
* but message execution fails. Failed messages can then be retried with `retryMessageExecution` | ||
*/ | ||
event MessageExecutionFailed( | ||
bytes32 indexed originBlockchainID, uint256 indexed messageID, TeleporterMessage message | ||
bytes32 indexed messageID, bytes32 indexed originBlockchainID, TeleporterMessage message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if we'd care about originBlockchainID
in this event and MessageExecuted
if message ids are unique
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I thought about removing it entirely, but think it could still be helpful in tracking down the source chain message since that is not known for a given messageID
even though the message ID is unique.
function getNextMessageID(bytes32 destinationBlockchainID) external view returns (uint256) { | ||
return _getNextMessageID(destinationBlockchainID); | ||
function getNextMessageID(bytes32 destinationBlockchainID) external view returns (bytes32) { | ||
bytes32 blockchainID_ = blockchainID; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the decision between this and calling initializeBlockChainID
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we call initializeBlockChainID
, this function could not be view
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm leaning towards initializeBlockChainID
, since we'll want to intiailize it eventually, don't think having view
is a must, and right now you can't getNextMessageID
until it is initialized, so user's can't call it for messageNonce=1
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel pretty strongly about keeping any get
method as view
actually. I think it'd be an anti-pattern to have a get method alter the state of the contract. I agree it's odd to not be able to call getNextMessageID
before a message has been sent or received though. I think the best option here is to just make initializeBlockchainID
public, so that it can be explicitly initialized before sending/receiving messages, if desired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good call, I rethought about it and it is good to have get
methods as read only on state. Agree that we can just have initializeBlockchainID
public.
* value from the Warp precompile. | ||
* @return The current blockchain ID. | ||
*/ | ||
function _initializeBlockchainID() private returns (bytes32) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we initialized move the blockchainID
initialization out of constructor for deployment reasons, but now that we call initialize on a bunch of places, what is the difference with setting it in the state variable declaration again?. i.e.
bytes32 public immutable blockchainID = WARP_MESSENGER.getBlockchainID();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The state variable would still be initialized during deployment, so the same consideration around WARP_MESSENGER.getBlockchainID
reverting would still apply.
…er into use-nonce-for-receipts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally makes sense to me. I've left a few minor comments.
@@ -8,7 +8,7 @@ pragma solidity 0.8.18; | |||
// A message receipt identifies the message ID that was delivered |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the comment be updated to refer to a message nonce instead of a message ID?
mapping(bytes32 messageID => bytes32 messageHash) public receivedFailedMessageHashes; | ||
|
||
// Tracks the message nonce for each message that has been received. The key is the message ID, | ||
// and the value is the nonce value that was received as a part for that message. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "as a part for" -> "as a part of"
|
||
// Tracks the message nonce for each message that has been received. The key is the message ID, | ||
// and the value is the nonce value that was received as a part for that message. | ||
// Note: the `messageNonce` values are also used to determine if a given message has been received or not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should a slightly more extended explanation of the messageNonce be provided here? This might be the first point that someone comes across the term messageNonce.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the comment above for messageNonce
on lines 50-53 explain it in more detail, but happy to expand on it there if you think there's a piece that's missing.
// If the blockchain ID has yet to be initialized, no messages have ever been received by | ||
// this contract, meaning that the receipt(s) will not be found in any event. | ||
// Thus, don't need to initialize the blockchain ID here. | ||
bytes32 blockchainID_ = blockchainID; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree in general that we should avoid code-level optimizations for small gas savings if they hurt readability. It really should be the job of the compiler to perform these optimizations. With that said, because of the reverts that Michael pointed out, the functions will need comments about initialized/uninitialized variables no matter which way we go. So perhaps just having the comments is enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚢
77c1831
Globally Unique Message IDs
🚨 Report Summary
For more details view the full report in OpenZeppelin Code Inspector |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
8ea6f20
Note: The diff of this PR is relatively large due to the number of tests that needed to be updated to account for a type change from uint256 to bytes32. When reviewing, I recommend starting with ITeleporterMessenger.sol and TeleporterMessenger.sol as those files contain the only core changes.
Why this should be merged
Globally unique Teleporter message IDs will provide a significant UX improvement for querying and debugging Teleporter messages.
How it works
Currently the TeleporterMessenger contract assigns a unique ID to each message sent by track how many messages have been sent to a given destination chain, and incrementing the "message ID" to be used messages sent to that chain by each time a message is sent to it. The message ID is used when TeleporterMessenger receives a message to provide replay protection by ensuring that the given message has never been received in the past.
While this is sufficient for the TeleporterMessenger contract itself, these message IDs are not unique across chains or TeleporterMessenger versions. The first message from one TeleporterMessenger instance to any other chain will alway have a message ID of 1, etc. On a global Avalanche level, in order to identify a specific Teleporter message, you must specify:
This makes indexing and querying for Teleporter messages more cumbersome.
In this PR, the unique messageID assigned to each message by the TeleporterMessenger contract is changed from a uint256 value to a bytes32 value. Instead of being a sequence number of the number of message that have been sent by the TeleporterMessenger instance to another blockchain ID, it is computed as:
The message_nonce above is tracked as the total number of message sent from the given TeleporterMessenger instance to any other blockchain ID. The resulting
bytes32
message ID is guaranteed to be unique for each message sent by the contract given that themessage_nonce
value will be different for every message. It can be used in the same exact way as the previous message IDuint256
was to provide replay protection.The new message ID hash is not included in the TeleporterMessage that is sent to the destination. Only the message nonce used to create the message ID hash is included in the message. When
receiveCrossChainMessage
is called to receive a message on the destination, the message ID hash is recomputed by the receiving chain using the source blockchain ID of the Warp message, and the message nonce included in the message. This ensures that message IDs received from different chains will be unique, even if a sending subnet is corrupted.Similarly, message receipts now only track message nonces, and the ID for the corresponding messages are recomputed when the receipt is received back on the origin chain. Destination chains are not able to forge receipts for messages that were sent to different chains because of the fact that the message ID depends on the
destinationBlockchainID
. If a corrupted subnet were to send a receipt with a message nonce for a message that was sent to a different chain, when that receipt is received back on the origin chain, the resulting message ID would not match any message that had been sent, and the "forged" receipt is effectively a no-op.In order to account for
sendSpecificedReceipts
still taking message IDs as a parameter, which is the only value easily available to users,TeleporterMessenger
now tracks_receivedMessageNonces
to map received message IDs to the their nonce, and allow for looking up the nonce value to send for specific receipts. This mapping is know used to provide replay protection rather thanrelayerRewardAddresses
, since message nonces will never be 0. This allows for removing the requirement that therelayerRewardAddress
provided toreceiveCrossChainMessage
be non-zero.These unique message IDs allow for simplifying the
ITeleporterMessenger
interface to no longer need thedestinationBlockchainID
as a parameter for many functions in order to look up a sent message.How this was tested
Unit tests, E2E test
How is this documented
TODO