Formatting updates
This commit is contained in:
parent
00489162ac
commit
37fd166829
|
@ -4,173 +4,216 @@
|
|||
|
||||
### 3.1 Background
|
||||
|
||||
IBC uses a cross-chain message passing model that makes no assumptions about network synchrony. IBC *data packets* (hereafter just *packets*) are relayed from one blockchain to the other by external infrastructure. Chain _A_ and chain _B_ confirm new blocks independently, and packets from one chain to the other may be delayed or censored arbitrarily. The speed of packet transmission and confirmation is limited only by the speed of the underlying chains.
|
||||
IBC uses a cross-chain message passing model that makes no assumptions about network synchrony. IBC *data packets* (hereafter just *packets*) are relayed from one blockchain to the other by external infrastructure. Chain `A` and chain `B` confirm new blocks independently, and packets from one chain to the other may be delayed or censored arbitrarily. The speed of packet transmission and confirmation is limited only by the speed of the underlying chains.
|
||||
|
||||
The IBC protocol as defined here is payload-agnostic. The packet receiver on chain _B_ decides how to act upon the incoming message, and may add its own application logic to determine which state transactions to apply according to what data the packet contains. Both chains must only agree that the packet has been received and either accepted or rejected.
|
||||
The IBC protocol as defined here is payload-agnostic. The packet receiver on chain `B` decides how to act upon the incoming message, and may add its own application logic to determine which state transactions to apply according to what data the packet contains. Both chains must only agree that the packet has been received and either accepted or rejected.
|
||||
|
||||
To facilitate useful application logic, we introduce an IBC *channel*: a set of reliable messaging queues that allows us to guarantee a cross-chain causal ordering[[5](./references.md#5)] of IBC packets. Causal ordering means that if packet _x_ is processed before packet _y_ on chain _A_, packet _x_ must also be processed before packet _y_ on chain _B_.
|
||||
To facilitate useful application logic, we introduce an IBC *channel*: a set of reliable messaging queues that allows us to guarantee a cross-chain causal ordering[[5](./references.md#5)] of IBC packets. Causal ordering means that if packet *x* is processed before packet *y* on chain `A`, packet *x* must also be processed before packet *y* on chain `B`.
|
||||
|
||||
IBC channels implement a vector clock [2](references.md#2) for the restricted case of two processes (in our case, blockchains). Given _x_ → _y_ means _x_ is causally before _y_, and chains A and B, and _a_ ⇒ _b_ means _a_ implies _b_:
|
||||
IBC channels implement a vector clock[[2](references.md#2)] for the restricted case of two processes (in our case, blockchains). Given *x* -> *y* means *x* is causally before *y*, chains `A` and `B`, and *a* => *b* means *a* implies *b*:
|
||||
|
||||
_A:send(msg<sub>i </sub>)_ → _B:receive(msg<sub>i </sub>)_
|
||||
*A:send(msg<sub>i </sub>)* -> *B:receive(msg<sub>i </sub>)*
|
||||
|
||||
_B:receive(msg<sub>i </sub>)_ → _A:receipt(msg<sub>i </sub>)_
|
||||
*B:receive(msg<sub>i </sub>)* -> *A:receipt(msg<sub>i </sub>)*
|
||||
|
||||
_A:send(msg<sub>i </sub>)_ → _A:send(msg<sub>i+1 </sub>)_
|
||||
*A:send(msg<sub>i </sub>)* -> *A:send(msg<sub>i+1 </sub>)*
|
||||
|
||||
_x_ → _A:send(msg<sub>i </sub>)_ ⇒
|
||||
_x_ → _B:receive(msg<sub>i </sub>)_
|
||||
*x* -> *A:send(msg<sub>i </sub>)* =>
|
||||
*x* -> *B:receive(msg<sub>i </sub>)*
|
||||
|
||||
_y_ → _B:receive(msg<sub>i </sub>)_ ⇒
|
||||
_y_ → _A:receipt(msg<sub>i </sub>)_
|
||||
*y* -> *B:receive(msg<sub>i </sub>)* =>
|
||||
*y* -> *A:receipt(msg<sub>i </sub>)*
|
||||
|
||||
Every transaction on the same chain already has a well-defined causality relation (order in history). IBC provides an ordering guarantee across two chains which can be used to reason about the combined state of both chains as a whole.
|
||||
|
||||
For example, an application may wish to allow a single tokenized asset to be transferred between and held on multiple blockchains while preserving fungibility and conservation of supply. The application can mint asset vouchers on chain _B_ when a particular IBC packet is committed to chain _B_, and require outgoing sends of that packet on chain _A_ to escrow an equal amount of the asset on chain _A_ until the vouchers are later redeemed back to chain _A_ with an IBC packet in the reverse direction. This ordering guarantee along with correct application logic can ensure that total supply is preserved across both chains and that any vouchers minted on chain _B_ can later be redeemed back to chain _A_.
|
||||
For example, an application may wish to allow a single tokenized asset to be transferred between and held on multiple blockchains while preserving fungibility and conservation of supply. The application can mint asset vouchers on chain `B` when a particular IBC packet is committed to chain `B`, and require outgoing sends of that packet on chain `A` to escrow an equal amount of the asset on chain `A` until the vouchers are later redeemed back to chain `A` with an IBC packet in the reverse direction. This ordering guarantee along with correct application logic can ensure that total supply is preserved across both chains and that any vouchers minted on chain `B` can later be redeemed back to chain `A`.
|
||||
|
||||
This section provides definitions for packets and channels, a high-level specification of the queue interface, and a list of the necessary proofs. To implement wire-compatible IBC, chain _A_ and chain _B_ must also use a common encoding format. An example binary encoding format can be found in Appendix C.
|
||||
This section provides definitions for packets and channels, a high-level specification of the queue interface, and a list of the necessary proofs. To implement wire-compatible IBC, chain `A` and chain `B` must also use a common encoding format. An example binary encoding format can be found in [Appendix C](appendices.md#appendix-c-merkle-proof-format).
|
||||
|
||||
### 3.2 Definitions
|
||||
|
||||
#### 3.2.1 Packet
|
||||
|
||||
We define an IBC *packet* _P_ as the five-tuple *(type, sequence, source, destination, data)*, where:
|
||||
We define an IBC *packet* `P` as the five-tuple `(type, sequence, source, destination, data)`, where:
|
||||
|
||||
**type** is an opaque routing field (an integer or string)
|
||||
`type` is an opaque routing field
|
||||
|
||||
**sequence** is an unsigned, arbitrary-precision integer
|
||||
`sequence` is an unsigned, arbitrary-precision integer
|
||||
|
||||
**source** is a string uniquely identifying the chain, connection, and channel from which this packet was sent
|
||||
`source` is a string uniquely identifying the chain, connection, and channel from which this packet was sent
|
||||
|
||||
**destination** is a string uniquely identifying the chain, connection, and channel which should receive this packet
|
||||
`destination` is a string uniquely identifying the chain, connection, and channel which should receive this packet
|
||||
|
||||
**data** is an opaque application payload
|
||||
`data` is an opaque application payload
|
||||
|
||||
#### 3.2.2 Receipt
|
||||
|
||||
We define an IBC *receipt* _R_ as the four-tuple *(sequence, source, destination, result)*, where
|
||||
We define an IBC *receipt* `R` as the four-tuple `(sequence, source, destination, result)`, where
|
||||
|
||||
**sequence** is an unsigned, arbitrary-precision integer
|
||||
`sequence` is an unsigned, arbitrary-precision integer
|
||||
|
||||
**source** is a string uniquely identifying the chain, connection, and channel from which this packet was sent
|
||||
`source` is a string uniquely identifying the chain, connection, and channel from which this packet was sent
|
||||
|
||||
**destination** is a string uniquely identifying the chain, connection, and channel which should receive this packet
|
||||
`destination` is a string uniquely identifying the chain, connection, and channel which should receive this packet
|
||||
|
||||
**result** is a code of either *success* or *failure*
|
||||
`result` is a code of either `success` or `failure`
|
||||
|
||||
#### 3.2.3 Queue
|
||||
|
||||
To implement strict message ordering, we introduce an ordered *queue*. A queue can be conceptualized as a slice of an infinite array. Two numerical indices - _q<sub>head</sub>_ and _q<sub>tail</sub>_ - bound the slice, such that for every _index_ where _head <= index < tail_, there is a queue element _q[q<sub>index</sub>]_. Elements can be appended to the tail (end) and removed from the head (beginning). We introduce one further method, _advance_, to facilitate efficient queue cleanup.
|
||||
To implement strict message ordering, we introduce an ordered *queue*. A queue can be conceptualized as a slice of an infinite array. Two numerical indices - `q_head` and `q_tail` - bound the slice, such that for every `index` where `q_head <= index < q_tail`, there is a queue element `q[q_index]`. Elements can be appended to the tail (end) and removed from the head (beginning). We introduce one further method, `advance`, to facilitate efficient queue cleanup.
|
||||
|
||||
Each IBC-supporting blockchain must provide a queue abstraction with the following functionality:
|
||||
|
||||
**init**
|
||||
> set _q<sub>head</sub>_ = _0_
|
||||
> set _q<sub>tail</sub>_ = _0_
|
||||
`init`
|
||||
|
||||
**peek** ⇒ **e**
|
||||
> match _q<sub>head</sub> == q<sub>tail</sub>_ with
|
||||
> _true_ ⇒ return _nil_
|
||||
> _false_ ⇒ return _q[q<sub>head</sub>]_
|
||||
```
|
||||
set q_head = 0
|
||||
set q_tail = 0
|
||||
```
|
||||
|
||||
**pop** ⇒ **e**
|
||||
> match _q<sub>head</sub> == q<sub>tail</sub>_ with
|
||||
> _true_ ⇒ return _nil_
|
||||
> _false_ ⇒ set _q<sub>head</sub>_ = _q<sub>head</sub> + 1_; return _q[q<sub>head</sub>-1]_
|
||||
`peek => e`
|
||||
|
||||
**retrieve(i)** ⇒ **e**
|
||||
> match _q<sub>head</sub> <= i < q<sub>tail</sub>_ with
|
||||
> _true_ ⇒ return _q<sub>i</sub>_
|
||||
> _false_ ⇒ return _nil_
|
||||
```
|
||||
match q_head == q_tail with
|
||||
true => return nil
|
||||
false =>
|
||||
return q[q_head]
|
||||
```
|
||||
|
||||
**push(e)**
|
||||
> set _q[q<sub>tail</sub>]_ = _e_; set _q<sub>tail</sub>_ = _q<sub>tail</sub> + 1_
|
||||
`pop => e`
|
||||
|
||||
**advance(i)**
|
||||
> set _q<sub>head</sub>_ = _i_; set _q<sub>tail</sub>_ = _max(q<sub>tail</sub>, i)_
|
||||
```
|
||||
match q_head == q_tail with
|
||||
true => return nil
|
||||
false =>
|
||||
set q_head = q_head + 1
|
||||
return q_head - 1
|
||||
```
|
||||
|
||||
**head** ⇒ **i**
|
||||
> return _q<sub>head</sub>_
|
||||
`retrieve(i) => e`
|
||||
|
||||
**tail** ⇒ **i**
|
||||
> return _q<sub>tail</sub>_
|
||||
```
|
||||
match q_head <= i < q_tail with
|
||||
true => return q[i]
|
||||
false => return nil
|
||||
```
|
||||
|
||||
`push(e)`
|
||||
|
||||
```
|
||||
set q[q_tail] = e
|
||||
set q_tail = q_tail + 1
|
||||
```
|
||||
|
||||
`advance(i)`
|
||||
|
||||
```
|
||||
set q_head = i
|
||||
set q_tail = max(q_tail, i)
|
||||
```
|
||||
|
||||
`head => i`
|
||||
|
||||
```
|
||||
return q_head
|
||||
```
|
||||
|
||||
`tail => i`
|
||||
|
||||
```
|
||||
return q_tail
|
||||
```
|
||||
|
||||
#### 3.2.4 Channel
|
||||
|
||||
We introduce the abstraction of an IBC _channel_: a set of the required packet queues to facilitate ordered bidirectional communication between two blockchains _A_ and _B_. An IBC connection, as defined earlier, can have any number of associated channels. IBC connections handle header initialization & updates. All IBC channels use the same connection, but implement independent queues and thus independent ordering guarantees.
|
||||
We introduce the abstraction of an IBC *channel*: a set of the required packet queues to facilitate ordered bidirectional communication between two blockchains `A` and `B`. An IBC connection, as defined earlier, can have any number of associated channels. IBC connections handle header initialization & updates. All IBC channels use the same connection, but implement independent queues and thus independent ordering guarantees.
|
||||
|
||||
An IBC channel consists of four distinct queues, two on each chain:
|
||||
|
||||
_Outgoing<sub>A</sub>_: Outgoing IBC packets from chain _A_ to chain _B_, stored on chain _A_
|
||||
`outgoing_A`: Outgoing IBC packets from chain `A` to chain `B`, stored on chain `A`
|
||||
|
||||
_Incoming<sub>A</sub>_: Execution logs for incoming IBC packets from chain _B_, stored on chain _A_
|
||||
`incoming_A`: IBC receipts (execution logs) for incoming IBC packets from chain `B`, stored on chain `A`
|
||||
|
||||
_Outgoing<sub>B</sub>_: Outgoing IBC packets from chain _B_ to chain _A_, stored on chain _B_
|
||||
`outgoing_B`: Outgoing IBC packets from chain `B` to chain `A`, stored on chain `B`
|
||||
|
||||
_Incoming<sub>B</sub>_: Execution logs for incoming IBC packets from chain _A_, stored on chain _B_
|
||||
`incoming_B`: IBC receipts (execution logs) for incoming IBC packets from chain `A`, stored on chain `B`
|
||||
|
||||
### 3.3 Requirements
|
||||
|
||||
In order to provide the ordering guarantees specified above, each blockchain utilizing the IBC protocol must provide proofs that particular IBC packets have been stored at particular indices in the outgoing packet queue, and particular IBC packet execution results have been stored at particular indices in the incoming packet queue.
|
||||
|
||||
We use the previously-defined Merkle proof _M<sub>k,v,h</sub>_ to provide the requisite proofs. In order to do so, we must define a unique, deterministic key in the Merkle store for each message in the queue:
|
||||
We use the previously-defined Merkle proof `M_kvh` to provide the requisite proofs. In order to do so, we must define a unique, deterministic key in the Merkle store for each message in the queue:
|
||||
|
||||
**key**: _(queue name, [head|tail|index])_
|
||||
`key = (queue name, head | tail | index)`
|
||||
|
||||
The index is stored as a fixed-length unsigned integer in big endian format, so that the lexicographical order of the byte representation of the key is consistent with their sequence number. This allows us to quickly iterate over the queue, as well as prove the content of a packet (or lack of packet) at a given sequence. _head_ and _tail_ are two special constants that store an integer index, and are chosen such that their serializated representation cannot collide with that of any possible index.
|
||||
The index is stored as a fixed-length unsigned integer in big endian format, so that the lexicographical order of the byte representation of the key is consistent with their sequence number. This allows us to quickly iterate over the queue, as well as prove the content of a packet (or lack of packet) at a given sequence. `head` and `tail` are two special constants that store an integer index, and are chosen such that their serializated representation cannot collide with that of any possible index.
|
||||
|
||||
Once written to the queue, a packet must be immutable (except for deletion when popped from the queue). That is, if a value _v_ is written to a queue, then every valid proof _M<sub>k,v,h </sub>_ must refer to the same _v_. In practice, this means that an IBC implementation must ensure that only the IBC module can write to the IBC subspace of the blockchain's Merkle store. This property is essential to safely process asynchronous messages.
|
||||
Once written to the queue, a packet must be immutable (except for deletion when popped from the queue). That is, if a value `v` is written to a queue, then every valid proof `M_kvh` must refer to the same `v`. In practice, this means that an IBC implementation must ensure that only the IBC module can write to the IBC subspace of the blockchain's Merkle store. This property is essential to safely process asynchronous messages.
|
||||
|
||||
Each incoming & outgoing queue for each connection must be provably associated with another uniquely identified chain, so that an observer can prove that a message was intended for that chain and only that chain. This can easily be done by prefixing the queue keys in the Merkle store with a string unique to the other chain, such as the chain identifier or the hash of the genesis block.
|
||||
Each incoming & outgoing queue for each connection must be provably associated with another uniquely identified chain, connection, and channel so that an observer can prove that a message was intended for that chain and only that chain. This can easily be done by prefixing the queue keys in the Merkle store with strings unique to the chain (such as chain identifier), connection, and channel.
|
||||
|
||||
### 3.4 Sending a packet
|
||||
|
||||
{ todo: unify terms, clarify }
|
||||
|
||||
To send an IBC packet, an application module on the source chain must call the send method of the IBC module, providing a packet as defined above. The IBC module must ensure that the destination chain was already properly registered and that the calling module has permission to write this packet. If all is in order, the IBC module simply pushes the packet to the tail of _Outgoing<sub>A</sub>_, which enables all the proofs described above.
|
||||
To send an IBC packet, an application module on the source chain must call the send method of the IBC module, providing a packet as defined above. The IBC module must ensure that the destination chain was already properly registered and that the calling module has permission to write this packet. If all is in order, the IBC module simply pushes the packet to the tail of `outgoing_a`, which enables all the proofs described above.
|
||||
|
||||
If desired, the packet payload can contain additional module routing information in the form of a _kind_, so that different modules can write different kinds of packets and maintain any application-level invariants related to this area. For example, a "coin" module can ensure a fixed supply, or a "NFT" module can ensure token uniqueness. The IBC module must associate every supported message with a particular handler (_f<sub>kind</sub>_) and return an error for unsupported types.
|
||||
The packet must provide routing information in the `type` field, so that different modules can write different kinds of packets and maintain any application-level invariants related to this area. For example, a "coin" module can ensure a fixed supply, or a "NFT" module can ensure token uniqueness. The IBC module on the destination chain must associate every supported packet type with a particular handler (`f_type`) and return an error for unsupported types.
|
||||
|
||||
_(IBCsend(D, type, data)_ ⇒ _Success)_
|
||||
⇒ _push(q<sub>D.send</sub> ,V<sub>send</sub>{type, data})_
|
||||
`send(P{type, sequence, source, destination, data})`
|
||||
|
||||
```
|
||||
match source == (A, connection, channel) and sequence == tail(outgoing_A) with
|
||||
true => push(outgoing_A, P); success
|
||||
false => fail with "wrong sender"
|
||||
```
|
||||
|
||||
Note that the `sequence`, `source`, and `destination` can all be encoded in the Merkle tree key for the channel and do not need to be stored for each packet.
|
||||
|
||||
### 3.5 Receiving a packet
|
||||
|
||||
{ todo: unify terms }
|
||||
Upon packet receipt, chain `B` must check that the packet is valid, that it was intended for the destination, and that all previous packets have been processed. `receive` must write the receipt queue upon accepting a valid packet, even if the handler execution returned an error, so that future packets can be processed.
|
||||
|
||||
We also consider how a given blockchain _A_ is expected to receive the packet from a source chain _S_ with a merkle proof, given the current set of trusted headers for that chain, _T<sub>S</sub>_:
|
||||
To receive an IBC packet on blockchain `B` from a source chain `A`, with a Merkle proof `M_kvh` and the current set of trusted headers for that chain `T_A`:
|
||||
|
||||
_A:IBCreceive(S, M<sub>k,v,h</sub>)_ ⇒ _match_
|
||||
* _q<sub>S.receipt</sub> =_ ∅ ⇒ _Error("unregistered sender"),_
|
||||
* _k = (\_, reciept, \_)_ ⇒ _Error("must be a send"),_
|
||||
* _k = (d, \_, \_) and d_ ≠ _A_ ⇒ _Error("sent to a different chain"),_
|
||||
* _k = (\_, send, i) and head(q<sub>S.receipt</sub>)_ ≠ _i_ ⇒ _Error("out of order"),_
|
||||
* _H<sub>h</sub>_ ∉ _T<sub>S</sub>_ ⇒ _Error("must submit header for height h"),_
|
||||
* _valid(H<sub>h</sub> ,M<sub>k,v,h </sub>) = false_ ⇒ _Error("invalid merkle proof"),_
|
||||
* _v = (type, data)_ ⇒ _(result, err) := f<sub>type</sub>(data); push(q<sub>S.receipt </sub>, (result, err)); Success_
|
||||
`receive(P{type, sequence, source, destination, data}, M_kvh)`
|
||||
|
||||
Note that this requires not only an valid proof, but also that the proper header as well as all prior messages were previously submitted. This returns success upon accepting a proper message, even if the message execution returned an error (which must then be relayed to the sender).
|
||||
```
|
||||
case
|
||||
incoming_B == nil => fail with "unregistered sender"
|
||||
destination /= (B, connection, channel) => fail with "wrong destination"
|
||||
sequence /= head(Incoming_B) => fail with "out of order"
|
||||
H_h not in T_A => fail with "must submit header for height h"
|
||||
valid(H_h, M_kvh) == false => fail with "invalid Merkle proof"
|
||||
otherwise =>
|
||||
set (result, error) = f_type(data)
|
||||
push(incoming_B, R{})
|
||||
```
|
||||
|
||||
{ todo: check that v + k == packet, clearly define connection / channel }
|
||||
|
||||
### 3.6 Handling a receipt
|
||||
|
||||
{ todo: cleanup logic }
|
||||
|
||||
When we wish to create a transaction that atomically commits or rolls back across two chains, we must look at the receipts from sending the original message. For example, if I want to send tokens from Alice on chain A to Bob on chain B, chain A must decrement Alice's account _if and only if_ Bob's account was incremented on chain B. We can achieve that by storing a protected intermediate state on chain A, which is then committed or rolled back based on the result of executing the transaction on chain B.
|
||||
When we wish to create a transaction that atomically commits or rolls back across two chains, we must look at the receipts from sending the original message. For example, if I want to send tokens from Alice on chain `A` to Bob on chain `B`, chain `A` must decrement Alice's account *if and only if* Bob's account was incremented on chain `B`. We can achieve that by storing a protected intermediate state on chain `A` (escrowing the assets in question), which is then committed or rolled back based on the result of executing the transaction on chain `B`.
|
||||
|
||||
To do this requires that we not only provable send a message from chain A to chain B, but provably return the result of that message (the receipt) from chain B to chain A. As one noticed above in the implementation of _IBCreceive_, if the valid IBC message was sent from A to B, then the result of executing it, even if it was an error, is stored in _B:q<sub>A.receipt</sub>_. Since the receipts are stored in a queue with the same key construction as the sending queue, we can generate the same set of proofs for them, and perform a similar sequence of steps to handle a receipt coming back to _S_ for a message previously sent to _A_:
|
||||
To do this requires that we not only provably send a message from chain `A` to chain `B`, but provably return the result of that message (the receipt) from chain `B` to chain `A`. As one noticed above in the implementation of `receive`, if the valid IBC message was sent from `A` to `B`, then the result of executing it, even if it was an error, is stored in _B:q<sub>A.receipt</sub>_. Since the receipts are stored in a queue with the same key construction as the sending queue, we can generate the same set of proofs for them, and perform a similar sequence of steps to handle a receipt coming back to _S_ for a message previously sent to _A_:
|
||||
|
||||
_S:IBCreceipt(A, M<sub>k,v,h</sub>)_ ⇒ _match_
|
||||
* _q<sub>A.send</sub> =_ ∅ ⇒ _Error("unregistered sender"),_
|
||||
* _k = (\_, send, \_)_ ⇒ _Error("must be a recipient"),_
|
||||
* _k = (d, \_, \_) and d_ ≠ _S_ ⇒ _Error("sent to a different chain"),_
|
||||
* _H<sub>h</sub>_ ∉ _T<sub>A</sub>_ ⇒ _Error("must submit header for height h"),_
|
||||
* _not valid(H<sub>h </sub>, M<sub>k,v,h </sub>)_ ⇒ _Error("invalid merkle proof"),_
|
||||
* _k = (\_, receipt, head|tail)_ ⇒ _Error("only accepts message proofs"),_
|
||||
* _k = (\_, receipt, i) and head(q<sub>S.send</sub>)_ ≠ _i_ ⇒ _Error("out of order"),_
|
||||
`handle_receipt(A, M_kvh)`
|
||||
|
||||
```
|
||||
case
|
||||
outgoing_A == nil => fail with "unregistered sender"
|
||||
destination /= (A, connection, channel) => fail with "wrong destination"
|
||||
sequence /= head(incoming_A) => fail with "out of order"
|
||||
H_h not in T_B => fail with "must submit header for height h"
|
||||
valid(H_h, M_kvh) == false => fail with "invalid Merkle proof"
|
||||
* _v = (\_, error)_ ⇒ _(type, data) := pop(q<sub>S.send </sub>); rollback<sub>type</sub>(data); Success_
|
||||
* _v = (res, success)_ ⇒ _(type, data) := pop(q<sub>S.send </sub>); commit<sub>type</sub>(data, res); Success_
|
||||
```
|
||||
|
||||
This enforces that the receipts are processed in order, to allow some the application to make use of some basic assumptions about ordering. It also removes the message from the send queue, as there is now proof it was processed on the receiving chain and there is no more need to store this information.
|
||||
This enforces that the receipts are processed in order, to allow applications to make use of some basic assumptions about ordering. It also removes the message from the send queue, as there is now proof it was processed on the receiving chain and no need to store it.
|
||||
|
||||
![Successful Transaction](images/Receipts.png)
|
||||
|
||||
|
@ -180,11 +223,13 @@ This enforces that the receipts are processed in order, to allow some the applic
|
|||
|
||||
{ todo: cleanup wording & terms }
|
||||
|
||||
The blockchain itself only records the _intention_ to send the given message to the recipient chain, it doesn't make any network connections as that would add unbounded delays and non-determinism into the state machine. We define the concept of a _relay_ process that connects two chain by querying one for all proofs needed to prove outgoing messages and submit these proofs to the recipient chain.
|
||||
{ todo: one relay process can relay all the things }
|
||||
|
||||
The relay process must have access to accounts on both chains with sufficient balance to pay for transaction fees but needs no other permissions. Many _relay_ processes may run in parallel without violating any safety consideration. However, they will consume unnecessary fees if they submit the same proof multiple times, so some minimal coordination is ideal.
|
||||
The blockchain itself only records the *intention* to send the given message to the recipient chain, it doesn't make any network connections as that would add unbounded delays and non-determinism into the state machine. We define the concept of a *relay* process that connects two chain by querying one for all proofs needed to prove outgoing messages and submit these proofs to the recipient chain.
|
||||
|
||||
As an example, here is a naive algorithm for relaying send messages from A to B, without error handling. We must also concurrently run the relay of receipts from B back to A, in order to complete the cycle. Note that all reads of variables belonging to a chain imply queries and all function calls imply submitting a transaction to the blockchain.
|
||||
The relay process must have access to accounts on both chains with sufficient balance to pay for transaction fees but needs no other permissions. Many *relay* processes may run in parallel without violating any safety consideration. However, they will consume unnecessary fees if they submit the same proof multiple times, so some minimal coordination is ideal.
|
||||
|
||||
As an example, here is a naive algorithm for relaying send messages from `A` to `B`, without error handling. We must also concurrently run the relay of receipts from `B` back to `A`, in order to complete the cycle. Note that all reads of variables belonging to a chain imply queries and all function calls imply submitting a transaction to the blockchain.
|
||||
|
||||
```
|
||||
while true
|
||||
|
|
|
@ -2,83 +2,94 @@
|
|||
|
||||
([Back to table of contents](README.md#contents))
|
||||
|
||||
The basis of IBC is the ability to verify in the on-chain consensus ruleset of chain _B_ that a data packet received on chain _B_ was correctly generated on chain _A_. This establishes a cross-chain linearity guarantee: upon validation of that packet on chain _B_ we know that the packet has been executed on chain _A_ and any associated logic resolved (such as assets being escrowed), and we can safely perform application logic on chain _B_ (such as generating vouchers on chain _B_ for the chain _A_ assets which can later be redeemed with a packet in the opposite direction).
|
||||
The basis of IBC is the ability to verify in the on-chain consensus ruleset of chain `B` that a data packet received on chain `B` was correctly generated on chain `A`. This establishes a cross-chain linearity guarantee: upon validation of that packet on chain `B` we know that the packet has been executed on chain `A` and any associated logic resolved (such as assets being escrowed), and we can safely perform application logic on chain `B` (such as generating vouchers on chain `B` for the chain `A` assets which can later be redeemed with a packet in the opposite direction).
|
||||
|
||||
This section outlines the abstraction of an IBC _connection_: the state and consensus ruleset necessary to perform IBC packet verification.
|
||||
|
||||
### 2.1 Definitions
|
||||
|
||||
- Chain _A_ is the source blockchain from which the IBC packet is sent
|
||||
- Chain _B_ is the destination blockchain on which the IBC packet is received
|
||||
- _H<sub>h</sub>_ is the signed header of chain _A_ at height _h_
|
||||
- _C<sub>h</sub>_ is the consensus ruleset of chain _A_ at height _h_
|
||||
- _V<sub>k,h</sub>_ is the value stored on chain _A_ under key _k_ at height _h_
|
||||
- _P_ is the unbonding period of chain _A_, in units of time
|
||||
- Δ_(a, b)_ is the time difference between events _a_ and _b_
|
||||
- Chain `A` is the source blockchain from which the IBC packet is sent
|
||||
- Chain `B` is the destination blockchain on which the IBC packet is received
|
||||
- `H_h` is the signed header of chain `A` at height `h`
|
||||
- `C_h` is a subset of the consensus ruleset of chain `A` at height `h`
|
||||
- `V_kh` is the value stored on chain `A` under key `k` at height `h`
|
||||
- `P` is the unbonding period of chain `P`, in units of time
|
||||
- `dt(a, b)` is the time difference between events `a` and `b`
|
||||
|
||||
Note that of all these, only _H<sub>h</sub>_ defines a signature and is thus attributable.
|
||||
Note that of all these, only `H_h` defines a signature and is thus attributable.
|
||||
|
||||
### 2.2 Requirements
|
||||
|
||||
To facilitate an IBC connection, the two blockchains must provide the following proofs:
|
||||
|
||||
1. Given a trusted _H<sub>h</sub>_ and _C<sub>h</sub>_ and an attributable update message _U<sub>h'</sub>_,
|
||||
it is possible to prove _H<sub>h'</sub>_ where _C<sub>h'</sub> = C<sub>h</sub>_ and Δ_(now, H<sub>h</sub>) < P_
|
||||
2. Given a trusted _H<sub>h</sub>_ and _C<sub>h</sub>_ and an attributable change message _X<sub>h'</sub>_,
|
||||
it is possible to prove _H<sub>h'</sub>_ where _C<sub>h'</sub>_ ≠ _C<sub>h</sub>_ and Δ _(now, H<sub>h</sub>) < P_
|
||||
3. Given a trusted _H<sub>h</sub>_ and a merkle proof _M<sub>k,v,h</sub>_ it is possible to prove _V<sub>k,h</sub>_
|
||||
1. Given a trusted `H_h` and `C_h` and an attributable update message `U_h`,
|
||||
it is possible to prove `H_h'` where `C_h' == C_h` and `dt(now, H_h) < P`
|
||||
2. Given a trusted `H_h` and `C_h` and an attributable change message `X_h`,
|
||||
it is possible to prove `H_h'` where `C_h' /= C_h` and `dt(now, H_h) < P`
|
||||
3. Given a trusted `H_h` and a Merkle proof `M_kvh` it is possible to prove `V_kh`
|
||||
|
||||
It is possible to make use of the structure of BFT consensus to construct extremely lightweight and provable messages _U<sub>h'</sub>_ and _X<sub>h'</sub>_. The implementation of these requirements with Tendermint consensus is defined in Appendix E. Another algorithm able to provide equally strong guarantees (such as Casper) is also compatible with IBC but must define its own set of update and change messages.
|
||||
It is possible to make use of the structure of BFT consensus to construct extremely lightweight and provable messages `U_h'` and `X_h'`. The implementation of these requirements with Tendermint consensus is defined in [Appendix E](). Another algorithm able to provide equally strong guarantees (such as Casper) is also compatible with IBC but must define its own set of update and change messages.
|
||||
|
||||
The merkle proof _M<sub>k,v,h</sub>_ is a well-defined concept in the blockchain space, and provides a compact proof that the key value pair (_k, v)_ is consistent with a merkle root stored in _H<sub>h</sub>_. Handling the case where _k_ is not in the store requires a separate proof of non-existence, which is not supported by all merkle stores. Thus, we define the proof only as a proof of existence. There is no valid proof for missing keys, and we design the algorithm to work without it.
|
||||
The Merkle proof `M_kvh` is a well-defined concept in the blockchain space, and provides a compact proof that the key value pair `(k, v)` is consistent with a Merkle root stored in `H_h`. Handling the case where `k` is not in the store requires a separate proof of non-existence, which is not supported by all Merkle stores. Thus, we define the proof only as a proof of existence. There is no valid proof for missing keys, and we design the algorithm to work without it.
|
||||
|
||||
_valid(H<sub>h </sub>,M<sub>k,v,h </sub>)_ ⇒ _[true | false]_
|
||||
`valid(H_h, M_kvh) => true | false`
|
||||
|
||||
### 2.3 Connection Lifecycle
|
||||
|
||||
#### 2.3.1 Opening a Connection
|
||||
#### 2.3.1 Opening a connection
|
||||
|
||||
All proofs require an initial _H<sub>h</sub>_ and _C<sub>h</sub>_ for some _h_, where Δ_(now, H<sub>h</sub>) < P_.
|
||||
All proofs require an initial `H_h` and `C_h` for some `h`, where Δ_(now, H<sub>h</sub>) < P_.
|
||||
|
||||
Establishing a bidirectional initial root-of-trust between the two blockchains (_A_ to _B_ and _B_ to _A_) — _HA<sub>h</sub>_ and _CA<sub>h</sub>_ stored on chain _B_, and _HB<sub>h</sub>_ and _CB<sub>h</sub>_ stored on chain _A_ — is necessary before any IBC packets can be sent.
|
||||
Establishing a bidirectional initial root-of-trust between the two blockchains (`A` to `B` and `B` to `A`) — `H_ah` and `C_ah` stored on chain `B`, and `H_bh` and `C_bh` stored on chain `A` — is necessary before any IBC packets can be sent.
|
||||
|
||||
Any header may be from a malicious chain (e.g. shadowing a real chain state with a fake validator set), so a subjective decision is required before establishing a connection. This can be performed permissionlessly, in which case users later utilizing the IBC channel must check the root-of-trust themselves, or authorized by on-chain governance for additional assurance.
|
||||
|
||||
#### 2.3.2 Following Block Headers
|
||||
#### 2.3.2 Following block headers
|
||||
|
||||
We define two messages _U<sub>h</sub>_ and _X<sub>h</sub>_, which together allow us to securely advance our trust from some known _H<sub>n</sub>_ to some future _H<sub>h</sub>_ where _h > n_. Some implementations may require that _h = n + 1_ (all headers must be processed in order). IBC implemented on top of Tendermint or similar BFT algorithms requires only that Δ_<sub>vals</sub>(C<sub>n</sub>, C<sub>h</sub> ) < ⅓_ (each step must have a change of less than one-third of the validator set)[[4](./references.md#4)].
|
||||
We define two messages `U_h` and `X_h`, which together allow us to securely advance our trust from some known `H_n` to some future `H_h` where `h > n`. Some implementations may require that `h == n + 1` (all headers must be processed in order). IBC implemented on top of Tendermint or similar BFT algorithms requires only that `delta vals(C_n, C_h) < ⅓` (each step must have a change of less than one-third of the validator set)[[4](./references.md#4)].
|
||||
|
||||
Either requirement is compatible with IBC. However, by supporting proofs where _h_-_n > 1_, we can follow the block headers much more efficiently in situations where the majority of blocks do not include an IBC packet between chains _A_ and _B_, and enable low-bandwidth connections to be implemented at very low cost. If there are packets to relay every block, these two requirements collapse to the same case (every header must be relayed).
|
||||
Either requirement is compatible with IBC. However, by supporting proofs where `h_-_n > 1`, we can follow the block headers much more efficiently in situations where the majority of blocks do not include an IBC packet between chains `A` and `B`, and enable low-bandwidth connections to be implemented at very low cost. If there are packets to relay every block, these two requirements collapse to the same case (every header must be relayed).
|
||||
|
||||
Since these messages _U<sub>h</sub>_ and _X<sub>h</sub>_ provide all knowledge of the remote blockchain, we require that they not just be provable, but also attributable. As such, any attempt to violate the finality guarantees in headers posted to chain _B_ can be submitted back to chain _A_ for punishment, in the same manner that chain _A_ would independently punish (slash) identified Byzantine actors.
|
||||
Since these messages `U_h` and `X_h` provide all knowledge of the remote blockchain, we require that they not just be provable, but also attributable. As such, any attempt to violate the finality guarantees in headers posted to chain `B` can be submitted back to chain `A` for punishment, in the same manner that chain `A` would independently punish (slash) identified Byzantine actors.
|
||||
|
||||
More formally, given existing set of trust _T_ = _{(H<sub>i </sub>, C<sub>i </sub>), (H<sub>j </sub>, C<sub>j </sub>), …}_, we must provide:
|
||||
More formally, given existing set of trust `T` = `{(H_i, C_i), (H_j, C_j), …}`, we must provide:
|
||||
|
||||
_valid(T, X<sub>h </sub>|<sub> </sub>U<sub>h </sub>)_ ⇒ _[true | false | unknown]_
|
||||
`valid(T, X_h | U_h) => true | false | unknown`
|
||||
|
||||
_if H<sub>h-1</sub>_ ∈ _T then_:
|
||||
* _valid(T, X<sub>h </sub>|<sub> </sub>U<sub>h </sub>)_ ⇒ _[true | false]_
|
||||
* ∃ (U<sub>h</sub> | X<sub>h</sub>) ⇒ valid(T, X<sub>h</sub> | U<sub>h</sub>) {aren't there infinite? why is this necessary}
|
||||
`valid` must fulfill the following properties:
|
||||
|
||||
_if C<sub>h</sub>_ ∉ _T then_
|
||||
* _valid(T, U<sub>h </sub>)_ ⇒ _false_
|
||||
```
|
||||
if H_h-1 ∈ T then
|
||||
valid(T, X_h | U_h) => true | false
|
||||
∃ (U_h | X_h) => valid(T, X_h | U_h)
|
||||
```
|
||||
|
||||
```
|
||||
if C_h ∉ T then
|
||||
valid(T, U_h) => false
|
||||
```
|
||||
|
||||
We can then process update transactions as follows:
|
||||
|
||||
_update(T, X<sub>h </sub>|<sub> </sub>U<sub>h </sub>)_ ⇒ match _valid(T, X<sub>h </sub>|<sub> </sub>U<sub>h </sub>)_ with
|
||||
* _false_ ⇒ fail with `invalid proof`
|
||||
* _unknown_ ⇒ fail with `need a proof between current and h`
|
||||
* _true_ ⇒ set _T_ = _T_ ∪ _(H<sub>h </sub>,C<sub>h </sub>)_
|
||||
`update(T, X_h | U_h) => success | failure`
|
||||
|
||||
Define _max(T)_ as _max(h, where H<sub>h</sub>_ ∈ _T)_. For any _T_ with _max(T) = h-1_, there must exist some _X<sub>h </sub>|<sub> </sub>U<sub>h</sub>_ so that _max(update(T, X<sub>h </sub>|<sub> </sub>U<sub>h </sub>)) = h_.
|
||||
By induction, there must exist a set of proofs, such that _max(update…(T,...)) = h+n_ for any n.
|
||||
```
|
||||
update(T, X_h | U_h) = match valid(T, X_h | U_h) with
|
||||
false => fail with "invalid proof"
|
||||
unknown => fail with "need a proof between current and h"
|
||||
true =>
|
||||
set T = T ∪ (H_h, C_h)
|
||||
```
|
||||
|
||||
Bisection can be used to discover this set of proofs. That is, given _max(T) = n_ and _valid(T, X<sub>h </sub>|<sub> </sub>U<sub>h </sub>) = unknown_, we then try _update(T, X<sub>b </sub>|<sub> </sub>U<sub>b </sub>)_, where _b = (h+n)/2_. The base case is where _valid(T, X<sub>h </sub>|<sub> </sub>U<sub>h </sub>) = true_ and is guaranteed to exist if _h=max(T)+1_.
|
||||
Define `max(T)` as `max(h, where H_h ∈ T)`. For any `T` with `max(T) == h-1`, there must exist some `X_h | U_h` so that `max(update(T, X_h | U_h)) == h`.
|
||||
By induction, there must exist a set of proofs, such that `max(update…(T,...)) == h + n` for any `n`.
|
||||
|
||||
#### 2.3.3 Closing a Connection
|
||||
Bisection can be used to discover this set of proofs. That is, given `max(T) == n` and `valid(T, X_h | U_h) == unknown`, we then try `update(T, X_b | U_b)`, where _`b == (h + n) / 2`. The base case is where `valid(T, X_h | U_h) == true` and is guaranteed to exist if `h == max(T) + 1`.
|
||||
|
||||
IBC implementations may optionally include the ability to close an IBC connection and prevent further header updates, simply causing _update(T, X<sub>h </sub>|<sub> </sub>U<sub>h </sub>)_ as defined above to always return _false_.
|
||||
#### 2.3.3 Closing a connection
|
||||
|
||||
Closing a connection may break application invariants (such as fungiblity - token vouchers on chain _B_ will no longer be redeemable for tokens on chain _A_) and should only be undertaken in extreme circumstances such as Byzantine behavior of the connected chain.
|
||||
IBC implementations may optionally include the ability to close an IBC connection and prevent further header updates, simply causing `update(T, X_h | U_h)` as defined above to always return `false`.
|
||||
|
||||
Closing a connection may break application invariants (such as fungiblity - token vouchers on chain `B` will no longer be redeemable for tokens on chain `A`) and should only be undertaken in extreme circumstances such as Byzantine behavior of the connected chain.
|
||||
|
||||
Closure may be permissioned to an on-chain governance system, an identifiable party on the other chain (such as a signer quorum, although this will not work in some Byzantine cases), or any user who submits an application-specific fraud proof. When a connection is closed, application-specific measures may be undertaken to recover assets held on a Byzantine chain. We defer further discussion to [Appendix D](appendices.md#appendix-d-byzantine-recovery-strategies).
|
||||
|
|
|
@ -14,20 +14,20 @@ In this paper, we define a process of posting block headers and Merkle tree proo
|
|||
|
||||
### 1.2 Definitions
|
||||
|
||||
_Blockchain_ - A replicated fault-tolerant state machine with a distributed consensus algorithm. The smallest unit produced through consensus is a block, which may contain many transactions, each applying some arbitrary mutation to the state.
|
||||
*Blockchain* - A replicated fault-tolerant state machine with a distributed consensus algorithm. The smallest unit produced through consensus is a block, which may contain many transactions, each applying some arbitrary mutation to the state.
|
||||
|
||||
_Module_ - We assume that the state machine of each blockchain is comprised of multiple components that have limited rights to execute some particular set of state transfers (these are modules in the Cosmos SDK or smart contracts in Ethereum).
|
||||
*Module* - We assume that the state machine of each blockchain is comprised of multiple components that have limited rights to execute some particular set of state transfers (these are modules in the Cosmos SDK or smart contracts in Ethereum).
|
||||
|
||||
_Finality_ - The guarantee that a given block will not be reverted within some predefined conditions of a consensus algorithm. All proof-of-work systems offer probabilistic finality, which means that the difficulty of reverting a block increases as the block is embedded more deeply in the chain. Many proof-of-stake systems offer much weaker guarantees, based only on the honesty of the block producers. BFT algorithms such as Tendermint guarantee complete finality upon production of a block (unless over two thirds of the validators collude to break consensus, in which case the offenders can be identified and punished - further discussion of that scenario is outside the scope of this document).
|
||||
*Finality* - The guarantee that a given block will not be reverted within some predefined conditions of a consensus algorithm. All proof-of-work systems offer probabilistic finality, which means that the difficulty of reverting a block increases as the block is embedded more deeply in the chain. Many proof-of-stake systems offer much weaker guarantees, based only on the honesty of the block producers. BFT algorithms such as Tendermint guarantee complete finality upon production of a block (unless over two thirds of the validators collude to break consensus, in which case the offenders can be identified and punished - further discussion of that scenario is outside the scope of this document).
|
||||
|
||||
_Attributable_ - Knowledge of the pseudonymous identity which made a statement, whom we can punish with some deduction of value (slashing) if the statement is false. Synonymous with accountability.
|
||||
*Attributable* - Knowledge of the pseudonymous identity which made a statement, whom we can punish with some deduction of value (slashing) if the statement is false. Synonymous with accountability.
|
||||
|
||||
_Unbonding Period_ - Proof-of-stake algorithms need to lock the stake (prevent transfers) for some time to provide a lower bound for the length of a long-range attack [[3](./references.md#3)]. Complete finality is associated with a subset of the proof-of-stake class of consensus algorithms. We assume the proof-of-stake algorithms utilized by the two blockchains have some unbonding period P.
|
||||
*Unbonding period* - Proof-of-stake algorithms need to lock the stake (prevent transfers) for some time to provide a lower bound for the length of a long-range attack [[3](./references.md#3)]. Complete finality is associated with a subset of the proof-of-stake class of consensus algorithms. We assume the proof-of-stake algorithms utilized by the two blockchains have some unbonding period P.
|
||||
|
||||
### 1.3 Threat Models
|
||||
|
||||
_False statements_ - Any information we receive may be false.
|
||||
*False statements* - Any information we receive may be false.
|
||||
|
||||
_Network partitions and delays_ - We assume an asynchronous, adversarial network with unbounded latency. Network messages may be modified, reordered, duplicated, or selectively dropped. Actors may be arbitrarily partitioned by a powerful adversary. The IBC protocol favors correctness over liveness (and provides no particular guarantees of the latter).
|
||||
*Network partitions and delays* - We assume an asynchronous, adversarial network with unbounded latency. Network messages may be modified, reordered, duplicated, or selectively dropped. Actors may be arbitrarily partitioned by a powerful adversary. The IBC protocol favors correctness over liveness where applicable.
|
||||
|
||||
_Byzantine actors_ - An entire blockchain may not act according to protocol. This must be detectable and provable, allowing the communicating blockchain to revoke trust and take necessary action. Application-level protocols designed on top of IBC should consider this risk and mitigate it as possible in a manner suitable to their application.
|
||||
*Byzantine actors* - An entire blockchain may not act according to protocol. This must be detectable and provable, allowing the communicating blockchain to revoke trust and take necessary action. Application-level protocols designed on top of IBC should consider and mitigate this risk in a manner suitable to their application.
|
||||
|
|
Loading…
Reference in New Issue