diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06e85c907f59..b5869093359e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -144,6 +144,7 @@ jobs: run: | excludelist="$(find ./ -type f -name '*.go' | xargs grep -l 'DONTCOVER')" excludelist+=" $(find ./ -type f -name '*.pb.go')" + excludelist+=" $(find ./ -type f -name '*.pb.gw.go')" excludelist+=" $(find ./ -type f -path './tests/mocks/*.go')" for filename in ${excludelist}; do filename=$(echo $filename | sed 's/^./github.com\/cosmos\/cosmos-sdk/g') diff --git a/crypto/hd/hdpath_test.go b/crypto/hd/hdpath_test.go index 2620e0605c74..6dfda043eb04 100644 --- a/crypto/hd/hdpath_test.go +++ b/crypto/hd/hdpath_test.go @@ -19,13 +19,6 @@ func mnemonicToSeed(mnemonic string) []byte { return bip39.NewSeed(mnemonic, defaultBIP39Passphrase) } -func TestPathParamsString(t *testing.T) { - path := hd.NewParams(44, 0, 0, false, 0) - require.Equal(t, "m/44'/0'/0'/0/0", path.String()) - path = hd.NewParams(44, 33, 7, true, 9) - require.Equal(t, "m/44'/33'/7'/1/9", path.String()) -} - func TestStringifyFundraiserPathParams(t *testing.T) { path := hd.NewFundraiserParams(4, types.CoinType, 22) require.Equal(t, "m/44'/118'/4'/0/22", path.String()) @@ -99,98 +92,6 @@ func TestParamsFromPath(t *testing.T) { } -func TestBIP32Vecs(t *testing.T) { - - seed := mnemonicToSeed("barrel original fuel morning among eternal " + - "filter ball stove pluck matrix mechanic") - master, ch := hd.ComputeMastersFromSeed(seed) - fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)") - fmt.Println() - - // cosmos, absolute path - priv, err := hd.DerivePrivateKeyForPath(master, ch, types.FullFundraiserPath) - require.NoError(t, err) - require.NotEmpty(t, priv) - fmt.Println(hex.EncodeToString(priv[:])) - - absPrivKey := hex.EncodeToString(priv[:]) - - // cosmos, relative path - priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/118'/0'/0/0") - require.NoError(t, err) - require.NotEmpty(t, priv) - - relPrivKey := hex.EncodeToString(priv[:]) - - // check compatibility between relative and absolute HD paths - require.Equal(t, relPrivKey, absPrivKey) - - // bitcoin - priv, err = hd.DerivePrivateKeyForPath(master, ch, "m/44'/0'/0'/0/0") - require.NoError(t, err) - require.NotEmpty(t, priv) - fmt.Println(hex.EncodeToString(priv[:])) - - // ether - priv, err = hd.DerivePrivateKeyForPath(master, ch, "m/44'/60'/0'/0/0") - require.NoError(t, err) - require.NotEmpty(t, priv) - fmt.Println(hex.EncodeToString(priv[:])) - - // INVALID - priv, err = hd.DerivePrivateKeyForPath(master, ch, "m/X/0'/0'/0/0") - require.Error(t, err) - require.Empty(t, priv) - - priv, err = hd.DerivePrivateKeyForPath(master, ch, "m/-44/0'/0'/0/0") - require.Error(t, err) - require.Empty(t, priv) - - fmt.Println() - fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html") - fmt.Println() - - seed = mnemonicToSeed( - "advice process birth april short trust crater change bacon monkey medal garment " + - "gorilla ranch hour rival razor call lunar mention taste vacant woman sister") - master, ch = hd.ComputeMastersFromSeed(seed) - priv, _ = hd.DerivePrivateKeyForPath(master, ch, "m/44'/1'/1'/0/4") - fmt.Println(hex.EncodeToString(priv[:])) - - seed = mnemonicToSeed("idea naive region square margin day captain habit " + - "gun second farm pact pulse someone armed") - master, ch = hd.ComputeMastersFromSeed(seed) - priv, err = hd.DerivePrivateKeyForPath(master, ch, "m/44'/0'/0'/0/420") - require.NoError(t, err) - fmt.Println(hex.EncodeToString(priv[:])) - - fmt.Println() - fmt.Println("BIP 32 example") - fmt.Println() - - // bip32 path: m/0/7 - seed = mnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history") - master, ch = hd.ComputeMastersFromSeed(seed) - priv, err = hd.DerivePrivateKeyForPath(master, ch, "m/0/7") - require.NoError(t, err) // TODO: shouldn't this error? - fmt.Println(hex.EncodeToString(priv[:])) - - // Output: keys from fundraiser test-vector (cosmos, bitcoin, ether) - // - // bfcb217c058d8bbafd5e186eae936106ca3e943889b0b4a093ae13822fd3170c - // e77c3de76965ad89997451de97b95bb65ede23a6bf185a55d80363d92ee37c3d - // 7fc4d8a8146dea344ba04c593517d3f377fa6cded36cd55aee0a0bb968e651bc - // - // keys generated via https://coinomi.com/recovery-phrase-tool.html - // - // a61f10c5fecf40c084c94fa54273b6f5d7989386be4a37669e6d6f7b0169c163 - // 32c4599843de3ef161a629a461d12c60b009b676c35050be5f7ded3a3b23501f - // - // BIP 32 example - // - // c4c11d8c03625515905d7e89d25dfc66126fbc629ecca6db489a1a72fc4bda78 -} - func TestCreateHDPath(t *testing.T) { type args struct { coinType uint32 @@ -284,3 +185,99 @@ func TestDeriveHDPathRange(t *testing.T) { }) } } + +func ExampleStringifyPathParams() { + path := hd.NewParams(44, 0, 0, false, 0) + fmt.Println(path.String()) + path = hd.NewParams(44, 33, 7, true, 9) + fmt.Println(path.String()) + // Output: + // m/44'/0'/0'/0/0 + // m/44'/33'/7'/1/9 +} + +func ExampleSomeBIP32TestVecs() { + seed := mnemonicToSeed("barrel original fuel morning among eternal " + + "filter ball stove pluck matrix mechanic") + master, ch := hd.ComputeMastersFromSeed(seed) + fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)") + fmt.Println() + // cosmos + priv, err := hd.DerivePrivateKeyForPath(master, ch, types.FullFundraiserPath) + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + // bitcoin + priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + // ether + priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + // INVALID + priv, err = hd.DerivePrivateKeyForPath(master, ch, "X/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + priv, err = hd.DerivePrivateKeyForPath(master, ch, "-44/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + + fmt.Println() + fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html") + fmt.Println() + + seed = mnemonicToSeed( + "advice process birth april short trust crater change bacon monkey medal garment " + + "gorilla ranch hour rival razor call lunar mention taste vacant woman sister") + master, ch = hd.ComputeMastersFromSeed(seed) + priv, _ = hd.DerivePrivateKeyForPath(master, ch, "44'/1'/1'/0/4") + fmt.Println(hex.EncodeToString(priv[:])) + + seed = mnemonicToSeed("idea naive region square margin day captain habit " + + "gun second farm pact pulse someone armed") + master, ch = hd.ComputeMastersFromSeed(seed) + priv, _ = hd.DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/420") + fmt.Println(hex.EncodeToString(priv[:])) + + fmt.Println() + fmt.Println("BIP 32 example") + fmt.Println() + + // bip32 path: m/0/7 + seed = mnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history") + master, ch = hd.ComputeMastersFromSeed(seed) + priv, _ = hd.DerivePrivateKeyForPath(master, ch, "0/7") + fmt.Println(hex.EncodeToString(priv[:])) + + // Output: keys from fundraiser test-vector (cosmos, bitcoin, ether) + // + // bfcb217c058d8bbafd5e186eae936106ca3e943889b0b4a093ae13822fd3170c + // e77c3de76965ad89997451de97b95bb65ede23a6bf185a55d80363d92ee37c3d + // 7fc4d8a8146dea344ba04c593517d3f377fa6cded36cd55aee0a0bb968e651bc + // INVALID + // INVALID + // + // keys generated via https://coinomi.com/recovery-phrase-tool.html + // + // a61f10c5fecf40c084c94fa54273b6f5d7989386be4a37669e6d6f7b0169c163 + // 32c4599843de3ef161a629a461d12c60b009b676c35050be5f7ded3a3b23501f + // + // BIP 32 example + // + // c4c11d8c03625515905d7e89d25dfc66126fbc629ecca6db489a1a72fc4bda78 +} diff --git a/docs/architecture/adr-031-msg-service.md b/docs/architecture/adr-031-msg-service.md index bd67d090d87e..edd906fdb998 100644 --- a/docs/architecture/adr-031-msg-service.md +++ b/docs/architecture/adr-031-msg-service.md @@ -53,7 +53,7 @@ This isn't necessarily bad, but it does add overhead to creating modules. We decide to use protobuf `service` definitions for defining `Msg`s as well as the code generated by them as a replacement for `Msg` handlers. -Below we define how this will look for the `SubmitProposal` message from `x/gov` module. +Below we define how this will look for the `SubmitProposal` message from `x/gov` module. We start with a `Msg` `service` definition: ```proto @@ -105,7 +105,7 @@ should use the more canonical `Msg...Request` names. Currently, we are encoding `Msg`s as `Any` in `Tx`s which involves packing the binary-encoded `Msg` with its type URL. -The type URL for `MsgSubmitProposal` based on the proto3 spec is `/cosmos.gov.MsgSubmitProposal`. +The type URL for `MsgSubmitProposal` based on the proto3 spec is `/cosmos.gov.MsgSubmitProposal`. The fully-qualified name for the `SubmitProposal` service method above (also based on the proto3 and gRPC specs) is `/cosmos.gov.Msg/SubmitProposal` which varies @@ -117,7 +117,7 @@ In order to encode service methods in transactions, we encode them as `Any`s in the same `TxBody.messages` field as other `Msg`s. We simply set `Any.type_url` to the full-qualified method name (ex. `/cosmos.gov.Msg/SubmitProposal`) and set `Any.value` to the protobuf encoding of the request message -(`MsgSubmitProposal` in this case). +(`MsgSubmitProposal` in this case). ### Decoding @@ -125,7 +125,7 @@ When decoding, `TxBody.UnpackInterfaces` will need a special case to detect if `Any` type URLs match the service method format (ex. `/cosmos.gov.Msg/SubmitProposal`) by checking for two `/` characters. Messages that are method names plus request parameters instead of a normal `Any` messages will get unpacked into the `ServiceMsg` struct: - + ```go type ServiceMsg struct { // MethodName is the fully-qualified service name @@ -139,7 +139,7 @@ type ServiceMsg struct { In the future, `service` definitions may become the primary method for defining `Msg`s. As a starting point, we need to integrate with the SDK's existing routing -and `Msg` interface. +and `Msg` interface. To do this, `ServiceMsg` implements the `sdk.Msg` interface and its handler does the actual method routing, allowing this feature to be added incrementally on top of @@ -218,17 +218,25 @@ Separate handler definition is no longer needed with this approach. ## Consequences +This design changes how a module functionality is exposed and accessed. It deprecates the existing `Handler` interface and `AppModule.Route` in favor of [Protocol Buffer Services](https://developers.google.com/protocol-buffers/docs/proto3#services) and Service Routing described above. This dramatically simplifies the code. We don't need to create handlers and keepers any more. Use of Protocol Buffer auto-generated clients clearly separates the communication interfaces between the module and a modules user. The control logic (aka handlers and keepers) is not exposed any more. A module interface can be seen as a black box accessible through a client API. It's worth to note that the client interfaces are also generated by Protocol Buffers. + +This also allows us to change how we perform functional tests. Instead of mocking AppModules and Router, we will mock a client (server will stay hidden). More specifically: we will never mock `moduleA.MsgServer` in `moduleB`, but rather `moduleA.MsgClient`. One can think about it as working with external services (eg DBs, or online servers...). We assume that the transmission between clients and servers is correctly handled by generated Protocol Buffers. + +Finally, closing a module to client API opens desirable OCAP patterns discussed in ADR-033. Since server implementation and interface is hidden, nobody can hold "keepers"/servers and will be forced to relay on the client interface, which will drive developers for correct encapsulation and software engineering patterns. + ### Pros - communicates return type clearly - manual handler registration and return type marshaling is no longer needed, just implement the interface and register it -- some keeper code could be automatically generate, this would improve the UX of [\#7093](/~https://github.com/cosmos/cosmos-sdk/issues/7093) approach (1) if we chose to adopt that -- generated client code could be useful for clients +- communication interface is automatically generated, the developer can now focus only on the state transition methods - this would improve the UX of [\#7093](/~https://github.com/cosmos/cosmos-sdk/issues/7093) approach (1) if we chose to adopt that +- generated client code could be useful for clients and tests +- dramatically reduces and simplifies the code ### Cons - supporting both this and the current concrete `Msg` type approach simultaneously could be confusing (we could choose to deprecate the current approach) - using `service` definitions outside the context of gRPC could be confusing (but doesn’t violate the proto3 spec) + ## References - [Initial Github Issue \#7122](/~https://github.com/cosmos/cosmos-sdk/issues/7122) diff --git a/x/bank/atlas/atlas-v0.39.1.md b/x/bank/atlas/atlas-v0.39.1.md new file mode 100644 index 000000000000..ef34e02e0098 --- /dev/null +++ b/x/bank/atlas/atlas-v0.39.1.md @@ -0,0 +1,208 @@ +# x/bank + +The `x/bank` module is responsible for handling multi-asset coin transfers between +accounts and tracking special-case pseudo-transfers which must work differently +with particular kinds of accounts. + +## Usage + +1. Import the module. + + ```go + import ( + "github.com/cosmos/cosmos-sdk/x/bank" + ) + ``` + +2. Add `AppModuleBasic` to your `ModuleBasics`. + + ```go + var ( + ModuleBasics = module.NewBasicManager( + // ... + bank.AppModuleBasic{}, + } + ) + ``` + +3. Create the module's parameter subspace in your application constructor. + + ```go + func NewApp(...) *App { + // ... + app.subspaces[bank.ModuleName] = app.ParamsKeeper.Subspace(bank.DefaultParamspace) + } + ``` + +4. Create the keeper. Note, the `x/bank` module depends on the `x/auth` module + and a list of blacklisted account addresses which funds are not allowed to be + sent to. Your application will need to define this method based your needs. + + ```go + func NewApp(...) *App { + // ... + app.BankKeeper = bank.NewBaseKeeper( + app.AccountKeeper, app.subspaces[bank.ModuleName], app.BlacklistedAccAddrs(), + ) + } + ``` + +5. Add the `x/bank` module to the app's `ModuleManager`. + + ```go + func NewApp(...) *App { + // ... + app.mm = module.NewManager( + // ... + bank.NewAppModule(app.BankKeeper, app.AccountKeeper), + // ... + ) + } + ``` + +6. Set the `x/bank` module genesis order. + + ```go + func NewApp(...) *App { + // ... + app.mm.SetOrderInitGenesis(..., bank.ModuleName, ...) + } + ``` + +7. Add the `x/bank` module to the simulation manager (if you have one set). + + ```go + func NewApp(...) *App { + // ... + app.sm = module.NewSimulationManager( + // ... + bank.NewAppModule(app.BankKeeper, app.AccountKeeper), + // ... + ) + } + +## Genesis + +The `x/bank` module defines its genesis state as follows: + +```go +type GenesisState struct { + SendEnabled bool `json:"send_enabled" yaml:"send_enabled"` +} +``` + +The `SendEnabled` parameter determines if transfers are enabled or disabled +entirely on the chain. This can be used to start a network without enabling +transfers while ensuring critical network functionality is operating as expected. + +## Messages + +### `MsgSend` + +The `x/bank` module allows for transfer of funds from a source account to a +destination account. + +```go +type MsgSend struct { + FromAddress sdk.AccAddress `json:"from_address" yaml:"from_address"` + ToAddress sdk.AccAddress `json:"to_address" yaml:"to_address"` + Amount sdk.Coins `json:"amount" yaml:"amount"` +} +``` + +### `MsgMultiSend` + +The `x/bank` module also allows for multiple inputs and outputs. The sum of all +inputs must be equivalent to the sum of all outputs. + +```go +type Input struct { + Address sdk.AccAddress `json:"address" yaml:"address"` + Coins sdk.Coins `json:"coins" yaml:"coins"` +} + +type Output struct { + Address sdk.AccAddress `json:"address" yaml:"address"` + Coins sdk.Coins `json:"coins" yaml:"coins"` +} + +type MsgMultiSend struct { + Inputs []Input `json:"inputs" yaml:"inputs"` + Outputs []Output `json:"outputs" yaml:"outputs"` +} +``` + +## Client + +### CLI + +The `x/bank` supports the following transactional commands. + +1. Send tokens via a `MsgSend` message. + + ```shell + $ tx send [from_key_or_address] [to_address] [amount] [...flags] + ``` + +Note, the `x/bank` module does not natively support constructing a `MsgMultiSend` +message. This type of message must be constructed manually, but it may be signed +and broadcasted via the CLI. + +### REST + +The `x/bank` supports various query API endpoints and a `MsgSend` construction +endpoint. + +1. Construct an unsigned `MsgSend` transaction. + + | Method | Path | + | :----- | :----------------------- | + | `POST` | `/bank/accounts/{address}/transfers` | + + Sample payload: + + ```json + { + "base_req": { + "chain_id": "chain-foo", + "from": "cosmos1u3fneykx9carelvurc6av22vpjvptytj9wklk0", + "memo": "memo", + "fees": [ + { + "denom": "stake", + "amount": "25000" + } + ] + }, + "amount": [ + { + "denom": "stake", + "amount": "400000000" + } + ] + } + ``` + +2. Query for an account's balance. + + | Method | Path | + | :----- | :----------------------- | + | `GET` | `/bank/balances/{address}` | + + Sample response: + + ```json + { + "height": "0", + "result": [ + { + "denom": "node0token", + "amount": "1000000000" + }, + { + "denom": "stake", + "amount": "400000000" + } + ] + } + ``` diff --git a/x/bank/atlas/atlas.toml b/x/bank/atlas/atlas.toml new file mode 100644 index 000000000000..f10863e30b8a --- /dev/null +++ b/x/bank/atlas/atlas.toml @@ -0,0 +1,34 @@ +[module] +description = "The bank module is responsible for handling multi-asset coin transfers between accounts and tracking special-case pseudo-transfers which must work differently with particular kinds of accounts." +homepage = "/~https://github.com/cosmos/cosmos-sdk" +keywords = [ + "tokens", + "bank", + "transfer", + "asset", + "fungible", + "coins", +] + +name = "x/bank" + +[bug_tracker] +url = "/~https://github.com/cosmos/cosmos-sdk/issues" + +[[authors]] +name = "alexanderbez" + +[[authors]] +name = "fedekunze" + +[[authors]] +name = "cwgoes" + +[[authors]] +name = "alessio" + +[version] +documentation = "https://raw.githubusercontent.com/cosmos/cosmos-sdk/master/x/bank/atlas/atlas-v0.39.1.md" +repo = "/~https://github.com/cosmos/cosmos-sdk/releases/tag/v0.39.1" +sdk_compat = "v0.39.x" +version = "v0.39.1"