From 4eb8be224eb31b46d183c69b4d118bbe32ad1d5a Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Wed, 13 Mar 2024 12:15:21 +0100 Subject: [PATCH 01/18] start client functions and intro --- spec/micro/README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 spec/micro/README.md diff --git a/spec/micro/README.md b/spec/micro/README.md new file mode 100644 index 000000000..6679257ca --- /dev/null +++ b/spec/micro/README.md @@ -0,0 +1,55 @@ +# Micro IBC Architecture + +### Context + +The implementation of the entire IBC protocol as it currently stands is a large undertaking. While there exists ready-made implementations like ibc-go this is only deployable on the Cosmos-SDK. Similarly, there exists ibc-rs which is a library for chains to integrate. However, this requires the chain to be implemented in Rust, there still exists some non-trivial work to integrate the ibc-rs library into the target state machine, and certain limitations either in the state machine or in ibc-rs may prevent using the library for the target chain. + +Writing an implementation from scratch is a problem many ecosystems face as a major barrier for IBC adoption. + +The goal of this document is to serve as a "micro-IBC" specification that will allow new ecosystems to implement a protocol that can communicate with fully implemented IBC chains using the same security assumptions. + +The micro-IBC protocol must have the same security properties as IBC, and must be completely compatible with IBC applications. It may not have the full flexibility offered by standard IBC. + +### Desired Properties + +- Light-client backed security +- Unique identifiers for each channel end +- Authenticated application channel (must verify that the counterparty is running the correct client and app parameters) +- Applications must be mutually compatible with standard IBC applications. +- Must be capable of being implemented in smart contract environments with resource constraints and high gas costs. + +### Specification + +### Light Clients + +The light client module can be implemented exactly as-is with regards to its functionality. It **must** have external endpoints for relayers (off-chain processes that have full-node access to other chains in the network) to initialize a client, update the client, and submit misbehaviour in case the trust model of the client is violated by the counterparty consensus mechanism (e.g. committing to different headers for the same height). + +The implementation of each of these endpoints will be specific to the particular consensus mechanism targetted. The choice of consensus algorithm itself is arbitrary, it may be a Proof-of-Stake algorithm like CometBFT, or a multisig of trusted authorities, or a rollup that relies on an additional underlying client in order to verify its consensus. + +Thus, the endpoints themselves should accept arbitrary bytes for the arguments passed into these client endpoints as it is up to each individual client implementation to unmarshal these bytes into the structures they expect. + +```typescript +// initializes client with a starting client state containing all light client parameters +// and an initial consensus state that will act as a trusted seed from which to verify future headers +function createClient( + clientState: bytes, + consensusState: bytes, +): (Identifier, error) + +// once a client has been created, it can be referenced with the identifier and passed the header +// to keep the client up-to-date. In most cases, this will cause a new consensus state derived from the header +// to be stored in the client +function updateClient( + clientId: Identifier, + header: bytes, +): error + +// once a client has been created, relayers can submit misbehaviour that proves the counterparty chain +// The light client must verify the misbehaviour using the trust model of the consensus mechanism +// and execute some custom logic such as freezing the client from accepting future updates and proof verification. +function submitMisbehaviour( + clientId: Identifier, + misbehaviour: bytes, +): error +``` + From 9be5de4f6e6e165957343afe49fef2d4038288c6 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:46:21 +0100 Subject: [PATCH 02/18] start router logic --- spec/micro/README.md | 83 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/spec/micro/README.md b/spec/micro/README.md index 6679257ca..42b4196fa 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -53,3 +53,86 @@ function submitMisbehaviour( ): error ``` +### Router + +IBC in its essence is the ability for applications on different blockchains with different security models to communicate with each other through light-client backed security. Thus, IBC needs the light client described above and the IBC applications that define the packet data they wish to send and receive. In addition to these layers, core IBC introduces the connection and channel abstractions to connect these two fundamental layers. Micro IBC intends to compress only the necessary aspects of connection and channel layers to a new router layer but before doing this it is critical to understand what service they currently provide. + +Properties of Connection: + +- Verifies the validity of the counterparty client +- Establishes a unique identifier on each side for a shared abstract understanding (the connection) +- Establishes an agreement on the IBC version and supported features +- Allows multiple connections to be built against the same client pair +- Establishes the delay period so this security parameter can be instantiated differently for different connections against the same client pairing. +- Defines which channel orderings are supported + +Properties of Channel: + +- Separates applications into dedicated 1-1 communication channels. This prevents applications from writing into each other's channels. +- Allows applications to come to agreement on the application parameters (version negotiation). Ensures that each side can understand the other's communication and that they are running mutually compatible logic. This version negotiation is a multi-step process that allows the finalized version to differ substantially from the one initially proposed +- Establishes the ordering of the channel +- Establishes unique identifiers for the applications on either chain to use to reference each other when sending and receiving packets. +- The application protocol can be continually upgraded over time by using the upgrade handshake which allows the same channel which may have accumulated state to use new mutually agreed upon application packet data format(s) and associated new logic. +- Ensures exactly-once delivery of packet flow datagrams (Send, Receive, Acknowledge, Timeout) +- Ensures valid packet flow (Send => Receive => Acknowledge) XOR (Send => Timeout) + +Of these which are the critical properties that micro-IBC must maintain: + +Desired Properties of micro-IBC: + +##### Authenticating Counterparty Clients + +Before application data can flow between chains, we must ensure that the clients are both valid views of the counterparty consensus. + +In the router we must then introduce an ability to submit the counterparty client state and consensus state for verification against a client stored in our own chain. + +```typescript +function verifyCounterpartyClient( + localClient: Identifer, // this is the client of the counterparty that exists on our own chain + remoteClientStoreIdentifier: Identifier, // this is the identifier of the + remoteClient: ClientState, // this is the client on the counterparty chain that purports to be a client of ourselves + remoteConsensusState: ConsensusState, // this is the consensus state that is being used for verification of our consensus + remoteConsensusHeight: Height, // this is the height of our chain that the remote consensus state is associated with + // the proof fields are written in IBC convention, + // but implementations in practice will use []byte for proof + // and an unsigned integer for the height + // as their local client implementation will expect for VerifyMembership + proofClient: CommitmentProof, + proofConsensus: CommitmentProof, + proofHeight: Height, +) { + // validate that the remote client and remote consensus state + // are valid for our chain. Note: This requires the ability to introspect our own consensus within this function + // e.g. ability to verify that the validator set at the height of the consensus state was in fact the validator set on our chaina that height + validateSelfClient(remoteClient) + validateSelfConsensus(remoteConsensusState, remoteConsensusHeight) + + // use the local client to verify that the remote client and remote consensus state are stored as expected under the remoteClientStoreIdentifier with the ICS24 paths we expect. + clientPath = append(remoteClientStoreIdentifier, "/clientState") + consensusPath = append(remoteClientStoreIdentifier, "/consensusState/{remoteConsensusHeight}") + assert(localClient.VerifyMembership( + proofHeight, + 0, + 0, + proofClient, + clientPath, + proto.marshal(remoteClient), + )) + assert(localClient.VerifyMembership( + proofHeight, + 0, + 0, + proofConsensus, + consensusPath, + proto.marshal(remoteConsensusState), + ) +} +``` + +#### Identification + +// TODO store the counterparty ClientStoreIdentifier and ApplicationStoreIdentifier so we know where they are storing client states, and packet commitments +// A secure, unique connection consists of +// (ChainA, ClientStoreID, AppStoreID) => (ChainB, ClientStoreID, AppStoreID) +// where both sides are aware of each others store ID's + From be585b88a1722d2a31689f900278cf2fb4c8a9a2 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:37:50 +0200 Subject: [PATCH 03/18] start micro ibc spec --- .../README.md | 2 +- spec/micro/README.md | 504 +++++++++++++++++- 2 files changed, 503 insertions(+), 3 deletions(-) diff --git a/spec/core/ics-004-channel-and-packet-semantics/README.md b/spec/core/ics-004-channel-and-packet-semantics/README.md index 4ffdb2295..acd6ad7b6 100644 --- a/spec/core/ics-004-channel-and-packet-semantics/README.md +++ b/spec/core/ics-004-channel-and-packet-semantics/README.md @@ -946,7 +946,7 @@ function recvPacket( }) } - abortTransactionUnless(packetReceipt === null)) + abortTransactionUnless(packetReceipt === null) provableStore.set( packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence), SUCCESSFUL_RECEIPT diff --git a/spec/micro/README.md b/spec/micro/README.md index 42b4196fa..bd5a59ae8 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -88,8 +88,8 @@ In the router we must then introduce an ability to submit the counterparty clien ```typescript function verifyCounterpartyClient( - localClient: Identifer, // this is the client of the counterparty that exists on our own chain - remoteClientStoreIdentifier: Identifier, // this is the identifier of the + localClient: ClientState, // this is the client of the counterparty that exists on our own chain + remoteClientStoreIdentifier: CommitmentPath, // this is the identifier of the remoteClient: ClientState, // this is the client on the counterparty chain that purports to be a client of ourselves remoteConsensusState: ConsensusState, // this is the consensus state that is being used for verification of our consensus remoteConsensusHeight: Height, // this is the height of our chain that the remote consensus state is associated with @@ -136,3 +136,503 @@ function verifyCounterpartyClient( // (ChainA, ClientStoreID, AppStoreID) => (ChainB, ClientStoreID, AppStoreID) // where both sides are aware of each others store ID's +// TODO: Make this a 3-step handshake. Each side needs to know the remoteChannelStoreIdentifier and verify the counterparty stored it correctly. + +```typescript +function initializeMultiChannel( + localClientIdentifier: Identifier, + remoteClientIdentifier: Identifier, + remoteClientStoreIdentifier: CommitmentPath, + remoteChannelStoreIdentifier: CommitmentPath, + remoteClient: ClientState, + remoteConsensusState: ConsensusState, + remoteConsensusHeight: Height, + proofClient: CommitmentProof, + proofConsensus: CommitmentProof, + proofHeight: Height +) { + localClient = getClient(localClientIdentifier) + // first verify the counterparty client is a valid client of our chain + assert(localClient.verifyCounterpartyClient( + remoteClientIdentifier, + remoteClientStoreIdentifier, + remoteClient, + remoteConsensusState, + remoteConsensusHeight, + proofConsensus, + proofHeight, + )) + + channelId = generateIdentifier() + + multiChannel = Channel{ + state: OPEN, + ordering: UNORDERED, + counterpartyPortIdentifier: MULTI_IBC_PORT, + counterpartyChannelIdentifier: "", + connectionHops: [localClientIdentifier], + version: MULTI_IBC, + upgradeSequence: 0, + } + + channelStore = getChannelStore(localChannelStoreIdentifier) + set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", multiChannel) +} + +function openMultiChannel( + localClientIdentifier: Identifier, + remoteClientIdentifier: Identifier, + remoteClientStoreIdentifier: Identifier, + remoteChannelIdentifier: Identifier, + remoteChannelStoreIdentifier: Identifier, + remoteClient: ClientState, + remoteConsensusState: ConsensusState, + remoteConsensusHeight: Height, + proofClient: CommitmentProof, + proofConsensus: CommitmentProof, + proofChannel: CommitmentProof, + proofHeight: Height +) { + localClient = getClient(localClientIdentifier) + // first verify the counterparty client is a valid client of our chain + assert(localClient.verifyCounterpartyClient( + remoteClientIdentifier, + remoteClientStoreIdentifier, + remoteClient, + remoteConsensusState, + remoteConsensusHeight, + proofConsensus, + proofHeight, + )) + + expectedCounterpartyChannel = Channel{ + OPEN, + ordering: UNORDERED, + counterpartyPortIdentifier: MULTI_IBC_PORT, + counterpartyChannelIdentifier: "", + connectionHops: [remoteClientIdentifier], + version: MULTI_IBC, + upgradeSequence: 0, + } + channelPath = append(remoteChannelStoreIdentifier, "/channelEnds/ports/{MULTI_IBC_PORT}/channels/{remoteChannelIdentifier}") + assert(localClient.VerifyMembership( + proofHeight, + 0, + 0, + proofChannel, + channelPath, + proto.marshal(expectedCounterpartyChannel,) + )) + + channelId = generateIdentifier() + + multiChannel = Channel{ + state: OPEN, + ordering: UNORDERED, + counterpartyPortIdentifier: MULTI_IBC_PORT, + counterpartyChannelIdentifier: , + connectionHops: [localClientIdentifier], + version: MULTI_IBC, + upgradeSequence: 0, + } + + channelStore = getChannelStore(localChannelStoreIdentifier) + set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", multiChannel) +} +``` + +### Registering IBC applications on the router + +The IBC router contains a mapping from a reserved application port and the supported versions of that application as well as a mapping from channelIdentifiers to channels. + +```typescript +type IBCRouter struct { + apps: portID -> [Version] + callbacks: portID -> [Callback] + channels: channelId -> Channel +} +``` + +### Sending packets on the MultiChannel + + +Sending packets in the multichannel requires you to construct a packet data that contains a map from the application reserved portID to the requested version and opaque packet data. + +```typescript +type MultiPacketData struct { + AppData: Map[string]{ + AppPacketData{ + Version: string, + Data: []byte, + } + } +} +``` + +The router will check that the channelID exists and it has a `MULTI_PORT_ID`. It will send a verifyMembership of the packet to the underlying client. It will then iterate over the packet data's in the multiPacketData. It will check that each application supports the requested version, and then it will construct a packet that only contains the application packet data and send it to the application. It will collect acknowledgements from each and put it into a multiAcknowledgement. + +```typescript +type MultiAcknowledgment struct { + success: bool, + AppAcknowledgement: Map[string]Acknowledgement, +} +``` + +This will in turn be unpacked and sent to each application as an individual acknowledgment. However, the total success value must be the same for all apps since the packet receiving logic is atomic. + +### Router Methods + +```typescript +function sendPacket( + sourceChannel: Identifier, + timeoutHeight: Height, + timeoutTimestamp: uint64, + packetData: MultiPacketData): uint64 { + // get provable channel store + channelStore = getChannelStore(localChannelStoreIdentifier) + + channel = get(channelPath(MULTI_IBC_PORT, sourceChannel)) + assert(channel !== null) + assert(channel.state === OPEN) + + // get provable client store + clientStore = getClientStore(localClientStoreIdentifier) + // in this specification, the connection hops fields will house + // the underlying client identifier + client = get(clientPath(channel.connectionHops[0])) + assert(client !== null) + + // disallow packets with a zero timeoutHeight and timeoutTimestamp + assert(timeoutHeight !== 0 || timeoutTimestamp !== 0) + + // check that the timeout height hasn't already passed in the local client tracking the receiving chain + latestClientHeight = client.latestClientHeight() + assert(timeoutHeight === 0 || latestClientHeight < timeoutHeight) + + // increment the send sequence counter + sequence = channelStore.get(nextSequenceSendPath(MULTI_IBC_PORT, sourceChannel)) + channelStore.set(nextSequenceSendPath(MULTI_IBC_PORT, sourceChannel), sequence+1) + + // store commitment to the packet data & packet timeout + channelStore.set( + packetCommitmentPath(MULTI_IBC_PORT, sourceChannel, sequence), + hash(hash(data), timeoutHeight, timeoutTimestamp) + ) + + // log that a packet can be safely sent + emitLogEntry("sendPacket", { + sequence: sequence, + data: data, + timeoutHeight: timeoutHeight, + timeoutTimestamp: timeoutTimestamp + }) + + mulitPacketData.AppData.forEach((port, appData) => { + supportedVersions = app[port] + // check if router supports the desired port and version + if supportedVersions.contains(appData.Version) { + // send each individual packet data to application + // to do send packet logic. e.g. escrow tokens + appData = appData.Data + // abort transaction on the first failure in send packet + // note this is an additional callback as the flow of execution + // differs from traditional IBC + assert(callbacks[port].onSendPacket( + packet.sourcePort, + packet.sourceChannel, + sequence, + appData, + relayer)) + } + }) + + return sequence +} +``` + +```typescript +function recvPacket( + packet: OpaquePacket, + proof: CommitmentProof, + proofHeight: Height, + relayer: string): Packet { + // get provable channel store + channelStore = getChannelStore(localChannelStoreIdentifier) + + channel = get(channelPath(MULTI_IBC_PORT, sourceChannel)) + assert(channel !== null) + assert(channel.state === OPEN) + + assert(packet.sourcePort === channel.counterpartyPortIdentifier) + assert(packet.sourceChannel === channel.counterpartyChannelIdentifier) + + // get provable client store + clientStore = getClientStore(localClientStoreIdentifier) + // in this specification, the connection hops fields will house + // the underlying client identifier + client = get(clientPath(channel.connectionHops[0])) + assert(client !== null) + + packetPath = packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence) + proofPath = applyPrefix(client.counterpartyChannelStoreIdentifier, packetPath) + assert(client.verifyPacketData( + proofHeight, + 0, 0, // zeroed out delay period + proof, + proofPath, + hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp) + )) + + assert(packet.timeoutHeight === 0 || getConsensusHeight() < packet.timeoutHeight) + assert(packet.timeoutTimestamp === 0 || currentTimestamp() < packet.timeoutTimestamp) + + // we must set the receipt so it can be verified on the other side + // this receipt does not contain any data, since the packet has not yet been processed + // it's the sentinel success receipt: []byte{0x01} + packetReceipt = channelStore.get(packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence)) + assert(packetReceipt === null) + channelStore.set( + packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence), + SUCCESSFUL_RECEIPT + ) + + // log that a packet has been received + emitLogEntry("recvPacket", { + data: packet.data + timeoutHeight: packet.timeoutHeight, + timeoutTimestamp: packet.timeoutTimestamp, + sequence: packet.sequence, + sourcePort: packet.sourcePort, + sourceChannel: packet.sourceChannel, + destPort: packet.destPort, + destChannel: packet.destChannel, + order: channel.order, + connection: channel.connectionHops[0] + }) + + multiPacketData = unmarshal(packetData) + multiAck = MultiAcknowledgement{true, make(map[string]Acknowledgement)} + + // NEEDS DISCUSSION: Should we break early on first failure? + mulitPacketData.AppData.forEach((port, appData) => { + supportedVersions = app[port] + // check if router supports the desired port and version + if supportedVersions.contains(appData.Version) { + // create a new packet with just the application data + // in the packet data for the desired application + appPacket = Packet{ + sequence: packet.sequence, + timeoutHeight: packet.timeoutHeight, + timeoutTimestamp: packet.timeoutTimestamp, + sourcePort: packet.sourcePort, + sourceChannel: packet.sourceChannel, + destPort: packet.destPort, + destChannel: packet.destChannel, + data: appData.Data, + } + // TODO: Support aysnc acknowledgements + appAck = callbacks[port].onRecvPacket(packet, relayer) + // success of multiack must be false if even a single app acknowledgement returns false (atomic multipacket behaviour) + // and puts the custom acknowledgement in the app acknowledgement map under the port key + multiAck = multiAck{ + success: multiAck.success && appAck.Success(), + appAcknowledgement: multiAck.AppAcknowledgement.put(port, appAck.Acknowledgement()) + } + } else { + // requested port/version was not supported so we must error + multiAck = multiAck{ + success: false, + appAcknowledgement: multiAck + } + } + }) + + // write the acknowledgement + channelStore.set( + packetAcknowledgementPath(packet.destPort, packet.destChannel, packet.sequence), + hash(acknowledgement) + ) + + // log that a packet has been acknowledged + emitLogEntry("writeAcknowledgement", { + sequence: packet.sequence, + timeoutHeight: packet.timeoutHeight, + port: packet.destPort, + channel: packet.destChannel, + timeoutTimestamp: packet.timeoutTimestamp, + data: packet.data, + acknowledgement + }) +} +``` + +```typescript +function acknowledgePacket( + packet: OpaquePacket, + acknowledgement: bytes, + proof: CommitmentProof, + proofHeight: Height, + relayer: string): Packet { + // get provable channel store + channelStore = getChannelStore(localChannelStoreIdentifier) + + // check channel is open + channel = get(channelPath(MULTI_IBC_PORT, packet.sourceChannel)) + assert(channel !== null) + assert(channel.state === OPEN) + + // verify counterparty information + assert(packet.destPort === channel.counterpartyPortIdentifier) + assert(packet.destChannel === channel.counterpartyChannelIdentifier) + + client = provableStore.get(connectionPath(channel.connectionHops[0])) + assert(client !== null) + + // verify we sent the packet and haven't cleared it out yet + assert(channelStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) + === hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) + + assert(connection.verifyPacketAcknowledgement( + proofHeight, + proof, + packet.destPort, + packet.destChannel, + packet.sequence, + acknowledgement + )) + + // all assertions passed, we can alter state + + // delete our commitment so we can't "acknowledge" again + provableStore.delete(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) + + multiPacketData = unmarshal(packet.data) + + ackSuccess = multiAck.success + // send each app acknowledgement to relevant port + // and override the success value with the multiack success value + multiAck.forEach((port, ack) => { + supportedVersions = app[port] + appData = multiPacketData.AppData[port] + // check if router supports the desired port and version + if supportedVersions.contains(appData.Version) { + // create a new packet with just the application data + // in the packet data for the desired application + appPacket = Packet{ + sequence: packet.sequence, + timeoutHeight: packet.timeoutHeight, + timeoutTimestamp: packet.timeoutTimestamp, + sourcePort: packet.sourcePort, + sourceChannel: packet.sourceChannel, + destPort: packet.destPort, + destChannel: packet.destChannel, + data: appData.Data, + } + // construct app acknowledgement with multi-app success value + // and individual ack info + // NOTE: application MUST support the standard acknowledgement + // described in ICS-04 + var appAck AppAcknowledgement + if ackSuccess { + // the acknowledgement was a success, + // put app info into result + appAck = AppAcknowledgement{ + result: ack + } + } else { + // the acknowledgement was a failure, + // put app info into error. + // note it is possible that this application succeeded + // and its custom app info included information + // of a successfully executed callback + // however, we will still put the info in error; + // so that the application knows the receive failed + // on the other side. + // Thus the callback reversion logic must be implementable + // given the success boolean as opposed to specific information in the acknowledgement + appAck = AppAcknowledgement{ + error: ack + } + } + + // abort on first error in callbacks + // NEEDS DISCUSSION: Should we fail on first acknowledge error + // or optimistically try them all and succeed anyway + assert(callbacks[port].onAcknowledgePacket(appPacket, appAck, relayer)) + } else { + // should never happen + assert(false) + } + }) +} +``` + +```typescript +function onTimeoutPacket(packet: Packet, relayer: string) { + // get provable channel store + channelStore = getChannelStore(localChannelStoreIdentifier) + + // check channel is open + channel = get(channelPath(MULTI_IBC_PORT, packet.sourceChannel)) + assert(channel !== null) + assert(channel.state === OPEN) + + // verify counterparty information + assert(packet.destPort === channel.counterpartyPortIdentifier) + assert(packet.destChannel === channel.counterpartyChannelIdentifier) + + client = provableStore.get(connectionPath(channel.connectionHops[0])) + assert(client !== null) + + proofTimestamp, err = client.getTimestampAtHeight(connection, proofHeight) + + // check that timeout height or timeout timestamp has passed on the other end + abortTransactionUnless( + (packet.timeoutHeight > 0 && proofHeight >= packet.timeoutHeight) || + (packet.timeoutTimestamp > 0 && proofTimestamp >= packet.timeoutTimestamp)) + + // verify we actually sent this packet, check the store + abortTransactionUnless(provableStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) + === hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) + + // all assertions passed, we can alter state + + // delete our commitment so we can't "time out" again + provableStore.delete(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) + + // unordered channel: verify absence of receipt at packet index + abortTransactionUnless(connection.verifyPacketReceiptAbsence( + proofHeight, + proof, + packet.destPort, + packet.destChannel, + packet.sequence + )) + + mulitPacketData.AppData.forEach((port, appData) => { + supportedVersions = app[port] + // check if router supports the desired port and version + if supportedVersions.contains(appData.Version) { + // create a new packet with just the application data + // in the packet data for the desired application + appPacket = Packet{ + sequence: packet.sequence, + timeoutHeight: packet.timeoutHeight, + timeoutTimestamp: packet.timeoutTimestamp, + sourcePort: packet.sourcePort, + sourceChannel: packet.sourceChannel, + destPort: packet.destPort, + destChannel: packet.destChannel, + data: appData.Data, + } + // NEEDS DISCUSSION: Should we fail on first timeout error + // or optimistically try them all and succeed anyway + assert(callbacks[port].onTimeoutPacket(packet, relayer)) + } else { + // should never happen + assert(false) + } + }) + +} +``` \ No newline at end of file From 034bc1862df690edb8bf55827c32ebae7101c342 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Thu, 18 Apr 2024 15:31:05 +0200 Subject: [PATCH 04/18] add TODOs --- spec/micro/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/micro/README.md b/spec/micro/README.md index bd5a59ae8..9d6b09a11 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -53,6 +53,11 @@ function submitMisbehaviour( ): error ``` +``` +// TODO: Keep very limited buffer of consensus states +// Keep ability to migrate client (without necessarily consensus governance) +``` + ### Router IBC in its essence is the ability for applications on different blockchains with different security models to communicate with each other through light-client backed security. Thus, IBC needs the light client described above and the IBC applications that define the packet data they wish to send and receive. In addition to these layers, core IBC introduces the connection and channel abstractions to connect these two fundamental layers. Micro IBC intends to compress only the necessary aspects of connection and channel layers to a new router layer but before doing this it is critical to understand what service they currently provide. From e136266c24e3b63e89115a973c0d6d91c1603b92 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:43:32 +0200 Subject: [PATCH 05/18] add three step handshake --- spec/micro/README.md | 64 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/spec/micro/README.md b/spec/micro/README.md index 9d6b09a11..062130797 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -144,6 +144,12 @@ function verifyCounterpartyClient( // TODO: Make this a 3-step handshake. Each side needs to know the remoteChannelStoreIdentifier and verify the counterparty stored it correctly. ```typescript +type Counterparty struct { + clientStoreIdentifier: Identifier, + channelStoreIdentifier: Identifier, + clientIdentifier: Identifer +} + function initializeMultiChannel( localClientIdentifier: Identifier, remoteClientIdentifier: Identifier, @@ -171,7 +177,7 @@ function initializeMultiChannel( channelId = generateIdentifier() multiChannel = Channel{ - state: OPEN, + state: INIT, ordering: UNORDERED, counterpartyPortIdentifier: MULTI_IBC_PORT, counterpartyChannelIdentifier: "", @@ -182,6 +188,13 @@ function initializeMultiChannel( channelStore = getChannelStore(localChannelStoreIdentifier) set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", multiChannel) + + // TODO: Should we validate and prove this on the other side? + privateStore.set(counterpartyPath(), Counterparty{ + clientStoreIdentifier: remoteClientStoreIdentifier, + channelStoreIdentifier: remoteChannelStoreIdentifier, + clientIdentifier: remoteClientIdentifier + }) } function openMultiChannel( @@ -211,7 +224,7 @@ function openMultiChannel( )) expectedCounterpartyChannel = Channel{ - OPEN, + state: OPEN, ordering: UNORDERED, counterpartyPortIdentifier: MULTI_IBC_PORT, counterpartyChannelIdentifier: "", @@ -235,14 +248,57 @@ function openMultiChannel( state: OPEN, ordering: UNORDERED, counterpartyPortIdentifier: MULTI_IBC_PORT, - counterpartyChannelIdentifier: , + counterpartyChannelIdentifier: remoteChannelIdentifier, connectionHops: [localClientIdentifier], version: MULTI_IBC, upgradeSequence: 0, } channelStore = getChannelStore(localChannelStoreIdentifier) - set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", multiChannel) + channelStore.set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", multiChannel) + + privateStore.set(counterpartyPath(), Counterparty{ + clientStoreIdentifier: remoteClientStoreIdentifier, + channelStoreIdentifier: remoteChannelStoreIdentifier, + clientIdentifier: remoteClientIdentifier + }) +} + +function confirmMultiChannel( + localChannelIdentifier: Identifier, + remoteChannelidentifier: Identifier, + proofChannel: CommitmentProof, + proofHeight: Height +) { + channel = getChannel(localChannelIdentifier) + localClient = getClient(channel.connectionHops[0]) + + counterparty = privateStore.get(counterpartyPath()) + + expectedCounterpartyChannel = Channel{ + state: OPEN, + ordering: UNORDERED, + counterpartyPortIdentifier: MULTI_IBC_PORT, + counterpartyChannelIdentifier: localChannelIdentifier, + connectionHops: [counterparty.clientIdentifier], + version: MULTI_IBC, + upgradeSequence: 0, + } + + // verify counterparty channel opened under claimed paths + proofChannel = append(counterparty.remoteChannelStoreIdentifier, "/channelEnds/ports/{MULTI_IBC_PORT}/channels/{remoteChannelIdentifier}") + assert(localClient.VerifyMembership( + proofHeight, + 0, + 0, + proofChannel, + channelPath, + proto.marshal(expectedCounterpartyChannel,) + )) + + channel.counterpartyChannelIdentifier = remoteChannelIdentifier + channelStore = getChannelStore(localChannelStoreIdentifier) + channelStore.set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", channel) } ``` From f3a26f2fc527a33de1d606fd769c7150f86bae61 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Tue, 23 Apr 2024 18:03:07 +0200 Subject: [PATCH 06/18] allow for independent apps --- spec/micro/README.md | 180 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 156 insertions(+), 24 deletions(-) diff --git a/spec/micro/README.md b/spec/micro/README.md index 062130797..b1a77b108 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -150,7 +150,9 @@ type Counterparty struct { clientIdentifier: Identifer } -function initializeMultiChannel( +function initializeChannel( + portID: Identifier, + version: string, localClientIdentifier: Identifier, remoteClientIdentifier: Identifier, remoteClientStoreIdentifier: CommitmentPath, @@ -162,6 +164,8 @@ function initializeMultiChannel( proofConsensus: CommitmentProof, proofHeight: Height ) { + // chain must contain the requested port with the requested version + assert(router.apps[portID].contains(version)) localClient = getClient(localClientIdentifier) // first verify the counterparty client is a valid client of our chain assert(localClient.verifyCounterpartyClient( @@ -176,31 +180,34 @@ function initializeMultiChannel( channelId = generateIdentifier() - multiChannel = Channel{ + channel = Channel{ state: INIT, ordering: UNORDERED, - counterpartyPortIdentifier: MULTI_IBC_PORT, + counterpartyPortIdentifier: portID, counterpartyChannelIdentifier: "", connectionHops: [localClientIdentifier], - version: MULTI_IBC, + version: version, upgradeSequence: 0, } channelStore = getChannelStore(localChannelStoreIdentifier) - set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", multiChannel) + set("channelEnds/ports/{portID}/channels/{channelId}", channel) // TODO: Should we validate and prove this on the other side? - privateStore.set(counterpartyPath(), Counterparty{ + privateStore.set(counterpartyPath(channelId), Counterparty{ clientStoreIdentifier: remoteClientStoreIdentifier, channelStoreIdentifier: remoteChannelStoreIdentifier, clientIdentifier: remoteClientIdentifier }) } -function openMultiChannel( +function openChannel( + portID: Identifier, + version: string, localClientIdentifier: Identifier, remoteClientIdentifier: Identifier, remoteClientStoreIdentifier: Identifier, + remotePortIdentifier: Identifier, remoteChannelIdentifier: Identifier, remoteChannelStoreIdentifier: Identifier, remoteClient: ClientState, @@ -211,6 +218,8 @@ function openMultiChannel( proofChannel: CommitmentProof, proofHeight: Height ) { + // chain must contain the requested port with the requested version + assert(router.apps[portID].contains(version)) localClient = getClient(localClientIdentifier) // first verify the counterparty client is a valid client of our chain assert(localClient.verifyCounterpartyClient( @@ -226,13 +235,13 @@ function openMultiChannel( expectedCounterpartyChannel = Channel{ state: OPEN, ordering: UNORDERED, - counterpartyPortIdentifier: MULTI_IBC_PORT, + counterpartyPortIdentifier: "", counterpartyChannelIdentifier: "", connectionHops: [remoteClientIdentifier], - version: MULTI_IBC, + version: version, upgradeSequence: 0, } - channelPath = append(remoteChannelStoreIdentifier, "/channelEnds/ports/{MULTI_IBC_PORT}/channels/{remoteChannelIdentifier}") + channelPath = append(remoteChannelStoreIdentifier, "/channelEnds/ports/{remotePortIdentifier}/channels/{remoteChannelIdentifier}") assert(localClient.VerifyMembership( proofHeight, 0, @@ -244,49 +253,52 @@ function openMultiChannel( channelId = generateIdentifier() - multiChannel = Channel{ + channel = Channel{ state: OPEN, ordering: UNORDERED, - counterpartyPortIdentifier: MULTI_IBC_PORT, + counterpartyPortIdentifier: remotePortIdentifier, counterpartyChannelIdentifier: remoteChannelIdentifier, connectionHops: [localClientIdentifier], - version: MULTI_IBC, + version: version, upgradeSequence: 0, } channelStore = getChannelStore(localChannelStoreIdentifier) - channelStore.set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", multiChannel) + channelStore.set("channelEnds/ports/{portId}/channels/{channelId}", channel) + channelStore.set(nextSequenceSendPath(sourcePort, sourceChannel), 0) - privateStore.set(counterpartyPath(), Counterparty{ + privateStore.set(counterpartyPath(channelId), Counterparty{ clientStoreIdentifier: remoteClientStoreIdentifier, channelStoreIdentifier: remoteChannelStoreIdentifier, clientIdentifier: remoteClientIdentifier }) } -function confirmMultiChannel( - localChannelIdentifier: Identifier, +function confirmChannel( + portId: Identifier, + channelId: Identifier, + remotePortIdentifier: Identifier, remoteChannelidentifier: Identifier, proofChannel: CommitmentProof, proofHeight: Height ) { - channel = getChannel(localChannelIdentifier) + channel = getChannel(portId, channelId) localClient = getClient(channel.connectionHops[0]) - counterparty = privateStore.get(counterpartyPath()) + counterparty = privateStore.get(counterpartyPath(channelId)) expectedCounterpartyChannel = Channel{ state: OPEN, ordering: UNORDERED, - counterpartyPortIdentifier: MULTI_IBC_PORT, - counterpartyChannelIdentifier: localChannelIdentifier, + counterpartyPortIdentifier: portId, + counterpartyChannelIdentifier: channelId, connectionHops: [counterparty.clientIdentifier], - version: MULTI_IBC, + version: channel.version, upgradeSequence: 0, } // verify counterparty channel opened under claimed paths - proofChannel = append(counterparty.remoteChannelStoreIdentifier, "/channelEnds/ports/{MULTI_IBC_PORT}/channels/{remoteChannelIdentifier}") + proofChannel = append(counterparty.remoteChannelStoreIdentifier, "/channelEnds/ports/{remotePortIdentifier}/channels/{remoteChannelIdentifier}") assert(localClient.VerifyMembership( proofHeight, 0, @@ -298,7 +310,8 @@ function confirmMultiChannel( channel.counterpartyChannelIdentifier = remoteChannelIdentifier channelStore = getChannelStore(localChannelStoreIdentifier) - channelStore.set("channelEnds/ports/{MULTI_IBC_PORT}/channels/{channelId}", channel) + channelStore.set("channelEnds/ports/{portId}/channels/{channelId}", channel) + channelStore.set(nextSequenceSendPath(sourcePort, sourceChannel), 0) } ``` @@ -314,6 +327,125 @@ type IBCRouter struct { } ``` +### Packet Flow through the Router + +```typescript +function sendPacket( + sourcePort: Identifier, + sourceChannel: Identifier, + timeoutHeight: Height, + timeoutTimestamp: uint64, + packetData: []byte +): uint64 { + channel = getChannel(sourcePort, sourceChannel) + + // check that the channel must be OPEN to send packets; + abortTransactionUnless(channel !== null) + abortTransactionUnless(channel.state === OPEN) + + // get provable client store + clientStore = getClientStore(localClientStoreIdentifier) + // in this specification, the connection hops fields will house + // the underlying client identifier + client = get(clientPath(channel.connectionHops[0])) + assert(client !== null) + + // disallow packets with a zero timeoutHeight and timeoutTimestamp + assert(timeoutHeight !== 0 || timeoutTimestamp !== 0) + + // check that the timeout height hasn't already passed in the local client tracking the receiving chain + latestClientHeight = client.latestClientHeight() + assert(timeoutHeight === 0 || latestClientHeight < timeoutHeight) + + // increment the send sequence counter + sequence = channelStore.get(nextSequenceSendPath(sourcePort, sourceChannel)) + channelStore.set(nextSequenceSendPath(sourcePort, sourceChannel), sequence+1) + + // store commitment to the packet data & packet timeout + channelStore.set( + packetCommitmentPath(sourcePort, sourceChannel, sequence), + hash(hash(data), timeoutHeight, timeoutTimestamp) + ) + + // log that a packet can be safely sent + emitLogEntry("sendPacket", { + sequence: sequence, + data: data, + timeoutHeight: timeoutHeight, + timeoutTimestamp: timeoutTimestamp + }) + +} + +function recvPacket( + packet: OpaquePacket, + proof: CommitmentProof, + proofHeight: Height, + relayer: string): Packet { + // get provable channel store + channelStore = getChannelStore(localChannelStoreIdentifier) + + channel = get(channelPath(packet.destPort, packet.destChannel)) + assert(channel !== null) + assert(channel.state === OPEN) + + assert(packet.sourcePort === channel.counterpartyPortIdentifier) + assert(packet.sourceChannel === channel.counterpartyChannelIdentifier) + + // get provable client store + clientStore = getClientStore(localClientStoreIdentifier) + // in this specification, the connection hops fields will house + // the underlying client identifier + client = get(clientPath(channel.connectionHops[0])) + assert(client !== null) + + packetPath = packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence) + proofPath = applyPrefix(client.counterpartyChannelStoreIdentifier, packetPath) + assert(client.verifyPacketData( + proofHeight, + 0, 0, // zeroed out delay period + proof, + proofPath, + hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp) + )) + + assert(packet.timeoutHeight === 0 || getConsensusHeight() < packet.timeoutHeight) + assert(packet.timeoutTimestamp === 0 || currentTimestamp() < packet.timeoutTimestamp) + + // we must set the receipt so it can be verified on the other side + // this receipt does not contain any data, since the packet has not yet been processed + // it's the sentinel success receipt: []byte{0x01} + packetReceipt = channelStore.get(packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence)) + assert(packetReceipt === null) + channelStore.set( + packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence), + SUCCESSFUL_RECEIPT + ) + + // log that a packet has been received + emitLogEntry("recvPacket", { + data: packet.data + timeoutHeight: packet.timeoutHeight, + timeoutTimestamp: packet.timeoutTimestamp, + sequence: packet.sequence, + sourcePort: packet.sourcePort, + sourceChannel: packet.sourceChannel, + destPort: packet.destPort, + destChannel: packet.destChannel, + order: channel.order, + connection: channel.connectionHops[0] + }) + + cbs = callbacks[packet.destPort] + ack = cbs.OnRecvPacket(packet, relayer) + ... +} +``` + +------ + +IGNORE THE MULTICHANNEL WORK FOR NOW + ### Sending packets on the MultiChannel From 2f92cc81428eaf829684c8144b34fa34fa535b0c Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Thu, 25 Apr 2024 19:06:52 +0200 Subject: [PATCH 07/18] completely remove connection/channel, no handshakes, direct communication between pairs --- spec/micro/README.md | 289 +++++-------------------------------------- 1 file changed, 33 insertions(+), 256 deletions(-) diff --git a/spec/micro/README.md b/spec/micro/README.md index b1a77b108..aecdab993 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -58,7 +58,7 @@ function submitMisbehaviour( // Keep ability to migrate client (without necessarily consensus governance) ``` -### Router +### Core IBC Functionality IBC in its essence is the ability for applications on different blockchains with different security models to communicate with each other through light-client backed security. Thus, IBC needs the light client described above and the IBC applications that define the packet data they wish to send and receive. In addition to these layers, core IBC introduces the connection and channel abstractions to connect these two fundamental layers. Micro IBC intends to compress only the necessary aspects of connection and channel layers to a new router layer but before doing this it is critical to understand what service they currently provide. @@ -81,239 +81,30 @@ Properties of Channel: - Ensures exactly-once delivery of packet flow datagrams (Send, Receive, Acknowledge, Timeout) - Ensures valid packet flow (Send => Receive => Acknowledge) XOR (Send => Timeout) -Of these which are the critical properties that micro-IBC must maintain: +### IBC-Lite -Desired Properties of micro-IBC: +IBC lite will simply provide packet delivery between two chains communicating and identifying each other by on-chain light clients as specified in ICS-02 with application packet data being routed to their specific IBC applications with packet-flow semantics remaining as they were defined in ICS-04. -##### Authenticating Counterparty Clients - -Before application data can flow between chains, we must ensure that the clients are both valid views of the counterparty consensus. - -In the router we must then introduce an ability to submit the counterparty client state and consensus state for verification against a client stored in our own chain. +Thus, once two chains have set up clients for each other with specific Identifiers, they can send IBC packets like so. ```typescript -function verifyCounterpartyClient( - localClient: ClientState, // this is the client of the counterparty that exists on our own chain - remoteClientStoreIdentifier: CommitmentPath, // this is the identifier of the - remoteClient: ClientState, // this is the client on the counterparty chain that purports to be a client of ourselves - remoteConsensusState: ConsensusState, // this is the consensus state that is being used for verification of our consensus - remoteConsensusHeight: Height, // this is the height of our chain that the remote consensus state is associated with - // the proof fields are written in IBC convention, - // but implementations in practice will use []byte for proof - // and an unsigned integer for the height - // as their local client implementation will expect for VerifyMembership - proofClient: CommitmentProof, - proofConsensus: CommitmentProof, - proofHeight: Height, -) { - // validate that the remote client and remote consensus state - // are valid for our chain. Note: This requires the ability to introspect our own consensus within this function - // e.g. ability to verify that the validator set at the height of the consensus state was in fact the validator set on our chaina that height - validateSelfClient(remoteClient) - validateSelfConsensus(remoteConsensusState, remoteConsensusHeight) - - // use the local client to verify that the remote client and remote consensus state are stored as expected under the remoteClientStoreIdentifier with the ICS24 paths we expect. - clientPath = append(remoteClientStoreIdentifier, "/clientState") - consensusPath = append(remoteClientStoreIdentifier, "/consensusState/{remoteConsensusHeight}") - assert(localClient.VerifyMembership( - proofHeight, - 0, - 0, - proofClient, - clientPath, - proto.marshal(remoteClient), - )) - assert(localClient.VerifyMembership( - proofHeight, - 0, - 0, - proofConsensus, - consensusPath, - proto.marshal(remoteConsensusState), - ) +interface Packet { + sequence: uint64 + timeoutHeight: Height + timeoutTimestamp: uint64 + sourcePort: Identifier // identifier of the application on sender + sourceChannel: Identifier // identifier of the client of destination on sender chain + destPort: Identifier // identifier of the application on destination + destChannel: Identifier // identifier of the client of sender on the destination chain } ``` -#### Identification - -// TODO store the counterparty ClientStoreIdentifier and ApplicationStoreIdentifier so we know where they are storing client states, and packet commitments -// A secure, unique connection consists of -// (ChainA, ClientStoreID, AppStoreID) => (ChainB, ClientStoreID, AppStoreID) -// where both sides are aware of each others store ID's - -// TODO: Make this a 3-step handshake. Each side needs to know the remoteChannelStoreIdentifier and verify the counterparty stored it correctly. - -```typescript -type Counterparty struct { - clientStoreIdentifier: Identifier, - channelStoreIdentifier: Identifier, - clientIdentifier: Identifer -} - -function initializeChannel( - portID: Identifier, - version: string, - localClientIdentifier: Identifier, - remoteClientIdentifier: Identifier, - remoteClientStoreIdentifier: CommitmentPath, - remoteChannelStoreIdentifier: CommitmentPath, - remoteClient: ClientState, - remoteConsensusState: ConsensusState, - remoteConsensusHeight: Height, - proofClient: CommitmentProof, - proofConsensus: CommitmentProof, - proofHeight: Height -) { - // chain must contain the requested port with the requested version - assert(router.apps[portID].contains(version)) - localClient = getClient(localClientIdentifier) - // first verify the counterparty client is a valid client of our chain - assert(localClient.verifyCounterpartyClient( - remoteClientIdentifier, - remoteClientStoreIdentifier, - remoteClient, - remoteConsensusState, - remoteConsensusHeight, - proofConsensus, - proofHeight, - )) - - channelId = generateIdentifier() - - channel = Channel{ - state: INIT, - ordering: UNORDERED, - counterpartyPortIdentifier: portID, - counterpartyChannelIdentifier: "", - connectionHops: [localClientIdentifier], - version: version, - upgradeSequence: 0, - } - - channelStore = getChannelStore(localChannelStoreIdentifier) - set("channelEnds/ports/{portID}/channels/{channelId}", channel) - - // TODO: Should we validate and prove this on the other side? - privateStore.set(counterpartyPath(channelId), Counterparty{ - clientStoreIdentifier: remoteClientStoreIdentifier, - channelStoreIdentifier: remoteChannelStoreIdentifier, - clientIdentifier: remoteClientIdentifier - }) -} - -function openChannel( - portID: Identifier, - version: string, - localClientIdentifier: Identifier, - remoteClientIdentifier: Identifier, - remoteClientStoreIdentifier: Identifier, - remotePortIdentifier: Identifier, - remoteChannelIdentifier: Identifier, - remoteChannelStoreIdentifier: Identifier, - remoteClient: ClientState, - remoteConsensusState: ConsensusState, - remoteConsensusHeight: Height, - proofClient: CommitmentProof, - proofConsensus: CommitmentProof, - proofChannel: CommitmentProof, - proofHeight: Height -) { - // chain must contain the requested port with the requested version - assert(router.apps[portID].contains(version)) - localClient = getClient(localClientIdentifier) - // first verify the counterparty client is a valid client of our chain - assert(localClient.verifyCounterpartyClient( - remoteClientIdentifier, - remoteClientStoreIdentifier, - remoteClient, - remoteConsensusState, - remoteConsensusHeight, - proofConsensus, - proofHeight, - )) - - expectedCounterpartyChannel = Channel{ - state: OPEN, - ordering: UNORDERED, - counterpartyPortIdentifier: "", - counterpartyChannelIdentifier: "", - connectionHops: [remoteClientIdentifier], - version: version, - upgradeSequence: 0, - } - channelPath = append(remoteChannelStoreIdentifier, "/channelEnds/ports/{remotePortIdentifier}/channels/{remoteChannelIdentifier}") - assert(localClient.VerifyMembership( - proofHeight, - 0, - 0, - proofChannel, - channelPath, - proto.marshal(expectedCounterpartyChannel,) - )) +Since the packets are addressed **directly** with the underlying light clients, there are **no** more handshakes necessary. Instead the packet sender must be capable of providing the correct pair. - channelId = generateIdentifier() +Sending a packet with the wrong source client is equivalent to sending a packet with the wrong source channel. Sending a packet with the wrong destination client is a new source of errors, as the connection handshake was intended to connection pairwise clients and verify that they are indeed valid clients of each other. - channel = Channel{ - state: OPEN, - ordering: UNORDERED, - counterpartyPortIdentifier: remotePortIdentifier, - counterpartyChannelIdentifier: remoteChannelIdentifier, - connectionHops: [localClientIdentifier], - version: version, - upgradeSequence: 0, - } - - channelStore = getChannelStore(localChannelStoreIdentifier) - channelStore.set("channelEnds/ports/{portId}/channels/{channelId}", channel) - channelStore.set(nextSequenceSendPath(sourcePort, sourceChannel), 0) +If a user sends a packet with the wrong destination channel, then as we will see it will be impossible for the intended destination to correctly verify the packet thus, the packet will simply time out. - privateStore.set(counterpartyPath(channelId), Counterparty{ - clientStoreIdentifier: remoteClientStoreIdentifier, - channelStoreIdentifier: remoteChannelStoreIdentifier, - clientIdentifier: remoteClientIdentifier - }) -} - -function confirmChannel( - portId: Identifier, - channelId: Identifier, - remotePortIdentifier: Identifier, - remoteChannelidentifier: Identifier, - proofChannel: CommitmentProof, - proofHeight: Height -) { - channel = getChannel(portId, channelId) - localClient = getClient(channel.connectionHops[0]) - - counterparty = privateStore.get(counterpartyPath(channelId)) - - expectedCounterpartyChannel = Channel{ - state: OPEN, - ordering: UNORDERED, - counterpartyPortIdentifier: portId, - counterpartyChannelIdentifier: channelId, - connectionHops: [counterparty.clientIdentifier], - version: channel.version, - upgradeSequence: 0, - } - - // verify counterparty channel opened under claimed paths - proofChannel = append(counterparty.remoteChannelStoreIdentifier, "/channelEnds/ports/{remotePortIdentifier}/channels/{remoteChannelIdentifier}") - assert(localClient.VerifyMembership( - proofHeight, - 0, - 0, - proofChannel, - channelPath, - proto.marshal(expectedCounterpartyChannel,) - )) - - channel.counterpartyChannelIdentifier = remoteChannelIdentifier - channelStore = getChannelStore(localChannelStoreIdentifier) - channelStore.set("channelEnds/ports/{portId}/channels/{channelId}", channel) - channelStore.set(nextSequenceSendPath(sourcePort, sourceChannel), 0) -} -``` ### Registering IBC applications on the router @@ -323,7 +114,7 @@ The IBC router contains a mapping from a reserved application port and the suppo type IBCRouter struct { apps: portID -> [Version] callbacks: portID -> [Callback] - channels: channelId -> Channel + clients: clientId -> Client } ``` @@ -337,17 +128,8 @@ function sendPacket( timeoutTimestamp: uint64, packetData: []byte ): uint64 { - channel = getChannel(sourcePort, sourceChannel) - - // check that the channel must be OPEN to send packets; - abortTransactionUnless(channel !== null) - abortTransactionUnless(channel.state === OPEN) - - // get provable client store - clientStore = getClientStore(localClientStoreIdentifier) - // in this specification, the connection hops fields will house - // the underlying client identifier - client = get(clientPath(channel.connectionHops[0])) + // in this specification, the source channel will point to the client + client = router.get(sourceChannel) assert(client !== null) // disallow packets with a zero timeoutHeight and timeoutTimestamp @@ -358,15 +140,19 @@ function sendPacket( assert(timeoutHeight === 0 || latestClientHeight < timeoutHeight) // increment the send sequence counter - sequence = channelStore.get(nextSequenceSendPath(sourcePort, sourceChannel)) - channelStore.set(nextSequenceSendPath(sourcePort, sourceChannel), sequence+1) - + // if the sequence doesn't already exist, this call initializes the sequence to 0 + sequence = privateStore.get(nextSequenceSendPath(sourcePort, sourceChannel)) + // store commitment to the packet data & packet timeout channelStore.set( packetCommitmentPath(sourcePort, sourceChannel, sequence), hash(hash(data), timeoutHeight, timeoutTimestamp) ) + // increment the sequence. Thus there are monotonically increasing sequences for packet flow + // from sourcePort, sourceChannel pair + channelStore.set(nextSequenceSendPath(sourcePort, sourceChannel), sequence+1) + // log that a packet can be safely sent emitLogEntry("sendPacket", { sequence: sequence, @@ -382,26 +168,17 @@ function recvPacket( proof: CommitmentProof, proofHeight: Height, relayer: string): Packet { - // get provable channel store - channelStore = getChannelStore(localChannelStoreIdentifier) - - channel = get(channelPath(packet.destPort, packet.destChannel)) - assert(channel !== null) - assert(channel.state === OPEN) - - assert(packet.sourcePort === channel.counterpartyPortIdentifier) - assert(packet.sourceChannel === channel.counterpartyChannelIdentifier) - - // get provable client store - clientStore = getClientStore(localClientStoreIdentifier) - // in this specification, the connection hops fields will house - // the underlying client identifier - client = get(clientPath(channel.connectionHops[0])) + // in this specification, the destination channel specifies + // the client that exists on the destination chain tracking + // the sender chain + client = router.get(packet.destChannel) assert(client !== null) packetPath = packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence) - proofPath = applyPrefix(client.counterpartyChannelStoreIdentifier, packetPath) - assert(client.verifyPacketData( + // DISCUSSION NEEDED: Should we have an in-protocol notion of Prefixing the path + // or should we make this a concern of the client's VerifyMembership + // proofPath = applyPrefix(client.counterpartyChannelStoreIdentifier, packetPath) + assert(client.verifyMembership( proofHeight, 0, 0, // zeroed out delay period proof, From 7b7ca07d40761eeb72a98f8be446d88b0ddddce2 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Thu, 25 Apr 2024 19:09:51 +0200 Subject: [PATCH 08/18] add security consideration TODO --- spec/micro/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/micro/README.md b/spec/micro/README.md index aecdab993..82998dfc3 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -105,6 +105,7 @@ Sending a packet with the wrong source client is equivalent to sending a packet If a user sends a packet with the wrong destination channel, then as we will see it will be impossible for the intended destination to correctly verify the packet thus, the packet will simply time out. + ### Registering IBC applications on the router From 566cd50c48853ebcb21e27dbe48616d7392d71d3 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Mon, 6 May 2024 23:34:48 +0200 Subject: [PATCH 09/18] enough to start prototyping --- spec/micro/README.md | 54 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/spec/micro/README.md b/spec/micro/README.md index 82998dfc3..45a4ed6e0 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -53,10 +53,8 @@ function submitMisbehaviour( ): error ``` -``` // TODO: Keep very limited buffer of consensus states // Keep ability to migrate client (without necessarily consensus governance) -``` ### Core IBC Functionality @@ -83,7 +81,15 @@ Properties of Channel: ### IBC-Lite -IBC lite will simply provide packet delivery between two chains communicating and identifying each other by on-chain light clients as specified in ICS-02 with application packet data being routed to their specific IBC applications with packet-flow semantics remaining as they were defined in ICS-04. +In core IBC, the connection and channel handshakes serve to validate the clients are valid clients of the counterparty, ensure the IBC version and application versions are mutually compatible, as well as providing unique identifiers for each side to refer to the counterparty. + +Since we are removing handshakes in IBC lite, we must move some validation into a different layer (e.g. social consensus, application layer). + +The client validation that occurs in the connection handshake will be pushed into the social consensus layer. Thus, the connection will be identified by the (clientID-clientID) pair rather than a single connectionID. Note: that on a chainA, the client on chainA tracking chainB is a unique identifier. However, users may send from chainA on clientA to any number of clients on chainB; some of which may be tracking chainA but others may indeed be pointing to other chains. Sending a packet with an invalid (client-client) pair will be considered user error, and the packet flow is not guaranteed to complete or be safe. Thus, for a given clientA on chainA; the client-client pairs: `(clientA, clientB1), (clientA, clientB2), (clientA, clientB3)` all represent different trust models. IBC applications key on the channelID stored on their chain to determine the trust model of the packet data. For example, ICS-20 transfer will append the destination channel-id when receiving tokens as a sink zone. If the destination channel-id, only contained the destination client-id; this could potentially mix tokens that were coming in from different source clients; thus muddying the security model. + +To prevent this, we will append the counterparty client identifier to our own client identifier and use that as the channelID for our side. Thus for two chains: chainA and chainB, with respective clients: clientA and clientB, we will use the channelID: `clientA::clientB` for the channelID on chainA and the channelID: `clientB::clientA` on chainB. The specific separator used is not standardized here, as it is only necessary that a given implementation of IBC-Lite is capable of retrieving its on chain client-id from its own on chain channel-id while also providing a unique channel ID for every (client-client pair). Furthermore, such a separator isn't even necessary if there exists an out-of-band solution to ensure that only one (client-client) pairing for each on-chain client is possible (e.g. on-chain DNS or client registry). + +IBC lite will simply provide packet delivery between two chains communicating and identifying each other by on-chain light clients as specified in ICS-02 with application packet data being routed to their specific IBC applications with packet-flow semantics remaining as they were defined in ICS-04. The channelID derived from the clientIDs as mentioned above will tell the IBC router which chain to send the packets to and which chain a received packet came from, while the portID specifies which application on the router the packet should be sent to. Thus, once two chains have set up clients for each other with specific Identifiers, they can send IBC packets like so. @@ -93,9 +99,9 @@ interface Packet { timeoutHeight: Height timeoutTimestamp: uint64 sourcePort: Identifier // identifier of the application on sender - sourceChannel: Identifier // identifier of the client of destination on sender chain + sourceChannel: Identifier // identifier of the client of destination on sender chain + identifier of client of sender on destination chain destPort: Identifier // identifier of the application on destination - destChannel: Identifier // identifier of the client of sender on the destination chain + destChannel: Identifier // identifier of the client of sender on the destination chain + identifier of client of destination on source chain } ``` @@ -130,7 +136,9 @@ function sendPacket( packetData: []byte ): uint64 { // in this specification, the source channel will point to the client - client = router.get(sourceChannel) + // implementation detail on how to get source client from source channel + clientId = retrieveClientId(sourceChannel) + client = router.get(clientId) assert(client !== null) // disallow packets with a zero timeoutHeight and timeoutTimestamp @@ -169,10 +177,12 @@ function recvPacket( proof: CommitmentProof, proofHeight: Height, relayer: string): Packet { - // in this specification, the destination channel specifies + // in this specification, the destination channel contains // the client that exists on the destination chain tracking // the sender chain - client = router.get(packet.destChannel) + // implementation detail on how to get source client from source channel + clientId = retrieveClientId(destChannel) + client = router.get(clientId) assert(client !== null) packetPath = packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence) @@ -220,6 +230,34 @@ function recvPacket( } ``` +### Correctness + +Claim: If the clients are setup correctly, then a chain can always verify packet flow messages sent by a valid counterparty. + +If the clients are correct, then they can verify any key/value membership proof as well as a key non-membership proof. + +All packet flow message (SendPacket, RecvPacket, and TimeoutPacket) are sent with the full packet. The packet contains both sender and receiver identifiers. Thus on packet flow messages sent to the receiver (RecvPacket), we use the receiver identifier in the packet to retrieve our local client and the source identifier to determine which path the sender stored the packet under. We can thus use our retrieved client to verify a key/value membership proof to validate that the packet was sent by the counterparty. + +Similarly, for packet flow messages sent to the sender (AcknowledgePacket, TimeoutPacket); the packet is provided again. This time, we use the sender identifier to retrieve the local client and the destination identifier to determine the key path that the receiver must have written to when it received the packet. We can thus use our retrieved client to verify a key/value membership proof to validate that the packet was sent by the counterparty. In the case of timeout, if the packet receipt wasn't written to the receipt path determined by the destination identifier this can be verified by our retrieved client using the key nonmembership proof. + +### Soundness + +// To do after prototyping and going through some of these examples before writing it down + +Claim: If the clients are setup correctly, then a chain cannot mistake a packet flow message intended for a different chain as a valid message from a valid counterparty. + +We must note that client identifiers are unique to each chain but are not globally unique. Let us first consider a user that correctly specifies the source and destination identifiers in the packet. + +We wish to ensure that well-formed packets (i.e. packets with correctly setup client ids) cannot have packet flow messages succeed on third-party chains. Ill-formed packets (i.e. packets with invalid client ids) may in some cases complete in invalid states; however we must ensure that any completed state from these packets cannot mix with the state of other valid packets. + +We are guaranteed that the source identifier is unique on the source chain, the destination identifier is unique on the destination chain. Additionally, the destination identifier points to a valid client of the source chain, and the source identifier points to a valid client of the destination chain. + +Suppose the RecvPacket is sent to a chain other than the one identified by the sourceClient on the source chain. + +In the packet flow messages sent to the receiver (RecvPacket), the packet send is verified using the client on the destination chain (retrieved using destination identifier) with the packet commitment path derived by the source identifier. This verification check can only pass if the chain identified by the destination client committed the packet we received under the source channel identifier. This is only possible if the destination client is pointing to the original source chain, or if it is pointing to a different chain that committed the exact same packet. Pointing to the original source chain would mean we sent the packet to the correct . Since the sender only sends packets intended for the desination chain by setting to a unique source identifier, we can be sure the packet was indeed intended for us. Since our client on the reciver is also correctly pointing to the sender chain, we are verifying the proof against a specific consensus algorithm that we assume to be honest. If the packet is committed to the wrong key path, then we will not accept the packet. Similarly, if the packet is committed by the wrong chain then we will not be able to verify correctly. + + + ------ IGNORE THE MULTICHANNEL WORK FOR NOW From 84367b93e26cbe7b01f99f6c02b7a5bc71393414 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Wed, 8 May 2024 23:38:51 +0200 Subject: [PATCH 10/18] add provideCounterparty, merge ports, finish callbacks --- spec/micro/README.md | 192 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 163 insertions(+), 29 deletions(-) diff --git a/spec/micro/README.md b/spec/micro/README.md index 45a4ed6e0..c2621dd80 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -79,15 +79,36 @@ Properties of Channel: - Ensures exactly-once delivery of packet flow datagrams (Send, Receive, Acknowledge, Timeout) - Ensures valid packet flow (Send => Receive => Acknowledge) XOR (Send => Timeout) -### IBC-Lite +### Identifying Counterparties In core IBC, the connection and channel handshakes serve to validate the clients are valid clients of the counterparty, ensure the IBC version and application versions are mutually compatible, as well as providing unique identifiers for each side to refer to the counterparty. -Since we are removing handshakes in IBC lite, we must move some validation into a different layer (e.g. social consensus, application layer). +Since we are removing handshakes in IBC lite, we must have a different way to provide the chain with knowledge of the counterparty identifier. With a client, we can prove any key/value path on the counterparty. However, without knowing which identifier the counterparty uses when it sends messages to us; we cannot differentiate between messages sent from the counterparty to our chain vs messages sent from the counterparty with other chains. -The client validation that occurs in the connection handshake will be pushed into the social consensus layer. Thus, the connection will be identified by the (clientID-clientID) pair rather than a single connectionID. Note: that on a chainA, the client on chainA tracking chainB is a unique identifier. However, users may send from chainA on clientA to any number of clients on chainB; some of which may be tracking chainA but others may indeed be pointing to other chains. Sending a packet with an invalid (client-client) pair will be considered user error, and the packet flow is not guaranteed to complete or be safe. Thus, for a given clientA on chainA; the client-client pairs: `(clientA, clientB1), (clientA, clientB2), (clientA, clientB3)` all represent different trust models. IBC applications key on the channelID stored on their chain to determine the trust model of the packet data. For example, ICS-20 transfer will append the destination channel-id when receiving tokens as a sink zone. If the destination channel-id, only contained the destination client-id; this could potentially mix tokens that were coming in from different source clients; thus muddying the security model. +Thus, IBC lite will introduce a new message `ProvideCounterparty` that will associate the counterparty client of our chain with our client of the counterparty. Thus, if the `ProvideCounterparty` message is submitted to both sides correctly. Then both sides have mirrored pairs that can be treated as channel identifiers. Assuming they are correct, the client on each side is unique and provides an authenticated stream of packet data between the two chains. If the `ProvideCounterparty` message submits the wrong clientID, this can lead to invalid behaviour; but this is equivalent to a relayer submitting an invalid client in place of a correct client for the desired chain. In the simplest case, we can rely on out-of-band social consensus to only send on valid pairs that represent a connection between the desired chains of the user; just as we currently rely on out-of-band social consensus that a given clientID and channel built on top of it is the valid, canonical identifier of our desired chain. -To prevent this, we will append the counterparty client identifier to our own client identifier and use that as the channelID for our side. Thus for two chains: chainA and chainB, with respective clients: clientA and clientB, we will use the channelID: `clientA::clientB` for the channelID on chainA and the channelID: `clientB::clientA` on chainB. The specific separator used is not standardized here, as it is only necessary that a given implementation of IBC-Lite is capable of retrieving its on chain client-id from its own on chain channel-id while also providing a unique channel ID for every (client-client pair). Furthermore, such a separator isn't even necessary if there exists an out-of-band solution to ensure that only one (client-client) pairing for each on-chain client is possible (e.g. on-chain DNS or client registry). +```typescript +function ProvideCounterparty( + channelIdentifier: Identifier, // this will be our own client identifier representing our channel to desired chain + counterpartyChannelIdentifier: Identifier, // this is the counterparty's identifier of our chain + authentication: data, // implementation-specific authentication data +) { + assert(verify(authentication)) + + privateStore.set(counterpartyPath(channelIdentifier), counterpartyChannelIdentifier) +} + +// getCounterparty retrieves the stored counterparty identifier +// given the channelIdentifier on our chain once it is provided +function getCounterparty(channelIdentifier: Identifier): Identifier { + return privateStore.get(counterpartyPath(channelIdentifier)) +} +``` + +The `ProvideCounterparty` method allows for authentication data that implementations may verify before storing the provided counterparty identifier. The strongest authentication possible is to have a valid clientState and consensus state of our chain in the authentication along with a proof it was stored at the claimed counterparty identifier. This is equivalent to the `validateSelfClient` logic performed in the connection handshake. +A simpler but weaker authentication would simply be to check that the `ProvideCounterparty` message is sent by the same relayer that initialized the client. This would make the client parameters completely initialized by the relayer. Thus, users must verify that the client is pointing to the correct chain and that the counterparty identifier is correct as well before using the lite channel identified by the provided client-client pair. + +### IBC Lite IBC lite will simply provide packet delivery between two chains communicating and identifying each other by on-chain light clients as specified in ICS-02 with application packet data being routed to their specific IBC applications with packet-flow semantics remaining as they were defined in ICS-04. The channelID derived from the clientIDs as mentioned above will tell the IBC router which chain to send the packets to and which chain a received packet came from, while the portID specifies which application on the router the packet should be sent to. @@ -99,19 +120,18 @@ interface Packet { timeoutHeight: Height timeoutTimestamp: uint64 sourcePort: Identifier // identifier of the application on sender - sourceChannel: Identifier // identifier of the client of destination on sender chain + identifier of client of sender on destination chain + sourceChannel: Identifier // identifier of the client of destination on sender chain destPort: Identifier // identifier of the application on destination - destChannel: Identifier // identifier of the client of sender on the destination chain + identifier of client of destination on source chain + destChannel: Identifier // identifier of the client of sender on the destination chain } ``` Since the packets are addressed **directly** with the underlying light clients, there are **no** more handshakes necessary. Instead the packet sender must be capable of providing the correct pair. -Sending a packet with the wrong source client is equivalent to sending a packet with the wrong source channel. Sending a packet with the wrong destination client is a new source of errors, as the connection handshake was intended to connection pairwise clients and verify that they are indeed valid clients of each other. +Sending a packet with the wrong source client is equivalent to sending a packet with the wrong source channel. Sending a packet on a channel with the wrong provided counterparty is a new source of errors, however this is added to the burden of out-of-band social consensus. -If a user sends a packet with the wrong destination channel, then as we will see it will be impossible for the intended destination to correctly verify the packet thus, the packet will simply time out. +If the client and counterparty identifiers are setup correctly, then the correctness and soundness properties of IBC holds. IBC packet flow is guaranteed to succeed. If a user sends a packet with the wrong destination channel, then as we will see it will be impossible for the intended destination to correctly verify the packet thus, the packet will simply time out. - ### Registering IBC applications on the router @@ -131,14 +151,13 @@ type IBCRouter struct { function sendPacket( sourcePort: Identifier, sourceChannel: Identifier, + destPort: Identifier, timeoutHeight: Height, timeoutTimestamp: uint64, packetData: []byte ): uint64 { - // in this specification, the source channel will point to the client - // implementation detail on how to get source client from source channel - clientId = retrieveClientId(sourceChannel) - client = router.get(clientId) + // in this specification, the source channel is the clientId + client = router.clients[packet.sourceChannel] assert(client !== null) // disallow packets with a zero timeoutHeight and timeoutTimestamp @@ -148,19 +167,29 @@ function sendPacket( latestClientHeight = client.latestClientHeight() assert(timeoutHeight === 0 || latestClientHeight < timeoutHeight) - // increment the send sequence counter + // IBC only commits sourcePort, sourceChannel, sequence in the commitment path + // and packet data, and timeout information in the value + // For IBC Lite, we can't automatically retrieve the destination port since we don't have an actual stored channel + // in order to get around this, if sourcePort and destinationPort are the same we will leave the path as-is. + // if the ports are different than we must append the port ids together + // so the receiver can verify the requested destination port + commitPort = sourcePort + if sourcePort != destPort { + commitPort = sourcePort + "/" + destPort + } + // if the sequence doesn't already exist, this call initializes the sequence to 0 - sequence = privateStore.get(nextSequenceSendPath(sourcePort, sourceChannel)) + sequence = channelStore.get(nextSequenceSendPath(commitPort, sourceChannel)) // store commitment to the packet data & packet timeout channelStore.set( - packetCommitmentPath(sourcePort, sourceChannel, sequence), + packetCommitmentPath(commitPort, sourceChannel, sequence), hash(hash(data), timeoutHeight, timeoutTimestamp) ) // increment the sequence. Thus there are monotonically increasing sequences for packet flow // from sourcePort, sourceChannel pair - channelStore.set(nextSequenceSendPath(sourcePort, sourceChannel), sequence+1) + channelStore.set(nextSequenceSendPath(commitPort, sourceChannel), sequence+1) // log that a packet can be safely sent emitLogEntry("sendPacket", { @@ -177,15 +206,19 @@ function recvPacket( proof: CommitmentProof, proofHeight: Height, relayer: string): Packet { - // in this specification, the destination channel contains - // the client that exists on the destination chain tracking - // the sender chain - // implementation detail on how to get source client from source channel - clientId = retrieveClientId(destChannel) - client = router.get(clientId) + // in this specification, the destination channel is the clientId + client = router.clients[packet.destChannel] assert(client !== null) - packetPath = packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence) + counterparty = getCounterparty(packet.destChannel) + assert(packet.sourceChannel == counterparty) + + srcCommitPort = packet.sourcePort + if packet.sourcePort != packet.destPort { + srcCommitPort = packet.sourcePort + "/" + packet.destPort + } + + packetPath = packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence) // DISCUSSION NEEDED: Should we have an in-protocol notion of Prefixing the path // or should we make this a concern of the client's VerifyMembership // proofPath = applyPrefix(client.counterpartyChannelStoreIdentifier, packetPath) @@ -193,20 +226,25 @@ function recvPacket( proofHeight, 0, 0, // zeroed out delay period proof, - proofPath, + packetPath, hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp) )) assert(packet.timeoutHeight === 0 || getConsensusHeight() < packet.timeoutHeight) assert(packet.timeoutTimestamp === 0 || currentTimestamp() < packet.timeoutTimestamp) + dstCommitPort = packet.destPort + if packet.sourcePort != packet.destPort { + dstCommitPort = packet.destPort + "/" + packet.sourcePort + } + // we must set the receipt so it can be verified on the other side // this receipt does not contain any data, since the packet has not yet been processed // it's the sentinel success receipt: []byte{0x01} - packetReceipt = channelStore.get(packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence)) + packetReceipt = channelStore.get(packetReceiptPath(dstCommitPort, packet.destChannel, packet.sequence)) assert(packetReceipt === null) channelStore.set( - packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence), + packetReceiptPath(destCommitPort, packet.destChannel, packet.sequence), SUCCESSFUL_RECEIPT ) @@ -224,9 +262,105 @@ function recvPacket( connection: channel.connectionHops[0] }) - cbs = callbacks[packet.destPort] + cbs = router.callbacks[packet.destPort] + // IMPORTANT: if the ack is error, then the callback reverts its internal state changes, but the entire tx continues ack = cbs.OnRecvPacket(packet, relayer) - ... + + channelStore.set(packetAcknowledgementPath(destCommitPort, packet.destChannel, packet.sequence), ack) +} + +function acknowledgePacket( + packet: OpaquePacket, + acknowledgement: bytes, + proof: CommitmentProof, + proofHeight: Height, + relayer: string +) { + // in this specification, the source channel is the clientId + client = router.clients[packet.sourceChannel] + assert(client !== null) + + counterparty = getCounterparty(packet.sourceChannel) + assert(packet.destChannel == counterparty) + + srcCommitPort = packet.sourcePort + if packet.sourcePort != packet.destPort { + srcCommitPort = packet.sourcePort + "/" + packet.destPort + } + + // verify we sent the packet and haven't cleared it out yet + assert(provableStore.get(packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence)) + === hash(hash(packet.data), packet.timeoutHeight, packet.timeoutTimestamp)) + + destCommitPort = packet.destPort + if packet.sourcePort != packet.destPort { + destCommitPort = packet.destPort + "/" + packet.sourcePort + } + + ackPath = packetAcknowledgementPath(destCommitPort, packet.destChannel) + assert(client.verifyMembership( + proofHeight, + 0, 0, + proof, + ackPath, + hash(acknowledgement) + )) + + cbs = router.callbacks[packet.sourcePort] + cbs.OnAcknowledgePacket(packet, acknowledgement, relayer) + + channelStore.delete(packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence)) +} + +function timeoutPacket( + packet: OpaquePacket, + proof: CommitmentProof, + proofHeight: Height, + relayer: string +) { + // in this specification, the source channel is the clientId + client = router.clients[packet.sourceChannel] + assert(client !== null) + + counterparty = getCounterparty(packet.sourceChannel) + assert(packet.destChannel == counterparty) + + srcCommitPort = packet.sourcePort + if packet.sourcePort != packet.destPort { + srcCommitPort = packet.sourcePort + "/" + packet.destPort + } + + // verify we sent the packet and haven't cleared it out yet + assert(provableStore.get(packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence)) + === hash(hash(packet.data), packet.timeoutHeight, packet.timeoutTimestamp)) + + // get the timestamp from the final consensus state in the channel path + var proofTimestamp + proofTimestamp = client.getTimestampAtHeight(proofHeight) + assert(err != nil) + + // check that timeout height or timeout timestamp has passed on the other end + asert( + (packet.timeoutHeight > 0 && proofHeight >= packet.timeoutHeight) || + (packet.timeoutTimestamp > 0 && proofTimestamp >= packet.timeoutTimestamp)) + + destCommitPort = packet.destPort + if packet.sourcePort != packet.destPort { + destCommitPort = packet.destPort + "/" + packet.sourcePort + } + + receiptPath = packetReceiptPath(destCommitPort, packet.destChannel, packet.sequence) + assert(client.verifyNonMembership( + proofHeight + 0, 0, + proof, + receiptPath + )) + + cbs = router.callbacks[packet.sourcePort] + cbs.OnTimeoutPacket(packet, relayer) + + channelStore.delete(packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence)) } ``` From 67fe813f7e4ec603a7c5dec35bc654f3b012afda Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:56:13 +0200 Subject: [PATCH 11/18] add counterparty channel and port assertions, as well as counterparty key prefix prepending before proof verification --- spec/micro/README.md | 115 +++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 63 deletions(-) diff --git a/spec/micro/README.md b/spec/micro/README.md index c2621dd80..427c68aa8 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -83,24 +83,35 @@ Properties of Channel: In core IBC, the connection and channel handshakes serve to validate the clients are valid clients of the counterparty, ensure the IBC version and application versions are mutually compatible, as well as providing unique identifiers for each side to refer to the counterparty. -Since we are removing handshakes in IBC lite, we must have a different way to provide the chain with knowledge of the counterparty identifier. With a client, we can prove any key/value path on the counterparty. However, without knowing which identifier the counterparty uses when it sends messages to us; we cannot differentiate between messages sent from the counterparty to our chain vs messages sent from the counterparty with other chains. +Since we are removing handshakes in IBC lite, we must have a different way to provide the chain with knowledge of the counterparty. With a client, we can prove any key/value path on the counterparty. However, without knowing which identifier the counterparty uses when it sends messages to us; we cannot differentiate between messages sent from the counterparty to our chain vs messages sent from the counterparty with other chains. Most implementations will not be able to store the ICS-24 paths directly as a key in the global namespace; but will instead write to a reserved, prefixed keyspace so as not to conflict with other application state writes. Thus the counteparty information we must have includes both its identifier for our chain as well as the key prefix under which it will write the provable ICS-24 paths. Thus, IBC lite will introduce a new message `ProvideCounterparty` that will associate the counterparty client of our chain with our client of the counterparty. Thus, if the `ProvideCounterparty` message is submitted to both sides correctly. Then both sides have mirrored pairs that can be treated as channel identifiers. Assuming they are correct, the client on each side is unique and provides an authenticated stream of packet data between the two chains. If the `ProvideCounterparty` message submits the wrong clientID, this can lead to invalid behaviour; but this is equivalent to a relayer submitting an invalid client in place of a correct client for the desired chain. In the simplest case, we can rely on out-of-band social consensus to only send on valid pairs that represent a connection between the desired chains of the user; just as we currently rely on out-of-band social consensus that a given clientID and channel built on top of it is the valid, canonical identifier of our desired chain. ```typescript +interface Counterparty { + channelId: Identifier + keyPrefix: CommitmentPrefix +} + function ProvideCounterparty( channelIdentifier: Identifier, // this will be our own client identifier representing our channel to desired chain counterpartyChannelIdentifier: Identifier, // this is the counterparty's identifier of our chain + counterpartyKeyPrefix: CommitmentPrefix, authentication: data, // implementation-specific authentication data ) { assert(verify(authentication)) - privateStore.set(counterpartyPath(channelIdentifier), counterpartyChannelIdentifier) + counterparty = Counterparty{ + channelId: counterpartyChannelIdentifier, + keyPrefix: counterpartyKeyPrefix + } + + privateStore.set(counterpartyPath(channelIdentifier), counterparty) } // getCounterparty retrieves the stored counterparty identifier // given the channelIdentifier on our chain once it is provided -function getCounterparty(channelIdentifier: Identifier): Identifier { +function getCounterparty(channelIdentifier: Identifier): Counterparty { return privateStore.get(counterpartyPath(channelIdentifier)) } ``` @@ -139,9 +150,10 @@ The IBC router contains a mapping from a reserved application port and the suppo ```typescript type IBCRouter struct { - apps: portID -> [Version] + versions: portID -> [Version] callbacks: portID -> [Callback] clients: clientId -> Client + ports: portID -> counterpartyPortID } ``` @@ -151,7 +163,6 @@ type IBCRouter struct { function sendPacket( sourcePort: Identifier, sourceChannel: Identifier, - destPort: Identifier, timeoutHeight: Height, timeoutTimestamp: uint64, packetData: []byte @@ -167,17 +178,6 @@ function sendPacket( latestClientHeight = client.latestClientHeight() assert(timeoutHeight === 0 || latestClientHeight < timeoutHeight) - // IBC only commits sourcePort, sourceChannel, sequence in the commitment path - // and packet data, and timeout information in the value - // For IBC Lite, we can't automatically retrieve the destination port since we don't have an actual stored channel - // in order to get around this, if sourcePort and destinationPort are the same we will leave the path as-is. - // if the ports are different than we must append the port ids together - // so the receiver can verify the requested destination port - commitPort = sourcePort - if sourcePort != destPort { - commitPort = sourcePort + "/" + destPort - } - // if the sequence doesn't already exist, this call initializes the sequence to 0 sequence = channelStore.get(nextSequenceSendPath(commitPort, sourceChannel)) @@ -210,15 +210,15 @@ function recvPacket( client = router.clients[packet.destChannel] assert(client !== null) + // assert source channel is destChannel's counterparty channel identifier counterparty = getCounterparty(packet.destChannel) - assert(packet.sourceChannel == counterparty) + assert(packet.sourceChannel == counterparty.channelId) - srcCommitPort = packet.sourcePort - if packet.sourcePort != packet.destPort { - srcCommitPort = packet.sourcePort + "/" + packet.destPort - } + // assert source port is destPort's counterparty port identifier + assert(packet.sourcePort == ports[packet.destPort]) - packetPath = packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence) + packetPath = packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence) + merklePath = applyPrefix(counterparty.keyPrefix, packetPath) // DISCUSSION NEEDED: Should we have an in-protocol notion of Prefixing the path // or should we make this a concern of the client's VerifyMembership // proofPath = applyPrefix(client.counterpartyChannelStoreIdentifier, packetPath) @@ -226,25 +226,22 @@ function recvPacket( proofHeight, 0, 0, // zeroed out delay period proof, - packetPath, + merklePath, hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp) )) assert(packet.timeoutHeight === 0 || getConsensusHeight() < packet.timeoutHeight) assert(packet.timeoutTimestamp === 0 || currentTimestamp() < packet.timeoutTimestamp) - dstCommitPort = packet.destPort - if packet.sourcePort != packet.destPort { - dstCommitPort = packet.destPort + "/" + packet.sourcePort - } - + // we must set the receipt so it can be verified on the other side // this receipt does not contain any data, since the packet has not yet been processed // it's the sentinel success receipt: []byte{0x01} - packetReceipt = channelStore.get(packetReceiptPath(dstCommitPort, packet.destChannel, packet.sequence)) + packetReceipt = channelStore.get(packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence)) assert(packetReceipt === null) + channelStore.set( - packetReceiptPath(destCommitPort, packet.destChannel, packet.sequence), + packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence), SUCCESSFUL_RECEIPT ) @@ -266,7 +263,9 @@ function recvPacket( // IMPORTANT: if the ack is error, then the callback reverts its internal state changes, but the entire tx continues ack = cbs.OnRecvPacket(packet, relayer) - channelStore.set(packetAcknowledgementPath(destCommitPort, packet.destChannel, packet.sequence), ack) + if ack != nil { + channelStore.set(packetAcknowledgementPath(packet.destPort, packet.destChannel, packet.sequence), ack) + } } function acknowledgePacket( @@ -280,36 +279,31 @@ function acknowledgePacket( client = router.clients[packet.sourceChannel] assert(client !== null) - counterparty = getCounterparty(packet.sourceChannel) - assert(packet.destChannel == counterparty) - - srcCommitPort = packet.sourcePort - if packet.sourcePort != packet.destPort { - srcCommitPort = packet.sourcePort + "/" + packet.destPort - } + // assert dest channel is sourceChannel's counterparty channel identifier + counterparty = getCounterparty(packet.destChannel) + assert(packet.sourceChannel == counterparty.channelId) + + // assert dest port is sourcePort's counterparty port identifier + assert(packet.destPort == ports[packet.sourcePort]) // verify we sent the packet and haven't cleared it out yet - assert(provableStore.get(packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence)) + assert(provableStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) === hash(hash(packet.data), packet.timeoutHeight, packet.timeoutTimestamp)) - destCommitPort = packet.destPort - if packet.sourcePort != packet.destPort { - destCommitPort = packet.destPort + "/" + packet.sourcePort - } - - ackPath = packetAcknowledgementPath(destCommitPort, packet.destChannel) + ackPath = packetAcknowledgementPath(packet.destPort, packet.destChannel) + merklePath = applyPrefix(counterparty.keyPrefix, ackPath) assert(client.verifyMembership( proofHeight, 0, 0, proof, - ackPath, + merklePath, hash(acknowledgement) )) cbs = router.callbacks[packet.sourcePort] cbs.OnAcknowledgePacket(packet, acknowledgement, relayer) - channelStore.delete(packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence)) + channelStore.delete(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) } function timeoutPacket( @@ -322,16 +316,15 @@ function timeoutPacket( client = router.clients[packet.sourceChannel] assert(client !== null) - counterparty = getCounterparty(packet.sourceChannel) - assert(packet.destChannel == counterparty) - - srcCommitPort = packet.sourcePort - if packet.sourcePort != packet.destPort { - srcCommitPort = packet.sourcePort + "/" + packet.destPort - } + // assert dest channel is sourceChannel's counterparty channel identifier + counterparty = getCounterparty(packet.destChannel) + assert(packet.sourceChannel == counterparty.channelId) + + // assert dest port is sourcePort's counterparty port identifier + assert(packet.destPort == ports[packet.sourcePort]) // verify we sent the packet and haven't cleared it out yet - assert(provableStore.get(packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence)) + assert(provableStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) === hash(hash(packet.data), packet.timeoutHeight, packet.timeoutTimestamp)) // get the timestamp from the final consensus state in the channel path @@ -344,23 +337,19 @@ function timeoutPacket( (packet.timeoutHeight > 0 && proofHeight >= packet.timeoutHeight) || (packet.timeoutTimestamp > 0 && proofTimestamp >= packet.timeoutTimestamp)) - destCommitPort = packet.destPort - if packet.sourcePort != packet.destPort { - destCommitPort = packet.destPort + "/" + packet.sourcePort - } - - receiptPath = packetReceiptPath(destCommitPort, packet.destChannel, packet.sequence) + receiptPath = packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence) + merklePath = applyPrefix(counterparty.keyPrefix, receiptPath) assert(client.verifyNonMembership( proofHeight 0, 0, proof, - receiptPath + merklePath )) cbs = router.callbacks[packet.sourcePort] cbs.OnTimeoutPacket(packet, relayer) - channelStore.delete(packetCommitmentPath(srcCommitPort, packet.sourceChannel, packet.sequence)) + channelStore.delete(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) } ``` From 607f5deced624d589d43fcb0ac4c627a0fbbad4b Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:19:37 +0200 Subject: [PATCH 12/18] start to address reviews --- spec/micro/README.md | 393 +------------------------------------------ 1 file changed, 2 insertions(+), 391 deletions(-) diff --git a/spec/micro/README.md b/spec/micro/README.md index 427c68aa8..1afb16710 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -6,7 +6,7 @@ The implementation of the entire IBC protocol as it currently stands is a large Writing an implementation from scratch is a problem many ecosystems face as a major barrier for IBC adoption. -The goal of this document is to serve as a "micro-IBC" specification that will allow new ecosystems to implement a protocol that can communicate with fully implemented IBC chains using the same security assumptions. +The goal of this document is to serve as a "micro-IBC" specification that will allow new ecosystems to implement a protocol that can communicate with fully implemented IBC chains using the same security assumptions. It will also explain the motivations of the original design choices of the IBC protocol and how the micro-ibc architecture rethinks these design choices while still retaining the desired properties of IBC. The micro-IBC protocol must have the same security properties as IBC, and must be completely compatible with IBC applications. It may not have the full flexibility offered by standard IBC. @@ -24,7 +24,7 @@ The micro-IBC protocol must have the same security properties as IBC, and must b The light client module can be implemented exactly as-is with regards to its functionality. It **must** have external endpoints for relayers (off-chain processes that have full-node access to other chains in the network) to initialize a client, update the client, and submit misbehaviour in case the trust model of the client is violated by the counterparty consensus mechanism (e.g. committing to different headers for the same height). -The implementation of each of these endpoints will be specific to the particular consensus mechanism targetted. The choice of consensus algorithm itself is arbitrary, it may be a Proof-of-Stake algorithm like CometBFT, or a multisig of trusted authorities, or a rollup that relies on an additional underlying client in order to verify its consensus. +The implementation of each of these endpoints will be specific to the particular consensus mechanism targetted. The choice of consensus algorithm itself is arbitrary, it may be a Proof-of-Stake algorithm like CometBFT, or a multisig of trusted authorities, or a rollup that relies on an additional underlying client in order to verify its consensus. However, a light client must have the ability to define finality for a given snapshot of the state machine, this may be either through single-slot finality or a finality gadget. Thus, the endpoints themselves should accept arbitrary bytes for the arguments passed into these client endpoints as it is up to each individual client implementation to unmarshal these bytes into the structures they expect. @@ -379,392 +379,3 @@ Suppose the RecvPacket is sent to a chain other than the one identified by the s In the packet flow messages sent to the receiver (RecvPacket), the packet send is verified using the client on the destination chain (retrieved using destination identifier) with the packet commitment path derived by the source identifier. This verification check can only pass if the chain identified by the destination client committed the packet we received under the source channel identifier. This is only possible if the destination client is pointing to the original source chain, or if it is pointing to a different chain that committed the exact same packet. Pointing to the original source chain would mean we sent the packet to the correct . Since the sender only sends packets intended for the desination chain by setting to a unique source identifier, we can be sure the packet was indeed intended for us. Since our client on the reciver is also correctly pointing to the sender chain, we are verifying the proof against a specific consensus algorithm that we assume to be honest. If the packet is committed to the wrong key path, then we will not accept the packet. Similarly, if the packet is committed by the wrong chain then we will not be able to verify correctly. - - ------- - -IGNORE THE MULTICHANNEL WORK FOR NOW - -### Sending packets on the MultiChannel - - -Sending packets in the multichannel requires you to construct a packet data that contains a map from the application reserved portID to the requested version and opaque packet data. - -```typescript -type MultiPacketData struct { - AppData: Map[string]{ - AppPacketData{ - Version: string, - Data: []byte, - } - } -} -``` - -The router will check that the channelID exists and it has a `MULTI_PORT_ID`. It will send a verifyMembership of the packet to the underlying client. It will then iterate over the packet data's in the multiPacketData. It will check that each application supports the requested version, and then it will construct a packet that only contains the application packet data and send it to the application. It will collect acknowledgements from each and put it into a multiAcknowledgement. - -```typescript -type MultiAcknowledgment struct { - success: bool, - AppAcknowledgement: Map[string]Acknowledgement, -} -``` - -This will in turn be unpacked and sent to each application as an individual acknowledgment. However, the total success value must be the same for all apps since the packet receiving logic is atomic. - -### Router Methods - -```typescript -function sendPacket( - sourceChannel: Identifier, - timeoutHeight: Height, - timeoutTimestamp: uint64, - packetData: MultiPacketData): uint64 { - // get provable channel store - channelStore = getChannelStore(localChannelStoreIdentifier) - - channel = get(channelPath(MULTI_IBC_PORT, sourceChannel)) - assert(channel !== null) - assert(channel.state === OPEN) - - // get provable client store - clientStore = getClientStore(localClientStoreIdentifier) - // in this specification, the connection hops fields will house - // the underlying client identifier - client = get(clientPath(channel.connectionHops[0])) - assert(client !== null) - - // disallow packets with a zero timeoutHeight and timeoutTimestamp - assert(timeoutHeight !== 0 || timeoutTimestamp !== 0) - - // check that the timeout height hasn't already passed in the local client tracking the receiving chain - latestClientHeight = client.latestClientHeight() - assert(timeoutHeight === 0 || latestClientHeight < timeoutHeight) - - // increment the send sequence counter - sequence = channelStore.get(nextSequenceSendPath(MULTI_IBC_PORT, sourceChannel)) - channelStore.set(nextSequenceSendPath(MULTI_IBC_PORT, sourceChannel), sequence+1) - - // store commitment to the packet data & packet timeout - channelStore.set( - packetCommitmentPath(MULTI_IBC_PORT, sourceChannel, sequence), - hash(hash(data), timeoutHeight, timeoutTimestamp) - ) - - // log that a packet can be safely sent - emitLogEntry("sendPacket", { - sequence: sequence, - data: data, - timeoutHeight: timeoutHeight, - timeoutTimestamp: timeoutTimestamp - }) - - mulitPacketData.AppData.forEach((port, appData) => { - supportedVersions = app[port] - // check if router supports the desired port and version - if supportedVersions.contains(appData.Version) { - // send each individual packet data to application - // to do send packet logic. e.g. escrow tokens - appData = appData.Data - // abort transaction on the first failure in send packet - // note this is an additional callback as the flow of execution - // differs from traditional IBC - assert(callbacks[port].onSendPacket( - packet.sourcePort, - packet.sourceChannel, - sequence, - appData, - relayer)) - } - }) - - return sequence -} -``` - -```typescript -function recvPacket( - packet: OpaquePacket, - proof: CommitmentProof, - proofHeight: Height, - relayer: string): Packet { - // get provable channel store - channelStore = getChannelStore(localChannelStoreIdentifier) - - channel = get(channelPath(MULTI_IBC_PORT, sourceChannel)) - assert(channel !== null) - assert(channel.state === OPEN) - - assert(packet.sourcePort === channel.counterpartyPortIdentifier) - assert(packet.sourceChannel === channel.counterpartyChannelIdentifier) - - // get provable client store - clientStore = getClientStore(localClientStoreIdentifier) - // in this specification, the connection hops fields will house - // the underlying client identifier - client = get(clientPath(channel.connectionHops[0])) - assert(client !== null) - - packetPath = packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence) - proofPath = applyPrefix(client.counterpartyChannelStoreIdentifier, packetPath) - assert(client.verifyPacketData( - proofHeight, - 0, 0, // zeroed out delay period - proof, - proofPath, - hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp) - )) - - assert(packet.timeoutHeight === 0 || getConsensusHeight() < packet.timeoutHeight) - assert(packet.timeoutTimestamp === 0 || currentTimestamp() < packet.timeoutTimestamp) - - // we must set the receipt so it can be verified on the other side - // this receipt does not contain any data, since the packet has not yet been processed - // it's the sentinel success receipt: []byte{0x01} - packetReceipt = channelStore.get(packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence)) - assert(packetReceipt === null) - channelStore.set( - packetReceiptPath(packet.destPort, packet.destChannel, packet.sequence), - SUCCESSFUL_RECEIPT - ) - - // log that a packet has been received - emitLogEntry("recvPacket", { - data: packet.data - timeoutHeight: packet.timeoutHeight, - timeoutTimestamp: packet.timeoutTimestamp, - sequence: packet.sequence, - sourcePort: packet.sourcePort, - sourceChannel: packet.sourceChannel, - destPort: packet.destPort, - destChannel: packet.destChannel, - order: channel.order, - connection: channel.connectionHops[0] - }) - - multiPacketData = unmarshal(packetData) - multiAck = MultiAcknowledgement{true, make(map[string]Acknowledgement)} - - // NEEDS DISCUSSION: Should we break early on first failure? - mulitPacketData.AppData.forEach((port, appData) => { - supportedVersions = app[port] - // check if router supports the desired port and version - if supportedVersions.contains(appData.Version) { - // create a new packet with just the application data - // in the packet data for the desired application - appPacket = Packet{ - sequence: packet.sequence, - timeoutHeight: packet.timeoutHeight, - timeoutTimestamp: packet.timeoutTimestamp, - sourcePort: packet.sourcePort, - sourceChannel: packet.sourceChannel, - destPort: packet.destPort, - destChannel: packet.destChannel, - data: appData.Data, - } - // TODO: Support aysnc acknowledgements - appAck = callbacks[port].onRecvPacket(packet, relayer) - // success of multiack must be false if even a single app acknowledgement returns false (atomic multipacket behaviour) - // and puts the custom acknowledgement in the app acknowledgement map under the port key - multiAck = multiAck{ - success: multiAck.success && appAck.Success(), - appAcknowledgement: multiAck.AppAcknowledgement.put(port, appAck.Acknowledgement()) - } - } else { - // requested port/version was not supported so we must error - multiAck = multiAck{ - success: false, - appAcknowledgement: multiAck - } - } - }) - - // write the acknowledgement - channelStore.set( - packetAcknowledgementPath(packet.destPort, packet.destChannel, packet.sequence), - hash(acknowledgement) - ) - - // log that a packet has been acknowledged - emitLogEntry("writeAcknowledgement", { - sequence: packet.sequence, - timeoutHeight: packet.timeoutHeight, - port: packet.destPort, - channel: packet.destChannel, - timeoutTimestamp: packet.timeoutTimestamp, - data: packet.data, - acknowledgement - }) -} -``` - -```typescript -function acknowledgePacket( - packet: OpaquePacket, - acknowledgement: bytes, - proof: CommitmentProof, - proofHeight: Height, - relayer: string): Packet { - // get provable channel store - channelStore = getChannelStore(localChannelStoreIdentifier) - - // check channel is open - channel = get(channelPath(MULTI_IBC_PORT, packet.sourceChannel)) - assert(channel !== null) - assert(channel.state === OPEN) - - // verify counterparty information - assert(packet.destPort === channel.counterpartyPortIdentifier) - assert(packet.destChannel === channel.counterpartyChannelIdentifier) - - client = provableStore.get(connectionPath(channel.connectionHops[0])) - assert(client !== null) - - // verify we sent the packet and haven't cleared it out yet - assert(channelStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) - === hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) - - assert(connection.verifyPacketAcknowledgement( - proofHeight, - proof, - packet.destPort, - packet.destChannel, - packet.sequence, - acknowledgement - )) - - // all assertions passed, we can alter state - - // delete our commitment so we can't "acknowledge" again - provableStore.delete(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) - - multiPacketData = unmarshal(packet.data) - - ackSuccess = multiAck.success - // send each app acknowledgement to relevant port - // and override the success value with the multiack success value - multiAck.forEach((port, ack) => { - supportedVersions = app[port] - appData = multiPacketData.AppData[port] - // check if router supports the desired port and version - if supportedVersions.contains(appData.Version) { - // create a new packet with just the application data - // in the packet data for the desired application - appPacket = Packet{ - sequence: packet.sequence, - timeoutHeight: packet.timeoutHeight, - timeoutTimestamp: packet.timeoutTimestamp, - sourcePort: packet.sourcePort, - sourceChannel: packet.sourceChannel, - destPort: packet.destPort, - destChannel: packet.destChannel, - data: appData.Data, - } - // construct app acknowledgement with multi-app success value - // and individual ack info - // NOTE: application MUST support the standard acknowledgement - // described in ICS-04 - var appAck AppAcknowledgement - if ackSuccess { - // the acknowledgement was a success, - // put app info into result - appAck = AppAcknowledgement{ - result: ack - } - } else { - // the acknowledgement was a failure, - // put app info into error. - // note it is possible that this application succeeded - // and its custom app info included information - // of a successfully executed callback - // however, we will still put the info in error; - // so that the application knows the receive failed - // on the other side. - // Thus the callback reversion logic must be implementable - // given the success boolean as opposed to specific information in the acknowledgement - appAck = AppAcknowledgement{ - error: ack - } - } - - // abort on first error in callbacks - // NEEDS DISCUSSION: Should we fail on first acknowledge error - // or optimistically try them all and succeed anyway - assert(callbacks[port].onAcknowledgePacket(appPacket, appAck, relayer)) - } else { - // should never happen - assert(false) - } - }) -} -``` - -```typescript -function onTimeoutPacket(packet: Packet, relayer: string) { - // get provable channel store - channelStore = getChannelStore(localChannelStoreIdentifier) - - // check channel is open - channel = get(channelPath(MULTI_IBC_PORT, packet.sourceChannel)) - assert(channel !== null) - assert(channel.state === OPEN) - - // verify counterparty information - assert(packet.destPort === channel.counterpartyPortIdentifier) - assert(packet.destChannel === channel.counterpartyChannelIdentifier) - - client = provableStore.get(connectionPath(channel.connectionHops[0])) - assert(client !== null) - - proofTimestamp, err = client.getTimestampAtHeight(connection, proofHeight) - - // check that timeout height or timeout timestamp has passed on the other end - abortTransactionUnless( - (packet.timeoutHeight > 0 && proofHeight >= packet.timeoutHeight) || - (packet.timeoutTimestamp > 0 && proofTimestamp >= packet.timeoutTimestamp)) - - // verify we actually sent this packet, check the store - abortTransactionUnless(provableStore.get(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) - === hash(packet.data, packet.timeoutHeight, packet.timeoutTimestamp)) - - // all assertions passed, we can alter state - - // delete our commitment so we can't "time out" again - provableStore.delete(packetCommitmentPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) - - // unordered channel: verify absence of receipt at packet index - abortTransactionUnless(connection.verifyPacketReceiptAbsence( - proofHeight, - proof, - packet.destPort, - packet.destChannel, - packet.sequence - )) - - mulitPacketData.AppData.forEach((port, appData) => { - supportedVersions = app[port] - // check if router supports the desired port and version - if supportedVersions.contains(appData.Version) { - // create a new packet with just the application data - // in the packet data for the desired application - appPacket = Packet{ - sequence: packet.sequence, - timeoutHeight: packet.timeoutHeight, - timeoutTimestamp: packet.timeoutTimestamp, - sourcePort: packet.sourcePort, - sourceChannel: packet.sourceChannel, - destPort: packet.destPort, - destChannel: packet.destChannel, - data: appData.Data, - } - // NEEDS DISCUSSION: Should we fail on first timeout error - // or optimistically try them all and succeed anyway - assert(callbacks[port].onTimeoutPacket(packet, relayer)) - } else { - // should never happen - assert(false) - } - }) - -} -``` \ No newline at end of file From ffd67d4c489ee694b9b58c23bbe47fc403032cc7 Mon Sep 17 00:00:00 2001 From: Aditya <14364734+AdityaSripal@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:55:36 +0200 Subject: [PATCH 13/18] Apply suggestions from code review Co-authored-by: Aleksandr Bezobchuk --- spec/micro/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/micro/README.md b/spec/micro/README.md index 1afb16710..e2c73a67a 100644 --- a/spec/micro/README.md +++ b/spec/micro/README.md @@ -81,7 +81,7 @@ Properties of Channel: ### Identifying Counterparties -In core IBC, the connection and channel handshakes serve to validate the clients are valid clients of the counterparty, ensure the IBC version and application versions are mutually compatible, as well as providing unique identifiers for each side to refer to the counterparty. +In core IBC, the connection and channel handshakes serve to ensure the validity of counterparty clients, ensure the IBC and application versions are mutually compatible, as well as providing unique identifiers for each side to refer to the counterparty. Since we are removing handshakes in IBC lite, we must have a different way to provide the chain with knowledge of the counterparty. With a client, we can prove any key/value path on the counterparty. However, without knowing which identifier the counterparty uses when it sends messages to us; we cannot differentiate between messages sent from the counterparty to our chain vs messages sent from the counterparty with other chains. Most implementations will not be able to store the ICS-24 paths directly as a key in the global namespace; but will instead write to a reserved, prefixed keyspace so as not to conflict with other application state writes. Thus the counteparty information we must have includes both its identifier for our chain as well as the key prefix under which it will write the provable ICS-24 paths. From d591e4e0a55f58dc881b2e091590743b0cc3ab2d Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:30:14 +0200 Subject: [PATCH 14/18] rename to eureka --- spec/{micro => eureka}/README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) rename spec/{micro => eureka}/README.md (95%) diff --git a/spec/micro/README.md b/spec/eureka/README.md similarity index 95% rename from spec/micro/README.md rename to spec/eureka/README.md index e2c73a67a..1a7f826da 100644 --- a/spec/micro/README.md +++ b/spec/eureka/README.md @@ -1,4 +1,15 @@ -# Micro IBC Architecture +# IBC Eureka + +--- +ics: TBD +title: IBC Eureka +stage: EXPERIMENTAL +category: IBC/TAO +kind: interface +version compatibility: ibc-go v10.0.0 +author: Aditya Sripal +created: 2024-08-15 +--- ### Context @@ -6,9 +17,9 @@ The implementation of the entire IBC protocol as it currently stands is a large Writing an implementation from scratch is a problem many ecosystems face as a major barrier for IBC adoption. -The goal of this document is to serve as a "micro-IBC" specification that will allow new ecosystems to implement a protocol that can communicate with fully implemented IBC chains using the same security assumptions. It will also explain the motivations of the original design choices of the IBC protocol and how the micro-ibc architecture rethinks these design choices while still retaining the desired properties of IBC. +The goal of this document is to serve as the simplest IBC specification that will allow new ecosystems to implement a protocol that can communicate with fully implemented IBC chains using the same security assumptions. It will also explain the motivations of the original design choices of the IBC protocol and how the new ibc architecture rethinks these design choices while still retaining the desired properties of IBC. -The micro-IBC protocol must have the same security properties as IBC, and must be completely compatible with IBC applications. It may not have the full flexibility offered by standard IBC. +The IBC eureka protocol must have the same security properties as IBC, and must be completely compatible with IBC applications. It may not have the full flexibility offered by standard IBC. ### Desired Properties @@ -58,7 +69,7 @@ function submitMisbehaviour( ### Core IBC Functionality -IBC in its essence is the ability for applications on different blockchains with different security models to communicate with each other through light-client backed security. Thus, IBC needs the light client described above and the IBC applications that define the packet data they wish to send and receive. In addition to these layers, core IBC introduces the connection and channel abstractions to connect these two fundamental layers. Micro IBC intends to compress only the necessary aspects of connection and channel layers to a new router layer but before doing this it is critical to understand what service they currently provide. +IBC in its essence is the ability for applications on different blockchains with different security models to communicate with each other through light-client backed security. Thus, IBC needs the light client described above and the IBC applications that define the packet data they wish to send and receive. In addition to these layers, core IBC introduces the connection and channel abstractions to connect these two fundamental layers. IBC Eureka intends to compress only the necessary aspects of connection and channel layers to a new router layer but before doing this it is critical to understand what service they currently provide. Properties of Connection: From 3c91e5678cc042520d7236060011f92407e5c1af Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:21:41 +0200 Subject: [PATCH 15/18] linter --- spec/eureka/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/eureka/README.md b/spec/eureka/README.md index 1a7f826da..ee6e42b6a 100644 --- a/spec/eureka/README.md +++ b/spec/eureka/README.md @@ -1,5 +1,3 @@ -# IBC Eureka - --- ics: TBD title: IBC Eureka @@ -11,6 +9,8 @@ author: Aditya Sripal created: 2024-08-15 --- +# IBC Eureka + ### Context The implementation of the entire IBC protocol as it currently stands is a large undertaking. While there exists ready-made implementations like ibc-go this is only deployable on the Cosmos-SDK. Similarly, there exists ibc-rs which is a library for chains to integrate. However, this requires the chain to be implemented in Rust, there still exists some non-trivial work to integrate the ibc-rs library into the target state machine, and certain limitations either in the state machine or in ibc-rs may prevent using the library for the target chain. @@ -154,7 +154,6 @@ Sending a packet with the wrong source client is equivalent to sending a packet If the client and counterparty identifiers are setup correctly, then the correctness and soundness properties of IBC holds. IBC packet flow is guaranteed to succeed. If a user sends a packet with the wrong destination channel, then as we will see it will be impossible for the intended destination to correctly verify the packet thus, the packet will simply time out. - ### Registering IBC applications on the router The IBC router contains a mapping from a reserved application port and the supported versions of that application as well as a mapping from channelIdentifiers to channels. @@ -389,4 +388,3 @@ We are guaranteed that the source identifier is unique on the source chain, the Suppose the RecvPacket is sent to a chain other than the one identified by the sourceClient on the source chain. In the packet flow messages sent to the receiver (RecvPacket), the packet send is verified using the client on the destination chain (retrieved using destination identifier) with the packet commitment path derived by the source identifier. This verification check can only pass if the chain identified by the destination client committed the packet we received under the source channel identifier. This is only possible if the destination client is pointing to the original source chain, or if it is pointing to a different chain that committed the exact same packet. Pointing to the original source chain would mean we sent the packet to the correct . Since the sender only sends packets intended for the desination chain by setting to a unique source identifier, we can be sure the packet was indeed intended for us. Since our client on the reciver is also correctly pointing to the sender chain, we are verifying the proof against a specific consensus algorithm that we assume to be honest. If the packet is committed to the wrong key path, then we will not accept the packet. Similarly, if the packet is committed by the wrong chain then we will not be able to verify correctly. - From a1f66d17294da1eb3696ef6654ea730670e49999 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:22:37 +0200 Subject: [PATCH 16/18] linter --- spec/eureka/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/eureka/README.md b/spec/eureka/README.md index ee6e42b6a..e2239347c 100644 --- a/spec/eureka/README.md +++ b/spec/eureka/README.md @@ -9,7 +9,7 @@ author: Aditya Sripal created: 2024-08-15 --- -# IBC Eureka +## IBC Eureka ### Context From a5b6e1c87ce6b9f715edb390306b2a1572c43744 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:38:27 +0200 Subject: [PATCH 17/18] add note and v2 folder --- spec/eureka/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/eureka/README.md b/spec/eureka/README.md index e2239347c..147a9635d 100644 --- a/spec/eureka/README.md +++ b/spec/eureka/README.md @@ -13,6 +13,8 @@ created: 2024-08-15 ### Context +Note: This specification is in an experimental phase. The final specification of the v2 of the IBC protocol is being specified in the [v2 folder of the TAO protocol](../core/v2). This document will serve as a placeholder for people to view and comment on as a more formal specification is being discussed and implemented. + The implementation of the entire IBC protocol as it currently stands is a large undertaking. While there exists ready-made implementations like ibc-go this is only deployable on the Cosmos-SDK. Similarly, there exists ibc-rs which is a library for chains to integrate. However, this requires the chain to be implemented in Rust, there still exists some non-trivial work to integrate the ibc-rs library into the target state machine, and certain limitations either in the state machine or in ibc-rs may prevent using the library for the target chain. Writing an implementation from scratch is a problem many ecosystems face as a major barrier for IBC adoption. From 9c6e1b5a465126ba0e928ac4853dcdb056fd6462 Mon Sep 17 00:00:00 2001 From: Aditya Sripal <14364734+AdityaSripal@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:40:49 +0200 Subject: [PATCH 18/18] remove v2 folder --- spec/eureka/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/eureka/README.md b/spec/eureka/README.md index 147a9635d..8fee7e571 100644 --- a/spec/eureka/README.md +++ b/spec/eureka/README.md @@ -13,7 +13,7 @@ created: 2024-08-15 ### Context -Note: This specification is in an experimental phase. The final specification of the v2 of the IBC protocol is being specified in the [v2 folder of the TAO protocol](../core/v2). This document will serve as a placeholder for people to view and comment on as a more formal specification is being discussed and implemented. +Note: This specification is in an experimental phase. The final specification of the v2 of the IBC protocol is being specified in the v2 folder of the TAO protocol. This document will serve as a placeholder for people to view and comment on as a more formal specification is being discussed and implemented. The implementation of the entire IBC protocol as it currently stands is a large undertaking. While there exists ready-made implementations like ibc-go this is only deployable on the Cosmos-SDK. Similarly, there exists ibc-rs which is a library for chains to integrate. However, this requires the chain to be implemented in Rust, there still exists some non-trivial work to integrate the ibc-rs library into the target state machine, and certain limitations either in the state machine or in ibc-rs may prevent using the library for the target chain.