diff --git a/docs/spec/relayer/Definitions.md b/docs/spec/relayer/Definitions.md new file mode 100644 index 0000000000..5d8830a717 --- /dev/null +++ b/docs/spec/relayer/Definitions.md @@ -0,0 +1,292 @@ +# Data structure and helper function definitions + +This document defines data types and helper functions used by the relayer logic. + +## Data Types + +### Chain + +Chain is a data structure that captures relayer's perspective of a given chain and contains all important +information that allows relayer to communicate with a chain. A provider is a Tendermint full node through +which a relayer read information about the given chain and submit transactions. A relayer maintains a list +of full nodes (*peerList*) as a current provider could be faulty, so it can be replaced by another full node. +For each chain a relayer is connected to, the relayer has a light client that provides the relayer +access to the trusted headers (used as part of data verification). + +```go +type Chain { + chainID string + clientID Identifier + peerList List> + provider Pair + lc LightClient +} +``` + +### Client state and consensus state + +```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 +} +``` + +```go +type ConsensusState { + timestamp uint64 + validatorSet List> + commitmentRoot []byte +} +``` + +### Membership proof + +```go +type MembershipProof struct { + Height Height + Proof Proof +} +``` + +### Connection + +```go +type ConnectionEnd { + state ConnectionState + counterpartyConnectionIdentifier Identifier + counterpartyPrefix CommitmentPrefix + clientIdentifier Identifier + counterpartyClientIdentifier Identifier + version []string +} + +enum ConnectionState { + INIT, + TRYOPEN, + OPEN, +} +``` + +### Channel + +```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 Packet { + sequence uint64 + timeoutHeight Height + timeoutTimestamp uint64 + sourcePort Identifier + sourceChannel Identifier + destPort Identifier + destChannel Identifier + data []byte +} +``` + +```go +type PacketRecv { + packet Packet + proof CommitmentProof + proofHeight Height +} +``` + +```go +type PacketAcknowledgement { + packet Packet + acknowledgement byte[] + proof CommitmentProof + proofHeight Height +} +``` + +## Helper functions + +We assume the existence of the following helper functions: + +```go +// Returns channel end with a commitment proof. +GetChannel(chain Chain, + portId Identifier, + channelId Identifier, + proofHeight Height) (ChannelEnd, CommitmentProof, Error) + +// Returns connection end with a commitment proof. +GetConnection(chain Chain, + connectionId Identifier, + proofHeight Height) (ConnectionEnd, CommitmentProof, Error) + + +// Returns client state with a commitment proof. +GetClientState(chain Chain, + clientId Identifier, + proofHeight Height) (ClientState, CommitmentProof, Error) + +// Returns consensus state with a commitment proof. +GetConsensusState(chain Chain, + clientId Identifier, + targetHeight Height, + proofHeight Height) (ConsensusState, CommitmentProof, Error) + + +// Returns packet commitment with a commitment proof. +GetPacketCommitment(chain Chain, + portId Identifier, + channelId Identifier, + sequence uint64, + proofHeight Height) (bytes, CommitmentProof, Error) + +// Returns next recv sequence number with a commitment proof. +GetNextSequenceRecv(chain Chain, + portId Identifier, + channelId Identifier, + proofHeight Height) (uint64, CommitmentProof, Error) + + +// Returns next recv sequence number with a commitment proof. +GetNextSequenceAck(chain Chain, + portId Identifier, + channelId Identifier, + proofHeight Height) (uint64, CommitmentProof, Error) + + +// Returns packet acknowledgment with a commitment proof. +GetPacketAcknowledgement(chain Chain, + portId Identifier, + channelId Identifier, + sequence uint64, + proofHeight Height) (bytes, CommitmentProof, Error) + + +// Returns packet receipt with a commitment proof. +GetPacketReceipt(chain Chain, + portId Identifier, + channelId Identifier, + sequence uint64, + proofHeight Height) (String, CommitmentProof, Error) + + +// 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 + +// Verify that the data is written at the given path using provided membership proof and the root hash. +VerifyMembership(rootHash []byte, + proofHeight Height, + proof MembershipProof, + path String, + data []byte) boolean + +// Create IBC datagram as part of processing event at chainA. +CreateDatagram(ev IBCEvent, + chainA Chain, + chainB Chain, + installedHeight Height) (IBCDatagram, Error) + +// Create UpdateClient datagrams from the list of signed headers +CreateUpdateClientDatagrams(shs []SignedHeader) IBCDatagram[] + +// Submit given datagram to a given chain +Submit(chain Chain, datagram IBCDatagram) Error + +// Return the correspondin chain for a given chainID +// We assume that the relayer maintains a map of known chainIDs and the corresponding chains. +GetChain(chainID String) Chain +``` + +For functions that return proof, if `error == 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. + +Helper functions listed above assume querying (parts of the) application state using Tendermint RPC. For example, +`GetChannel` relies on `QueryChannel`. RPC calls can fail if: + +- no response is received within some timeout or +- malformed response is received. + +In both cases, error handling logic should be defined by the caller. For example, in the former case, the caller might +retry sending the same request to a same provider (full node), while in the latter case the request might be sent to +some other provider node. Although these kinds of errors could be due to network infrastructure issues, it is normally +simpler to blame the provider (assume implicitly network is always correct and reliable). Therefore, correct provider +always respond timely with a correct response, while in case of errors we consider the provider node faulty, and then +we replace it with a different node. + +We assume the following error types: + +```golang +enum Error { + RETRY, // transient processing error (for example due to optimistic send); function can be retried later + DROP, // event has already been received by the destination chain so it should be dropped + BADPROVIDER, // provider does not reply timely or with a correct data; it normally leads to replacing provider + BADLIGHTCLIENT // light client does not reply timely or with a correct data +} +``` + +We now show the pseudocode for one of those functions: + +```go +func GetChannel(chain Chain, + portId Identifier, + channelId Identifier, + proofHeight Height) (ChannelEnd, CommitmentProof, Error) { + + // Query provable store exposed by the full node of chain. + // The path for the channel end is at channelEnds/ports/{portId}/channels/{channelId}". + // The channel and the membership proof returned is read at height proofHeight - 1. + channel, proof, error = QueryChannel(chain.provider, portId, channelId, proofHeight) + if error != nil { return (nil, nil, Error.BADPROVIDER) } + + header, error = GetHeader(chain.lc, proofHeight) // get header for height proofHeight using light client + if error != nil { return (nil, nil, Error.BADLIGHTCLIENT) } // return if light client can't provide header for the given height + + // 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) { + // membership check fails; therefore provider is faulty. Try to elect new provider + return (nil, nil, Error.BadProvider) + } + + return (channel, proof, nil) +} +``` + +If *LATEST_HEIGHT* is passed as a parameter, the data should be read (and the corresponding proof created) +at the most recent height. + + + + diff --git a/docs/spec/relayer/Packets.md b/docs/spec/relayer/Packets.md index 1de2ff1f79..b4374ac4c1 100644 --- a/docs/spec/relayer/Packets.md +++ b/docs/spec/relayer/Packets.md @@ -2,10 +2,11 @@ This document specifies datagram creation logic for packets. It is used by the relayer. -## Data Types +## Packet related IBC events ```go -type Packet { +type SendPacketEvent { + height Height sequence uint64 timeoutHeight Height timeoutTimestamp uint64 @@ -13,238 +14,88 @@ type Packet { sourceChannel Identifier destPort Identifier destChannel Identifier - data bytes -} -``` - -```go -type PacketRecv { - packet Packet - proof CommitmentProof - proofHeight Height + data []byte } ``` ```go -type SendPacketEvent { +type WriteAcknowledgementEvent { height Height + port Identifier + channel Identifier 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, + timeoutTimestamp uint64 + data []byte + acknowledgement []byte } ``` -```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 -} -``` -## Helper functions - -We assume the existence of the following helper functions: - -```go -// 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. -GetConnection(chain Chain, - connectionId Identifier, - proofHeight Height) (ConnectionEnd, CommitmentProof) +## Event handlers +### SendPacketEvent handler -// Returns client state with a commitment proof. -GetClientState(chain Chain, - clientId Identifier, - proofHeight Height) (ClientState, CommitmentProof) +Successful handling of *SendPacketEvent* leads to *PacketRecv* datagram creation. -// 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 with a commitment proof. -GetNextSequenceRecv(chain Chain, - portId Identifier, - channelId Identifier, - proofHeight Height) (uint64, CommitmentProof) - -// Returns packet acknowledgment with a commitment proof. -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 - -``` - -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(chain, ev.sourcePort, ev.sourceChannel, ev.Height) - if proof == nil return nil - - connectionId = channel.connectionHops[0] - connection, proof = GetConnection(chain, connectionId, ev.Height) - if proof == nil return nil - - clientState = GetClientState(chain, connection.clientIdentifier, ev.Height) - return getHostInfo(clientState.chainID) - ... - } -} -``` - -## Datagram creation logic - -### PacketRecv datagram creation +// NOTE: Stateful relayer might keep packet that are not acked in the state so the following logic +// can be a bit simpler. ```golang -func createPacketRecvDatagram(ev SendPacketEvent, chainA Chain, chainB Chain, installedHeight Height) PacketRecv { +func CreateDatagram(ev SendPacketEvent, + chainA Chain, // source chain + chainB Chain, // destination chain + proofHeight Height) (PacketRecv, Error) { // Stage 1 // Verify if packet is committed to chain A and it is still pending (commitment exists) - proofHeight = installedHeight - 1 - packetCommitment, packetCommitmentProof = + packetCommitment, packetCommitmentProof, error = GetPacketCommitment(chainA, ev.sourcePort, ev.sourceChannel, ev.sequence, proofHeight) - if packetCommitmentProof != nil { return nil } + if error != nil { return (nil, error) } - if packetCommitment == null OR - packetCommitment != hash(concat(ev.data, ev.timeoutHeight, ev.timeoutTimestamp)) { return nil } + if packetCommitment == nil OR + packetCommitment != hash(concat(ev.data, ev.timeoutHeight, ev.timeoutTimestamp)) { + // invalid event; bad provider + return (nil, Error.BADPROVIDER) + } // Stage 2 // Execute checks IBC handler on chainB will execute - channel, proof = GetChannel(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) - if proof != nil { return nil } + channel, proof, error = GetChannel(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) + if error != nil { return (nil, error) } - if channel == null OR - channel.state != OPEN OR - ev.sourcePort != channel.counterpartyPortIdentifier OR - ev.sourceChannel != channel.counterpartyChannelIdentifier { return nil } + if channel != nil AND + (channel.state == CLOSED OR + ev.sourcePort != channel.counterpartyPortIdentifier OR + ev.sourceChannel != channel.counterpartyChannelIdentifier) { return (nil, Error.DROP) } + if channel == nil OR channel.state != OPEN { return (nil, Error.RETRY) } + // TODO: Maybe we shouldn't even enter handle loop for packets if the corresponding channel is not open! + connectionId = channel.connectionHops[0] - connection, proof = GetConnection(chainB, connectionId, LATEST_HEIGHT) - if proof != nil { return nil } + connection, proof, error = GetConnection(chainB, connectionId, LATEST_HEIGHT) + if error != nil { return (nil, error) } - if connection == null OR connection.state != OPEN { return nil } + if connection == nil OR connection.state != OPEN { return (nil, Error.RETRY) } - if ev.timeoutHeight != 0 AND GetConsensusHeight(chainB) >= ev.timeoutHeight { return nil } - if ev.timeoutTimestamp != 0 AND GetCurrentTimestamp(chainB) >= ev.timeoutTimestamp { return nil } + if ev.timeoutHeight != 0 AND GetConsensusHeight(chainB) >= ev.timeoutHeight { return (nil, Error.DROP) } + if ev.timeoutTimestamp != 0 AND GetCurrentTimestamp(chainB) >= ev.timeoutTimestamp { return (nil, Error.DROP) } // we now check if this packet is already received by the destination chain - if (channel.ordering === ORDERED) { - nextSequenceRecv, proof = GetNextSequenceRecv(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) - if proof != nil { return nil } + if channel.ordering === ORDERED { + nextSequenceRecv, proof, error = GetNextSequenceRecv(chainB, ev.destPort, ev.destChannel, LATEST_HEIGHT) + if error != nil { return (nil, error) } - if ev.sequence != nextSequenceRecv { return nil } // packet has already been delivered by another relayer + if ev.sequence != nextSequenceRecv { return (nil, Error.DROP) } // packet has already been delivered by another relayer } else { - packetAcknowledgement, proof = - GetPacketAcknowledgement(chainB, ev.destPort, ev.destChannel, ev.sequence, LATEST_HEIGHT) - if proof != nil { return nil } + // Note that absence of receipt (packetReceipt == nil) is also proven also and we should be able to verify it. + packetReceipt, proof, error = + GetPacketReceipt(chainB, ev.destPort, ev.destChannel, ev.sequence, LATEST_HEIGHT) + if error != nil { return (nil, error) } - if packetAcknowledgement != nil { return nil } + if packetReceipt != nil { return (nil, Error.DROP) } // packet has already been delivered by another relayer } // Stage 3 @@ -260,8 +111,86 @@ func createPacketRecvDatagram(ev SendPacketEvent, chainA Chain, chainB Chain, in data: ev.data } - return PacketRecv { packet, packetCommitmentProof, proofHeight } + return (PacketRecv { packet, packetCommitmentProof, proofHeight }, nil) } ``` +### WriteAcknowledgementEvent handler + +Successful handling of *WriteAcknowledgementEvent* leads to *PacketAcknowledgement* datagram creation. + +```golang +func CreateDatagram(ev WriteAcknowledgementEvent, + chainA Chain, // source chain + chainB Chain, // destination chain + proofHeight Height) (PacketAcknowledgement, Error) { + + // Stage 1 + // Verify if acknowledment is committed to chain A and it is still pending + packetAck, packetAckCommitmentProof, error = + GetPacketAcknowledgement(chainA, ev.port, ev.channel, ev.sequence, proofHeight) + if error != nil { return (nil, error) } + + if packetAck == nil OR packetAck != hash(ev.acknowledgement) { + // invalid event; bad provider + return (nil, Error.BADPROVIDER) + } + // Stage 2 + // Execute checks IBC handler on chainB will execute + + // Fetch channelEnd from the chainA to be able to compute port and chain ids on destination chain + channelA, proof, error = GetChannel(chainA, ev.port, ev.channel, ev.height) + if error != nil { return (nil, error) } + + channelB, proof, error = + GetChannel(chainB, channelA.counterpartyPortIdentifier, channelA.counterpartyChannelIdentifier, LATEST_HEIGHT) + if error != nil { return (nil, error) } + + if channelB == nil OR channel.state != OPEN { (nil, Error.DROP) } + // Note that we checked implicitly above that counterparty identifiers match each other + + connectionId = channelB.connectionHops[0] + connection, proof, error = GetConnection(chainB, connectionId, LATEST_HEIGHT) + if error != nil { return (nil, error) } + + if connection == nil OR connection.state != OPEN { return (nil, Error.DROP) } + + // verify the packet is sent by chainB and hasn't been cleared out yet + packetCommitment, packetCommitmentProof, error = + GetPacketCommitment(chainB, channelA.counterpartyPortIdentifier, + channelA.counterpartyChannelIdentifier, ev.sequence, LATEST_HEIGHT) + if error != nil { return (nil, error) } + + if packetCommitment == nil OR + packetCommitment != hash(concat(ev.data, ev.timeoutHeight, ev.timeoutTimestamp)) { + // invalid event; bad provider + return (nil, Error.BADPROVIDER) + } + + // abort transaction unless acknowledgement is processed in order + if channelB.ordering === ORDERED { + nextSequenceAck, proof, error = + GetNextSequenceAck(chainB, channelA.counterpartyPortIdentifier, + channelA.counterpartyChannelIdentifier, ev.sequence, LATEST_HEIGHT) + if error != nil { return (nil, error) } + + if ev.sequence != nextSequenceAck { return (nil, Error.DROP) } + } + + // Stage 3 + // Build datagram as all checks has passed + packet = Packet { + sequence: ev.sequence, + timeoutHeight: ev.timeoutHeight, + timeoutTimestamp: ev.timeoutTimestamp, + sourcePort: channelA.counterpartyPortIdentifier, + sourceChannel: channelA.counterpartyChannelIdentifier, + destPort: ev.port, + destChannel: ev.channel, + data: ev.data + } + + return (PacketAcknowledgement { packet, ev.acknowledgement, packetAckCommitmentProof, proofHeight }, nil) +} +``` diff --git a/docs/spec/relayer/Relayer.md b/docs/spec/relayer/Relayer.md index c8fc6e315a..4158c38dfe 100644 --- a/docs/spec/relayer/Relayer.md +++ b/docs/spec/relayer/Relayer.md @@ -24,155 +24,6 @@ eventually received by the module B. **[ICS18-Validity]**: If a module B receives an IBC datagram m from a module A, then m was sent by the module A to the module B. -## Data Types - -```go -type ClientState struct { - Height Height - SignedHeader SignedHeader -} -``` - -```go -type MembershipProof struct { - Height Height - Proof Proof -} -``` - -## Relayer algorithm - -We assume the existence of the following helper functions: - -```go -// returns ClientState for the targetHeight if it exists; otherwise returns ClientState at the latest height. -// We assume that this function handles non-responsive full node error by switching to a different full node. -queryClientConsensusState(chainA, targetHeight) (ClientState, MembershipProof) -verifyClientStateProof(clientStateAonB, membershipProof, sh.appHash) boolean -pendingDatagrams(height, chainA, chainB) IBCDatagram[] -verifyProof(datagrams, sh.appHash) boolean -createUpdateClientDatagrams(shs) IBCDatagram[] -submit(datagrams) error -replaceFullNode(chain) -``` - -The main relayer event loop is a pipeline of three stages. Assuming some IBC event at height `h` on `chainA`, -the relayer: - -1. Updates (on `chainB`) the IBC client for `chainA` to a certain height `H` where `H >= h+1`. -2. Create IBC datagrams at height `H-1`. -3. Submit the datagrams from stage (2) to `chainB`. - -Note that an IBC event at height `h` corresponds to the modifications to the data store made as part of executing -block at height `h`. The corresponding proof (that data is indeed written to the data store) can be verified using -the data store root hash that is part of the header at height `h+1`. - -Once stage 1 finishes correctly, stage 2 should succeed assuming that `chainB` has not already processed the event. The -interface between stage 1 and stage 2 is just the height `H`. Once stage 2 finishes correctly, stage 3 should -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) { - // 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. - installedHeight, error := updateIBCClient(chainB, chainA, targetHeight) - if error != nil { - return error - } - - // Stage 3. - // Create the IBC datagrams including `ev` & verify them. - datagram = createDatagram(ev, chainA, chainB, installedHeight) - - // Stage 4. - // Submit datagrams. - if datagram != nil { - chainB.submit(datagram) - } -} - - -// 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} -} -``` - ## System model We assume that a correct relayer operates in the following model: @@ -181,8 +32,8 @@ We assume that a correct relayer operates in the following model: Relayer transfers data between two chains: chainA and chainB. For simplicity, we assume Tendermint chains. Each chain operates under Tendermint security model: -- given a block b at height h committed at time t = b.Header.Time, +2/3 of voting power behaves correctly -at least before t + UNBONDING_PERIOD, where UNBONDING_PERIOD is a system parameter (typically order of weeks). +- given a block b at height h committed at time `t = b.Header.Time`, `+2/3` of voting power behaves correctly +at least before `t + UNBONDING_PERIOD`, where `UNBONDING_PERIOD` is a system parameter (typically order of weeks). Validators sets can be changed in every block, and we don't assume any constraint on the way validators are changed (application specific logic). @@ -220,9 +71,126 @@ 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 +`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. +## Relayer algorithm + +The main relayer event loop is a pipeline of four stages. Assuming some IBC event at height `h` on `chainA`, +the relayer: + +1. Determines destination chain (`chainB`) +2. Updates (on `chainB`) the IBC client for `chainA` to a certain height `H` where `H >= h+1`. +3. Creates IBC datagram at height `H-1`. +4. Submits the datagram from stage (2) to `chainB`. + +Note that an IBC event at height `h` corresponds to the modifications to the data store made as part of executing +block at height `h`. The corresponding proof (that data is indeed written to the data store) can be verified using +the data store root hash that is part of the header at height `h+1`. + +Once stage 2 finishes correctly, stage 3 should succeed assuming that `chainB` has not already processed the event. The +interface between stage 2 and stage 3 is just the height `H`. Once stage 3 finishes correctly, stage 4 should +succeed. The interface between stage 3 and stage 4 is an IBC datagram. + +We assume that the corresponding light client is correctly installed on each chain. + +Data structures and helper function definitions are provided +[here](https://github.com/informalsystems/ibc-rs/blob/master/docs/spec/relayer/Definitions.md). + +```golang +func handleEvent(ev, chainA) Error { + // 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. + + // Stage 1. + // Determine destination chain + chainB, error = getDestinationInfo(ev, chainA) + if error != nil { return error } + + // Stage 2. + // Update on `chainB` the IBC client for `chainA` to height `>= targetHeight`. + targetHeight = ev.height + 1 + // See the code for `updateIBCClient` below. + proofHeight, error := updateIBCClient(chainB, chainA, targetHeight) + if error != nil { return error } + + // Stage 3. + // Create the IBC datagrams including `ev` & verify them. + datagram, error = CreateDatagram(ev, chainA, chainB, proofHeight) + if error != nil { return error } + + // Stage 4. + // Submit datagrams. + error = Submit(chainB, datagram) + if error != nil { return error } +} + +func getDestinationInfo(ev IBCEvent, chain Chain) (Chain, Error) { + switch ev.type { + case SendPacketEvent: + chainId, error = getChainId(chain, ev.sourcePort, ev.sourceChannel, ev.Height) + if error != nil { return (nil, error) } + + chain = GetChain(chainId) + if chain == nil { return (nil, Error.DROP) } + + return (chain, nil) + + case WriteAcknowledgementEvent: + chainId, error = getChainId(chain, ev.Port, ev.Channel, ev.Height) + if error != nil { return (nil, error) } + + chain = GetChain(chainId) + if chain == nil { nil, Error.DROP } + + return (chain, nil) + } +} + +// Return chaindId of the destination chain based on port and channel info for the given chain +func getChainId(chain Chain, port Identifier, channel Identifier, height Height) (String, Error) { + channel, proof, error = GetChannel(chain, port, channel, height) + if error != nil { return (nil, error) } + + connectionId = channel.connectionHops[0] + connection, proof, error = GetConnection(chain, connectionId, height) + if error != nil { return (nil, error) } + + clientState, proof, error = GetClientState(chain, connection.clientIdentifier, height) + if error != nil { return (nil, error) } + + return (clientState.chainID, error) +} + +// 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 some of verification steps fail +func updateIBCClient(dest Chain, src Chain, targetHeight Height) -> (Height, Error) { + + clientState, proof, error = GetClientState(dest, dest.clientId, LATEST_HEIGHT) + if error != nil { return (nil, error) } + // NOTE: What if a full node we are connected to send us stale (but correct) information regarding targetHeight? + + // if installed height is smaller than the targetHeight, we need to update client with targetHeight + while (clientState.latestHeight < targetHeight) { + // Do an update to IBC client for `src` on `dest`. + shs, error = src.lc.getMinimalSet(clientState.latestHeight, targetHeight) + if error != nil { return (nil, error) } + + error = dest.submit(createUpdateClientDatagrams(shs)) + if error != nil { return (nil, error) } + + clientState, proof, error = GetClientState(dest, dest.clientId, LATEST_HEIGHT) + if error != nil { return (nil, error) } + } + + // NOTE: semantic check of the installed header is done using fork detection component + return { clientState.Height, nil } +} +``` @@ -230,10 +198,3 @@ chain at height *h*. The trusted header is provided by the corresponding light c -- it transfers data between two chains: chainA and chainB. This implies that a -relayer has connections with full nodes from chainA and chainB in order to inspect their -state. We assume that blockchain applications that operates on top of chainA and chainB writes -relevant data into publicly available data store (for example IBC packets). -- in order to verify data written by the application to its store, a relayer needs -light client node for each connected chain. Light client will on its own establish connections -with multiple