-
Notifications
You must be signed in to change notification settings - Fork 580
/
trace.go
246 lines (207 loc) · 8.26 KB
/
trace.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
package types
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmtypes "github.com/tendermint/tendermint/types"
channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types"
host "github.com/cosmos/ibc-go/v6/modules/core/24-host"
)
// ParseDenomTrace parses a string with the ibc prefix (denom trace) and the base denomination
// into a DenomTrace type.
//
// Examples:
//
// - "portidone/channel-0/uatom" => DenomTrace{Path: "portidone/channel-0", BaseDenom: "uatom"}
// - "portidone/channel-0/portidtwo/channel-1/uatom" => DenomTrace{Path: "portidone/channel-0/portidtwo/channel-1", BaseDenom: "uatom"}
// - "portidone/channel-0/gamm/pool/1" => DenomTrace{Path: "portidone/channel-0", BaseDenom: "gamm/pool/1"}
// - "gamm/pool/1" => DenomTrace{Path: "", BaseDenom: "gamm/pool/1"}
// - "uatom" => DenomTrace{Path: "", BaseDenom: "uatom"}
func ParseDenomTrace(rawDenom string) DenomTrace {
denomSplit := strings.Split(rawDenom, "/")
if denomSplit[0] == rawDenom {
return DenomTrace{
Path: "",
BaseDenom: rawDenom,
}
}
path, baseDenom := extractPathAndBaseFromFullDenom(denomSplit)
return DenomTrace{
Path: path,
BaseDenom: baseDenom,
}
}
// 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 {
hash := sha256.Sum256([]byte(dt.GetFullDenomPath()))
return hash[:]
}
// 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("%s/%s", DenomPrefix, dt.Hash())
}
return dt.BaseDenom
}
// GetFullDenomPath returns the full denomination according to the ICS20 specification:
// tracePath + "/" + baseDenom
// If there exists no trace then the base denomination is returned.
func (dt DenomTrace) GetFullDenomPath() string {
if dt.Path == "" {
return dt.BaseDenom
}
return dt.GetPrefix() + dt.BaseDenom
}
// extractPathAndBaseFromFullDenom returns the trace path and the base denom from
// the elements that constitute the complete denom.
func extractPathAndBaseFromFullDenom(fullDenomItems []string) (string, string) {
var (
path []string
baseDenom []string
)
length := len(fullDenomItems)
for i := 0; i < length; i += 2 {
// The IBC specification does not guarantee the expected format of the
// destination port or destination channel identifier. A short term solution
// to determine base denomination is to expect the channel identifier to be the
// one ibc-go specifies. A longer term solution is to separate the path and base
// denomination in the ICS20 packet. If an intermediate hop prefixes the full denom
// with a channel identifier format different from our own, the base denomination
// will be incorrectly parsed, but the token will continue to be treated correctly
// as an IBC denomination. The hash used to store the token internally on our chain
// will be the same value as the base denomination being correctly parsed.
if i < length-1 && length > 2 && channeltypes.IsValidChannelID(fullDenomItems[i+1]) {
path = append(path, fullDenomItems[i], fullDenomItems[i+1])
} else {
baseDenom = fullDenomItems[i:]
break
}
}
return strings.Join(path, "/"), strings.Join(baseDenom, "/")
}
func validateTraceIdentifiers(identifiers []string) error {
if len(identifiers) == 0 || len(identifiers)%2 != 0 {
return fmt.Errorf("trace info must come in pairs of port and channel identifiers '{portID}/{channelID}', got the identifiers: %s", identifiers)
}
// 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'
//
// 'baseDenom' may or may not contain '/'s
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")
}
path, _ := extractPathAndBaseFromFullDenom(denomSplit)
if path == "" {
// NOTE: base denom contains slashes, so no base denomination validation
return nil
}
identifiers := strings.Split(path, "/")
return validateTraceIdentifiers(identifiers)
}
// ValidateIBCDenom validates that the given denomination is either:
//
// - A valid base denomination (eg: 'uatom' or 'gamm/pool/1' as in https://github.com/cosmos/ibc-go/issues/894)
// - A valid fungible token representation (i.e 'ibc/{hash}') per ADR 001 https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-001-coin-source-tracing.md
func ValidateIBCDenom(denom string) error {
if err := sdk.ValidateDenom(denom); err != nil {
return err
}
denomSplit := strings.SplitN(denom, "/", 2)
switch {
case denom == DenomPrefix:
return sdkerrors.Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", denom)
case len(denomSplit) == 2 && denomSplit[0] == DenomPrefix:
if strings.TrimSpace(denomSplit[1]) == "" {
return sdkerrors.Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", 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
}