Skip to content
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

feat: support l2 plus #1157

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open

feat: support l2 plus #1157

wants to merge 20 commits into from

Conversation

karlem
Copy link
Contributor

@karlem karlem commented Sep 26, 2024

Part of #1080

This PR focuses on:

Integration Tests in Solidity (L3-Level Testing)

  • Write and run integration tests to cover the following scenarios:
    • Happy Path Scenarios:
      • From root to L3. - DONE
      • From L3 to root. - DONE
      • Between sibling L3 nodes. - DONE
    • Failure Scenarios:
      • Errors at the destination. - DONE
      • Errors midway through execution. Not sure this is really an issue.
    • Edge Case Testing:
      • Supply source mismatches. - This should really not be an issue - as the only cross net messages allowed are Call kind. (not trasfers)
      • Non-existent destinations. - DONE
    • Ensure all error cases propagate correctly. - DONE

Improvements

  • Improved visibility by adding new events into the cross-message propagation process. A cross-message ID has been added to each event for better tracking and transparency:
    • CheckpointCommitted
    • NewBottomUpMessage
    • MessageStoredInPostbox
    • MessagePropagatedFromPostbox
  • Automated cross-message propagation from the postbox. Previously, this process required an external caller to propagate the message, but now the IPC automatically propagates them after a checkpoint has been committed or after top-down messages have been applied.
  • Added a check to verify that the destination address is indeed a contract and not an EOA (Externally Owned Account).
  • Changed the behavior when applying messages: instead of reverting when a message is invalid, a result receipt is returned. This ensures that one invalid message does not disrupt the entire checkpoint submission process.
  • Extracted message validation into a separate function to ensure validation is completed before committing the message. The validation function does not revert since reversion is only desirable when the message is directly triggered by a user. In scenarios where the message originates from a different subnet (e.g., in the form of a checkpoint), reversion is not appropriate. For the same reason, the commit functions (top-down, bottom-up) should also not revert, as they are triggered by automatic postbox propagation. Instead, validation is performed beforehand.

@karlem karlem changed the title Support l2 plus feat: support l2 plus Oct 11, 2024
@karlem karlem marked this pull request as ready for review October 11, 2024 20:03
@karlem karlem requested a review from a team as a code owner October 11, 2024 20:03
Copy link
Contributor

@raulk raulk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments.

contracts/contracts/gateway/GatewayMessengerFacet.sol Outdated Show resolved Hide resolved
contracts/contracts/gateway/GatewayMessengerFacet.sol Outdated Show resolved Hide resolved
contracts/contracts/gateway/router/CheckpointingFacet.sol Outdated Show resolved Hide resolved
contracts/contracts/lib/AssetHelper.sol Outdated Show resolved Hide resolved
@@ -27,6 +27,7 @@ contract XnetMessagingFacet is GatewayActorModifiers {
/// @dev It requires the caller to be the system actor.
/// @param crossMsgs The array of cross-network messages to be applied.
function applyCrossMessages(IpcEnvelope[] calldata crossMsgs) external systemActorOnly {
LibGateway.applyMessages(s.networkName.getParentSubnet(), crossMsgs);
LibGateway.applyTopDownMessages(s.networkName.getParentSubnet(), crossMsgs);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyCrossMessages only handles topdown messages?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is only used to apply top-down messages locally after they have been finalized in Fendermint.

@@ -199,4 +203,8 @@ library CrossMsgHelper {

return true;
}

function validateCrossMessage(IpcEnvelope memory crossMsg) internal view returns (CrossMessageValidationOutcome) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

caller why not just invoke LibGateway directly instead, it's internal anyways.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raul has suggested a nice syntax sugar update. So I have added it after his comment.

function down(SubnetID calldata subnet1, SubnetID calldata subnet2) public pure returns (SubnetID memory) {
if (subnet1.root != subnet2.root) {
revert DifferentRootNetwork();
return SubnetID({root: 0, route: new address[](0)});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason why it's not reverted? Feels like it's an error that should be handled instead of introducing root = 0 as invalid subnet. Probably in some use cases root: 0, route: [] is a valid use case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like this needs multi-return? SubnetID + bool?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that if I revert, I lose control elsewhere. The method is also used invalidateCrossMessage, where reverting is not desirable. Additionally, commonParent method here returns the empty subnet ID instead of reverting. But I understand your point—if this were deployed on Ethereum, the root could be zero. Would returning a boolean (indicating whether it was found or not) help?

revert NotRegisteredSubnet();
}

function commitTopDownMsg(Subnet storage subnet, IpcEnvelope memory crossMessage) internal {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like we just need the subnet id as the param

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. This also feels unsafe since this function could accidentally modify the subnet state, since it's passed with the storage modifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I can pass the subnetID down, but then I would need to look up the subnet again with getSubnet(subnetId), since this function actually updates the subnet state (and has been).

@@ -199,4 +203,8 @@ library CrossMsgHelper {

return true;
}

function validateCrossMessage(IpcEnvelope memory crossMsg) internal view returns (CrossMessageValidationOutcome) {
return LibGateway.validateCrossMessage(crossMsg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still kind of feel CrossMessageValidationOutcome should not be exposed, unless different callers might handle the outcomes differently? Now every call of validateCrossMessage must handle the outcome, dont feel it's necessary though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless different callers might handle the outcomes differently? - that's exactly the case :)

});

address[] memory nativeSubnetPath;
nativeSubnet = createNativeSubnet(nativeSubnetPath, rootNetwork.gatewayAddr, rootNetwork.id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe call this nativeL2Subnet?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to pass an empty address[] as the first argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raulk Solved by using the parent route instead.

IpcEnvelope memory crossMessage = TestUtils.newXnetCallMsg(
IPCAddress({subnetId: params.subnetL3.id, rawAddress: FvmAddressHelper.from(params.callerAddr)}),
IPCAddress({subnetId: params.root.id, rawAddress: FvmAddressHelper.from(params.recipientAddr)}),
params.amount,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the param amount could exceed the fundSubnet amount?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

// submit checkoint so the result message can be propagated to L2
submitBottomUpCheckpoint(
callCreateBottomUpCheckpointFromChildSubnet(params.subnetL3.id, params.subnetL3.gateway),
params.subnetL3.subnetActor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be subnetL2.subnetActor? The checkpoint should be propagated to L2 right? Not to L3 subnet actor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

params.subnetL3.subnetActor corresponds to the L2 subnet actor. I agree it can be confusing, but I'm building around the already existing TestSubnetDefinition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if you find it super confusing I can rename it. But it might affect a lot of files :)

// this would normally submitted by releayer. It call the subnet actor on the L2 network.
submitBottomUpCheckpoint(
callCreateBottomUpCheckpointFromChildSubnet(params.subnetL3.id, params.subnetL3.gateway),
params.subnetL3.subnetActor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the params.subnetL3.subnetActor refers to the SubnetActorDiamond in L2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

}

function createNativeSubnet(
address[] memory subnetPath,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt this be parent subnet id? How do you know the subnet path before it's created?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is supposed to be the parent subnet path, but it seems redundant since we can just get it from parentId.path?

Copy link
Contributor Author

@karlem karlem Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good catch. The parent id route can be used.

@cryptoAtwill
Copy link
Contributor

@karlem Another question I have is regarding nonce. Say a bottom up message is created in L3, the nonce will be set by the local network. Then in L2, will the cross network message nonce change? If so, then the id (keccak) will change too?

Copy link
Contributor

@raulk raulk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments.

contracts/contracts/errors/IPCErrors.sol Outdated Show resolved Hide resolved
revert NotRegisteredSubnet();
}

function commitTopDownMsg(Subnet storage subnet, IpcEnvelope memory crossMessage) internal {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. This also feels unsafe since this function could accidentally modify the subnet state, since it's passed with the storage modifier.

contracts/contracts/lib/LibGateway.sol Outdated Show resolved Hide resolved
contracts/contracts/lib/LibGateway.sol Outdated Show resolved Hide resolved
@@ -404,11 +444,20 @@ library LibGateway {
// should increase the appliedNonce to allow the execution of the next message
// of the batch (this is way we have this after the nonce logic).
if (!crossMsg.to.subnetId.equals(s.networkName)) {
CrossMessageValidationOutcome outcome = validateCrossMessage(crossMsg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cryptoAtwill unfortunately Solidity's try/catch is profoundly crippled because it only works for cross-contract calls.

contracts/test/helpers/TestUtils.sol Outdated Show resolved Hide resolved
@@ -226,3 +226,31 @@ contract MockIpcContractPayable is IIpcHandler {

receive() external payable {}
}

contract MockContractFailbackable {
// receive() external payable {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to test this with receive as well?

IPCAddress({
subnetId: gatewayDiamond.getter().getNetworkName().getParentSubnet(),
rawAddress: FvmAddressHelper.from(receipient)
}),
IPCAddress({subnetId: id, rawAddress: FvmAddressHelper.from(address(this))}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it was labeled as a top-down message, but the path (from, to) made it a bottom-up message, the test was incorrect. Since we now have checks ensuring that top-down messages are indeed top-down (as a parent can't send a message to itself), this would cause a revert.

});

address[] memory nativeSubnetPath;
nativeSubnet = createNativeSubnet(nativeSubnetPath, rootNetwork.gatewayAddr, rootNetwork.id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to pass an empty address[] as the first argument?

}

function createNativeSubnet(
address[] memory subnetPath,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is supposed to be the parent subnet path, but it seems redundant since we can just get it from parentId.path?

@karlem
Copy link
Contributor Author

karlem commented Oct 24, 2024

@karlem Another question I have is regarding nonce. Say a bottom up message is created in L3, the nonce will be set by the local network. Then in L2, will the cross network message nonce change? If so, then the id (keccak) will change too?

Hmm that's a good point. I will think about how to mitigate this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Backlog
Development

Successfully merging this pull request may close these issues.

3 participants