-
Notifications
You must be signed in to change notification settings - Fork 42
External interop poc #879
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
base: main
Are you sure you want to change the base?
External interop poc #879
Conversation
…o making sure commits are signed correctly :)
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.
Semgrep PRO found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
Signed-off-by: Jacob Turner <[email protected]>
| import {ITeleporterReceiver} from "./ITeleporterReceiver.sol"; | ||
| import {ReentrancyGuards} from "@utilities/ReentrancyGuards.sol"; | ||
| import {IWarpExt} from "./IWarpExt.sol"; | ||
| import {IWarpMessenger} from "../../lib/subnet-evm/contracts/contracts/interfaces/IWarpMessenger.sol"; |
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.
Just a heads up, in main we've removed the subnet-evm submodule in favor of just copying over the 3 contracts we use from there.
| package utils | ||
|
|
||
| import ( | ||
| "bytes" |
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.
Looks like your IDE went and replaced the tabs with spaces in this files and teleporter_utils. Tbh I didn't even know we used tabs, but definitely don't want the entire file in the diff
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 noticed that. Sigh. Although it is much better to have spaces than tabs, that is definitely not something we should do in this PR. The IDE is currently set to autoformat on file save and I guess that did it
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 used to be in the spaces camp but gofmt uses tabs and we should do the same.
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.
Ok, I've adjusted the settings in my IDE and hopefully that fixes the issue.
| uint256 validatorSetID = nextValidatorSetID++; | ||
| _validatorSets[validatorSetID] = validatorSet; |
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.
Is there a reason you chose a monotonic counter here instead of using something like the avalancheBlockChainID or a hash of avalancheNetworkID, avalancheBlockChainID?
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 inherited this decision, but in its defense, it makes finding the latest n validator sets very easy. This is nice for updating the current validator set and for only validating sufficiently fresh messages.
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.
Also, these are all the validators for a specific L1, so they can't be keyed by avalancheNetworkID, avalancheBlockChainID, etc as those will be constant
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 guess the current setup allows registering the validators of any given L1 multiple times, whereas keying on avalancheBlockChainID would require a single set
Co-authored-by: Geoff Stuart <[email protected]> Signed-off-by: Jacob Turner <[email protected]>
| sourceChainID: message.avalancheSourceBlockchainID, | ||
| // N.B. This prevents this function from being used for internal interop calls | ||
| // to teleporter | ||
| originSenderAddress: address(0), |
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 we need to use the actual sender address here (eventually). We'll need a way of initializing this contract with the address of teleporter v2 on Avalanche. If we don't check that the message was emitted by teleporter v2, any contract on Avalanche could spoof any other contract on the same source blockchain.
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.
So I'm not sure about this. It may not be clear from this function's location (which can be fixed), but this is called only by EthWarp (which is called directly by teleporter) after checking for a quorum of signatures. So I think we trust any message signed by a quorum of signatures, regardless of origin.
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.
So yes, we've checked that the message was signed by a quorum of signatures, but we don't know what contract sent the message. The reason we use Nick's method to deploy Teleporter to the same address on Avalanche is so that on the receiving side, we can know exactly what code emitted the warp message.
As-is, a malicious contract on the same chain could emit a warp message that exactly spoofs any other Teleporter message. Basically the way this contract will (probably) have to work is that we'll create the Avalanche Teleporter V2 contract, deploy via Nick's method, and then hard-code that address here so we can verify which contract emitted the message.
| for (uint256 i = 0; i < length; i++) { | ||
| target[cursor + i] = source[i]; | ||
| } | ||
| return (target, cursor + length); |
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 the new ending position is deterministic, we don't really need to calculate and return it here
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.
indeed, it was just to keep the function calling this clean.
| // TODO: Do we need to check the avalanche source blockchain ID? | ||
| // It's expected to be different in cases where the message is from the primary network. | ||
| // if ( | ||
| // message.unsignedMessage.avalancheSourceBlockchainID | ||
| // != validatorSet.avalancheBlockchainID | ||
| // ) { | ||
| // revert("Invalid avalanche source blockchain ID"); | ||
| // } |
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, we definitely need to check the source blockchain ID to know which validator set to compare it against (if we want to allow paths other than Eth <-> C-chain)
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.
So there is a subtlety here. This function needs to verify two different types of messages: normal teleporter messages and validator set updates. In the first instance, this check is done at a higher scope by the calling EthWarp contract. For the second, EthWarp is bypassed altogether since it is outside the scope of teleporter. In the e2e test, the initial validator set is actually signed over by the P-chain, which would cause this check to fail. Hence the comment
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.
Got it. So for validator set updates, message.unsignedMessage.avalancheSourceBlockchainID would be the p-chain, but validatorSet.avalancheBlockchainID would be the blockchainID of the validator set being updated.
ethereum/contracts/utils/ICM.sol
Outdated
| if (!result) { | ||
| revert("Invalid signature"); | ||
| } | ||
| return result; |
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.
It seems strange to have both reverts and a boolean return value. We can go with one or the other
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.
Hah! I hadn't even noticed that
Co-authored-by: Geoff Stuart <[email protected]> Signed-off-by: Jacob Turner <[email protected]>
| */ | ||
| function registerChain(bytes32 avaBlockchainId, address verifyWarpMessage) external { | ||
| require(!isChainRegistered(avaBlockchainId), "This chain is already registered"); | ||
| require(verifyWarpMessage != address(0), "Provided address does not exist"); |
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.
No need for a change yet, but the way we implemented registering contracts in ICTT iirc is that we had a register method on the given contract (so on the verifyWarpMessage contract in this case) so that we could at least be sure that the address supplied corresponded to a contract with the register method and wasn't incorrectly input (though someone could create a malicious dummy contract and point to that).
| function bytesToBoolArray( | ||
| bytes memory data | ||
| ) internal pure returns (bool[] memory) { |
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'm not sure if its feasible, but it may be more efficient to operate on the bitset directly instead of transforming it into a bool array (can be a future investigation)
| // TODO: Do we need to check the avalanche source blockchain ID? | ||
| // It's expected to be different in cases where the message is from the primary network. | ||
| // if ( | ||
| // message.unsignedMessage.avalancheSourceBlockchainID | ||
| // != validatorSet.avalancheBlockchainID | ||
| // ) { | ||
| // revert("Invalid avalanche source blockchain ID"); | ||
| // } |
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.
Got it. So for validator set updates, message.unsignedMessage.avalancheSourceBlockchainID would be the p-chain, but validatorSet.avalancheBlockchainID would be the blockchainID of the validator set being updated.
| sourceChainID: message.avalancheSourceBlockchainID, | ||
| // N.B. This prevents this function from being used for internal interop calls | ||
| // to teleporter | ||
| originSenderAddress: address(0), |
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.
So yes, we've checked that the message was signed by a quorum of signatures, but we don't know what contract sent the message. The reason we use Nick's method to deploy Teleporter to the same address on Avalanche is so that on the receiving side, we can know exactly what code emitted the warp message.
As-is, a malicious contract on the same chain could emit a warp message that exactly spoofs any other Teleporter message. Basically the way this contract will (probably) have to work is that we'll create the Avalanche Teleporter V2 contract, deploy via Nick's method, and then hard-code that address here so we can verify which contract emitted the message.
| require( | ||
| ByteComparator.compare(unformattedPublicKey, previousPublicKey) > 0, | ||
| "BLS public key must be greater than the latest public key" | ||
| ); | ||
| uint64 weight = uint64(bytes8(ByteSlicer.slice(data, offset + 96, 8))); | ||
| require(weight > 0, "Validator weight must be greater than 0"); |
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.
For now best to keep these checks, but we can probably assume if the message is signed by a quorum of validators that it is indeed valid.
| return ValidatorSetStatePayload({ | ||
| avalancheBlockchainID: avalancheBlockchainID, | ||
| pChainHeight: pChainHeight, | ||
| pChainTimestamp: pChainTimestamp, | ||
| validatorSetHash: validatorSetHash | ||
| }); |
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.
Is this a message that the p-chain has to sign?
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.
That's the way it currently is. In theory, the L1 validator set should be sufficient except for maybe the first message.
| 1. Creates an Avalanche L1 on the Fuji testnet with 3 local validators, running a custom version of AvalancheGo. | ||
| 1. Deploys an `AvalancheValidatorSetRegistry` contract on the Sepolia testnet. | ||
| 1. Registers the Avalanche L1's validator set in the registry via an ICM message from the P-Chain, self-signed by the validator set of the L1. | ||
| 1. Sends a transaction on the L1 to send an ICM message to Sepolia. | ||
| 1. Gets an aggregate signature of the ICM message using the ICM signature aggregator. | ||
| 1. Broadcasts the signed ICM message to Sepolia, and confirms successful inclusion and verification. | ||
| 1. Adds a new validator to the L1, representing 40% of the new total weight. | ||
| 1. Updates the validator set reigstered on the Sepolia registry via a validator set update message signed by sufficient weight of the previous validator set. | ||
| 1. Sends and verifies another ICM message from the L1 to Sepolia. |
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.
Numbering here is broken
|
I was wondering if this PR could be broken up into multiple smaller, standalone PRs? The breadth of the PR makes it a bit harder to review |
Co-authored-by: Geoff Stuart <[email protected]> Signed-off-by: Jacob Turner <[email protected]>
Yes, I'm doing that now. |
This draft PR is to give visibility to the POC for bridging avalanche to ethereum