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 23:46:26 +02:00 committed by GitHub
parent 8de96d16f9
commit 3022fe9044
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2788 additions and 235 deletions

View File

@ -44,7 +44,7 @@ jobs:
test-coverage-run-1:
runs-on: ubuntu-latest
needs: split-test-files
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
- uses: technote-space/get-diff-action@v3
@ -60,7 +60,7 @@ jobs:
if: "env.GIT_DIFF != ''"
- name: test & coverage report creation
run: |
cat xaa.txt | xargs go test -mod=readonly -timeout 8m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
cat xaa.txt | xargs go test -mod=readonly -timeout 15m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
if: "env.GIT_DIFF != ''"
- name: filter out DONTCOVER
run: |
@ -81,7 +81,7 @@ jobs:
test-coverage-run-2:
runs-on: ubuntu-latest
needs: split-test-files
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
- uses: technote-space/get-diff-action@v3
@ -97,7 +97,7 @@ jobs:
if: "env.GIT_DIFF != ''"
- name: test & coverage report creation
run: |
cat xab.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
cat xab.txt | xargs go test -mod=readonly -timeout 15m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
if: "env.GIT_DIFF != ''"
- name: filter out DONTCOVER
run: |
@ -118,7 +118,7 @@ jobs:
test-coverage-run-3:
runs-on: ubuntu-latest
needs: split-test-files
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
- uses: technote-space/get-diff-action@v3
@ -134,7 +134,7 @@ jobs:
if: "env.GIT_DIFF != ''"
- name: test & coverage report creation
run: |
cat xac.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
cat xac.txt | xargs go test -mod=readonly -timeout 15m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
if: "env.GIT_DIFF != ''"
- name: filter out DONTCOVER
run: |
@ -155,7 +155,7 @@ jobs:
test-coverage-run-4:
runs-on: ubuntu-latest
needs: split-test-files
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
- uses: technote-space/get-diff-action@v3
@ -171,7 +171,7 @@ jobs:
if: "env.GIT_DIFF != ''"
- name: test & coverage report creation
run: |
cat xad.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
cat xad.txt | xargs go test -mod=readonly -timeout 15m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
if: "env.GIT_DIFF != ''"
- name: filter out DONTCOVER
run: |

View File

@ -283,9 +283,10 @@ Buffers for state serialization instead of Amino.
### Improvements
* (x/ibc-transfer) [\#6871](https://github.com/cosmos/cosmos-sdk/pull/6871) Implement [ADR 001 - Coin Source Tracing](./docs/architecture/adr-001-coin-source-tracing.md).
* (types) [\#7027](https://github.com/cosmos/cosmos-sdk/pull/7027) `Coin(s)` and `DecCoin(s)` updates:
* Bump denomination max length to 128
* Allow unicode letters and numbers in denominations
* Allow uppercase letters and numbers in denominations to support [ADR 001](./docs/architecture/adr-001-coin-source-tracing.md)
* Added `Validate` function that returns a descriptive error
* (baseapp) [\#6186](https://github.com/cosmos/cosmos-sdk/issues/6186) Support emitting events during `AnteHandler` execution.
* (x/auth) [\#5702](https://github.com/cosmos/cosmos-sdk/pull/5702) Add parameter querying support for `x/auth`.

View File

@ -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.

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

View File

@ -4,11 +4,16 @@ package ibc.transfer;
option go_package = "github.com/cosmos/cosmos-sdk/x/ibc-transfer/types";
import "gogoproto/gogo.proto";
import "ibc/transfer/transfer.proto";
// GenesisState is currently only used to ensure that the InitGenesis gets run
// by the module manager
message GenesisState {
string port_id = 1 [
(gogoproto.moretags) = "yaml:\"port_id\""
];
// GenesisState defines the ibc-transfer genesis state
message GenesisState{
string port_id = 1 [
(gogoproto.moretags) = "yaml:\"port_id\""
];
repeated DenomTrace denom_traces = 2 [
(gogoproto.castrepeated) = "Traces",
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"denom_traces\""
];
}

View File

@ -0,0 +1,51 @@
syntax = "proto3";
package ibc.transfer;
import "gogoproto/gogo.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "ibc/transfer/transfer.proto";
import "google/api/annotations.proto";
option go_package = "github.com/cosmos/cosmos-sdk/x/ibc-transfer/types";
// Query provides defines the gRPC querier service.
service Query {
// DenomTrace queries a denomination trace information.
rpc DenomTrace(QueryDenomTraceRequest) returns (QueryDenomTraceResponse) {
option (google.api.http).get = "/ibc_transfer/v1beta1/denom_traces/{hash}";
}
// DenomTraces queries all denomination traces.
rpc DenomTraces(QueryDenomTracesRequest) returns (QueryDenomTracesResponse) {
option (google.api.http).get = "/ibc_transfer/v1beta1/denom_traces";
}
}
// QueryDenomTraceRequest is the request type for the Query/DenomTrace RPC method
message QueryDenomTraceRequest {
// hash (in hex format) of the denomination trace information.
string hash = 1;
}
// QueryDenomTraceResponse is the response type for the Query/DenomTrace RPC method.
message QueryDenomTraceResponse {
// denom_trace returns the requested denomination trace information.
DenomTrace denom_trace = 1;
}
// QueryConnectionsRequest is the request type for the Query/DenomTraces RPC method
message QueryDenomTracesRequest {
// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}
// QueryConnectionsResponse is the response type for the Query/DenomTraces RPC method.
message QueryDenomTracesResponse {
// denom_traces returns all denominations trace information.
repeated DenomTrace denom_traces = 1 [
(gogoproto.castrepeated) = "Traces",
(gogoproto.nullable) = false
];
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

View File

@ -17,8 +17,7 @@ message MsgTransfer {
// the tokens to be transferred
cosmos.base.v1beta1.Coin token = 3 [(gogoproto.nullable) = false];
// the sender address
bytes sender = 4
[(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"];
bytes sender = 4 [(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"];
// the recipient address on the destination chain
string receiver = 5;
// Timeout height relative to the current block height.
@ -26,8 +25,7 @@ message MsgTransfer {
uint64 timeout_height = 6 [(gogoproto.moretags) = "yaml:\"timeout_height\""];
// Timeout timestamp (in nanoseconds) relative to the current block timestamp.
// The timeout is disabled when set to 0.
uint64 timeout_timestamp = 7
[(gogoproto.moretags) = "yaml:\"timeout_timestamp\""];
uint64 timeout_timestamp = 7 [(gogoproto.moretags) = "yaml:\"timeout_timestamp\""];
}
// FungibleTokenPacketData defines a struct for the packet payload
@ -52,3 +50,13 @@ message FungibleTokenPacketAcknowledgement {
bool success = 1;
string error = 2;
}
// DenomTrace contains the base denomination for ICS20 fungible tokens and the source tracing
// information path.
message DenomTrace {
// path defines the chain of port/channel identifiers used for tracing the source of the fungible
// token.
string path = 1;
// base denomination of the relayed fungible token.
string base_denom = 2;
}

View File

@ -76,6 +76,7 @@ func TestCoinIsValid(t *testing.T) {
{Coin{"ATOM", OneInt()}, true},
{Coin{"a", OneInt()}, false},
{Coin{loremIpsum, OneInt()}, false},
{Coin{"ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", OneInt()}, true},
{Coin{"atOm", OneInt()}, true},
{Coin{" ", OneInt()}, false},
}
@ -632,6 +633,8 @@ func TestParse(t *testing.T) {
{"1.2btc", false, nil}, // amount must be integer
{"5foo:bar", false, nil}, // invalid separator
{"10atom10", true, Coins{{"atom10", NewInt(10)}}},
{"200transfer/channelToA/uatom", true, Coins{{"transfer/channelToA/uatom", NewInt(200)}}},
{"50ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", true, Coins{{"ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", NewInt(50)}}},
}
for tcIndex, tc := range cases {

View File

@ -6,6 +6,23 @@ import (
"github.com/cosmos/cosmos-sdk/client"
)
// GetQueryCmd returns the query commands for IBC connections
func GetQueryCmd() *cobra.Command {
queryCmd := &cobra.Command{
Use: "ibc-transfer",
Short: "IBC fungible token transfer query subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
}
queryCmd.AddCommand(
GetCmdQueryDenomTrace(),
GetCmdQueryDenomTraces(),
)
return queryCmd
}
// NewTxCmd returns the transaction commands for IBC fungible token transfer
func NewTxCmd() *cobra.Command {
txCmd := &cobra.Command{

View File

@ -0,0 +1,87 @@
package cli
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
)
// GetCmdQueryDenomTrace defines the command to query a a denomination trace from a given hash.
func GetCmdQueryDenomTrace() *cobra.Command {
cmd := &cobra.Command{
Use: "denom-trace [hash]",
Short: "Query the denom trace info from a given trace hash",
Long: "Query the denom trace info from a given trace hash",
Example: fmt.Sprintf("%s query ibc-transfer denom-trace [hash]", version.AppName),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)
clientCtx, err := client.ReadQueryCommandFlags(clientCtx, cmd.Flags())
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
req := &types.QueryDenomTraceRequest{
Hash: args[0],
}
res, err := queryClient.DenomTrace(context.Background(), req)
if err != nil {
return err
}
return clientCtx.PrintOutput(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
return cmd
}
// GetCmdQueryDenomTraces defines the command to query all the denomination trace infos
// that this chain mantains.
func GetCmdQueryDenomTraces() *cobra.Command {
cmd := &cobra.Command{
Use: "denom-traces",
Short: "Query the trace info for all token denominations",
Long: "Query the trace info for all token denominations",
Example: fmt.Sprintf("%s query ibc-transfer denom-traces", version.AppName),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)
clientCtx, err := client.ReadQueryCommandFlags(clientCtx, cmd.Flags())
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
pageReq, err := client.ReadPageRequest(cmd.Flags())
if err != nil {
return err
}
req := &types.QueryDenomTracesRequest{
Pagination: pageReq,
}
res, err := queryClient.DenomTraces(context.Background(), req)
if err != nil {
return err
}
return clientCtx.PrintOutput(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
flags.AddPaginationFlagsToCmd(cmd, "denominations trace")
return cmd
}

View File

@ -49,6 +49,11 @@ to the counterparty channel. Any timeout set to 0 is disabled.`),
return err
}
if !strings.HasPrefix(coin.Denom, "ibc/") {
denomTrace := types.ParseDenomTrace(coin.Denom)
coin.Denom = denomTrace.IBCDenom()
}
timeoutHeight, err := cmd.Flags().GetUint64(flagPacketTimeoutHeight)
if err != nil {
return err

View File

@ -21,7 +21,7 @@ func RegisterRoutes(clientCtx client.Context, r *mux.Router) {
// TransferTxReq defines the properties of a transfer tx request's body.
type TransferTxReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
Token sdk.Coin `json:"token" yaml:"token"`
Receiver string `json:"receiver" yaml:"receiver"`
TimeoutHeight uint64 `json:"timeout_height" yaml:"timeout_height"`
TimeoutTimestamp uint64 `json:"timeout_timestamp" yaml:"timeout_timestamp"`

View File

@ -56,7 +56,7 @@ func transferHandlerFn(clientCtx client.Context) http.HandlerFunc {
msg := types.NewMsgTransfer(
portID,
channelID,
req.Amount,
req.Token,
fromAddr,
req.Receiver,
req.TimeoutHeight,

View File

@ -1,38 +0,0 @@
package transfer
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/keeper"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
)
// InitGenesis binds to portid from genesis state
func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, state types.GenesisState) {
keeper.SetPort(ctx, state.PortId)
// Only try to bind to port if it is not already bound, since we may already own
// port capability from capability InitGenesis
if !keeper.IsBound(ctx, state.PortId) {
// transfer module binds to the transfer port on InitChain
// and claims the returned capability
err := keeper.BindPort(ctx, state.PortId)
if err != nil {
panic(fmt.Sprintf("could not claim port capability: %v", err))
}
}
// check if the module account exists
moduleAcc := keeper.GetTransferAccount(ctx)
if moduleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
}
}
// ExportGenesis exports transfer module's portID into its geneis state
func ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState {
return &types.GenesisState{
PortId: keeper.GetPort(ctx),
}
}

View File

@ -29,7 +29,7 @@ func handleMsgTransfer(ctx sdk.Context, k keeper.Keeper, msg *types.MsgTransfer)
return nil, err
}
k.Logger(ctx).Info("IBC transfer: %s from %s to %s", msg.Token, msg.Sender, msg.Receiver)
k.Logger(ctx).Info("IBC fungible token transfer", "token", msg.Token, "sender", msg.Sender, "receiver", msg.Receiver)
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(

View File

@ -48,9 +48,9 @@ func (suite *HandlerTestSuite) TestHandleMsgTransfer() {
err = suite.coordinator.RelayPacket(suite.chainA, suite.chainB, clientA, clientB, packet, ack.GetBytes())
suite.Require().NoError(err) // relay committed
// check that voucher exists on chain
voucherDenom := types.GetPrefixedDenom(packet.GetDestPort(), packet.GetDestChannel(), sdk.DefaultBondDenom)
balance := suite.chainB.App.BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), voucherDenom)
// check that voucher exists on chain B
voucherDenomTrace := types.ParseDenomTrace(types.GetPrefixedDenom(packet.GetDestPort(), packet.GetDestChannel(), sdk.DefaultBondDenom))
balance := suite.chainB.App.BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), voucherDenomTrace.IBCDenom())
coinToSendBackToA := types.GetTransferCoin(channelB.PortID, channelB.ID, sdk.DefaultBondDenom, 100)
suite.Require().Equal(coinToSendBackToA, balance)
@ -62,7 +62,9 @@ func (suite *HandlerTestSuite) TestHandleMsgTransfer() {
suite.Require().NoError(err) // message committed
// relay send
fungibleTokenPacket = types.NewFungibleTokenPacketData(coinToSendBackToA.Denom, coinToSendBackToA.Amount.Uint64(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String())
// NOTE: fungible token is prefixed with the full trace in order to verify the packet commitment
voucherDenom := voucherDenomTrace.GetPrefix() + voucherDenomTrace.BaseDenom
fungibleTokenPacket = types.NewFungibleTokenPacketData(voucherDenom, coinToSendBackToA.Amount.Uint64(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String())
packet = channeltypes.NewPacket(fungibleTokenPacket.GetBytes(), 1, channelB.PortID, channelB.ID, channelA.PortID, channelA.ID, 110, 0)
err = suite.coordinator.RelayPacket(suite.chainB, suite.chainA, clientB, clientA, packet, ack.GetBytes())
suite.Require().NoError(err) // relay committed
@ -76,6 +78,10 @@ func (suite *HandlerTestSuite) TestHandleMsgTransfer() {
escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
balance = suite.chainA.App.BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, sdk.DefaultBondDenom)
suite.Require().Equal(sdk.NewCoin(sdk.DefaultBondDenom, sdk.ZeroInt()), balance)
// check that balance on chain B is empty
balance = suite.chainB.App.BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), voucherDenomTrace.IBCDenom())
suite.Require().Zero(balance.Amount.Int64())
}
func TestHandlerTestSuite(t *testing.T) {

View File

@ -0,0 +1,42 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
)
// InitGenesis initializes the ibc-transfer state and binds to PortID.
func (k Keeper) InitGenesis(ctx sdk.Context, state types.GenesisState) {
k.SetPort(ctx, state.PortId)
for _, trace := range state.DenomTraces {
k.SetDenomTrace(ctx, trace)
}
// Only try to bind to port if it is not already bound, since we may already own
// port capability from capability InitGenesis
if !k.IsBound(ctx, state.PortId) {
// transfer module binds to the transfer port on InitChain
// and claims the returned capability
err := k.BindPort(ctx, state.PortId)
if err != nil {
panic(fmt.Sprintf("could not claim port capability: %v", err))
}
}
// check if the module account exists
moduleAcc := k.GetTransferAccount(ctx)
if moduleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
}
}
// ExportGenesis exports ibc-transfer module's portID and denom trace info into its genesis state.
func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState {
return &types.GenesisState{
PortId: k.GetPort(ctx),
DenomTraces: k.GetAllDenomTraces(ctx),
}
}

View File

@ -0,0 +1,39 @@
package keeper_test
import (
"fmt"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
)
func (suite *KeeperTestSuite) TestGenesis() {
var (
path string
traces types.Traces
)
for i := 0; i < 5; i++ {
prefix := fmt.Sprintf("transfer/channelToChain%d", i)
if i == 0 {
path = prefix
} else {
path = prefix + "/" + path
}
denomTrace := types.DenomTrace{
BaseDenom: "uatom",
Path: path,
}
traces = append(types.Traces{denomTrace}, traces...)
suite.chainA.App.TransferKeeper.SetDenomTrace(suite.chainA.GetContext(), denomTrace)
}
genesis := suite.chainA.App.TransferKeeper.ExportGenesis(suite.chainA.GetContext())
suite.Require().Equal(types.PortID, genesis.PortId)
suite.Require().Equal(traces.Sort(), genesis.DenomTraces)
suite.Require().NotPanics(func() {
suite.chainA.App.TransferKeeper.InitGenesis(suite.chainA.GetContext(), *genesis)
})
}

View File

@ -0,0 +1,73 @@
package keeper
import (
"context"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
)
var _ types.QueryServer = Keeper{}
// DenomTrace implements the Query/DenomTrace gRPC method
func (q Keeper) DenomTrace(c context.Context, req *types.QueryDenomTraceRequest) (*types.QueryDenomTraceResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
hash, err := types.ParseHexHash(req.Hash)
if err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid denom trace hash %s, %s", req.Hash, err))
}
ctx := sdk.UnwrapSDKContext(c)
denomTrace, found := q.GetDenomTrace(ctx, hash)
if !found {
return nil, status.Error(
codes.NotFound,
sdkerrors.Wrap(types.ErrTraceNotFound, req.Hash).Error(),
)
}
return &types.QueryDenomTraceResponse{
DenomTrace: &denomTrace,
}, nil
}
// DenomTraces implements the Query/DenomTraces gRPC method
func (q Keeper) DenomTraces(c context.Context, req *types.QueryDenomTracesRequest) (*types.QueryDenomTracesResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
traces := types.Traces{}
store := prefix.NewStore(ctx.KVStore(q.storeKey), types.DenomTraceKey)
pageRes, err := query.Paginate(store, req.Pagination, func(_, value []byte) error {
var result types.DenomTrace
if err := q.cdc.UnmarshalBinaryBare(value, &result); err != nil {
return err
}
traces = append(traces, result)
return nil
})
if err != nil {
return nil, err
}
return &types.QueryDenomTracesResponse{
DenomTraces: traces,
Pagination: pageRes,
}, nil
}

View File

@ -0,0 +1,135 @@
package keeper_test
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
)
func (suite *KeeperTestSuite) TestQueryConnection() {
var (
req *types.QueryDenomTraceRequest
expTrace types.DenomTrace
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{
"invalid hex hash",
func() {
req = &types.QueryDenomTraceRequest{
Hash: "!@#!@#!",
}
},
false,
},
{
"not found denom trace",
func() {
expTrace.Path = "transfer/channelToA/transfer/channelToB"
expTrace.BaseDenom = "uatom"
req = &types.QueryDenomTraceRequest{
Hash: expTrace.Hash().String(),
}
},
false,
},
{
"success",
func() {
expTrace.Path = "transfer/channelToA/transfer/channelToB"
expTrace.BaseDenom = "uatom"
suite.chainA.App.TransferKeeper.SetDenomTrace(suite.chainA.GetContext(), expTrace)
req = &types.QueryDenomTraceRequest{
Hash: expTrace.Hash().String(),
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.chainA.GetContext())
res, err := suite.queryClient.DenomTrace(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(&expTrace, res.DenomTrace)
} else {
suite.Require().Error(err)
}
})
}
}
func (suite *KeeperTestSuite) TestQueryConnections() {
var (
req *types.QueryDenomTracesRequest
expTraces = types.Traces(nil)
)
testCases := []struct {
msg string
malleate func()
expPass bool
}{
{
"empty pagination",
func() {
req = &types.QueryDenomTracesRequest{}
},
true,
},
{
"success",
func() {
expTraces = append(expTraces, types.DenomTrace{Path: "", BaseDenom: "uatom"})
expTraces = append(expTraces, types.DenomTrace{Path: "transfer/channelToB", BaseDenom: "uatom"})
expTraces = append(expTraces, types.DenomTrace{Path: "transfer/channelToA/transfer/channelToB", BaseDenom: "uatom"})
for _, trace := range expTraces {
suite.chainA.App.TransferKeeper.SetDenomTrace(suite.chainA.GetContext(), trace)
}
req = &types.QueryDenomTracesRequest{
Pagination: &query.PageRequest{
Limit: 5,
CountTotal: false,
},
}
},
true,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
tc.malleate()
ctx := sdk.WrapSDKContext(suite.chainA.GetContext())
res, err := suite.queryClient.DenomTraces(ctx, req)
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expTraces.Sort(), res.DenomTraces)
} else {
suite.Require().Error(err)
}
})
}
}

View File

@ -3,9 +3,11 @@ package keeper
import (
"fmt"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
"github.com/tendermint/tendermint/libs/log"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
@ -109,6 +111,61 @@ func (k Keeper) SetPort(ctx sdk.Context, portID string) {
store.Set(types.PortKey, []byte(portID))
}
// GetDenomTrace retreives the full identifiers trace and base denomination from the store.
func (k Keeper) GetDenomTrace(ctx sdk.Context, denomTraceHash tmbytes.HexBytes) (types.DenomTrace, bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DenomTraceKey)
bz := store.Get(denomTraceHash)
if bz == nil {
return types.DenomTrace{}, false
}
var denomTrace types.DenomTrace
k.cdc.MustUnmarshalBinaryBare(bz, &denomTrace)
return denomTrace, true
}
// HasDenomTrace checks if a the key with the given denomination trace hash exists on the store.
func (k Keeper) HasDenomTrace(ctx sdk.Context, denomTraceHash tmbytes.HexBytes) bool {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DenomTraceKey)
return store.Has(denomTraceHash)
}
// SetDenomTrace sets a new {trace hash -> denom trace} pair to the store.
func (k Keeper) SetDenomTrace(ctx sdk.Context, denomTrace types.DenomTrace) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DenomTraceKey)
bz := k.cdc.MustMarshalBinaryBare(&denomTrace)
store.Set(denomTrace.Hash(), bz)
}
// GetAllDenomTraces returns the trace information for all the denominations.
func (k Keeper) GetAllDenomTraces(ctx sdk.Context) types.Traces {
traces := types.Traces{}
k.IterateDenomTraces(ctx, func(denomTrace types.DenomTrace) bool {
traces = append(traces, denomTrace)
return false
})
return traces.Sort()
}
// IterateDenomTraces iterates over the denomination traces in the store
// and performs a callback function.
func (k Keeper) IterateDenomTraces(ctx sdk.Context, cb func(denomTrace types.DenomTrace) bool) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.DenomTraceKey)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var denomTrace types.DenomTrace
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &denomTrace)
if cb(denomTrace) {
break
}
}
}
// ClaimCapability allows the transfer module that can claim a capability that IBC module
// passes to it
func (k Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error {

View File

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/tendermint/tendermint/crypto"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
ibctesting "github.com/cosmos/cosmos-sdk/x/ibc/testing"
@ -19,12 +20,18 @@ type KeeperTestSuite struct {
// testing chains used for convenience and readability
chainA *ibctesting.TestChain
chainB *ibctesting.TestChain
queryClient types.QueryClient
}
func (suite *KeeperTestSuite) SetupTest() {
suite.coordinator = ibctesting.NewCoordinator(suite.T(), 2)
suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(0))
suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(1))
queryHelper := baseapp.NewQueryServerTestHelper(suite.chainA.GetContext(), suite.chainA.App.InterfaceRegistry())
types.RegisterQueryServer(queryHelper, suite.chainA.App.TransferKeeper)
suite.queryClient = types.NewQueryClient(queryHelper)
}
func (suite *KeeperTestSuite) TestGetTransferAccount() {

View File

@ -1,6 +1,8 @@
package keeper
import (
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
@ -40,7 +42,6 @@ import (
// 4. A -> C : sender chain is sink zone. Denom upon receiving: 'C/B/denom'
// 5. C -> B : sender chain is sink zone. Denom upon receiving: 'B/denom'
// 6. B -> A : sender chain is sink zone. Denom upon receiving: 'denom'
func (k Keeper) SendTransfer(
ctx sdk.Context,
sourcePort,
@ -70,17 +71,30 @@ func (k Keeper) SendTransfer(
// begin createOutgoingPacket logic
// See spec for this logic: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay
channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel))
if !ok {
return sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
}
// NOTE: denomination and hex hash correctness checked during msg.ValidateBasic
fullDenomPath := token.Denom
var err error
// 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
}
}
// NOTE: SendTransfer simply sends the denomination as it exists on its own
// chain inside the packet data. The receiving chain will perform denom
// prefixing as necessary.
if types.SenderChainIsSource(sourcePort, sourceChannel, token.Denom) {
if types.SenderChainIsSource(sourcePort, sourceChannel, fullDenomPath) {
// create the escrow address for the tokens
escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel)
@ -110,7 +124,7 @@ func (k Keeper) SendTransfer(
}
packetData := types.NewFungibleTokenPacketData(
token.Denom, token.Amount.Uint64(), sender.String(), receiver,
fullDenomPath, token.Amount.Uint64(), sender.String(), receiver,
)
packet := channeltypes.NewPacket(
@ -133,7 +147,10 @@ func (k Keeper) SendTransfer(
// back tokens this chain originally transferred to it, the tokens are
// unescrowed and sent to the receiving address.
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
// NOTE: packet data type already checked in handler.go
// validate packet data upon receiving
if err := data.ValidateBasic(); err != nil {
return err
}
// decode the receiver address
receiver, err := sdk.AccAddressFromBech32(data.Receiver)
@ -168,7 +185,25 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data t
sourcePrefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
// NOTE: sourcePrefix contains the trailing "/"
prefixedDenom := sourcePrefix + data.Denom
voucher := sdk.NewCoin(prefixedDenom, sdk.NewIntFromUint64(data.Amount))
// construct the denomination trace from the full raw denomination
denomTrace := types.ParseDenomTrace(prefixedDenom)
traceHash := denomTrace.Hash()
if !k.HasDenomTrace(ctx, traceHash) {
k.SetDenomTrace(ctx, denomTrace)
}
voucherDenom := denomTrace.IBCDenom()
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeDenomTrace,
sdk.NewAttribute(types.AttributeKeyTraceHash, traceHash.String()),
sdk.NewAttribute(types.AttributeKeyDenom, voucherDenom),
),
)
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(
@ -230,3 +265,21 @@ func (k Keeper) refundPacketToken(ctx sdk.Context, packet channeltypes.Packet, d
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(token))
}
// 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 := types.ParseHexHash(hexHash)
if err != nil {
return "", sdkerrors.Wrap(types.ErrInvalidDenomForTransfer, err.Error())
}
denomTrace, found := k.GetDenomTrace(ctx, hash)
if !found {
return "", sdkerrors.Wrap(types.ErrTraceNotFound, hexHash)
}
fullDenomPath := denomTrace.GetFullDenomPath()
return fullDenomPath, nil
}

View File

@ -75,7 +75,7 @@ func (AppModuleBasic) GetTxCmd() *cobra.Command {
// GetQueryCmd implements AppModuleBasic interface
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
return nil
return cli.GetQueryCmd()
}
// RegisterInterfaces registers module concrete types into protobuf Any.
@ -118,21 +118,23 @@ func (am AppModule) LegacyQuerierHandler(codec.JSONMarshaler) sdk.Querier {
// RegisterQueryService registers a GRPC query service to respond to the
// module-specific GRPC queries.
func (am AppModule) RegisterQueryService(grpc.Server) {}
func (am AppModule) RegisterQueryService(server grpc.Server) {
types.RegisterQueryServer(server, am.keeper)
}
// InitGenesis performs genesis initialization for the ibc transfer module. It returns
// InitGenesis performs genesis initialization for the ibc-transfer module. It returns
// no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONMarshaler, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState types.GenesisState
cdc.MustUnmarshalJSON(data, &genesisState)
// TODO: check if the IBC transfer module account is set
InitGenesis(ctx, am.keeper, genesisState)
am.keeper.InitGenesis(ctx, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis returns the exported genesis state as raw bytes for the ibc-transfer
// module.
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONMarshaler) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
gs := am.keeper.ExportGenesis(ctx)
return cdc.MustMarshalJSON(gs)
}
@ -288,10 +290,12 @@ func (am AppModule) OnRecvPacket(
if err := types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
return nil, nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal ICS-20 transfer packet data: %s", err.Error())
}
acknowledgement := types.FungibleTokenPacketAcknowledgement{
Success: true,
Error: "",
}
if err := am.keeper.OnRecvPacket(ctx, packet, data); err != nil {
acknowledgement = types.FungibleTokenPacketAcknowledgement{
Success: false,

View File

@ -4,7 +4,7 @@ order: 4
# Messages
### MsgTransfer
## MsgTransfer
A fungible token cross chain transfer is achieved by using the `MsgTransfer`:
@ -29,6 +29,7 @@ This message is expected to fail if:
- `Sender` is empty
- `Receiver` is empty
- `TimeoutHeight` and `TimeoutTimestamp` are both zero
- `Token.Denom` is not a valid IBC denomination as per [ADR 001 - Coin Source Tracing](./../../../docs/architecture/adr-001-coin-source-tracing.md).
This message will send a fungible token to the counterparty chain represented
by the counterparty Channel End connected to the Channel End with the identifiers
@ -37,4 +38,3 @@ by the counterparty Channel End connected to the Channel End with the identifier
The denomination provided for transfer should correspond to the same denomination
represented on this chain. The prefixes will be added as necessary upon by the
receiving chain.

View File

@ -21,6 +21,7 @@ order: 5
| fungible_token_packet | receiver | {receiver} |
| fungible_token_packet | denom | {denom} |
| fungible_token_packet | amount | {amount} |
| denomination_trace | trace_hash | {hex_hash} |
## OnAcknowledgePacket callback
@ -40,8 +41,3 @@ order: 5
| fungible_token_packet | refund_receiver | {receiver} |
| fungible_token_packet | denom | {denom} |
| fungible_token_packet | amount | {amount} |

View File

@ -48,5 +48,6 @@ func GetPrefixedCoin(portID, channelID string, coin sdk.Coin) sdk.Coin {
// GetTransferCoin creates a transfer coin with the port ID and channel ID
// prefixed to the base denom.
func GetTransferCoin(portID, channelID, baseDenom string, amount int64) sdk.Coin {
return sdk.NewInt64Coin(GetPrefixedDenom(portID, channelID, baseDenom), amount)
denomTrace := ParseDenomTrace(GetPrefixedDenom(portID, channelID, baseDenom))
return sdk.NewInt64Coin(denomTrace.IBCDenom(), amount)
}

View File

@ -10,4 +10,5 @@ var (
ErrInvalidDenomForTransfer = sdkerrors.Register(ModuleName, 3, "invalid denomination for cross-chain transfer")
ErrInvalidVersion = sdkerrors.Register(ModuleName, 4, "invalid ICS20 version")
ErrInvalidAmount = sdkerrors.Register(ModuleName, 5, "invalid token amount")
ErrTraceNotFound = sdkerrors.Register(ModuleName, 6, "denomination trace not found")
)

View File

@ -6,6 +6,7 @@ const (
EventTypePacket = "fungible_token_packet"
EventTypeTransfer = "ibc_transfer"
EventTypeChannelClose = "channel_closed"
EventTypeDenomTrace = "denomination_trace"
AttributeKeyReceiver = "receiver"
AttributeKeyDenom = "denom"
@ -15,4 +16,5 @@ const (
AttributeKeyRefundAmount = "refund_amount"
AttributeKeyAckSuccess = "success"
AttributeKeyAckError = "error"
AttributeKeyTraceHash = "trace_hash"
)

View File

@ -4,15 +4,27 @@ import (
host "github.com/cosmos/cosmos-sdk/x/ibc/24-host"
)
// NewGenesisState creates a new ibc-transfer GenesisState instance.
func NewGenesisState(portID string, denomTraces Traces) *GenesisState {
return &GenesisState{
PortId: portID,
DenomTraces: denomTraces,
}
}
// DefaultGenesisState returns a GenesisState with "transfer" as the default PortID.
func DefaultGenesisState() *GenesisState {
return &GenesisState{
PortId: PortID,
PortId: PortID,
DenomTraces: Traces{},
}
}
// Validate performs basic genesis state validation returning an error upon any
// failure.
func (gs GenesisState) Validate() error {
return host.PortIdentifierValidator(gs.PortId)
if err := host.PortIdentifierValidator(gs.PortId); err != nil {
return err
}
return gs.DenomTraces.Validate()
}

View File

@ -23,10 +23,10 @@ var _ = math.Inf
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// GenesisState is currently only used to ensure that the InitGenesis gets run
// by the module manager
// GenesisState defines the ibc-transfer genesis state
type GenesisState struct {
PortId string `protobuf:"bytes,1,opt,name=port_id,json=portId,proto3" json:"port_id,omitempty" yaml:"port_id"`
PortId string `protobuf:"bytes,1,opt,name=port_id,json=portId,proto3" json:"port_id,omitempty" yaml:"port_id"`
DenomTraces Traces `protobuf:"bytes,2,rep,name=denom_traces,json=denomTraces,proto3,castrepeated=Traces" json:"denom_traces" yaml:"denom_traces"`
}
func (m *GenesisState) Reset() { *m = GenesisState{} }
@ -69,6 +69,13 @@ func (m *GenesisState) GetPortId() string {
return ""
}
func (m *GenesisState) GetDenomTraces() Traces {
if m != nil {
return m.DenomTraces
}
return nil
}
func init() {
proto.RegisterType((*GenesisState)(nil), "ibc.transfer.GenesisState")
}
@ -76,20 +83,24 @@ func init() {
func init() { proto.RegisterFile("ibc/transfer/genesis.proto", fileDescriptor_c13b8463155e05c2) }
var fileDescriptor_c13b8463155e05c2 = []byte{
// 194 bytes of a gzipped FileDescriptorProto
// 265 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0xca, 0x4c, 0x4a, 0xd6,
0x2f, 0x29, 0x4a, 0xcc, 0x2b, 0x4e, 0x4b, 0x2d, 0xd2, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c,
0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0xc9, 0x4c, 0x4a, 0xd6, 0x83, 0xc9, 0x49, 0x89,
0xa4, 0xe7, 0xa7, 0xe7, 0x83, 0x25, 0xf4, 0x41, 0x2c, 0x88, 0x1a, 0x25, 0x6b, 0x2e, 0x1e, 0x77,
0x88, 0xa6, 0xe0, 0x92, 0xc4, 0x92, 0x54, 0x21, 0x6d, 0x2e, 0xf6, 0x82, 0xfc, 0xa2, 0x92, 0xf8,
0xcc, 0x14, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x4e, 0x27, 0xa1, 0x4f, 0xf7, 0xe4, 0xf9, 0x2a, 0x13,
0x73, 0x73, 0xac, 0x94, 0xa0, 0x12, 0x4a, 0x41, 0x6c, 0x20, 0x96, 0x67, 0x8a, 0x93, 0xf7, 0x89,
0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3,
0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0x19, 0xa6, 0x67, 0x96, 0x64, 0x94, 0x26,
0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x27, 0xe7, 0x17, 0xe7, 0xe6, 0x17, 0x43, 0x29, 0xdd, 0xe2, 0x94,
0x6c, 0xfd, 0x0a, 0xfd, 0xcc, 0xa4, 0x64, 0x5d, 0xb8, 0xab, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x93,
0xd8, 0xc0, 0x0e, 0x32, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x68, 0xf3, 0x81, 0x61, 0xd2, 0x00,
0x00, 0x00,
0xa4, 0xe7, 0xa7, 0xe7, 0x83, 0x25, 0xf4, 0x41, 0x2c, 0x88, 0x1a, 0x29, 0x69, 0x14, 0xfd, 0x30,
0x06, 0x44, 0x52, 0x69, 0x3e, 0x23, 0x17, 0x8f, 0x3b, 0xc4, 0xc8, 0xe0, 0x92, 0xc4, 0x92, 0x54,
0x21, 0x6d, 0x2e, 0xf6, 0x82, 0xfc, 0xa2, 0x92, 0xf8, 0xcc, 0x14, 0x09, 0x46, 0x05, 0x46, 0x0d,
0x4e, 0x27, 0xa1, 0x4f, 0xf7, 0xe4, 0xf9, 0x2a, 0x13, 0x73, 0x73, 0xac, 0x94, 0xa0, 0x12, 0x4a,
0x41, 0x6c, 0x20, 0x96, 0x67, 0x8a, 0x50, 0x12, 0x17, 0x4f, 0x4a, 0x6a, 0x5e, 0x7e, 0x6e, 0x7c,
0x49, 0x51, 0x62, 0x72, 0x6a, 0xb1, 0x04, 0x93, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0x84, 0x1e, 0xb2,
0xab, 0xf4, 0x5c, 0x40, 0x2a, 0x42, 0x40, 0x0a, 0x9c, 0x54, 0x4f, 0xdc, 0x93, 0x67, 0xf8, 0x74,
0x4f, 0x5e, 0x18, 0x62, 0x1e, 0xb2, 0x5e, 0xa5, 0x55, 0xf7, 0xe5, 0xd9, 0xc0, 0xaa, 0x8a, 0x83,
0xb8, 0x53, 0xe0, 0x5a, 0x8a, 0x9d, 0xbc, 0x4f, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1,
0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e,
0x21, 0xca, 0x30, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0x3f, 0x39, 0xbf,
0x38, 0x37, 0xbf, 0x18, 0x4a, 0xe9, 0x16, 0xa7, 0x64, 0xeb, 0x57, 0xe8, 0x67, 0x26, 0x25, 0xeb,
0x22, 0xfc, 0x5d, 0x59, 0x90, 0x5a, 0x9c, 0xc4, 0x06, 0xf6, 0xb5, 0x31, 0x20, 0x00, 0x00, 0xff,
0xff, 0xcf, 0x4f, 0x0f, 0xb0, 0x54, 0x01, 0x00, 0x00,
}
func (m *GenesisState) Marshal() (dAtA []byte, err error) {
@ -112,6 +123,20 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if len(m.DenomTraces) > 0 {
for iNdEx := len(m.DenomTraces) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.DenomTraces[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintGenesis(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x12
}
}
if len(m.PortId) > 0 {
i -= len(m.PortId)
copy(dAtA[i:], m.PortId)
@ -143,6 +168,12 @@ func (m *GenesisState) Size() (n int) {
if l > 0 {
n += 1 + l + sovGenesis(uint64(l))
}
if len(m.DenomTraces) > 0 {
for _, e := range m.DenomTraces {
l = e.Size()
n += 1 + l + sovGenesis(uint64(l))
}
}
return n
}
@ -213,6 +244,40 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error {
}
m.PortId = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field DenomTraces", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenesis
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthGenesis
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.DenomTraces = append(m.DenomTraces, DenomTrace{})
if err := m.DenomTraces[len(m.DenomTraces)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenesis(dAtA[iNdEx:])

View File

@ -27,8 +27,12 @@ const (
QuerierRoute = ModuleName
)
// PortKey defines the key to store the port ID in store
var PortKey = []byte{0x01}
var (
// PortKey defines the key to store the port ID in store
PortKey = []byte{0x01}
// DenomTraceKey defines the key to store the denomination trace info in store
DenomTraceKey = []byte{0x02}
)
// GetEscrowAddress returns the escrow address for the specified channel
//

View File

@ -59,12 +59,7 @@ func (msg MsgTransfer) ValidateBasic() error {
if msg.Receiver == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing recipient address")
}
// sanity check that validate basic on fungible token packet passes
// NOTE: this should always pass since validation checks should be the
// same. Please open an issue if you encounter an error on this line.
packet := NewFungibleTokenPacketData(msg.Token.Denom, msg.Token.Amount.Uint64(), msg.Sender.String(), msg.Receiver)
return packet.ValidateBasic()
return ValidateIBCDenom(msg.Token.Denom)
}
// GetSignBytes implements sdk.Msg

View File

@ -28,8 +28,10 @@ var (
emptyAddr sdk.AccAddress
coin = sdk.NewCoin("atom", sdk.NewInt(100))
ibcCoin = sdk.NewCoin("ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", sdk.NewInt(100))
invalidIBCCoin = sdk.NewCoin("notibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", sdk.NewInt(100))
invalidDenomCoin = sdk.Coin{Denom: "0atom", Amount: sdk.NewInt(100)}
negativeCoin = sdk.Coin{Denom: "atoms", Amount: sdk.NewInt(-100)}
zeroCoin = sdk.Coin{Denom: "atoms", Amount: sdk.NewInt(0)}
)
// TestMsgTransferRoute tests Route for MsgTransfer
@ -53,7 +55,9 @@ func TestMsgTransferValidation(t *testing.T) {
msg *MsgTransfer
expPass bool
}{
{"valid msg", NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 10, 0), true},
{"valid msg with base denom", NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 10, 0), true},
{"valid msg with trace hash", NewMsgTransfer(validPort, validChannel, ibcCoin, addr1, addr2, 10, 0), true},
{"invalid ibc denom", NewMsgTransfer(validPort, validChannel, invalidIBCCoin, addr1, addr2, 10, 0), false},
{"too short port id", NewMsgTransfer(invalidShortPort, validChannel, coin, addr1, addr2, 10, 0), false},
{"too long port id", NewMsgTransfer(invalidLongPort, validChannel, coin, addr1, addr2, 10, 0), false},
{"port id contains non-alpha", NewMsgTransfer(invalidPort, validChannel, coin, addr1, addr2, 10, 0), false},
@ -61,7 +65,7 @@ func TestMsgTransferValidation(t *testing.T) {
{"too long channel id", NewMsgTransfer(validPort, invalidLongChannel, coin, addr1, addr2, 10, 0), false},
{"channel id contains non-alpha", NewMsgTransfer(validPort, invalidChannel, coin, addr1, addr2, 10, 0), false},
{"invalid denom", NewMsgTransfer(validPort, validChannel, invalidDenomCoin, addr1, addr2, 10, 0), false},
{"negative coin", NewMsgTransfer(validPort, validChannel, negativeCoin, addr1, addr2, 10, 0), false},
{"zero coin", NewMsgTransfer(validPort, validChannel, zeroCoin, addr1, addr2, 10, 0), false},
{"missing sender address", NewMsgTransfer(validPort, validChannel, coin, emptyAddr, addr2, 10, 0), false},
{"missing recipient address", NewMsgTransfer(validPort, validChannel, coin, addr1, "", 10, 0), false},
{"empty coin", NewMsgTransfer(validPort, validChannel, sdk.Coin{}, addr1, addr2, 10, 0), false},

View File

@ -36,9 +36,6 @@ func NewFungibleTokenPacketData(
// ValidateBasic is used for validating the token transfer
func (ftpd FungibleTokenPacketData) ValidateBasic() error {
if strings.TrimSpace(ftpd.Denom) == "" {
return sdkerrors.Wrap(ErrInvalidDenomForTransfer, "denom cannot be empty")
}
if ftpd.Amount == 0 {
return sdkerrors.Wrap(ErrInvalidAmount, "amount cannot be 0")
}
@ -48,7 +45,7 @@ func (ftpd FungibleTokenPacketData) ValidateBasic() error {
if strings.TrimSpace(ftpd.Receiver) == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be blank")
}
return nil
return ValidatePrefixedDenom(ftpd.Denom)
}
// GetBytes is a helper for serialising

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,264 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: ibc/transfer/query.proto
/*
Package types is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package types
import (
"context"
"io"
"net/http"
"github.com/golang/protobuf/descriptor"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/status"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = descriptor.ForMessage
func request_Query_DenomTrace_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryDenomTraceRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["hash"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "hash")
}
protoReq.Hash, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "hash", err)
}
msg, err := client.DenomTrace(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_DenomTrace_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryDenomTraceRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["hash"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "hash")
}
protoReq.Hash, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "hash", err)
}
msg, err := server.DenomTrace(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_Query_DenomTraces_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_Query_DenomTraces_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryDenomTracesRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_DenomTraces_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.DenomTraces(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_DenomTraces_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryDenomTracesRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_DenomTraces_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.DenomTraces(ctx, &protoReq)
return msg, metadata, err
}
// RegisterQueryHandlerServer registers the http handlers for service Query to "mux".
// UnaryRPC :call QueryServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features (such as grpc.SendHeader, etc) to stop working. Consider using RegisterQueryHandlerFromEndpoint instead.
func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server QueryServer) error {
mux.Handle("GET", pattern_Query_DenomTrace_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_DenomTrace_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_DenomTrace_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_DenomTraces_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_DenomTraces_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_DenomTraces_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterQueryHandlerFromEndpoint is same as RegisterQueryHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterQueryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterQueryHandler(ctx, mux, conn)
}
// RegisterQueryHandler registers the http handlers for service Query to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterQueryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterQueryHandlerClient(ctx, mux, NewQueryClient(conn))
}
// RegisterQueryHandlerClient registers the http handlers for service Query
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "QueryClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "QueryClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "QueryClient" to call the correct interceptors.
func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client QueryClient) error {
mux.Handle("GET", pattern_Query_DenomTrace_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_DenomTrace_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_DenomTrace_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_DenomTraces_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_DenomTraces_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_DenomTraces_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Query_DenomTrace_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"ibc_transfer", "v1beta1", "denom_traces", "hash"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Query_DenomTraces_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"ibc_transfer", "v1beta1", "denom_traces"}, "", runtime.AssumeColonVerbOpt(true)))
)
var (
forward_Query_DenomTrace_0 = runtime.ForwardResponseMessage
forward_Query_DenomTraces_0 = runtime.ForwardResponseMessage
)

View File

@ -0,0 +1,195 @@
package types
import (
"encoding/hex"
"errors"
fmt "fmt"
"sort"
"strings"
"github.com/tendermint/tendermint/crypto/tmhash"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmtypes "github.com/tendermint/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
host "github.com/cosmos/cosmos-sdk/x/ibc/24-host"
)
// ParseDenomTrace parses a string with the ibc prefix (denom trace) and the base denomination
// into a DenomTrace type.
//
// Examples:
//
// - "portidone/channelidone/uatom" => DenomTrace{Path: "portidone/channelidone", BaseDenom: "uatom"}
// - "uatom" => DenomTrace{Path: "", BaseDenom: "uatom"}
func ParseDenomTrace(rawDenom string) DenomTrace {
denomSplit := strings.Split(rawDenom, "/")
if denomSplit[0] == rawDenom {
return DenomTrace{
Path: "",
BaseDenom: rawDenom,
}
}
return DenomTrace{
Path: strings.Join(denomSplit[:len(denomSplit)-1], "/"),
BaseDenom: denomSplit[len(denomSplit)-1],
}
}
// 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([]byte(dt.GetFullDenomPath()))
}
// GetPrefix returns the receiving denomination prefix composed by the trace info and a separator.
func (dt DenomTrace) GetPrefix() string {
return dt.Path + "/"
}
// 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.Path != "" {
return fmt.Sprintf("ibc/%s", dt.Hash())
}
return dt.BaseDenom
}
// GetFullDenomPath returns the full denomination according to the ICS20 specification:
// tracePath + "/" + baseDenom
func (dt DenomTrace) GetFullDenomPath() string {
return dt.GetPrefix() + dt.BaseDenom
}
func validateTraceIdentifiers(identifiers []string) error {
if len(identifiers) == 0 || len(identifiers)%2 != 0 {
return errors.New("trace info must come in pairs of port and channel identifiers '{portID}/{channelID}'")
}
// validate correctness of port and channel identifiers
for i := 0; i < len(identifiers); i += 2 {
if err := host.PortIdentifierValidator(identifiers[i]); err != nil {
return sdkerrors.Wrapf(err, "invalid port ID at position %d", i)
}
if err := host.ChannelIdentifierValidator(identifiers[i+1]); err != nil {
return sdkerrors.Wrapf(err, "invalid channel ID at position %d", i)
}
}
return nil
}
// Validate performs a basic validation of the DenomTrace fields.
func (dt DenomTrace) Validate() error {
// empty trace is accepted when token lives on the original chain
switch {
case dt.Path == "" && dt.BaseDenom != "":
return nil
case strings.TrimSpace(dt.BaseDenom) == "":
return fmt.Errorf("base denomination cannot be blank")
}
// NOTE: no base denomination validation
identifiers := strings.Split(dt.Path, "/")
return validateTraceIdentifiers(identifiers)
}
// Traces defines a wrapper type for a slice of DenomTrace.
type Traces []DenomTrace
// Validate performs a basic validation of each denomination trace info.
func (t Traces) Validate() error {
seenTraces := make(map[string]bool)
for i, trace := range t {
hash := trace.Hash().String()
if seenTraces[hash] {
return fmt.Errorf("duplicated denomination trace with hash %s", trace.Hash())
}
if err := trace.Validate(); err != nil {
return sdkerrors.Wrapf(err, "failed denom trace %d validation", i)
}
seenTraces[hash] = true
}
return nil
}
var _ sort.Interface = Traces{}
// Len implements sort.Interface for Traces
func (t Traces) Len() int { return len(t) }
// Less implements sort.Interface for Traces
func (t Traces) Less(i, j int) bool { return t[i].GetFullDenomPath() < t[j].GetFullDenomPath() }
// Swap implements sort.Interface for Traces
func (t Traces) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
// Sort is a helper function to sort the set of denomination traces in-place
func (t Traces) Sort() Traces {
sort.Sort(t)
return t
}
// ValidatePrefixedDenom checks that the denomination for an IBC fungible token packet denom is correctly prefixed.
// The function will return no error if the given string follows one of the two formats:
//
// - Prefixed denomination: '{portIDN}/{channelIDN}/.../{portID0}/{channelID0}/baseDenom'
// - Unprefixed denomination: 'baseDenom'
func ValidatePrefixedDenom(denom string) error {
denomSplit := strings.Split(denom, "/")
if denomSplit[0] == denom && strings.TrimSpace(denom) != "" {
// NOTE: no base denomination validation
return nil
}
if strings.TrimSpace(denomSplit[len(denomSplit)-1]) == "" {
return sdkerrors.Wrap(ErrInvalidDenomForTransfer, "base denomination cannot be blank")
}
identifiers := denomSplit[:len(denomSplit)-1]
return validateTraceIdentifiers(identifiers)
}
// 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 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)
}
if _, err := ParseHexHash(denomSplit[1]); err != nil {
return sdkerrors.Wrapf(err, "invalid denom trace hash %s", denomSplit[1])
}
return nil
}
// ParseHexHash parses a hex hash in string format to bytes and validates its correctness.
func ParseHexHash(hexHash string) (tmbytes.HexBytes, error) {
hash, err := hex.DecodeString(hexHash)
if err != nil {
return nil, err
}
if err := tmtypes.ValidateHash(hash); err != nil {
return nil, err
}
return hash, nil
}

View File

@ -0,0 +1,150 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParseDenomTrace(t *testing.T) {
testCases := []struct {
name string
denom string
expTrace DenomTrace
}{
{"empty denom", "", DenomTrace{}},
{"base denom", "uatom", DenomTrace{BaseDenom: "uatom"}},
{"trace info", "transfer/channelToA/uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA"}},
{"incomplete path", "transfer/uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer"}},
{"invalid path (1)", "transfer//uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer/"}},
{"invalid path (2)", "transfer/channelToA/uatom/", DenomTrace{BaseDenom: "", Path: "transfer/channelToA/uatom"}},
}
for _, tc := range testCases {
trace := ParseDenomTrace(tc.denom)
require.Equal(t, tc.expTrace, trace, tc.name)
}
}
func TestDenomTrace_IBCDenom(t *testing.T) {
testCases := []struct {
name string
trace DenomTrace
expDenom string
}{
{"base denom", DenomTrace{BaseDenom: "uatom"}, "uatom"},
{"trace info", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA"}, "ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2"},
}
for _, tc := range testCases {
denom := tc.trace.IBCDenom()
require.Equal(t, tc.expDenom, denom, tc.name)
}
}
func TestDenomTrace_Validate(t *testing.T) {
testCases := []struct {
name string
trace DenomTrace
expError bool
}{
{"base denom only", DenomTrace{BaseDenom: "uatom"}, false},
{"empty DenomTrace", DenomTrace{}, true},
{"valid single trace info", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA"}, false},
{"valid multiple trace info", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"}, false},
{"single trace identifier", DenomTrace{BaseDenom: "uatom", Path: "transfer"}, true},
{"invalid port ID", DenomTrace{BaseDenom: "uatom", Path: "(transfer)/channelToA"}, true},
{"invalid channel ID", DenomTrace{BaseDenom: "uatom", Path: "transfer/(channelToA)"}, true},
{"empty base denom with trace", DenomTrace{BaseDenom: "", Path: "transfer/channelToA"}, true},
}
for _, tc := range testCases {
err := tc.trace.Validate()
if tc.expError {
require.Error(t, err, tc.name)
continue
}
require.NoError(t, err, tc.name)
}
}
func TestTraces_Validate(t *testing.T) {
testCases := []struct {
name string
traces Traces
expError bool
}{
{"empty Traces", Traces{}, false},
{"valid multiple trace info", Traces{{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"}}, false},
{
"valid multiple trace info",
Traces{
{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"},
{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"},
},
true,
},
{"empty base denom with trace", Traces{{BaseDenom: "", Path: "transfer/channelToA"}}, true},
}
for _, tc := range testCases {
err := tc.traces.Validate()
if tc.expError {
require.Error(t, err, tc.name)
continue
}
require.NoError(t, err, tc.name)
}
}
func TestValidatePrefixedDenom(t *testing.T) {
testCases := []struct {
name string
denom string
expError bool
}{
{"prefixed denom", "transfer/channelToA/uatom", false},
{"base denom", "uatom", false},
{"empty denom", "", true},
{"empty prefix", "/uatom", true},
{"empty identifiers", "//uatom", true},
{"single trace identifier", "transfer/", true},
{"invalid port ID", "(transfer)/channelToA/uatom", true},
{"invalid channel ID", "transfer/(channelToA)/uatom", true},
}
for _, tc := range testCases {
err := ValidatePrefixedDenom(tc.denom)
if tc.expError {
require.Error(t, err, tc.name)
continue
}
require.NoError(t, err, tc.name)
}
}
func TestValidateIBCDenom(t *testing.T) {
testCases := []struct {
name string
denom string
expError bool
}{
{"denom with trace hash", "ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", false},
{"base denom", "uatom", false},
{"empty denom", "", true},
{"invalid prefixed denom", "transfer/channelToA/uatom", true},
{"denom 'ibc'", "ibc", true},
{"denom 'ibc/'", "ibc/", true},
{"invald prefix", "notibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", true},
{"invald hash", "ibc/!@#$!@#", true},
}
for _, tc := range testCases {
err := ValidateIBCDenom(tc.denom)
if tc.expError {
require.Error(t, err, tc.name)
continue
}
require.NoError(t, err, tc.name)
}
}

View File

@ -260,48 +260,108 @@ func (m *FungibleTokenPacketAcknowledgement) GetError() string {
return ""
}
// DenomTrace contains the base denomination for ICS20 fungible tokens and the source tracing
// information path.
type DenomTrace struct {
// path defines the chain of port/channel identifiers used for tracing the source of the fungible
// token.
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
// base denomination of the relayed fungible token.
BaseDenom string `protobuf:"bytes,2,opt,name=base_denom,json=baseDenom,proto3" json:"base_denom,omitempty"`
}
func (m *DenomTrace) Reset() { *m = DenomTrace{} }
func (m *DenomTrace) String() string { return proto.CompactTextString(m) }
func (*DenomTrace) ProtoMessage() {}
func (*DenomTrace) Descriptor() ([]byte, []int) {
return fileDescriptor_08134a70fd29e656, []int{3}
}
func (m *DenomTrace) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *DenomTrace) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_DenomTrace.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *DenomTrace) XXX_Merge(src proto.Message) {
xxx_messageInfo_DenomTrace.Merge(m, src)
}
func (m *DenomTrace) XXX_Size() int {
return m.Size()
}
func (m *DenomTrace) XXX_DiscardUnknown() {
xxx_messageInfo_DenomTrace.DiscardUnknown(m)
}
var xxx_messageInfo_DenomTrace proto.InternalMessageInfo
func (m *DenomTrace) GetPath() string {
if m != nil {
return m.Path
}
return ""
}
func (m *DenomTrace) GetBaseDenom() string {
if m != nil {
return m.BaseDenom
}
return ""
}
func init() {
proto.RegisterType((*MsgTransfer)(nil), "ibc.transfer.MsgTransfer")
proto.RegisterType((*FungibleTokenPacketData)(nil), "ibc.transfer.FungibleTokenPacketData")
proto.RegisterType((*FungibleTokenPacketAcknowledgement)(nil), "ibc.transfer.FungibleTokenPacketAcknowledgement")
proto.RegisterType((*DenomTrace)(nil), "ibc.transfer.DenomTrace")
}
func init() { proto.RegisterFile("ibc/transfer/transfer.proto", fileDescriptor_08134a70fd29e656) }
var fileDescriptor_08134a70fd29e656 = []byte{
// 505 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x53, 0x4f, 0x6f, 0xd3, 0x30,
0x1c, 0x6d, 0xe8, 0x9f, 0x0d, 0x77, 0x43, 0x60, 0x8d, 0x91, 0x15, 0x94, 0x56, 0x39, 0xf5, 0xd2,
0x44, 0x05, 0x21, 0x24, 0x4e, 0xb4, 0x43, 0x88, 0x09, 0x21, 0x4d, 0x56, 0x4f, 0x5c, 0xa6, 0xc4,
0xf9, 0x91, 0x5a, 0x6d, 0xec, 0xca, 0x76, 0x06, 0x13, 0x5f, 0x82, 0xef, 0xc2, 0x97, 0xd8, 0x71,
0x47, 0x4e, 0x11, 0x6a, 0xbf, 0x41, 0x8f, 0x9c, 0x50, 0x62, 0xb7, 0xac, 0xd2, 0xb4, 0x93, 0xfd,
0xde, 0xfb, 0xf9, 0x97, 0xe7, 0xf7, 0x73, 0xd0, 0x73, 0x16, 0xd3, 0x50, 0xcb, 0x88, 0xab, 0xaf,
0x20, 0xb7, 0x9b, 0x60, 0x21, 0x85, 0x16, 0xf8, 0x80, 0xc5, 0x34, 0xd8, 0x70, 0x9d, 0xa3, 0x54,
0xa4, 0xa2, 0x12, 0xc2, 0x72, 0x67, 0x6a, 0x3a, 0x1e, 0x15, 0x2a, 0x13, 0x2a, 0x8c, 0x23, 0x05,
0xe1, 0xe5, 0x30, 0x06, 0x1d, 0x0d, 0x43, 0x2a, 0x18, 0x37, 0xba, 0xff, 0xab, 0x8e, 0xda, 0x9f,
0x55, 0x3a, 0xb1, 0x5d, 0xf0, 0x1b, 0xd4, 0x56, 0x22, 0x97, 0x14, 0x2e, 0x16, 0x42, 0x6a, 0xd7,
0xe9, 0x39, 0xfd, 0x87, 0xe3, 0xe3, 0x75, 0xd1, 0xc5, 0x57, 0x51, 0x36, 0x7f, 0xeb, 0xdf, 0x12,
0x7d, 0x82, 0x0c, 0x3a, 0x17, 0x52, 0xe3, 0x77, 0xe8, 0x91, 0xd5, 0xe8, 0x34, 0xe2, 0x1c, 0xe6,
0xee, 0x83, 0xea, 0xec, 0xc9, 0xba, 0xe8, 0x3e, 0xdd, 0x39, 0x6b, 0x75, 0x9f, 0x1c, 0x1a, 0xe2,
0xd4, 0x60, 0xfc, 0x1a, 0x35, 0xb5, 0x98, 0x01, 0x77, 0xeb, 0x3d, 0xa7, 0xdf, 0x7e, 0x79, 0x12,
0x18, 0xeb, 0x41, 0x69, 0x3d, 0xb0, 0xd6, 0x83, 0x53, 0xc1, 0xf8, 0xb8, 0x71, 0x5d, 0x74, 0x6b,
0xc4, 0x54, 0xe3, 0x33, 0xd4, 0x52, 0xc0, 0x13, 0x90, 0x6e, 0xa3, 0xe7, 0xf4, 0x0f, 0xc6, 0xc3,
0xbf, 0x45, 0x77, 0x90, 0x32, 0x3d, 0xcd, 0xe3, 0x80, 0x8a, 0x2c, 0xb4, 0x01, 0x98, 0x65, 0xa0,
0x92, 0x59, 0xa8, 0xaf, 0x16, 0xa0, 0x82, 0x11, 0xa5, 0xa3, 0x24, 0x91, 0xa0, 0x14, 0xb1, 0x0d,
0x70, 0x07, 0xed, 0x4b, 0xa0, 0xc0, 0x2e, 0x41, 0xba, 0xcd, 0xd2, 0x3d, 0xd9, 0xe2, 0xf2, 0x7e,
0x9a, 0x65, 0x20, 0x72, 0x7d, 0x31, 0x05, 0x96, 0x4e, 0xb5, 0xdb, 0xea, 0x39, 0xfd, 0xc6, 0xed,
0xfb, 0xed, 0xea, 0x3e, 0x39, 0xb4, 0xc4, 0xc7, 0x0a, 0xe3, 0x33, 0xf4, 0x64, 0x53, 0x51, 0xae,
0x4a, 0x47, 0xd9, 0xc2, 0xdd, 0xab, 0x9a, 0xbc, 0x58, 0x17, 0x5d, 0x77, 0xb7, 0xc9, 0xb6, 0xc4,
0x27, 0x8f, 0x2d, 0x37, 0xd9, 0x52, 0x3f, 0xd0, 0xb3, 0x0f, 0x39, 0x4f, 0x59, 0x3c, 0x87, 0x49,
0x19, 0xc2, 0x79, 0x44, 0x67, 0xa0, 0xdf, 0x47, 0x3a, 0xc2, 0x47, 0xa8, 0x99, 0x00, 0x17, 0x99,
0x19, 0x1d, 0x31, 0x00, 0x1f, 0xa3, 0x56, 0x94, 0x89, 0x9c, 0xeb, 0x6a, 0x2a, 0x0d, 0x62, 0x51,
0xc9, 0xdb, 0xf0, 0xea, 0x55, 0xf9, 0x5d, 0x49, 0x34, 0x76, 0x93, 0xf0, 0x27, 0xc8, 0xbf, 0xe3,
0xe3, 0x23, 0x3a, 0xe3, 0xe2, 0xdb, 0x1c, 0x92, 0x14, 0x32, 0xe0, 0x1a, 0xbb, 0x68, 0x4f, 0xe5,
0x94, 0x82, 0x52, 0x95, 0x93, 0x7d, 0xb2, 0x81, 0xa5, 0x43, 0x90, 0x52, 0x48, 0xf3, 0x40, 0x88,
0x01, 0xe3, 0x4f, 0xd7, 0x4b, 0xcf, 0xb9, 0x59, 0x7a, 0xce, 0x9f, 0xa5, 0xe7, 0xfc, 0x5c, 0x79,
0xb5, 0x9b, 0x95, 0x57, 0xfb, 0xbd, 0xf2, 0x6a, 0x5f, 0x86, 0xf7, 0x0e, 0xf3, 0x7b, 0xc8, 0x62,
0x3a, 0xf8, 0xff, 0x8b, 0x94, 0xb3, 0x8d, 0x5b, 0xd5, 0xe3, 0x7e, 0xf5, 0x2f, 0x00, 0x00, 0xff,
0xff, 0x0d, 0x50, 0xec, 0xf7, 0x3f, 0x03, 0x00, 0x00,
// 541 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x53, 0xc1, 0x6e, 0xd3, 0x4c,
0x10, 0x8e, 0xff, 0xba, 0x69, 0xbb, 0x69, 0x7f, 0xc1, 0xaa, 0x14, 0x37, 0x80, 0x13, 0xf9, 0x94,
0x4b, 0x6c, 0x05, 0x84, 0x90, 0xb8, 0x40, 0xd2, 0x0a, 0x51, 0x21, 0xa4, 0xca, 0xca, 0x89, 0x4b,
0xb4, 0x5e, 0x0f, 0x8e, 0x95, 0x78, 0x37, 0xda, 0x5d, 0x17, 0x2a, 0x5e, 0x82, 0x77, 0xe1, 0x25,
0x7a, 0xec, 0x91, 0x53, 0x84, 0x92, 0x37, 0xc8, 0x91, 0x13, 0x5a, 0xef, 0x26, 0x34, 0x52, 0xc5,
0xc9, 0xfb, 0x7d, 0xdf, 0xcc, 0xf8, 0x9b, 0xd9, 0x59, 0xf4, 0x24, 0x4f, 0x68, 0xa4, 0x04, 0x61,
0xf2, 0x33, 0x88, 0xcd, 0x21, 0x9c, 0x09, 0xae, 0x38, 0x3e, 0xcc, 0x13, 0x1a, 0xae, 0xb9, 0xe6,
0x71, 0xc6, 0x33, 0x5e, 0x09, 0x91, 0x3e, 0x99, 0x98, 0xa6, 0x4f, 0xb9, 0x2c, 0xb8, 0x8c, 0x12,
0x22, 0x21, 0xba, 0xea, 0x25, 0xa0, 0x48, 0x2f, 0xa2, 0x3c, 0x67, 0x46, 0x0f, 0x7e, 0xec, 0xa0,
0xc6, 0x47, 0x99, 0x0d, 0x6d, 0x15, 0xfc, 0x0a, 0x35, 0x24, 0x2f, 0x05, 0x85, 0xd1, 0x8c, 0x0b,
0xe5, 0x39, 0x6d, 0xa7, 0x73, 0x30, 0x38, 0x59, 0xcd, 0x5b, 0xf8, 0x9a, 0x14, 0xd3, 0xd7, 0xc1,
0x1d, 0x31, 0x88, 0x91, 0x41, 0x97, 0x5c, 0x28, 0xfc, 0x16, 0xfd, 0x6f, 0x35, 0x3a, 0x26, 0x8c,
0xc1, 0xd4, 0xfb, 0xaf, 0xca, 0x3d, 0x5d, 0xcd, 0x5b, 0x8f, 0xb6, 0x72, 0xad, 0x1e, 0xc4, 0x47,
0x86, 0x38, 0x33, 0x18, 0xbf, 0x44, 0xbb, 0x8a, 0x4f, 0x80, 0x79, 0x3b, 0x6d, 0xa7, 0xd3, 0x78,
0x7e, 0x1a, 0x1a, 0xeb, 0xa1, 0xb6, 0x1e, 0x5a, 0xeb, 0xe1, 0x19, 0xcf, 0xd9, 0xc0, 0xbd, 0x99,
0xb7, 0x6a, 0xb1, 0x89, 0xc6, 0x17, 0xa8, 0x2e, 0x81, 0xa5, 0x20, 0x3c, 0xb7, 0xed, 0x74, 0x0e,
0x07, 0xbd, 0xdf, 0xf3, 0x56, 0x37, 0xcb, 0xd5, 0xb8, 0x4c, 0x42, 0xca, 0x8b, 0xc8, 0x0e, 0xc0,
0x7c, 0xba, 0x32, 0x9d, 0x44, 0xea, 0x7a, 0x06, 0x32, 0xec, 0x53, 0xda, 0x4f, 0x53, 0x01, 0x52,
0xc6, 0xb6, 0x00, 0x6e, 0xa2, 0x7d, 0x01, 0x14, 0xf2, 0x2b, 0x10, 0xde, 0xae, 0x76, 0x1f, 0x6f,
0xb0, 0xee, 0x4f, 0xe5, 0x05, 0xf0, 0x52, 0x8d, 0xc6, 0x90, 0x67, 0x63, 0xe5, 0xd5, 0xdb, 0x4e,
0xc7, 0xbd, 0xdb, 0xdf, 0xb6, 0x1e, 0xc4, 0x47, 0x96, 0x78, 0x5f, 0x61, 0x7c, 0x81, 0x1e, 0xae,
0x23, 0xf4, 0x57, 0x2a, 0x52, 0xcc, 0xbc, 0xbd, 0xaa, 0xc8, 0xd3, 0xd5, 0xbc, 0xe5, 0x6d, 0x17,
0xd9, 0x84, 0x04, 0xf1, 0x03, 0xcb, 0x0d, 0x37, 0xd4, 0x37, 0xf4, 0xf8, 0x5d, 0xc9, 0xb2, 0x3c,
0x99, 0xc2, 0x50, 0x0f, 0xe1, 0x92, 0xd0, 0x09, 0xa8, 0x73, 0xa2, 0x08, 0x3e, 0x46, 0xbb, 0x29,
0x30, 0x5e, 0x98, 0xab, 0x8b, 0x0d, 0xc0, 0x27, 0xa8, 0x4e, 0x0a, 0x5e, 0x32, 0x55, 0xdd, 0x8a,
0x1b, 0x5b, 0xa4, 0x79, 0x3b, 0xbc, 0x9d, 0x2a, 0xfc, 0xbe, 0x49, 0xb8, 0xdb, 0x93, 0x08, 0x86,
0x28, 0xb8, 0xe7, 0xe7, 0x7d, 0x3a, 0x61, 0xfc, 0xcb, 0x14, 0xd2, 0x0c, 0x0a, 0x60, 0x0a, 0x7b,
0x68, 0x4f, 0x96, 0x94, 0x82, 0x94, 0x95, 0x93, 0xfd, 0x78, 0x0d, 0xb5, 0x43, 0x10, 0x82, 0x0b,
0xb3, 0x20, 0xb1, 0x01, 0xc1, 0x1b, 0x84, 0xce, 0xb5, 0xd5, 0xa1, 0x20, 0x14, 0x30, 0x46, 0xee,
0x8c, 0xa8, 0xb1, 0x6d, 0xa2, 0x3a, 0xe3, 0x67, 0x08, 0xe9, 0x55, 0x18, 0x99, 0xf6, 0x4c, 0xf2,
0x81, 0x66, 0xaa, 0xbc, 0xc1, 0x87, 0x9b, 0x85, 0xef, 0xdc, 0x2e, 0x7c, 0xe7, 0xd7, 0xc2, 0x77,
0xbe, 0x2f, 0xfd, 0xda, 0xed, 0xd2, 0xaf, 0xfd, 0x5c, 0xfa, 0xb5, 0x4f, 0xbd, 0x7f, 0x6e, 0xc3,
0xd7, 0x28, 0x4f, 0x68, 0xf7, 0xef, 0x1b, 0xd3, 0xcb, 0x91, 0xd4, 0xab, 0xd7, 0xf1, 0xe2, 0x4f,
0x00, 0x00, 0x00, 0xff, 0xff, 0x8b, 0x58, 0xed, 0x2d, 0x80, 0x03, 0x00, 0x00,
}
func (m *MsgTransfer) Marshal() (dAtA []byte, err error) {
@ -464,6 +524,43 @@ func (m *FungibleTokenPacketAcknowledgement) MarshalToSizedBuffer(dAtA []byte) (
return len(dAtA) - i, nil
}
func (m *DenomTrace) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *DenomTrace) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *DenomTrace) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if len(m.BaseDenom) > 0 {
i -= len(m.BaseDenom)
copy(dAtA[i:], m.BaseDenom)
i = encodeVarintTransfer(dAtA, i, uint64(len(m.BaseDenom)))
i--
dAtA[i] = 0x12
}
if len(m.Path) > 0 {
i -= len(m.Path)
copy(dAtA[i:], m.Path)
i = encodeVarintTransfer(dAtA, i, uint64(len(m.Path)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func encodeVarintTransfer(dAtA []byte, offset int, v uint64) int {
offset -= sovTransfer(v)
base := offset
@ -548,6 +645,23 @@ func (m *FungibleTokenPacketAcknowledgement) Size() (n int) {
return n
}
func (m *DenomTrace) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.Path)
if l > 0 {
n += 1 + l + sovTransfer(uint64(l))
}
l = len(m.BaseDenom)
if l > 0 {
n += 1 + l + sovTransfer(uint64(l))
}
return n
}
func sovTransfer(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7
}
@ -1081,6 +1195,123 @@ func (m *FungibleTokenPacketAcknowledgement) Unmarshal(dAtA []byte) error {
}
return nil
}
func (m *DenomTrace) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTransfer
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: DenomTrace: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: DenomTrace: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Path", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTransfer
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthTransfer
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthTransfer
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Path = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field BaseDenom", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTransfer
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthTransfer
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthTransfer
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.BaseDenom = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipTransfer(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthTransfer
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthTransfer
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipTransfer(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0

View File

@ -7,7 +7,7 @@ order: 1
> NOTE: if you are not familiar with the IBC terminology and concepts, please read
this [document](https://github.com/cosmos/ics/blob/master/ibc/1_IBC_TERMINOLOGY.md) as prerequisite reading.
### Connection Version Negotation
## Connection Version Negotation
During the handshake procedure for connections a version string is agreed
upon between the two parties. This occurs during the first 3 steps of the
@ -33,7 +33,7 @@ A valid connection version is considered to be in the following format:
- the version tuple must be enclosed in parentheses
- the feature set must be enclosed in brackets
- there should be no space between the comma separting the identifier and the
- there should be no space between the comma separating the identifier and the
feature set
- the version identifier must no contain any commas
- each feature must not contain any commas
@ -46,7 +46,7 @@ with regards to version selection in `ConnOpenTry`. Each version in a set of
versions should have a unique version identifier.
:::
### Channel Version Negotation
## Channel Version Negotation
During the channel handshake procedure a version must be agreed upon between
the two parties. The selection process is largely left to the callers and

View File

@ -55,6 +55,8 @@ which call each ICS submodule's handlers (i.e `x/ibc/{XX-ICS}/handler.go`).
The following ADR provide the design and architecture decision of IBC-related components.
* [ADR 001 - Coin Source Tracing](../../../docs/architecture/adr-001-coin-source-tracing.md): standard to hash the ICS20's fungible token
denomination trace path in order to support special characters and limit the maximum denomination length.
* [ADR 17 - Historical Header Module](../../../docs/architecture/adr-017-historical-header-module.md): Introduces the ability to introspect past
consensus states in order to verify their membership in the counterparty clients.
* [ADR 19 - Protobuf State Encoding](../../../docs/architecture/adr-019-protobuf-state-encoding.md): Migration from Amino to Protobuf for state encoding.

View File

@ -50,7 +50,7 @@ const (
// Default params variables used to create a TM client
var (
DefaultTrustLevel ibctmtypes.Fraction = ibctmtypes.DefaultTrustLevel
TestHash = []byte("TESTING HASH")
TestHash = tmhash.Sum([]byte("TESTING HASH"))
TestCoin = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))
ConnectionVersion = connectiontypes.GetCompatibleEncodedVersions()[0]