diff --git a/misc/aspell_dict b/misc/aspell_dict index b69184176..1d0122460 100644 --- a/misc/aspell_dict +++ b/misc/aspell_dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 564 +personal_ws-1.1 en 567 ABCI ABI Agoric @@ -302,6 +302,8 @@ getHeight getMerkleRoot getRoot getStoredRecentConsensusStateCount +getTimestamp +getTimestampAtHeight getValue getVerifiedRoot handleChanCloseConfirm @@ -506,6 +508,7 @@ timeoutOnClose timeoutPacket timeoutPacketClose timeoutPropose +timeoutTimestamp tokenising tokenize tokenized diff --git a/spec.pdf b/spec.pdf index 06c113e6d..a29fd9bc9 100644 Binary files a/spec.pdf and b/spec.pdf differ diff --git a/spec/ics-002-client-semantics/README.md b/spec/ics-002-client-semantics/README.md index 1beb66e06..fc1666c97 100644 --- a/spec/ics-002-client-semantics/README.md +++ b/spec/ics-002-client-semantics/README.md @@ -134,6 +134,12 @@ type ConsensusState = bytes The `ConsensusState` MUST be stored under a particular key, defined below, so that other chains can verify that a particular consensus state has been stored. +The `ConsensusState` MUST define a `getTimestamp()` method which returns the timestamp associated with that consensus state: + +```typescript +type getTimestamp = ConsensusState => uint64 +``` + #### Header A `Header` is an opaque data structure defined by a client type which provides information to update a `ConsensusState`. diff --git a/spec/ics-003-connection-semantics/README.md b/spec/ics-003-connection-semantics/README.md index cc9275bcf..5bbd1f4b4 100644 --- a/spec/ics-003-connection-semantics/README.md +++ b/spec/ics-003-connection-semantics/README.md @@ -217,6 +217,13 @@ function verifyNextSequenceRecv( client = queryClient(connection.clientIdentifier) return client.verifyNextSequenceRecv(connection, height, connection.counterpartyPrefix, proof, portIdentifier, channelIdentifier, nextSequenceRecv) } + +function getTimestampAtHeight( + connection: ConnectionEnd, + height: uint64) { + client = queryClient(connection.clientIdentifier) + return client.queryConsensusState(height).getTimestamp() +} ``` ### Sub-protocols diff --git a/spec/ics-004-channel-and-packet-semantics/README.md b/spec/ics-004-channel-and-packet-semantics/README.md index d29c6045f..84c78013d 100644 --- a/spec/ics-004-channel-and-packet-semantics/README.md +++ b/spec/ics-004-channel-and-packet-semantics/README.md @@ -103,6 +103,7 @@ A `Packet`, in the interblockchain communication protocol, is a particular inter interface Packet { sequence: uint64 timeoutHeight: uint64 + timeoutTimestamp: uint64 sourcePort: Identifier sourceChannel: Identifier destPort: Identifier @@ -113,6 +114,7 @@ interface Packet { - The `sequence` number corresponds to the order of sends and receives, where a packet with an earlier sequence number must be sent and received before a packet with a later sequence number. - The `timeoutHeight` indicates a consensus height on the destination chain after which the packet will no longer be processed, and will instead count as having timed-out. +- The `timeoutTimestamp` indicates a timestamp on the destination chain after which the packet will no longer be processed, and will instead count as having timed-out. - The `sourcePort` identifies the port on the sending chain. - The `sourceChannel` identifies the channel end on the sending chain. - The `destPort` identifies the port on the receiving chain. @@ -521,7 +523,7 @@ function sendPacket(packet: Packet) { // sanity-check that the timeout height hasn't already passed in our local client tracking the receiving chain latestClientHeight = provableStore.get(clientPath(connection.clientIdentifier)).latestClientHeight() - abortTransactionUnless(latestClientHeight < packet.timeoutHeight) + abortTransactionUnless(packet.timeoutHeight === 0 || latestClientHeight < packet.timeoutHeight) nextSequenceSend = provableStore.get(nextSequenceSendPath(packet.sourcePort, packet.sourceChannel)) abortTransactionUnless(packet.sequence === nextSequenceSend) @@ -530,10 +532,11 @@ function sendPacket(packet: Packet) { nextSequenceSend = nextSequenceSend + 1 provableStore.set(nextSequenceSendPath(packet.sourcePort, packet.sourceChannel), nextSequenceSend) - provableStore.set(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence), hash(packet.data, packet.timeoutHeight)) + provableStore.set(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence), + hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) // log that a packet has been sent - emitLogEntry("sendPacket", {sequence: packet.sequence, data: packet.data, timeout: packet.timeoutHeight}) + emitLogEntry("sendPacket", {sequence: packet.sequence, data: packet.data, timeoutHeight: packet.timeoutHeight, timeoutTimestamp: packet.timeoutTimestamp}) } ``` @@ -572,7 +575,8 @@ function recvPacket( abortTransactionUnless(connection !== null) abortTransactionUnless(connection.state === OPEN) - abortTransactionUnless(getConsensusHeight() < packet.timeoutHeight) + abortTransactionUnless(packet.timeoutHeight === 0 || getConsensusHeight() < packet.timeoutHeight) + abortTransactionUnless(packet.timeoutTimestamp === 0 || currentTimestamp() < packet.timeoutTimestamp) abortTransactionUnless(connection.verifyPacketData( proofHeight, @@ -580,7 +584,7 @@ function recvPacket( packet.sourcePort, packet.sourceChannel, packet.sequence, - concat(packet.data, packet.timeoutHeight) + concat(packet.data, packet.timeoutHeight, packet.timeoutTimestamp) )) // all assertions passed (except sequence check), we can alter state @@ -599,7 +603,8 @@ function recvPacket( } // log that a packet has been received & acknowledged - emitLogEntry("recvPacket", {sequence: packet.sequence, timeout: packet.timeoutHeight, data: packet.data, acknowledgement}) + emitLogEntry("recvPacket", {sequence: packet.sequence, timeoutHeight: packet.timeoutHeight, + timeoutTimestamp: packet.timeoutTimestamp, data: packet.data, acknowledgement}) // return transparent packet return packet @@ -635,7 +640,7 @@ function acknowledgePacket( // verify we sent the packet and haven't cleared it out yet abortTransactionUnless(provableStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) - === hash(packet.data, packet.timeoutHeight)) + === hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) // abort transaction unless correct acknowledgement on counterparty chain abortTransactionUnless(connection.verifyPacketAcknowledgement( @@ -666,7 +671,7 @@ Note that in order to avoid any possible "double-spend" attacks, the timeout alg ##### Sending end The `timeoutPacket` function 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 +where the timeout height or timeout timestamp 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. Calling modules MAY atomically execute appropriate application timeout-handling logic in conjunction with calling `timeoutPacket`. @@ -695,15 +700,17 @@ function timeoutPacket( // note: the connection may have been closed abortTransactionUnless(packet.destPort === channel.counterpartyPortIdentifier) - // check that timeout height has passed on the other end - abortTransactionUnless(proofHeight >= packet.timeoutHeight) + // check that timeout height or timeout timestamp has passed on the other end + abortTransactionUnless( + (packet.timeoutHeight > 0 && proofHeight >= packet.timeoutHeight) || + (packet.timeoutTimestamp > 0 && connection.getTimestampAtHeight(proofHeight) > packet.timeoutTimestamp)) // check that packet has not been received abortTransactionUnless(nextSequenceRecv < packet.sequence) // verify we actually sent this packet, check the store abortTransactionUnless(provableStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) - === hash(packet.data, packet.timeoutHeight)) + === hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) if channel.order === ORDERED // ordered channel: check that the recv sequence is as claimed @@ -744,7 +751,7 @@ function timeoutPacket( The `timeoutOnClose` function 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). +(even if the `timeoutHeight` or `timeoutTimestamp` has not yet been reached). ```typescript function timeoutOnClose( @@ -765,7 +772,7 @@ function timeoutOnClose( // verify we actually sent this packet, check the store abortTransactionUnless(provableStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) - === hash(packet.data, packet.timeoutHeight)) + === hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) // check that the opposing channel end has closed expected = ChannelEnd{CLOSED, channel.order, channel.portIdentifier, @@ -837,7 +844,7 @@ function cleanupPacket( // verify we actually sent the packet, check the store abortTransactionUnless(provableStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) - === hash(packet.data, packet.timeoutHeight)) + === hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) if channel.order === ORDERED // check that the recv sequence is as claimed diff --git a/spec/ics-018-relayer-algorithms/README.md b/spec/ics-018-relayer-algorithms/README.md index 000783e7e..b1f93d757 100644 --- a/spec/ics-018-relayer-algorithms/README.md +++ b/spec/ics-018-relayer-algorithms/README.md @@ -168,7 +168,8 @@ function pendingDatagrams(chain: Chain, counterparty: Chain): List sentPacketLogs = queryByTopic(height, "sendPacket") for (const logEntry of sentPacketLogs) { // relay packet with this sequence number - packetData = Packet{logEntry.sequence, logEntry.timeout, localEnd.portIdentifier, localEnd.channelIdentifier, + packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp, + localEnd.portIdentifier, localEnd.channelIdentifier, remoteEnd.portIdentifier, remoteEnd.channelIdentifier, logEntry.data} counterpartyDatagrams.push(PacketRecv{ packet: packetData, @@ -180,7 +181,8 @@ function pendingDatagrams(chain: Chain, counterparty: Chain): List recvPacketLogs = queryByTopic(height, "recvPacket") for (const logEntry of recvPacketLogs) { // relay packet acknowledgement with this sequence number - packetData = Packet{logEntry.sequence, logEntry.timeout, localEnd.portIdentifier, localEnd.channelIdentifier, + packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp, + localEnd.portIdentifier, localEnd.channelIdentifier, remoteEnd.portIdentifier, remoteEnd.channelIdentifier, logEntry.data} counterpartyDatagrams.push(PacketAcknowledgement{ packet: packetData, diff --git a/spec/ics-024-host-requirements/README.md b/spec/ics-024-host-requirements/README.md index 7830ca753..47b594c4a 100644 --- a/spec/ics-024-host-requirements/README.md +++ b/spec/ics-024-host-requirements/README.md @@ -199,6 +199,8 @@ Host chains MUST provide a current Unix timestamp, accessible with `currentTimes type currentTimestamp = () => uint64 ``` +In order for timestamps to be used safely in timeouts, timestamps in subsequent headers MUST be non-decreasing. + ### Port system Host state machines MUST implement a port system, where the IBC handler can allow different modules in the host state machine to bind to uniquely named ports. Ports are identified by an `Identifier`.