x/ibc-transfer: ADR001 source tracing implementation (#6871)

* x/ibc-transfer: ADR001 source tracing implementation

* gRPC proto file

* validation

* fix validation

* import export genesis

* relay.go updates

* gRPC service methods

* client CLI

* update implementation

* build

* trace test

* fix CLI tx args

* genesis import/export tests

* update comments

* update proto files

* GRPC tests

* remove field from packet

* fix coin validation bug

* more validations

* update comments

* minor refactor

* update relay.go

* try fix test

* minor updates

* fix tests

* fix test

* ADR updates and comments

* build

* Apply suggestions from code review

Co-authored-by: Aditya <adityasripal@gmail.com>
Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com>

* address a few comments from review

* gRPC annotations

* update proto files

* Apply suggestions from code review

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

* address comments

* docs and changelog

* sort traces

* final changes to ADR

* client support for full path denom prefixes

* address @AdityaSripal comments

* address TODO

* increase test timeouts

Co-authored-by: Aditya <adityasripal@gmail.com>
Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com>
This commit is contained in:
Federico Kunze
2020-08-14 17:46:26 -04:00
committed by GitHub
co-authored by Aditya colin axnér
parent 8de96d16f9
commit 3022fe9044
43 changed files with 2788 additions and 235 deletions
@@ -3,10 +3,11 @@
## Changelog
- 2020-07-09: Initial Draft
- 2020-08-11: Implementation changes
## Status
Proposed
Accepted, Implemented
## Context
@@ -114,7 +115,7 @@ 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)
ibcDenom = "ibc/" + hash(trace path + "/" + base denom)
```
The hash function will be a SHA256 hash of the fields of the `DenomTrace`:
@@ -124,7 +125,7 @@ The hash function will be a SHA256 hash of the fields of the `DenomTrace`:
// information
message DenomTrace {
// chain of port/channel identifiers used for tracing the source of the fungible token
string trace = 1;
string path = 1;
// base denomination of the relayed fungible token
string base_denom = 2;
}
@@ -133,50 +134,23 @@ message DenomTrace {
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.
// Hash returns the hex bytes of the SHA256 hash of the DenomTrace fields using the following formula:
//
// hash = sha256(tracePath + "/" + baseDenom)
func (dt DenomTrace) Hash() tmbytes.HexBytes {
return tmhash.Sum(dt.Trace + "/" + dt.BaseDenom)
return tmhash.Sum(dt.Path + "/" + 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.
// IBCDenom a coin denomination for an ICS20 fungible token in the format 'ibc/{hash(tracePath + baseDenom)}'.
// If the trace is empty, it will return the base denomination.
func (dt DenomTrace) IBCDenom() string {
if dt.Trace != "" {
if dt.Path != "" {
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
@@ -217,44 +191,30 @@ 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
}
// ...
return ValidateIBCDenom(msg.Token.Denom)
}
```
```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
}
// ValidateIBCDenom validates that the given denomination is either:
//
// - A valid base denomination (eg: 'uatom')
// - A valid fungible token representation (i.e 'ibc/{hash}') per ADR 001 https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-001-coin-source-tracing.md
func ValidateIBCDenom(denom string) error {
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]))
case strings.TrimSpace(denom) == "",
len(denomSplit) == 1 && denomSplit[0] == "ibc",
len(denomSplit) == 2 && (denomSplit[0] != "ibc" || strings.TrimSpace(denomSplit[1]) == ""):
return sdkerrors.Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", denom)
case denomSplit[0] == denom && strings.TrimSpace(denom) != "":
return sdk.ValidateDenom(denom)
}
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)
if _, err := ParseHexHash(denomSplit[1]); err != nil {
return Wrapf(err, "invalid denom trace hash %s", denomSplit[1])
}
return nil
@@ -266,6 +226,46 @@ 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
// SendTransfer
// ...
fullDenomPath := token.Denom
// deconstruct the token denomination into the denomination trace info
// to determine if the sender is the source chain
if strings.HasPrefix(token.Denom, "ibc/") {
fullDenomPath, err = k.DenomPathFromHash(ctx, token.Denom)
if err != nil {
return err
}
}
if types.SenderChainIsSource(sourcePort, sourceChannel, fullDenomPath) {
//...
```
```golang
// DenomPathFromHash returns the full denomination path prefix from an ibc denom with a hash
// component.
func (k Keeper) DenomPathFromHash(ctx sdk.Context, denom string) (string, error) {
hexHash := denom[4:]
hash, err := ParseHexHash(hexHash)
if err != nil {
return "", Wrap(ErrInvalidDenomForTransfer, err.Error())
}
denomTrace, found := k.GetDenomTrace(ctx, hash)
if !found {
return "", Wrap(ErrTraceNotFound, hexHash)
}
fullDenomPath := denomTrace.GetFullDenomPath()
return fullDenomPath, nil
}
```
```golang
// OnRecvPacket
// ...
@@ -277,19 +277,13 @@ The denomination trace info only needs to be updated when token is received:
// 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))
voucherPrefix := types.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
unprefixedDenom := data.Denom[len(voucherPrefix):]
token := sdk.NewCoin(unprefixedDenom, sdk.NewIntFromUint64(data.Amount))
// unescrow tokens
escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
@@ -298,11 +292,13 @@ if ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data
// 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, we must prefix denomination here
sourcePrefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
// NOTE: sourcePrefix contains the trailing "/"
prefixedDenom := sourcePrefix + data.Denom
// since SendPacket did not prefix the denomination with the voucherPrefix, we must add it here
denomTrace.AddPrefix(packet.GetDestPort(), packet.GetDestChannel())
// construct the denomination trace from the full raw denomination
denomTrace := types.ParseDenomTrace(prefixedDenom)
// set the value to the lookup table if not stored already
traceHash := denomTrace.Hash()
@@ -310,7 +306,8 @@ if !k.HasDenomTrace(ctx, traceHash) {
k.SetDenomTrace(ctx, traceHash, denomTrace)
}
voucher := sdk.NewCoin(denomTrace.IBCDenom(), sdk.NewIntFromUint64(data.Amount))
voucherDenom := denomTrace.IBCDenom()
voucher := sdk.NewCoin(voucherDenom, sdk.NewIntFromUint64(data.Amount))
// mint new tokens if the source of the transfer is the same chain
if err := k.bankKeeper.MintCoins(
@@ -347,7 +344,8 @@ The coin denomination validation will need to be updated to reflect these change
function will now:
- Accept slash separators (`"/"`) and uppercase characters (due to the `HexBytes` format)
- Bump the maximum character length to 64
- Bump the maximum character length to 128, as the hex representation used by Tendermint's
`HexBytes` type contains 64 characters.
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.
+1 -1
View File
@@ -7,7 +7,7 @@
## Status
> A decision may be "proposed" if the project stakeholders haven't agreed with it yet, or "accepted" once it is agreed. If a later ADR changes or reverses a decision, it may be marked as "deprecated" or "superseded" with a reference to its replacement.
> {Deprecated|Proposed|Accepted}
> {Deprecated|Proposed|Accepted} {Implemented|Not Implemented}
## Context