From 28299a3e2083651d8310497b823d33710f93d153 Mon Sep 17 00:00:00 2001 From: Zarko Milosevic Date: Wed, 9 Sep 2020 16:59:08 +0200 Subject: [PATCH 1/5] Initial work on packets spec --- docs/spec/relayer/Packets.md | 327 +++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 docs/spec/relayer/Packets.md diff --git a/docs/spec/relayer/Packets.md b/docs/spec/relayer/Packets.md new file mode 100644 index 0000000000..a9013e0b33 --- /dev/null +++ b/docs/spec/relayer/Packets.md @@ -0,0 +1,327 @@ +# IBC packet handling + +This document specifies relayer's logic for packet handling. + +## Data Types + +```go +type Packet { + sequence uint64 + timeoutHeight Height + timeoutTimestamp uint64 + sourcePort Identifier + sourceChannel Identifier + destPort Identifier + destChannel Identifier + data bytes +} +``` + +```go +type PacketRecv { + packet Packet + proof CommitmentProof + proofHeight Height +} +``` + +```go +type SendPacketEvent { + height Height + sequence uint64 + timeoutHeight Height + timeoutTimestamp uint64 + sourcePort Identifier + sourceChannel Identifier + destPort Identifier + destChannel Identifier + data bytes +} +``` + +```go +type ChannelEnd { + state ChannelState + ordering ChannelOrder + counterpartyPortIdentifier Identifier + counterpartyChannelIdentifier Identifier + connectionHops [Identifier] + version string +} + +enum ChannelState { + INIT, + TRYOPEN, + OPEN, + CLOSED, +} + +enum ChannelOrder { + ORDERED, + UNORDERED, +} +``` + +```go +type ConnectionEnd { + state ConnectionState + counterpartyConnectionIdentifier Identifier + counterpartyPrefix CommitmentPrefix + clientIdentifier Identifier + counterpartyClientIdentifier Identifier + version []string +} + +enum ConnectionState { + INIT, + TRYOPEN, + OPEN, +} +``` + +```go +type ClientState { + chainID string + validatorSet List> + trustLevel Rational + trustingPeriod uint64 + unbondingPeriod uint64 + latestHeight Height + latestTimestamp uint64 + frozenHeight Maybe + upgradeCommitmentPrefix CommitmentPrefix + upgradeKey []byte + maxClockDrift uint64 + proofSpecs []ProofSpec +} +``` + + +## Relayer algorithm for packet handling + +```golang +func handleSendPacketEvent(ev, chainA) { + // NOTE: we don't verify if event data are valid at this point. We trust full node we are connected to + // until some verification fails. Otherwise, we can have Stage 2 (datagram creation being done first). + + // Stage 1. + // Update on `chainB` the IBC client for `chainA` to height `>= targetHeight`. + targetHeight = ev.height + 1 + // See the code for `updateIBCClient` below. + + installedHeight, error := updateIBCClient(chainB, chainA, targetHeight) + if error != nil { + return error + } + + // Stage 2. + // Create the IBC datagrams including `ev` & verify them. + + channel = GetChannel(chainA, ev.sourcePort, ev.sourceChannel) // might query chainA or have this info in state + connectionId = sourceChannel.connectionHops[0] + connection = GetConnection(chainA, connectionId) // might query chainA or have this info in state + clientState = GetClientState(chainA, connection.clientIdentifier) // might query chainA or have this info in state + chainB = getHostInfo(clientState.chainID) + + sh = chainA.lc.get_header(installedHeight) + + // we now query for packet data to try to build PacketRecv based on packet data + // read at height installedHeight - 1 + + // is packet for sequence number ev.sequence still pending? + // we first check if commitment proof is present and valid + + provableStore.set(nextSequenceSendPath(packet.sourcePort, packet.sourceChannel), nextSequenceSend) + provableStore.set(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence), + hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) + + // We query chainA for packetCommitment + proofHeight = installedHeight - 1 + packetCommitment, proof = GetPacketCommitment(chainA, ev.sourcePort, ev.sourceChannel, ev.sequence, proofHeight) + TODO: check what this function is doing! + if !verifyPacketData(connection, + proofHeight, + proof, + ev.sourcePort, + ev.sourceChannel, + ev.sequence, + concat(ev.data, ev.timeoutHeight, ev.timeoutTimestamp) + ) { + panic // full node we are talking to is faulty + } + // if packet commitment is empty, then packet is already received by the counter party + if packetCommitment == null return + if packetCommitment != hash(ev.data, ev.timeoutHeight, ev.timeoutTimestamp { + panic // invalid data; probably + } + + // we now check if this packet is already received by the destination chain + + + packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp, + localEnd.portIdentifier, localEnd.channelIdentifier, + remoteEnd.portIdentifier, remoteEnd.channelIdentifier, logEntry.data} + + + + while (true) { + datagrams = createDatagram(installedHeight - 1, chainA, chainB) + if verifyProof(datagrams, sh.appHash) { + break; + } + // Full node for `chainA` is faulty. Connect to different node of `chainA` and retry. + replaceFullNode(src) + } + + // Stage 3. + // Submit datagrams. + chainB.submit(datagrams) +} + + +// Perform an update on `dest` chain for the IBC client for `src` chain. +// Preconditions: +// - `src` chain has height greater or equal to `targetHeight` +// Postconditions: +// - returns the installedHeight >= targetHeight +// - return error if verification of client state fails +func updateIBCClient(dest, src, targetHeight) -> {installedHeight, error} { + + while (true) { + // Check if targetHeight exists already on destination chain. + // Query state of IBC client for `src` on chain `dest`. + clientState, membershipProof = dest.queryClientConsensusState(src, targetHeight) + // NOTE: What if a full node we are connected to send us stale (but correct) information regarding targetHeight? + + // Verify the result of the query + sh = dest.lc.get_header(membershipProof.Height + 1) + // NOTE: Headers we obtain from the light client are trusted. + if verifyClientStateProof(clientState, membershipProof, sh.appHash) { + break; + } + replaceFullNode(dst) + } + + // At this point we know that clientState is indeed part of the state on dest chain. + // Verify if installed header is equal to the header obtained the from the local client + // at the same height. + if !src.lc.get_header(clientState.Height) == clientState.SignedHeader.Header { + // We know at this point that conflicting header is installed at the dst chain. + // We need to create proof of fork and submit it to src chain and to dst chain so light client is frozen. + src.lc.createAndSubmitProofOfFork(dst, clientState) + return {nil, error} + } + + while (clientState.Height < targetHeight) { + // Installed height is smaller than the target height. + // Do an update to IBC client for `src` on `dest`. + shs = src.lc.get_minimal_set(clientState.Height, targetHeight) + // Blocking call. Wait until transaction is committed to the dest chain. + dest.submit(createUpdateClientDatagrams(shs)) + + while (true) { + // Check if targetHeight exists already on destination chain. + // Query state of IBC client for `src` on chain `dest`. + clientState, membershipProof = dest.queryClientConsensusState(src, targetHeight) + // NOTE: What if a full node we are connected to send us stale (but correct) information regarding targetHeight? + + // Verify the result of the query + sh = dest.lc.get_header(membershipProof.Height + 1) + // NOTE: Headers we obtain from the light client are trusted. + if verifyClientStateProof(clientState, membershipProof, sh.appHash) { + break; + } + replaceFullNode(dst) + } + + // At this point we know that clientState is indeed part of the state on dest chain. + // Verify if installed header is equal to the header obtained the from the local client + // at the same height. + if !src.lc.get_header(clientState.Height) == clientState.SignedHeader.Header { + // We know at this point that conflicting header is installed at the dst chain. + // We need to create proof of fork and submit it to src chain and to dst chain so light client is frozen. + src.lc.createAndSubmitProofOfFork(dst, clientState) + return {nil, error} + } + } + + return {clientState.Height, nil} +} + +func getDestinationHost(ev SendPacketEvent, chainA Chain) Chain { + channel = GetChannel(chainA, ev.sourcePort, ev.sourceChannel) // might query chainA or have this info in state + connectionId = sourceChannel.connectionHops[0] + connection = GetConnection(chainA, connectionId) // might query chainA or have this info in state + clientState = GetClientState(chainA, connection.clientIdentifier) // might query chainA or have this info in state + return getHostInfo(clientState.chainID) +} +``` + +// Deal with packets + // First, scan logs for sent packets and relay all of them + sentPacketLogs = queryByTopic(height, "sendPacket") + for (const logEntry of sentPacketLogs) { + // relay packet with this sequence number + packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp, + localEnd.portIdentifier, localEnd.channelIdentifier, + remoteEnd.portIdentifier, remoteEnd.channelIdentifier, logEntry.data} + counterpartyDatagrams.push(PacketRecv{ + packet: packetData, + proof: packet.proof(), + proofHeight: height, + }) + } + + +function recvPacket( + packet: OpaquePacket, + proof: CommitmentProof, + proofHeight: Height, + acknowledgement: bytes): Packet { + + channel = provableStore.get(channelPath(packet.destPort, packet.destChannel)) + abortTransactionUnless(channel !== null) + abortTransactionUnless(channel.state === OPEN) + abortTransactionUnless(authenticateCapability(channelCapabilityPath(packet.destPort, packet.destChannel), capability)) + abortTransactionUnless(packet.sourcePort === channel.counterpartyPortIdentifier) + abortTransactionUnless(packet.sourceChannel === channel.counterpartyChannelIdentifier) + + abortTransactionUnless(connection !== null) + abortTransactionUnless(connection.state === OPEN) + + abortTransactionUnless(packet.timeoutHeight === 0 || getConsensusHeight() < packet.timeoutHeight) + abortTransactionUnless(packet.timeoutTimestamp === 0 || currentTimestamp() < packet.timeoutTimestamp) + + abortTransactionUnless(connection.verifyPacketData( + proofHeight, + proof, + packet.sourcePort, + packet.sourceChannel, + packet.sequence, + concat(packet.data, packet.timeoutHeight, packet.timeoutTimestamp) + )) + + // all assertions passed (except sequence check), we can alter state + + // always set the acknowledgement so that it can be verified on the other side + provableStore.set( + packetAcknowledgementPath(packet.destPort, packet.destChannel, packet.sequence), + hash(acknowledgement) + ) + + if (channel.order === ORDERED) { + nextSequenceRecv = provableStore.get(nextSequenceRecvPath(packet.destPort, packet.destChannel)) + abortTransactionUnless(packet.sequence === nextSequenceRecv) + nextSequenceRecv = nextSequenceRecv + 1 + provableStore.set(nextSequenceRecvPath(packet.destPort, packet.destChannel), nextSequenceRecv) + } else + abortTransactionUnless(provableStore.get(packetAcknowledgementPath(packet.destPort, packet.destChannel, packet.sequence) === null)) + + // log that a packet has been received & acknowledged + emitLogEntry("recvPacket", {sequence: packet.sequence, timeoutHeight: packet.timeoutHeight, + timeoutTimestamp: packet.timeoutTimestamp, data: packet.data, acknowledgement}) + + // return transparent packet + return packet +} + From 68603efc38b278088d7d4e9d08d9774620a28d8f Mon Sep 17 00:00:00 2001 From: Zarko Milosevic Date: Thu, 10 Sep 2020 11:53:19 +0200 Subject: [PATCH 2/5] Add checks for packet receptions --- docs/spec/relayer/Packets.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/spec/relayer/Packets.md b/docs/spec/relayer/Packets.md index a9013e0b33..617da3600a 100644 --- a/docs/spec/relayer/Packets.md +++ b/docs/spec/relayer/Packets.md @@ -152,11 +152,20 @@ func handleSendPacketEvent(ev, chainA) { // if packet commitment is empty, then packet is already received by the counter party if packetCommitment == null return if packetCommitment != hash(ev.data, ev.timeoutHeight, ev.timeoutTimestamp { - panic // invalid data; probably + panic // invalid data; probably fork } // we now check if this packet is already received by the destination chain - + if (channel.order === ORDERED) { + // TODO: All get call should either return from the relayer state, or if needed + // query full node of the chain. In the latter case, proof verification should be done + // in the call. + nextSequenceRecv = GetNextSequenceRecv(chainB, ev.destPort, ev.destChannel) + if ev.sequence != nextSequenceRecv return // packet has already been delivered by another relayer + } else { + packetAcknowledgement = GetPacketAcknowledgement(ev.destPort, ev.destChannel, ev.sequence) + if packetAcknowledgement != null return + } packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp, localEnd.portIdentifier, localEnd.channelIdentifier, From bbacfad0c56bdfcbf2182689ba80c893cfbc305a Mon Sep 17 00:00:00 2001 From: Zarko Milosevic Date: Thu, 10 Sep 2020 18:24:15 +0200 Subject: [PATCH 3/5] First version of PacketRecv creation logic --- docs/spec/relayer/Packets.md | 288 +++++++++-------------------------- docs/spec/relayer/Relayer.md | 27 ++-- 2 files changed, 82 insertions(+), 233 deletions(-) diff --git a/docs/spec/relayer/Packets.md b/docs/spec/relayer/Packets.md index 617da3600a..b4067fd3b8 100644 --- a/docs/spec/relayer/Packets.md +++ b/docs/spec/relayer/Packets.md @@ -1,6 +1,6 @@ # IBC packet handling -This document specifies relayer's logic for packet handling. +This document specifies datagram creation logic for packets. It is used by the relayer. ## Data Types @@ -96,241 +96,93 @@ type ClientState { } ``` - -## Relayer algorithm for packet handling +## Computing destination chain ```golang -func handleSendPacketEvent(ev, chainA) { - // NOTE: we don't verify if event data are valid at this point. We trust full node we are connected to - // until some verification fails. Otherwise, we can have Stage 2 (datagram creation being done first). - - // Stage 1. - // Update on `chainB` the IBC client for `chainA` to height `>= targetHeight`. - targetHeight = ev.height + 1 - // See the code for `updateIBCClient` below. - - installedHeight, error := updateIBCClient(chainB, chainA, targetHeight) - if error != nil { - return error +func getDestinationInfo(ev IBCEvent, chainA Chain) Chain { + switch ev.type { + case SendPacketEvent: + channel, proof = GetChannel(chainA, ev.sourcePort, ev.sourceChannel, ev.Height) + if proof == nil return nil + + connectionId = channel.connectionHops[0] + connection, proof = GetConnection(chainA, connectionId, ev.Height) + if proof == nil return nil + + clientState = GetClientState(chainA, connection.clientIdentifier, ev.Height) + return getHostInfo(clientStateA.chainID) } +} +``` - // Stage 2. - // Create the IBC datagrams including `ev` & verify them. - - channel = GetChannel(chainA, ev.sourcePort, ev.sourceChannel) // might query chainA or have this info in state - connectionId = sourceChannel.connectionHops[0] - connection = GetConnection(chainA, connectionId) // might query chainA or have this info in state - clientState = GetClientState(chainA, connection.clientIdentifier) // might query chainA or have this info in state - chainB = getHostInfo(clientState.chainID) - - sh = chainA.lc.get_header(installedHeight) - - // we now query for packet data to try to build PacketRecv based on packet data - // read at height installedHeight - 1 - - // is packet for sequence number ev.sequence still pending? - // we first check if commitment proof is present and valid +## Datagram creation logic + +### PacketRecv datagram creation + +```golang +func createDatagram(ev SendPacketEvent, chainA Chain, chainB Chain, installedHeight Height) PacketRecv { - provableStore.set(nextSequenceSendPath(packet.sourcePort, packet.sourceChannel), nextSequenceSend) - provableStore.set(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence), - hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) + // Stage 1 + // Verify if packet is committed to chain A - // We query chainA for packetCommitment proofHeight = installedHeight - 1 - packetCommitment, proof = GetPacketCommitment(chainA, ev.sourcePort, ev.sourceChannel, ev.sequence, proofHeight) - TODO: check what this function is doing! - if !verifyPacketData(connection, - proofHeight, - proof, - ev.sourcePort, - ev.sourceChannel, - ev.sequence, - concat(ev.data, ev.timeoutHeight, ev.timeoutTimestamp) - ) { - panic // full node we are talking to is faulty - } + packetCommitment, packetCommitmentProof = + GetPacketCommitment(chainA, ev.sourcePort, ev.sourceChannel, ev.sequence, proofHeight) + if packetCommitmentProof != nil { return nil } + // if packet commitment is empty, then packet is already received by the counter party - if packetCommitment == null return - if packetCommitment != hash(ev.data, ev.timeoutHeight, ev.timeoutTimestamp { - panic // invalid data; probably fork - } - - // we now check if this packet is already received by the destination chain - if (channel.order === ORDERED) { - // TODO: All get call should either return from the relayer state, or if needed - // query full node of the chain. In the latter case, proof verification should be done - // in the call. - nextSequenceRecv = GetNextSequenceRecv(chainB, ev.destPort, ev.destChannel) - if ev.sequence != nextSequenceRecv return // packet has already been delivered by another relayer - } else { - packetAcknowledgement = GetPacketAcknowledgement(ev.destPort, ev.destChannel, ev.sequence) - if packetAcknowledgement != null return - } + if packetCommitment == null OR + if packetCommitment != hash(concat(ev.data, ev.timeoutHeight, ev.timeoutTimestamp)) { return nil } + + // Stage 2 + // Execute checks IBC handler on chainB will execute - packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp, - localEnd.portIdentifier, localEnd.channelIdentifier, - remoteEnd.portIdentifier, remoteEnd.channelIdentifier, logEntry.data} + channelB, proof = GetChannel(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) + if proof == nil { return nil } + if channelB == null OR + channelB.state != OPEN OR + ev.sourcePort != channelB.counterpartyPortIdentifier OR + ev.sourceChannel != channelB.counterpartyChannelIdentifier { return nil } + connectionIdB = channelB.connectionHops[0] + connectionB, proof = GetConnection(chainB, connectionIdB, LATEST_HEIGHT) + if proof == nil { return nil } - while (true) { - datagrams = createDatagram(installedHeight - 1, chainA, chainB) - if verifyProof(datagrams, sh.appHash) { - break; - } - // Full node for `chainA` is faulty. Connect to different node of `chainA` and retry. - replaceFullNode(src) - } + if connectionB == null OR connectionB.state != OPEN { return nil } - // Stage 3. - // Submit datagrams. - chainB.submit(datagrams) -} - - -// Perform an update on `dest` chain for the IBC client for `src` chain. -// Preconditions: -// - `src` chain has height greater or equal to `targetHeight` -// Postconditions: -// - returns the installedHeight >= targetHeight -// - return error if verification of client state fails -func updateIBCClient(dest, src, targetHeight) -> {installedHeight, error} { + if ev.timeoutHeight != 0 AND GetConsensusHeight(chainB, LATEST_HEIGHT) >= ev.timeoutHeight { return nil } + if ev.timeoutTimestamp != 0 AND GetCurrentTimestamp(chainB, LATEST_HEIGHT) >= ev.timeoutTimestamp { return nil } - while (true) { - // Check if targetHeight exists already on destination chain. - // Query state of IBC client for `src` on chain `dest`. - clientState, membershipProof = dest.queryClientConsensusState(src, targetHeight) - // NOTE: What if a full node we are connected to send us stale (but correct) information regarding targetHeight? + // we now check if this packet is already received by the destination chain + if (channel.order === ORDERED) { + nextSequenceRecv, proof = GetNextSequenceRecv(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) + if proof != nil { return nil } - // Verify the result of the query - sh = dest.lc.get_header(membershipProof.Height + 1) - // NOTE: Headers we obtain from the light client are trusted. - if verifyClientStateProof(clientState, membershipProof, sh.appHash) { - break; - } - replaceFullNode(dst) - } + if ev.sequence != nextSequenceRecv { return nil } // packet has already been delivered by another relayer - // At this point we know that clientState is indeed part of the state on dest chain. - // Verify if installed header is equal to the header obtained the from the local client - // at the same height. - if !src.lc.get_header(clientState.Height) == clientState.SignedHeader.Header { - // We know at this point that conflicting header is installed at the dst chain. - // We need to create proof of fork and submit it to src chain and to dst chain so light client is frozen. - src.lc.createAndSubmitProofOfFork(dst, clientState) - return {nil, error} + } else { + packetAcknowledgement, proof = GetPacketAcknowledgement(ev.destPort, ev.destChannel, ev.sequence) + if proof != nil { return nil } + + if packetAcknowledgement != nil { return nil } } - - while (clientState.Height < targetHeight) { - // Installed height is smaller than the target height. - // Do an update to IBC client for `src` on `dest`. - shs = src.lc.get_minimal_set(clientState.Height, targetHeight) - // Blocking call. Wait until transaction is committed to the dest chain. - dest.submit(createUpdateClientDatagrams(shs)) - while (true) { - // Check if targetHeight exists already on destination chain. - // Query state of IBC client for `src` on chain `dest`. - clientState, membershipProof = dest.queryClientConsensusState(src, targetHeight) - // NOTE: What if a full node we are connected to send us stale (but correct) information regarding targetHeight? - - // Verify the result of the query - sh = dest.lc.get_header(membershipProof.Height + 1) - // NOTE: Headers we obtain from the light client are trusted. - if verifyClientStateProof(clientState, membershipProof, sh.appHash) { - break; - } - replaceFullNode(dst) - } - - // At this point we know that clientState is indeed part of the state on dest chain. - // Verify if installed header is equal to the header obtained the from the local client - // at the same height. - if !src.lc.get_header(clientState.Height) == clientState.SignedHeader.Header { - // We know at this point that conflicting header is installed at the dst chain. - // We need to create proof of fork and submit it to src chain and to dst chain so light client is frozen. - src.lc.createAndSubmitProofOfFork(dst, clientState) - return {nil, error} - } - } - - return {clientState.Height, nil} -} - -func getDestinationHost(ev SendPacketEvent, chainA Chain) Chain { - channel = GetChannel(chainA, ev.sourcePort, ev.sourceChannel) // might query chainA or have this info in state - connectionId = sourceChannel.connectionHops[0] - connection = GetConnection(chainA, connectionId) // might query chainA or have this info in state - clientState = GetClientState(chainA, connection.clientIdentifier) // might query chainA or have this info in state - return getHostInfo(clientState.chainID) -} + // Stage 3 + // Build datagram as all checks has passed + packet = Packet { + sequence: ev.sequence, + timeoutHeight: ev.timeoutHeight, + timeoutTimestamp: ev.timeoutTimestamp, + sourcePort: ev.sourcePort, + sourceChannel: ev.sourceChannel, + destPort: ev.destPort, + destChannel: ev.destChannel, + data: ev.data + } + + return PacketRecv { packet, packetCommitmentProof, proofHeight } +} ``` -// Deal with packets - // First, scan logs for sent packets and relay all of them - sentPacketLogs = queryByTopic(height, "sendPacket") - for (const logEntry of sentPacketLogs) { - // relay packet with this sequence number - packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp, - localEnd.portIdentifier, localEnd.channelIdentifier, - remoteEnd.portIdentifier, remoteEnd.channelIdentifier, logEntry.data} - counterpartyDatagrams.push(PacketRecv{ - packet: packetData, - proof: packet.proof(), - proofHeight: height, - }) - } - - -function recvPacket( - packet: OpaquePacket, - proof: CommitmentProof, - proofHeight: Height, - acknowledgement: bytes): Packet { - - channel = provableStore.get(channelPath(packet.destPort, packet.destChannel)) - abortTransactionUnless(channel !== null) - abortTransactionUnless(channel.state === OPEN) - abortTransactionUnless(authenticateCapability(channelCapabilityPath(packet.destPort, packet.destChannel), capability)) - abortTransactionUnless(packet.sourcePort === channel.counterpartyPortIdentifier) - abortTransactionUnless(packet.sourceChannel === channel.counterpartyChannelIdentifier) - - abortTransactionUnless(connection !== null) - abortTransactionUnless(connection.state === OPEN) - - abortTransactionUnless(packet.timeoutHeight === 0 || getConsensusHeight() < packet.timeoutHeight) - abortTransactionUnless(packet.timeoutTimestamp === 0 || currentTimestamp() < packet.timeoutTimestamp) - - abortTransactionUnless(connection.verifyPacketData( - proofHeight, - proof, - packet.sourcePort, - packet.sourceChannel, - packet.sequence, - concat(packet.data, packet.timeoutHeight, packet.timeoutTimestamp) - )) - - // all assertions passed (except sequence check), we can alter state - - // always set the acknowledgement so that it can be verified on the other side - provableStore.set( - packetAcknowledgementPath(packet.destPort, packet.destChannel, packet.sequence), - hash(acknowledgement) - ) - - if (channel.order === ORDERED) { - nextSequenceRecv = provableStore.get(nextSequenceRecvPath(packet.destPort, packet.destChannel)) - abortTransactionUnless(packet.sequence === nextSequenceRecv) - nextSequenceRecv = nextSequenceRecv + 1 - provableStore.set(nextSequenceRecvPath(packet.destPort, packet.destChannel), nextSequenceRecv) - } else - abortTransactionUnless(provableStore.get(packetAcknowledgementPath(packet.destPort, packet.destChannel, packet.sequence) === null)) - - // log that a packet has been received & acknowledged - emitLogEntry("recvPacket", {sequence: packet.sequence, timeoutHeight: packet.timeoutHeight, - timeoutTimestamp: packet.timeoutTimestamp, data: packet.data, acknowledgement}) - - // return transparent packet - return packet -} diff --git a/docs/spec/relayer/Relayer.md b/docs/spec/relayer/Relayer.md index 8ea6377d15..ce13c8fe61 100644 --- a/docs/spec/relayer/Relayer.md +++ b/docs/spec/relayer/Relayer.md @@ -74,11 +74,15 @@ succeed. The interface between stage 2 and stage 3 is a set of datagrams. We assume that the corresponding light client is correctly installed on each chain. ```golang -func handleEvent(ev, chainA, chainB) { +func handleEvent(ev, chainA) { // NOTE: we don't verify if event data are valid at this point. We trust full node we are connected to // until some verification fails. Otherwise, we can have Stage 2 (datagram creation being done first). - + // Stage 1. + // Determine destination chain + chainB = GetDestinationInfo(ev, chainA) + + // Stage 2. // Update on `chainB` the IBC client for `chainA` to height `>= targetHeight`. targetHeight = ev.height + 1 // See the code for `updateIBCClient` below. @@ -87,22 +91,15 @@ func handleEvent(ev, chainA, chainB) { return error } - // Stage 2. + // Stage 3. // Create the IBC datagrams including `ev` & verify them. + datagram = createDatagram(ev, chainA, chainB, installedHeight) - sh = chainA.lc.get_header(installedHeight) - while (true) { - datagrams = pendingDatagrams(installedHeight - 1, chainA, chainB) - if verifyProof(datagrams, sh.appHash) { - break; - } - // Full node for `chainA` is faulty. Connect to different node of `chainA` and retry. - replaceFullNode(src) - } - - // Stage 3. + // Stage 4. // Submit datagrams. - chainB.submit(datagrams) + if datagram != nil { + chainB.submit(datagram) + } } From 2201a9d9dc30019032cdcd3cbb4df7d56e8a5121 Mon Sep 17 00:00:00 2001 From: Zarko Milosevic Date: Fri, 11 Sep 2020 13:30:12 +0200 Subject: [PATCH 4/5] Add helper functions --- docs/spec/relayer/Packets.md | 98 +++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/docs/spec/relayer/Packets.md b/docs/spec/relayer/Packets.md index b4067fd3b8..fb14e218b6 100644 --- a/docs/spec/relayer/Packets.md +++ b/docs/spec/relayer/Packets.md @@ -94,12 +94,73 @@ type ClientState { maxClockDrift uint64 proofSpecs []ProofSpec } +``` +## Helper functions + +We assume the existence of the following helper functions: + +```go +// Returns channel end with a commitment proof. If proof != nil, then it is being verified with the corresponding light client. +// Channel end is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, +// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +GetChannel(chain Chain, + portId Identifier, + channelId Identifier, + proofHeight Height) (ChannelEnd, CommitmentProof) + +// Returns connection end with a commitment proof. If proof != nil, then it is being verified with the corresponding light client. +// Connection end is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, +// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +GetConnection(chain Chain, + connectionId Identifier, + proofHeight Height) (ConnectionEnd, CommitmentProof) + + +// Returns client connection with a commitment proof. If proof != nil, then it is being verified with the corresponding light client. +// Client state is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, +// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +GetClientState(chain Chain, + clientId Identifier, + proofHeight Height) (ClientState, CommitmentProof) + +// Returns packet commitment with a commitment proof. If proof != nil, then it is being verified with the corresponding light client. +// Packet commitment is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, +// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +GetPacketCommitment(chain Chain, + portId Identifier, + channelId Identifier, + sequence uint64, + proofHeight Height) (bytes, CommitmentProof) + +// Returns next recv sequence number a commitment proof. If proof != nil, then it is being verified with the corresponding +// light client. It is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, +// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +GetNextSequenceRecv(chain Chain, + portId Identifier, + channelId Identifier, + proofHeight Height) (uint64, CommitmentProof) + +// Returns packet acknowledgment with a commitment proof. If proof != nil, then it is being verified with the +// corresponding light client. Packet acknowledgment is queried at the given chain at the height proofHeight. +// If LATEST_HEIGHT is passed as a parameter, the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +GetPacketAcknowledgement(chain Chain, + portId Identifier, + channelId Identifier, + sequence uint64, + proofHeight Height) (bytes, CommitmentProof) + +// Returns estimate of the consensus height on the given chain. +GetConsensusHeight(chain Chain) Height + +// Returns estimate of the current time on the given chain. +GetCurrentTimestamp(chainB) uint64 + ``` ## Computing destination chain ```golang -func getDestinationInfo(ev IBCEvent, chainA Chain) Chain { +func GetDestinationInfo(ev IBCEvent, chainA Chain) Chain { switch ev.type { case SendPacketEvent: channel, proof = GetChannel(chainA, ev.sourcePort, ev.sourceChannel, ev.Height) @@ -110,7 +171,8 @@ func getDestinationInfo(ev IBCEvent, chainA Chain) Chain { if proof == nil return nil clientState = GetClientState(chainA, connection.clientIdentifier, ev.Height) - return getHostInfo(clientStateA.chainID) + return getHostInfo(clientStateA.chainID) + ... } } ``` @@ -123,46 +185,46 @@ func getDestinationInfo(ev IBCEvent, chainA Chain) Chain { func createDatagram(ev SendPacketEvent, chainA Chain, chainB Chain, installedHeight Height) PacketRecv { // Stage 1 - // Verify if packet is committed to chain A + // Verify if packet is committed to chain A and it is still pending (commitment exists) proofHeight = installedHeight - 1 packetCommitment, packetCommitmentProof = GetPacketCommitment(chainA, ev.sourcePort, ev.sourceChannel, ev.sequence, proofHeight) if packetCommitmentProof != nil { return nil } - // if packet commitment is empty, then packet is already received by the counter party if packetCommitment == null OR - if packetCommitment != hash(concat(ev.data, ev.timeoutHeight, ev.timeoutTimestamp)) { return nil } + packetCommitment != hash(concat(ev.data, ev.timeoutHeight, ev.timeoutTimestamp)) { return nil } // Stage 2 // Execute checks IBC handler on chainB will execute - channelB, proof = GetChannel(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) - if proof == nil { return nil } + channel, proof = GetChannel(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) + if proof != nil { return nil } - if channelB == null OR - channelB.state != OPEN OR - ev.sourcePort != channelB.counterpartyPortIdentifier OR - ev.sourceChannel != channelB.counterpartyChannelIdentifier { return nil } + if channel == null OR + channel.state != OPEN OR + ev.sourcePort != channel.counterpartyPortIdentifier OR + ev.sourceChannel != channel.counterpartyChannelIdentifier { return nil } - connectionIdB = channelB.connectionHops[0] - connectionB, proof = GetConnection(chainB, connectionIdB, LATEST_HEIGHT) - if proof == nil { return nil } + connectionId = channel.connectionHops[0] + connection, proof = GetConnection(chainB, connectionId, LATEST_HEIGHT) + if proof != nil { return nil } if connectionB == null OR connectionB.state != OPEN { return nil } - if ev.timeoutHeight != 0 AND GetConsensusHeight(chainB, LATEST_HEIGHT) >= ev.timeoutHeight { return nil } - if ev.timeoutTimestamp != 0 AND GetCurrentTimestamp(chainB, LATEST_HEIGHT) >= ev.timeoutTimestamp { return nil } + if ev.timeoutHeight != 0 AND GetConsensusHeight(chainB) >= ev.timeoutHeight { return nil } + if ev.timeoutTimestamp != 0 AND GetCurrentTimestamp(chainB) >= ev.timeoutTimestamp { return nil } // we now check if this packet is already received by the destination chain - if (channel.order === ORDERED) { + if (channel.ordering === ORDERED) { nextSequenceRecv, proof = GetNextSequenceRecv(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) if proof != nil { return nil } if ev.sequence != nextSequenceRecv { return nil } // packet has already been delivered by another relayer } else { - packetAcknowledgement, proof = GetPacketAcknowledgement(ev.destPort, ev.destChannel, ev.sequence) + packetAcknowledgement, proof = + GetPacketAcknowledgement(chainB, ev.destPort, ev.destChannel, ev.sequence, LATEST_HEIGHT) if proof != nil { return nil } if packetAcknowledgement != nil { return nil } From 2d5019d37be1af29f2bc7583f99ec8e6fa935afb Mon Sep 17 00:00:00 2001 From: Zarko Milosevic Date: Tue, 15 Sep 2020 14:44:35 +0200 Subject: [PATCH 5/5] Address reviewer feedback --- docs/spec/relayer/Packets.md | 65 +++++++++++++++++++++++------------- docs/spec/relayer/Relayer.md | 24 +++++++------ 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/docs/spec/relayer/Packets.md b/docs/spec/relayer/Packets.md index fb14e218b6..1de2ff1f79 100644 --- a/docs/spec/relayer/Packets.md +++ b/docs/spec/relayer/Packets.md @@ -100,49 +100,37 @@ type ClientState { We assume the existence of the following helper functions: ```go -// Returns channel end with a commitment proof. If proof != nil, then it is being verified with the corresponding light client. -// Channel end is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, -// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +// Returns channel end with a commitment proof. GetChannel(chain Chain, portId Identifier, channelId Identifier, proofHeight Height) (ChannelEnd, CommitmentProof) -// Returns connection end with a commitment proof. If proof != nil, then it is being verified with the corresponding light client. -// Connection end is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, -// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +// Returns connection end with a commitment proof. GetConnection(chain Chain, connectionId Identifier, proofHeight Height) (ConnectionEnd, CommitmentProof) -// Returns client connection with a commitment proof. If proof != nil, then it is being verified with the corresponding light client. -// Client state is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, -// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +// Returns client state with a commitment proof. GetClientState(chain Chain, clientId Identifier, proofHeight Height) (ClientState, CommitmentProof) -// Returns packet commitment with a commitment proof. If proof != nil, then it is being verified with the corresponding light client. -// Packet commitment is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, -// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +// Returns packet commitment with a commitment proof. GetPacketCommitment(chain Chain, portId Identifier, channelId Identifier, sequence uint64, proofHeight Height) (bytes, CommitmentProof) -// Returns next recv sequence number a commitment proof. If proof != nil, then it is being verified with the corresponding -// light client. It is queried at the given chain at the height proofHeight. If LATEST_HEIGHT is passed as a parameter, -// the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +// Returns next recv sequence number with a commitment proof. GetNextSequenceRecv(chain Chain, portId Identifier, channelId Identifier, proofHeight Height) (uint64, CommitmentProof) -// Returns packet acknowledgment with a commitment proof. If proof != nil, then it is being verified with the -// corresponding light client. Packet acknowledgment is queried at the given chain at the height proofHeight. -// If LATEST_HEIGHT is passed as a parameter, the query should be for latest height for which proof exists (MAX_HEIGHT - 1). +// Returns packet acknowledgment with a commitment proof. GetPacketAcknowledgement(chain Chain, portId Identifier, channelId Identifier, @@ -157,21 +145,50 @@ GetCurrentTimestamp(chainB) uint64 ``` +For functions that return proof, if proof != nil, then the returned value is being verified. +The value is being verified using the header's app hash that is provided by the corresponding light client. +We now show the pseudocode for one of those functions: + +```go +GetChannel(chain Chain, + portId Identifier, + channelId Identifier, + proofHeight Height) (ChannelEnd, CommitmentProof) { + + // Query provable store exposed by the full node of chain. + // The path for the channel end is at channelEnds/ports/{portId}/channels/{channelId}". + // The membership proof returned is read at height proofHeight. + channel, proof = QueryChannel(chain, portId, channelId, proofHeight) + if proof == nil return { (nil, nil) } + + header = GetHeader(chain.lc, proofHeight) // get header for height proofHeight using light client of the given chain + + // verify membership of the channel at path channelEnds/ports/{portId}/channels/{channelId} using + // the root hash header.AppHash + if verifyMembership(header.AppHash, proofHeight, proof, channelPath(portId, channelId), channel) { + return channel, proof + } else { return (nil, nil) } +} +``` +If LATEST_HEIGHT is passed as a parameter, the data should be read (and the corresponding proof created) +at the most recent height. + + ## Computing destination chain ```golang func GetDestinationInfo(ev IBCEvent, chainA Chain) Chain { switch ev.type { case SendPacketEvent: - channel, proof = GetChannel(chainA, ev.sourcePort, ev.sourceChannel, ev.Height) + channel, proof = GetChannel(chain, ev.sourcePort, ev.sourceChannel, ev.Height) if proof == nil return nil connectionId = channel.connectionHops[0] - connection, proof = GetConnection(chainA, connectionId, ev.Height) + connection, proof = GetConnection(chain, connectionId, ev.Height) if proof == nil return nil - clientState = GetClientState(chainA, connection.clientIdentifier, ev.Height) - return getHostInfo(clientStateA.chainID) + clientState = GetClientState(chain, connection.clientIdentifier, ev.Height) + return getHostInfo(clientState.chainID) ... } } @@ -182,7 +199,7 @@ func GetDestinationInfo(ev IBCEvent, chainA Chain) Chain { ### PacketRecv datagram creation ```golang -func createDatagram(ev SendPacketEvent, chainA Chain, chainB Chain, installedHeight Height) PacketRecv { +func createPacketRecvDatagram(ev SendPacketEvent, chainA Chain, chainB Chain, installedHeight Height) PacketRecv { // Stage 1 // Verify if packet is committed to chain A and it is still pending (commitment exists) @@ -210,7 +227,7 @@ func createDatagram(ev SendPacketEvent, chainA Chain, chainB Chain, installedHei connection, proof = GetConnection(chainB, connectionId, LATEST_HEIGHT) if proof != nil { return nil } - if connectionB == null OR connectionB.state != OPEN { return nil } + if connection == null OR connection.state != OPEN { return nil } if ev.timeoutHeight != 0 AND GetConsensusHeight(chainB) >= ev.timeoutHeight { return nil } if ev.timeoutTimestamp != 0 AND GetCurrentTimestamp(chainB) >= ev.timeoutTimestamp { return nil } diff --git a/docs/spec/relayer/Relayer.md b/docs/spec/relayer/Relayer.md index ce13c8fe61..c8fc6e315a 100644 --- a/docs/spec/relayer/Relayer.md +++ b/docs/spec/relayer/Relayer.md @@ -196,12 +196,12 @@ will eventually have access to a correct full node. ### Data availability -Note that data written to a store at height h as part of executing block b (b.Height = h) is effectively committed by +Note that data written to a store at height *h* as part of executing block *b* (`b.Height = h`) is effectively committed by the next block (at height h+1). The reason is the fact that the data store root hash as an effect of executing block at -height h is part of the block header at height h+1. Therefore data read at height h is available until time -`t = b.Header.Time + UNBONDING_PERIOD`, where `b.Header.Height = h+1`. After time t we cannot trust that data anymore. -Note that data present in the store are re-validated by each new block: data added/modified at block h are still -valid even if not altered after as they are still "covered" by the root hash of the store. +height h is part of the block header at height h+1. Therefore, data read at height h is available until time +`t = b.Header.Time + UNBONDING_PERIOD`, where `b.Header.Height = h+1`. After time *t* we cannot trust that data anymore. +Note that data present in the store are re-validated by each new block: data added/modified at block *h* are still +valid even if not altered after, as they are still "covered" by the root hash of the store. Therefore UNBONDING_PERIOD gives absolute time bound during which relayer needs to transfer data read at source chain to the destination chain. As we will explain below, due to fork detection and accountability protocols, the effective @@ -210,17 +210,19 @@ data availability period will be shorter than UNBONDING_PERIOD. ### Data verification As connected chains in IBC do not blindly trust each other, data coming from the opposite chain must be verified at -the destination before being acted upon. If we assume that data d is read from the data store of a chain at height h, - - -Data verification in IBC is implemented by relying on the concept of light client. +the destination before being acted upon. Data verification in IBC is implemented by relying on the concept of light client. Light client is a process that by relying on an initial trusted header (subjective initialisation), verifies and maintains set of trusted headers. Note that a light client does not maintain full blockchain and does not execute (verify) application transitions. It operates by relying on the Tendermint security model, and by applying header verification logic that operates only on signed headers (header + corresponding commit). -More details about light client assumptions and protocols can be found here. For the purpose of this document, we assume -that a relayer has access to the light client node that provides trusted headers. +More details about light client assumptions and protocols can be found +[here](https://github.com/tendermint/spec/tree/master/rust-spec/lightclient). For the purpose of this document, we assume +that a relayer has access to the light client node that provides trusted headers. +Given a data d read at a given path at height h with a proof p, we assume existence of a function +`verifyMembership(header.AppHash, h, proof, path, d)` that returns `true` if data was committed by the corresponding +chain at height *h*. The trusted header is provided by the corresponding light client. +