ADR 001: coin cross-chain transfer source tracing (#6662)

* adr: coin cross-chain transfer source tracing

* update pros and cons

* update spec README

* Update docs/architecture/adr-001-coin-source-tracing.md

Co-authored-by: billy rennekamp <billy.rennekamp@gmail.com>

* Apply suggestions from code review

Co-authored-by: billy rennekamp <billy.rennekamp@gmail.com>

* Update docs/architecture/adr-001-coin-source-tracing.md

* address comments from review

* update ADR with Send/Recv logic

* final touches

* Apply suggestions from code review

Co-authored-by: Christopher Goes <cwgoes@pluranimity.org>

* address comments from review

* address @aaronc review comments

* Apply suggestions from code review

Co-authored-by: colin axner <25233464+colin-axner@users.noreply.github.com>

* use SplitN

* custom denom validation reference

* address some comments from review

* more updates based on Colin's review

* final draft with changes to relay.go

* undo proto changes

* address @aaronc review comments

* why do I keep updating the proto files?

* address @AdityaSripal comments

* address more comments

* typos

* final ammendments

* minor fix

* address more comments

* update example

* Update docs/architecture/adr-001-coin-source-tracing.md

Co-authored-by: Anil Kumar Kammari <anil@vitwit.com>

* address more comments

* update prefix example

Co-authored-by: billy rennekamp <billy.rennekamp@gmail.com>
Co-authored-by: Christopher Goes <cwgoes@pluranimity.org>
Co-authored-by: colin axner <25233464+colin-axner@users.noreply.github.com>
Co-authored-by: Alexander Bezobchuk <alexanderbez@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Anil Kumar Kammari <anil@vitwit.com>
This commit is contained in:
Federico Kunze 2020-08-03 13:39:26 +02:00 committed by GitHub
parent 5f5bdcbadf
commit 6f3ca5c140
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 380 additions and 0 deletions

View File

@ -31,6 +31,7 @@ Please add a entry below in your Pull Request for an ADR.
## ADR Table of Contents
- [ADR 001: Coin Source Tracing](./adr-001-coin-source-tracing.md)
- [ADR 002: SDK Documentation Structure](./adr-002-docs-structure.md)
- [ADR 003: Dynamic Capability Store](./adr-003-dynamic-capability-store.md)
- [ADR 004: Split Denomination Keys](./adr-004-split-denomination-keys.md)
@ -50,3 +51,4 @@ Please add a entry below in your Pull Request for an ADR.
- [ADR 022: Custom baseapp panic handling](./adr-022-custom-panic-handling.md)
- [ADR 023: Protocol Buffer Naming and Versioning](./adr-023-protobuf-naming.md)
- [ADR 024: Coin Metadata](./adr-024-coin-metadata.md)
- [ADR 025: IBC Passive Channels](./adr-025-ibc-passive-channels.md)

View File

@ -0,0 +1,378 @@
# ADR 001: Coin Source Tracing
## Changelog
- 2020-07-09: Initial Draft
## Status
Proposed
## Context
The specification for IBC cross-chain fungible token transfers
([ICS20](https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer)), needs to
be aware of the origin of any token denomination in order to relay a `Packet` which contains the sender
and recipient addressed in the
[`FungibleTokenPacketData`](https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures).
The Packet relay sending works based in 2 cases (per
[specification](https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay) and [Colin Axnér](https://github.com/colin-axner)'s description):
1. Sender chain is acting as the source zone. The coins are transferred
to an escrow address (i.e locked) on the sender chain and then transferred
to the receiving chain through IBC TAO logic. It is expected that the
receiving chain will mint vouchers to the receiving address.
2. Sender chain is acting as the sink zone. The coins (vouchers) are burned
on the sender chain and then transferred to the receiving chain though IBC
TAO logic. It is expected that the receiving chain, which had previously
sent the original denomination, will unescrow the fungible token and send
it to the receiving address.
Another way of thinking of source and sink zones is through the token's
timeline. Each send to any chain other than the one it was previously
received from is a movement forwards in the token's timeline. This causes
trace to be added to the token's history and the destination port and
destination channel to be prefixed to the denomination. In these instances
the sender chain is acting as the source zone. When the token is sent back
to the chain it previously received from, the prefix is removed. This is
a backwards movement in the token's timeline and the sender chain
is acting as the sink zone.
### Example
Assume the following channel connections exist and that all channels use the port ID `transfer`:
- chain `A` has channels with chain `B` and chain `C` with the IDs `channelToB` and `channelToC`, respectively
- chain `B` has channels with chain `A` and chain `C` with the IDs `channelToA` and `channelToC`, respectively
- chain `C` has channels with chain `A` and chain `B` with the IDs `channelToA` and `channelToB`, respectively
These steps of transfer between chains occur in the following order: `A -> B -> C -> A -> C`. In particular:
1. `A -> B`: sender chain is source zone. `A` sends packet with `denom` (escrowed on `A`), `B` receives `denom` and mints and sends voucher `transfer/channelToA/denom` to recipient.
2. `B -> C`: sender chain is source zone. `B` sends packet with `transfer/channelToA/denom` (escrowed on `B`), `C` receives `transfer/channelToA/denom` and mints and sends voucher `transfer/channelToB/transfer/channelToA/denom` to recipient.
3. `C -> A`: sender chain is source zone. `C` sends packet with `transfer/channelToB/transfer/channelToA/denom` (escrowed on `C`), `A` receives `transfer/channelToB/transfer/channelToA/denom` and mints and sends voucher `transfer/channelToC/transfer/channelToB/transfer/channelToA/denom` to recipient.
4. `A -> C`: sender chain is sink zone. `A` sends packet with `transfer/channelToC/transfer/channelToB/transfer/channelToA/denom` (burned on `A`), `C` receives `transfer/channelToC/transfer/channelToB/transfer/channelToA/denom`, and unescrows and sends `transfer/channelToB/transfer/channelToA/denom` to recipient.
The token has a final denomination on chain `C` of `transfer/channelToB/transfer/channelToA/denom`, where `transfer/channelToB/transfer/channelToA` is the trace information.
In this context, upon a receive of a cross-chain fungible token transfer, if the sender chain is the source of the token, the protocol prefixes the denomination with the port and channel identifiers in the following format:
```typescript
prefix + denom = {destPortN}/{destChannelN}/.../{destPort0}/{destChannel0}/denom
```
Example: transferring `100 uatom` from port `HubPort` and channel `HubChannel` on the Hub to
Ethermint's port `EthermintPort` and channel `EthermintChannel` results in `100
EthermintPort/EthermintChannel/uatom`, where `EthermintPort/EthermintChannel/uatom` is the new
denomination on the receiving chain.
In the case those tokens are transferred back to the Hub (i.e the **source** chain), the prefix is
trimmed and the token denomination updated to the original one.
### Problem
The problem of adding additional information to the coin denomination is twofold:
1. The ever increasing length if tokens are transferred to zones other than the source:
If a token is transferred `n` times via IBC to a sink chain, the token denom will contain `n` pairs
of prefixes, as shown on the format example above. This poses a problem because, while port and
channel identifiers have a maximum length of 64 each, the SDK `Coin` type only accepts denoms up to
64 characters. Thus, a single cross-chain token, which again, is composed by the port and channels
identifiers plus the base denomination, can exceed the length validation for the SDK `Coins`.
This can result in undesired behaviours such as tokens not being able to be transferred to multiple
sink chains if the denomination exceeds the length or unexpected `panics` due to denomination
validation failing on the receiving chain.
2. The existence of special characters and uppercase letters on the denomination:
In the SDK every time a `Coin` is initialized through the constructor function `NewCoin`, a validation
of a coin's denom is performed according to a
[Regex](https://github.com/cosmos/cosmos-sdk/blob/a940214a4923a3bf9a9161cd14bd3072299cd0c9/types/coin.go#L583),
where only lowercase alphanumeric characters are accepted. While this is desirable for native denominations
to keep a clean UX, it presents a challenge for IBC as ports and channels might be randomly
generated with special and uppercase characters as per the [ICS 024 - Host
Requirements](https://github.com/cosmos/ics/tree/master/spec/ics-024-host-requirements#paths-identifiers-separators)
specification.
## Decision
The issues outlined above, are applicable only to SDK-based chains, and thus the proposed solution
are do not require specification changes that would result in modification to other implementations
of the ICS20 spec.
Instead of adding the identifiers on the coin denomination directly, the proposed solution hashes
the denomination prefix in order to get a consistent length for all the cross-chain fungible tokens.
This will be used for internal storage only, and when transferred via IBC to a different chain, the
denomination specified on the packed data will be the full prefix path of the identifiers needed to
trace the token back to the originating chain, as specified on ICS20.
The new proposed format will be the following:
```golang
ibcDenom = "ibc/" + hash(trace + "/" + base denom)
```
The hash function will be a SHA256 hash of the fields of the `DenomTrace`:
```protobuf
// DenomTrace contains the base denomination for ICS20 fungible tokens and the source tracing
// information
message DenomTrace {
// chain of port/channel identifiers used for tracing the source of the fungible token
string trace = 1;
// base denomination of the relayed fungible token
string base_denom = 2;
}
```
The `IBCDenom` function constructs the `Coin` denomination used when creating the ICS20 fungible token packet data:
```golang
// Hash returns the hex bytes of the SHA256 hash of the DenomTrace fields.
func (dt DenomTrace) Hash() tmbytes.HexBytes {
return tmhash.Sum(dt.Trace + "/" + dt.BaseDenom)
}
// IBCDenom a coin denomination for an ICS20 fungible token in the format 'ibc/{hash(trace + baseDenom)}'. If the trace is empty, it will return the base denomination.
func (dt DenomTrace) IBCDenom() string {
if dt.Trace != "" {
return fmt.Sprintf("ibc/%s", dt.Hash())
}
return dt.BaseDenom
}
```
In order to trim the denomination trace prefix when sending/receiving fungible tokens, the `RemovePrefix` function is provided.
> NOTE: the prefix addition must be done on the client side.
```golang
// RemovePrefix trims the first portID/channelID pair from the trace info. If the trace is already empty it will perform a no-op. If the trace is incorrectly constructed or doesn't have separators it will return an error.
func (dt *DenomTrace) RemovePrefix() error {
if dt.Trace == "" {
return nil
}
traceSplit := strings.SplitN(dt.Trace, "/", 3)
var err error
switch {
// NOTE: other cases are checked during msg validation
case len(traceSplit) == 2:
dt.Trace = ""
case len(traceSplit) == 3:
dt.Trace = traceSplit[2]
}
if err != nil {
return err
}
return nil
}
```
### `x/ibc-transfer` Changes
In order to retrieve the trace information from an IBC denomination, a lookup table needs to be
added to the `ibc-transfer` module. These values need to also be persisted between upgrades, meaning
that a new `[]DenomTrace` `GenesisState` field state needs to be added to the module:
```golang
// GetDenomTrace retrieves the full identifiers trace and base denomination from the store.
func (k Keeper) GetDenomTrace(ctx Context, denomTraceHash []byte) (DenomTrace, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.KeyDenomTrace(traceHash))
if bz == nil {
return &DenomTrace, false
}
var denomTrace DenomTrace
k.cdc.MustUnmarshalBinaryBare(bz, &denomTrace)
return denomTrace, true
}
// HasDenomTrace checks if a the key with the given trace hash exists on the store.
func (k Keeper) HasDenomTrace(ctx Context, denomTraceHash []byte) bool {
store := ctx.KVStore(k.storeKey)
return store.Has(types.KeyTrace(denomTraceHash))
}
// SetDenomTrace sets a new {trace hash -> trace} pair to the store.
func (k Keeper) SetDenomTrace(ctx Context, denomTrace DenomTrace) {
store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshalBinaryBare(&denomTrace)
store.Set(types.KeyTrace(denomTrace.Hash()), bz)
}
```
The `MsgTransfer` will validate that the `Coin` denomination from the `Token` field contains a valid
hash, if the trace info is provided, or that the base denominations matches:
```golang
func (msg MsgTransfer) ValidateBasic() error {
// ...
if err := msg.Trace.Validate(); err != nil {
return err
}
if err := ValidateIBCDenom(msg.Token.Denom, msg.Trace); err != nil {
return err
}
// ...
}
```
```golang
// ValidateIBCDenom checks that the denomination for an IBC fungible token is valid. It returns error if the trace `hash` is invalid
func ValidateIBCDenom(denom string, trace DenomTrace) error {
// Validate that base denominations are equal if the trace info is not provided
if trace.Trace == "" {
if trace.BaseDenom != denom {
return Wrapf(
ErrInvalidDenomForTransfer,
"token denom must match the trace base denom (%s ≠ %s)",
denom, trace.BaseDenom,
)
}
return nil
}
denomSplit := strings.SplitN(denom, "/", 2)
switch {
case denomSplit[0] != "ibc":
return Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", denom)
case len(denomSplit) == 2:
return tmtypes.ValidateHash([]byte(denomSplit[1]))
}
denomTraceHash := denomSplit[1]
traceHash := trace.Hash()
if !bytes.Equal(traceHash.Bytes(), denomTraceHash.Bytes()) {
return Errorf("token denomination trace hash mismatch, expected %s got %s", traceHash, denomTraceHash)
}
return nil
}
```
The denomination trace info only needs to be updated when token is received:
- Receiver is **source** chain: The receiver created the token and must have the trace lookup already stored (if necessary _ie_ native token case wouldn't need a lookup).
- Receiver is **not source** chain: Store the received info. For example, during step 1, when chain `B` receives `transfer/channelToA/denom`.
```golang
// OnRecvPacket
// ...
// This is the prefix that would have been prefixed to the denomination
// on sender chain IF and only if the token originally came from the
// receiving chain.
//
// NOTE: We use SourcePort and SourceChannel here, because the counterparty
// chain would have prefixed with DestPort and DestChannel when originally
// receiving this coin as seen in the "sender chain is the source" condition.
voucherPrefix := GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
if ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) {
// sender chain is not the source, unescrow tokens
// remove prefix added by sender chain
if err := denomTrace.RemovePrefix(); err != nil {
return err
}
// NOTE: since the sender is a sink chain, we already know the unprefixed denomination trace info
token := sdk.NewCoin(denomTrace.IBCDenom(), sdk.NewIntFromUint64(data.Amount))
// unescrow tokens
escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
return k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(token))
}
// sender chain is the source, mint vouchers
// construct the denomination trace from the full raw denomination
denomTrace := NewDenomTraceFromRawDenom(data.Denom)
// since SendPacket did not prefix the denomination with the voucherPrefix, we must add it here
denomTrace.AddPrefix(packet.GetDestPort(), packet.GetDestChannel())
// set the value to the lookup table if not stored already
traceHash := denomTrace.Hash()
if !k.HasDenomTrace(ctx, traceHash) {
k.SetDenomTrace(ctx, traceHash, denomTrace)
}
voucher := sdk.NewCoin(denomTrace.IBCDenom(), sdk.NewIntFromUint64(data.Amount))
// mint new tokens if the source of the transfer is the same chain
if err := k.bankKeeper.MintCoins(
ctx, types.ModuleName, sdk.NewCoins(voucher),
); err != nil {
return err
}
// send to receiver
return k.bankKeeper.SendCoinsFromModuleToAccount(
ctx, types.ModuleName, receiver, sdk.NewCoins(voucher),
)
```
```golang
func NewDenomTraceFromRawDenom(denom string) DenomTrace{
denomSplit := strings.Split(denom, "/")
trace := ""
if len(denomSplit) > 1 {
trace = strings.Join(denomSplit[:len(denomSplit)-1], "/")
}
return DenomTrace{
BaseDenom: denomSplit[len(denomSplit)-1],
Trace: trace,
}
}
```
One final remark is that the `FungibleTokenPacketData` will remain the same, i.e with the prefixed full denomination, since the receiving chain may not be an SDK-based chain.
### Coin Changes
The coin denomination validation will need to be updated to reflect these changes. In particular, the denomination validation
function will now:
- Accept slash separators (`"/"`) and uppercase characters (due to the `HexBytes` format)
- Bump the maximum character length to 64
Additional validation logic, such as verifying the length of the hash, the may be added to the bank module in the future if the [custom base denomination validation](https://github.com/cosmos/cosmos-sdk/pull/6755) is integrated into the SDK.
### Positive
- Clearer separation of the source tracing behaviour of the token (transfer prefix) from the original
`Coin` denomination
- Consistent validation of `Coin` fields (i.e no special characters, fixed max length)
- Cleaner `Coin` and standard denominations for IBC
- No additional fields to SDK `Coin`
### Negative
- Store each set of tracing denomination identifiers on the `ibc-transfer` module store
- Clients will have to fetch the base denomination every time they receive a new relayed fungible token over IBC. This can be mitigated using a map/cache for already seen hashes on the client side. Other forms of mitigation, would be opening a websocket connection subscribe to incoming events.
### Neutral
- Slight difference with the ICS20 spec
- Additional validation logic for IBC coins on the `ibc-transfer` module
- Additional genesis fields
- Slightly increases the gas usage on cross-chain transfers due to access to the store. This should
be inter-block cached if transfers are frequent.
## References
- [ICS 20 - Fungible token transfer](https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer)
- [Custom Coin Denomination validation](https://github.com/cosmos/cosmos-sdk/pull/6755)