Skip to content

Commit

Permalink
Merge pull request #206 from hyperledger-labs/packet-timeout
Browse files Browse the repository at this point in the history
Add packet timeout support

Signed-off-by: Jun Kimura <jun.kimura@datachain.jp>
  • Loading branch information
bluele committed Sep 25, 2023
2 parents e5b66f1 + 6ddaf01 commit 8d30d96
Show file tree
Hide file tree
Showing 22 changed files with 1,350 additions and 412 deletions.
8 changes: 4 additions & 4 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
IBCTest:testBenchmarkCreateMockClient() (gas: 213946)
IBCTest:testBenchmarkRecvPacket() (gas: 156383)
IBCTest:testBenchmarkSendPacket() (gas: 85195)
IBCTest:testBenchmarkCreateMockClient() (gas: 213968)
IBCTest:testBenchmarkRecvPacket() (gas: 156580)
IBCTest:testBenchmarkSendPacket() (gas: 85117)
IBCTest:testBenchmarkUpdateMockClient() (gas: 129390)
IBCTest:testConnectionOpenInit() (gas: 495954)
IBCTest:testConnectionOpenInit() (gas: 495976)
IBCTest:testToUint128((uint64,uint64)) (runs: 256, μ: 1051, ~: 1051)
4 changes: 4 additions & 0 deletions contracts/apps/20-transfer/ICS20Transfer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ abstract contract ICS20Transfer is IBCAppBase {
channelEscrowAddresses[channelId] = address(this);
}

function onTimeoutPacket(Packet.Data calldata packet, address) external virtual override onlyIBC {
_refundTokens(FungibleTokenPacketData.decode(packet.data), packet.source_port, packet.source_channel);
}

/// Internal functions ///

function _transferFrom(address sender, address receiver, string memory denom, uint256 amount)
Expand Down
7 changes: 7 additions & 0 deletions contracts/apps/commons/IBCAppBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,11 @@ abstract contract IBCAppBase is Context, IIBCModule {
* NOTE: You should apply an `onlyIBC` modifier to the function if a derived contract overrides it.
*/
function onAcknowledgementPacket(Packet.Data calldata, bytes calldata, address) external virtual override onlyIBC {}

/**
* @dev See IIBCModule-onTimeoutPacket
*
* NOTE: You should apply an `onlyIBC` modifier to the function if a derived contract overrides it.
*/
function onTimeoutPacket(Packet.Data calldata, address relayer) external virtual onlyIBC {}
}
2 changes: 1 addition & 1 deletion contracts/clients/IBFT2Client.sol
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ contract IBFT2Client is ILightClient {
function verifyNonMembership(bytes calldata proof, bytes32 root, bytes32 slot) internal pure returns (bool) {
bytes32 path = keccak256(abi.encodePacked(slot));
bytes memory dataHash = proof.verifyRLPProof(root, path); // reverts if proof is invalid
return dataHash.toRlpItem().toBytes().length == 0;
return dataHash.length == 0;
}

function parseBesuHeader(Header.Data memory header) internal pure returns (ParsedBesuHeader memory) {
Expand Down
240 changes: 234 additions & 6 deletions contracts/core/04-channel/IBCPacket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import "../04-channel/IIBCChannel.sol";
contract IBCPacket is IBCStore, IIBCPacket {
using IBCHeight for Height.Data;

bytes internal constant SUCCESSFUL_RECEIPT = hex"01";
bytes32 internal constant HASHED_SUCCESSFUL_RECEIPT = keccak256(SUCCESSFUL_RECEIPT);

/* Packet handlers */

/**
Expand All @@ -37,8 +40,10 @@ contract IBCPacket is IBCStore, IIBCPacket {
uint64 latestTimestamp;
ConnectionEnd.Data storage connection = connections[channel.connection_hops[0]];
ILightClient client = ILightClient(clientImpls[connection.client_id]);
require(address(client) != address(0), "client not found");

(Height.Data memory latestHeight, bool found) = client.getLatestHeight(connection.client_id);
require(found, "clientState not found");
require(
timeoutHeight.isZero() || latestHeight.lt(timeoutHeight),
"receiving chain block height >= packet timeout height"
Expand Down Expand Up @@ -115,17 +120,22 @@ contract IBCPacket is IBCStore, IIBCPacket {
);

if (channel.ordering == Channel.Order.ORDER_UNORDERED) {
require(
packetReceipts[msg_.packet.destination_port][msg_.packet.destination_channel][msg_.packet.sequence] == 0,
"packet sequence already has been received"
bytes32 commitmentKey = IBCCommitment.packetReceiptCommitmentKey(
msg_.packet.destination_port, msg_.packet.destination_channel, msg_.packet.sequence
);
packetReceipts[msg_.packet.destination_port][msg_.packet.destination_channel][msg_.packet.sequence] = 1;
require(commitments[commitmentKey] == bytes32(0), "packet receipt already exists");
commitments[commitmentKey] = HASHED_SUCCESSFUL_RECEIPT;
} else if (channel.ordering == Channel.Order.ORDER_ORDERED) {
require(
nextSequenceRecvs[msg_.packet.destination_port][msg_.packet.destination_channel] == msg_.packet.sequence,
"packet sequence != next receive sequence"
);
nextSequenceRecvs[msg_.packet.destination_port][msg_.packet.destination_channel]++;
commitments[IBCCommitment.nextSequenceRecvCommitmentKey(
msg_.packet.destination_port, msg_.packet.destination_channel
)] = keccak256(
uint64ToBigEndianBytes(nextSequenceRecvs[msg_.packet.destination_port][msg_.packet.destination_channel])
);
} else {
revert("unknown ordering type");
}
Expand Down Expand Up @@ -222,8 +232,212 @@ contract IBCPacket is IBCStore, IIBCPacket {
delete commitments[packetCommitmentKey];
}

function hashString(string memory s) private pure returns (bytes32) {
return keccak256(abi.encodePacked(s));
function timeoutPacket(IBCMsgs.MsgTimeoutPacket calldata msg_) external {
Channel.Data storage channel = channels[msg_.packet.source_port][msg_.packet.source_channel];
require(channel.state == Channel.State.STATE_OPEN, "channel state must be OPEN");

require(
hashString(msg_.packet.destination_port) == hashString(channel.counterparty.port_id),
"packet destination port doesn't match the counterparty's port"
);
require(
hashString(msg_.packet.destination_channel) == hashString(channel.counterparty.channel_id),
"packet destination channel doesn't match the counterparty's channel"
);

ConnectionEnd.Data storage connection = connections[channel.connection_hops[0]];
require(bytes(connection.client_id).length != 0, "connection not found");
ILightClient client = ILightClient(clientImpls[connection.client_id]);
require(address(client) != address(0), "client not found");
{
uint64 proofTimestamp;
(Height.Data memory latestHeight, bool found) = client.getLatestHeight(connection.client_id);
require(found, "clientState not found");
(proofTimestamp, found) = client.getTimestampAtHeight(connection.client_id, latestHeight);
require(found, "consensusState not found");
if (
(msg_.packet.timeout_height.isZero() || msg_.proofHeight.lt(msg_.packet.timeout_height))
&& (msg_.packet.timeout_timestamp == 0 || proofTimestamp < msg_.packet.timeout_timestamp)
) {
revert("packet timeout has not been reached for height or timestamp");
}
}

{
bytes32 commitment = commitments[IBCCommitment.packetCommitmentKey(
msg_.packet.source_port, msg_.packet.source_channel, msg_.packet.sequence
)];
// NOTE: if false, this indicates that the timeoutPacket already been executed
require(commitment != bytes32(0), "packet commitment not found");
require(
commitment
== keccak256(
abi.encodePacked(
sha256(
abi.encodePacked(
msg_.packet.timeout_timestamp,
msg_.packet.timeout_height.revision_number,
msg_.packet.timeout_height.revision_height,
sha256(msg_.packet.data)
)
)
)
),
"commitment bytes are not equal"
);
}

if (channel.ordering == Channel.Order.ORDER_ORDERED) {
// check that packet has not been received
require(msg_.nextSequenceRecv <= msg_.packet.sequence, "packet sequence > next receive sequence");
require(
client.verifyMembership(
connection.client_id,
msg_.proofHeight,
connection.delay_period,
calcBlockDelay(connection.delay_period),
msg_.proof,
connection.counterparty.prefix.key_prefix,
IBCCommitment.nextSequenceRecvCommitmentPath(
msg_.packet.destination_port, msg_.packet.destination_channel
),
uint64ToBigEndianBytes(msg_.nextSequenceRecv)
),
"failed to verify next sequence receive"
);
channel.state = Channel.State.STATE_CLOSED;
} else if (channel.ordering == Channel.Order.ORDER_UNORDERED) {
require(
client.verifyNonMembership(
connection.client_id,
msg_.proofHeight,
connection.delay_period,
calcBlockDelay(connection.delay_period),
msg_.proof,
connection.counterparty.prefix.key_prefix,
IBCCommitment.packetReceiptCommitmentPath(
msg_.packet.destination_port, msg_.packet.destination_channel, msg_.packet.sequence
)
),
"failed to verify packet receipt absense"
);
} else {
revert("unknown ordering type");
}

delete commitments[IBCCommitment.packetCommitmentKey(
msg_.packet.source_port, msg_.packet.source_channel, msg_.packet.sequence
)];
}

function buildCounterparty(Packet.Data memory packet) private pure returns (ChannelCounterparty.Data memory) {
return ChannelCounterparty.Data({port_id: packet.source_port, channel_id: packet.source_channel});
}

function timeoutOnClose(IBCMsgs.MsgTimeoutOnClose calldata msg_) external {
Channel.Data storage channel = channels[msg_.packet.source_port][msg_.packet.source_channel];
require(channel.state == Channel.State.STATE_OPEN, "channel state must be OPEN");

require(
hashString(msg_.packet.destination_port) == hashString(channel.counterparty.port_id),
"packet destination port doesn't match the counterparty's port"
);
require(
hashString(msg_.packet.destination_channel) == hashString(channel.counterparty.channel_id),
"packet destination channel doesn't match the counterparty's channel"
);

ConnectionEnd.Data storage connection = connections[channel.connection_hops[0]];
require(bytes(connection.client_id).length != 0, "connection not found");
ILightClient client = ILightClient(clientImpls[connection.client_id]);
require(address(client) != address(0), "client not found");

{
bytes32 commitment = commitments[IBCCommitment.packetCommitmentKey(
msg_.packet.source_port, msg_.packet.source_channel, msg_.packet.sequence
)];
// NOTE: if false, this indicates that the timeoutPacket already been executed
require(commitment != bytes32(0), "packet commitment not found");
require(
commitment
== keccak256(
abi.encodePacked(
sha256(
abi.encodePacked(
msg_.packet.timeout_timestamp,
msg_.packet.timeout_height.revision_number,
msg_.packet.timeout_height.revision_height,
sha256(msg_.packet.data)
)
)
)
),
"commitment bytes are not equal"
);
}

{
Channel.Data memory expectedChannel = Channel.Data({
state: Channel.State.STATE_CLOSED,
ordering: channel.ordering,
counterparty: ChannelCounterparty.Data({
port_id: msg_.packet.source_port,
channel_id: msg_.packet.source_channel
}),
connection_hops: buildConnectionHops(connection.counterparty.connection_id),
version: channel.version
});
require(
client.verifyMembership(
connection.client_id,
msg_.proofHeight,
connection.delay_period,
calcBlockDelay(connection.delay_period),
msg_.proofClose,
connection.counterparty.prefix.key_prefix,
IBCCommitment.channelPath(msg_.packet.destination_port, msg_.packet.destination_channel),
Channel.encode(expectedChannel)
),
"failed to verify channel state"
);
}

if (channel.ordering == Channel.Order.ORDER_ORDERED) {
// check that packet has not been received
require(msg_.nextSequenceRecv <= msg_.packet.sequence, "packet sequence > next receive sequence");
require(
client.verifyMembership(
connection.client_id,
msg_.proofHeight,
connection.delay_period,
calcBlockDelay(connection.delay_period),
msg_.proofUnreceived,
connection.counterparty.prefix.key_prefix,
IBCCommitment.nextSequenceRecvCommitmentPath(
msg_.packet.destination_port, msg_.packet.destination_channel
),
uint64ToBigEndianBytes(msg_.nextSequenceRecv)
),
"failed to verify next sequence receive"
);
} else if (channel.ordering == Channel.Order.ORDER_UNORDERED) {
require(
client.verifyNonMembership(
connection.client_id,
msg_.proofHeight,
connection.delay_period,
calcBlockDelay(connection.delay_period),
msg_.proofUnreceived,
connection.counterparty.prefix.key_prefix,
IBCCommitment.packetReceiptCommitmentPath(
msg_.packet.destination_port, msg_.packet.destination_channel, msg_.packet.sequence
)
),
"failed to verify packet receipt absense"
);
} else {
revert("unknown ordering type");
}
}

/* Verification functions */
Expand Down Expand Up @@ -275,4 +489,18 @@ contract IBCPacket is IBCStore, IIBCPacket {
}
return blockDelay;
}

function buildConnectionHops(string memory connectionId) private pure returns (string[] memory hops) {
hops = new string[](1);
hops[0] = connectionId;
return hops;
}

function hashString(string memory s) private pure returns (bytes32) {
return keccak256(abi.encodePacked(s));
}

function uint64ToBigEndianBytes(uint64 v) private pure returns (bytes memory) {
return abi.encodePacked(bytes8(v));
}
}
17 changes: 17 additions & 0 deletions contracts/core/04-channel/IIBCChannel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,21 @@ interface IIBCPacket {
* It will also increment NextSequenceAck in case of ORDERED channels.
*/
function acknowledgePacket(IBCMsgs.MsgPacketAcknowledgement calldata msg_) external;

/**
* @dev TimeoutPacket is called by a module which originally attempted to send a
* packet to a counterparty module, where the timeout height has passed on the
* counterparty chain without the packet being committed, to prove that the
* packet can no longer be executed and to allow the calling module to safely
* perform appropriate state transitions. Its intended usage is within the
* ante handler.
*/
function timeoutPacket(IBCMsgs.MsgTimeoutPacket calldata msg_) external;

/**
* @dev TimeoutOnClose is called by a module in order to prove that the channel to
* which an unreceived packet was addressed has been closed, so the packet will
* never be received (even if the timeoutHeight has not yet been reached).
*/
function timeoutOnClose(IBCMsgs.MsgTimeoutOnClose calldata msg_) external;
}
2 changes: 2 additions & 0 deletions contracts/core/05-port/IIBCModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,6 @@ interface IIBCModule {
function onRecvPacket(Packet.Data calldata, address relayer) external returns (bytes memory);

function onAcknowledgementPacket(Packet.Data calldata, bytes calldata acknowledgement, address relayer) external;

function onTimeoutPacket(Packet.Data calldata, address relayer) external;
}
1 change: 0 additions & 1 deletion contracts/core/24-host/IBCStore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ abstract contract IBCStore {
mapping(string => mapping(string => uint64)) internal nextSequenceSends;
mapping(string => mapping(string => uint64)) internal nextSequenceRecvs;
mapping(string => mapping(string => uint64)) internal nextSequenceAcks;
mapping(string => mapping(string => mapping(uint64 => uint8))) internal packetReceipts;
mapping(bytes => address[]) internal capabilities;

// Host parameters
Expand Down
15 changes: 15 additions & 0 deletions contracts/core/25-handler/IBCMsgs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,19 @@ library IBCMsgs {
bytes proof;
Height.Data proofHeight;
}

struct MsgTimeoutPacket {
Packet.Data packet;
bytes proof;
Height.Data proofHeight;
uint64 nextSequenceRecv;
}

struct MsgTimeoutOnClose {
Packet.Data packet;
bytes proofUnreceived;
bytes proofClose;
Height.Data proofHeight;
uint64 nextSequenceRecv;
}
}
Loading

0 comments on commit 8d30d96

Please sign in to comment.