-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into fedekunze/eip191
- Loading branch information
Showing
7 changed files
with
348 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name: Deploy Documentation | ||
# This job builds and deploys documenation to github pages. | ||
# It runs on every push to master with a change in the docs folder. | ||
on: | ||
push: | ||
branches: | ||
- master | ||
paths: | ||
- "docs/**" | ||
|
||
jobs: | ||
build-and-deploy: | ||
runs-on: ubuntu-latest | ||
container: | ||
image: tendermintdev/docker-website-deployment | ||
steps: | ||
- name: Checkout 🛎️ | ||
uses: actions/checkout@v3 | ||
with: | ||
persist-credentials: false | ||
fetch-depth: 0 | ||
|
||
- name: Install and Build 🔧 | ||
run: | | ||
apk add rsync | ||
make build-docs LEDGER_ENABLED=false | ||
- name: Deploy 🚀 | ||
uses: JamesIves/github-pages-deploy-action@v4.3.0 | ||
with: | ||
branch: gh-pages | ||
folder: docs/output | ||
single-commit: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ private | |
# Build | ||
vendor | ||
build | ||
docs/output | ||
docs/_build | ||
docs/tutorial | ||
docs/node_modules | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
<!-- | ||
order: 15 | ||
--> | ||
|
||
# Transaction Tips | ||
|
||
Transaction tips are a mechanism to pay for transaction fees using another denom than the native fee denom of the chain. {synopsis} | ||
|
||
## Context | ||
|
||
In a Cosmos ecosystem where more and more chains are connected via [IBC](https://ibc.cosmos.network/), it happens that users want to perform actions on chains where they don't have native tokens yet. An example would be an Osmosis user who wants to vote on a proposal on the Cosmos Hub, but they don't have ATOMs in their wallet. A solution would be to swap OSMO for ATOM just for voting on this proposal, but that is cumbersome. Cross-chain DeFi project [Emeris](https://emeris.com/) is another use case. | ||
|
||
Transaction tips is a new solution for cross-chain transaction fees payment, whereby the transaction initiator signs a transaction without specifying fees, but uses a new `Tip` field. They send this signed transaction to a fee relayer who will choose the transaction fees and broadcast the final transaction, and the SDK provides a mechanism that will transfer the pre-defined `Tip` to the fee payer, to cover for fees. | ||
|
||
Assuming we have two chains, A and B, we define the following terms: | ||
|
||
- **the tipper**: this is the initiator of the transaction, who wants to execute a `Msg` on chain A, but doesn't have any native chain A tokens, only chain B tokens. In our example above, the tipper is the Osmosis (chain B) user wanting to vote on a Cosmos Hub (chain A) proposal. | ||
- **the fee payer**: this is the party that will relay and broadcast the final transaction on chain A, and has chain A tokens. The tipper doesn't need to trust the feepayer. | ||
- **the target chain**: the chain where the `Msg` is executed, chain A in this case. | ||
|
||
## Transaction Tips Flow | ||
|
||
The transaction tips flow happens in multipe steps. | ||
|
||
1. The tipper sends via IBC some chain B tokens to chain A. These tokens will cover for fees on the target chain A. This means that chain A's bank module holds some IBC tokens under the tipper's address. | ||
|
||
2. The tipper drafts a transaction to be executed on the chain A. It can include chain A `Msg`s. However, instead of creating a normal transaction, they create the following `AuxSignerData` document: | ||
|
||
+++ https://github.com/cosmos/cosmos-sdk/blob/v0.46.0-beta1/proto/cosmos/tx/v1beta1/tx.proto#L230-L249 | ||
|
||
where we have defined `SignDocDirectAux` as: | ||
|
||
+++ https://github.com/cosmos/cosmos-sdk/blob/v0.46.0-beta1/proto/cosmos/tx/v1beta1/tx.proto#L67-L93 | ||
|
||
where `Tip` is defined as | ||
|
||
+++ https://github.com/cosmos/cosmos-sdk/blob/v0.46.0-beta1/proto/cosmos/tx/v1beta1/tx.proto#L219-L228 | ||
|
||
Notice that this document doesn't sign over the final chain A fees. Instead, it includes a `Tip` field. It also doesn't include the whole `AuthInfo` object as in `SIGN_MODE_DIRECT`, only the minimum information needed by the tipper | ||
|
||
3. The tipper signs the `SignDocDirectAux` document and attaches the signature to the `AuxSignerData`, then sends the signed `AuxSignerData` to the fee payer. | ||
|
||
4. From the signed `AuxSignerData` document, the fee payer constructs a transaction, using the following algorithm: | ||
|
||
- use as `TxBody` the exact `AuxSignerData.SignDocDirectAux.body_bytes`, to not alter the original intent of the tipper, | ||
- create an `AuthInfo` with: | ||
- `AuthInfo.Tip` copied from `AuxSignerData.SignDocDirectAux.Tip`, | ||
- `AuthInfo.Fee` chosen by the fee payer, which should cover for the transaction gas, but also be small enough so that the tip/fee exchange rate is economically interesting for the fee payer, | ||
- `AuthInfo.SignerInfos` has two signers: the first signer is the tipper, using the public key, sequence and sign mode specified in `AuxSignerData`; and the second signer is the fee payer, using their favorite sign mode, | ||
- a `Signatures` array with two items: the tipper's signature from `AuxSignerData.Sig`, and the final fee payer's signature. | ||
|
||
5. Broadcast the final transaction signed by the two parties to the target chain. Once included, the Cosmos SDK will trigger a transfer of the `Tip` specified in the transaction from the tipper address to the fee payer address. | ||
|
||
### Fee Payers Market | ||
|
||
The benefit of transaction tips for the tipper is clear: there is no need to swap tokens before executing a cross-chain message. | ||
|
||
For the fee payer, the benefit is in the tip v.s. fee exchange. Put simply, the fee payer pays the fees of an unknown tipper's transaction, and gets in exchange the tip that the tipper chose. There is an economic incentive for the fee payer to do so only when the tip is greater than the transaction fees, given the exchange rates between the two tokens. | ||
|
||
In the future, we imagine a market where fee payers will compete to include transactions from tippers, who on their side will optimize by specifying the lowest tip possible. A number of automated services might spin up to perform transaction gas simulation and exchange rate monitoring to optimize both the tip and fee values in real-time. | ||
|
||
### Tipper and Fee Payer Sign Modes | ||
|
||
As we mentioned in the flow above, the tipper signs over the `SignDocDirectAux`, and the fee payer signs over the whole final transaction. As such, both parties might use different sign modes. | ||
|
||
- The tipper MUST use `SIGN_MODE_DIRECT_AUX` or `SIGN_MODE_LEGACY_AMINO_JSON`. That is because the tipper needs to sign over the body, the tip, but not the other signers' information and not over the fee (which is unknown to the tipper). | ||
- The fee payer MUST use `SIGN_MODE_DIRECT` or `SIGN_MODE_LEGACY_AMINO_JSON`. The fee payer signs over the whole transaction. | ||
|
||
For example, if the fee payers signs the whole transaction with `SIGN_MODE_DIRECT_AUX`, it will be rejected by the node, as that would introduce malleability issues (`SIGN_MODE_DIRECT_AUX` doesn't sign over fees). | ||
|
||
In both cases, using `SIGN_MODE_LEGACY_AMINO_JSON` is recommended only if hardware wallet signing is needed. | ||
|
||
## Enabling Tips on your Chain | ||
|
||
The transaction tips functionality is introduced in Cosmos SDK v0.46, so earlier versions do not have support for tips. If you're using v0.46 or later, then enabling tips on your chain is as simple as adding the `TipMiddleware` in your middleware stack: | ||
|
||
```go | ||
// NewTxHandler defines a TxHandler middleware stack. | ||
func NewTxHandler(options TxHandlerOptions) (tx.Handler, error) { | ||
// --snip-- | ||
|
||
return ComposeMiddlewares( | ||
// base tx handler that executes Msgs | ||
NewRunMsgsTxHandler(options.MsgServiceRouter, options.LegacyRouter), | ||
// --snip other middlewares-- | ||
|
||
// Add the TipMiddleware | ||
NewTipMiddleware(options.BankKeeper), | ||
) | ||
} | ||
``` | ||
|
||
Notice that `NewTipMiddleware` needs a reference to the BankKeeper, for transferring the tip to the fee payer. | ||
|
||
If you are using the Cosmos SDK's default middleware stack `NewDefaultTxHandler()`, then the tip middleware is included by default. | ||
|
||
## CLI Usage | ||
|
||
The Cosmos SDK also provides some CLI tooling for the transaction tips flow, both for the tipper and for the feepayer. | ||
|
||
For the tipper, the CLI `tx` subcommand has two new flags: `--aux` and `--tip`. The `--aux` flag is used to denote that we are creating a `AuxSignerData` instead of a , and the `--tip` is used to populate its `Tip` field. | ||
|
||
```bash | ||
$ simd tx gov vote 16 yes --from <tipper_address> --aux --tip 50ibcdenom | ||
|
||
|
||
### Prints the AuxSignerData as JSON: | ||
### {"address":"cosmos1q0ayf5vq6fd2xxrwh30upg05hxdnyw2h5249a2","sign_doc":{"body_bytes":"CosBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmsKLWNvc21vczFxMGF5ZjV2cTZmZDJ4eHJ3aDMwdXBnMDVoeGRueXcyaDUyNDlhMhItY29zbW9zMXdlNWoyZXI2MHV5OXF3YzBta3ptdGdtdHA5Z3F5NXY2bjhnZGdlGgsKBXN0YWtlEgIxMA==","public_key":{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"AojOF/1luQ5H/nZDSrE1w3CyzGJhJdQuS7hFX5wAA6uJ"},"chain_id":"","account_number":"0","sequence":"1","tip":{"amount":[{"denom":"ibcdenom","amount":"50"}],"tipper":"cosmos1q0ayf5vq6fd2xxrwh30upg05hxdnyw2h5249a2"}},"mode":"SIGN_MODE_DIRECT_AUX","sig":"v/d/bGq9FGdecs6faMG2t//nRirFTiqwFtUB65M6kh0QdUeM6jg3r8oJX1o17xkoDxJ09EyJiSyvo6fbU7vUxg=="} | ||
``` | ||
|
||
It is useful to pipe the JSON output to a file, `> aux_signed_tx.json` | ||
|
||
For the fee payer, the Cosmos SDK added a `tx aux-to-fee` subcommand to include a `AuxSignerData` into a transaction, add fees to it, and broadcast it. | ||
|
||
```bash | ||
$ simd tx aux-to-fee aux_signed_tx.json --from <fee_payer_address> --fees 30atom | ||
|
||
### Prints the broadcasted tx response: | ||
### code: 0 | ||
### codespace: sdk | ||
### data: "" | ||
### events: [] | ||
### gas_used: "0" | ||
### gas_wanted: "0" | ||
### height: "0" | ||
### info: "" | ||
### logs: [] | ||
### timestamp: "" | ||
### tx: null | ||
``` | ||
|
||
Upon completion of the second command, the fee payer's balance will be down the `30atom` fees, and up the `50ibcdenom` tip. | ||
|
||
For both commands, the flag `--sign-mode=amino-json` is still available for hardware wallet signing. | ||
|
||
## Programmatic Usage | ||
|
||
For the tipper, the SDK exposes a new transaction builder, the `AuxTxBuilder`, for generating an `AuxSignerData`. The API of `AuxTxBuilder` is defined [in `client/tx`](https://github.com/cosmos/cosmos-sdk/blob/v0.46.0-beta1/client/tx/aux_builder.go#L16), and can be used as follows: | ||
|
||
```go | ||
// Note: there's no need to use clientCtx.TxConfig anymore. | ||
|
||
bldr := clienttx.NewAuxTxBuilder() | ||
err := bldr.SetMsgs(msgs...) | ||
bldr.SetAddress("cosmos1...") | ||
bldr.SetMemo(...) | ||
bldr.SetTip(...) | ||
bldr.SetPubKey(...) | ||
err := bldr.SetSignMode(...) // DIRECT_AUX or AMINO, or else error | ||
// ... other setters are also available | ||
|
||
// Get the bytes to sign. | ||
signBz, err := bldr.GetSignBytes() | ||
|
||
// Sign the bz using your favorite method. | ||
sig, err := privKey.sign(signBz) | ||
|
||
// Set the signature | ||
bldr.SetSig(sig) | ||
|
||
// Get the final auxSignerData to be sent to the fee payer | ||
auxSignerData, err:= bldr.GetAuxSignerData() | ||
``` | ||
|
||
For the fee payer, the SDK added a new method on the existing `TxBuilder` to import data from an `AuxSignerData`: | ||
|
||
```go | ||
// get `auxSignerData` from tipper, see code snippet above. | ||
|
||
txBuilder := clientCtx.TxConfig.NewTxBuilder() | ||
err := txBuilder.AddAuxSignerData(auxSignerData) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// A lot of fields will be populated in txBuilder, such as its Msgs, tip | ||
// memo, etc... | ||
|
||
// The fee payer choses the fee to set on the transaction. | ||
txBuilder.SetFeePayer(<fee_payer_address>) | ||
txBuilder.SetFeeAmount(...) | ||
txBuilder.SetGasLimit(...) | ||
|
||
// Usual signing code | ||
err = authclient.SignTx(...) | ||
if err != nil { | ||
return err | ||
} | ||
``` |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package distribution_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" | ||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" | ||
"github.com/cosmos/cosmos-sdk/simapp" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
"github.com/cosmos/cosmos-sdk/x/staking/teststaking" | ||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" | ||
"github.com/stretchr/testify/require" | ||
abci "github.com/tendermint/tendermint/abci/types" | ||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types" | ||
) | ||
|
||
const ( | ||
totalValidators = 6 | ||
lazyValidatorIdx = 2 | ||
power = 100 / totalValidators | ||
) | ||
|
||
var ( | ||
valTokens = sdk.TokensFromConsensusPower(50, sdk.DefaultPowerReduction) | ||
validatorCommissionRates = stakingtypes.NewCommissionRates(sdk.OneDec(), sdk.OneDec(), sdk.OneDec()) | ||
) | ||
|
||
type validator struct { | ||
addr sdk.ValAddress | ||
pubkey cryptotypes.PubKey | ||
votes []abci.VoteInfo | ||
} | ||
|
||
// Context in https://github.com/cosmos/cosmos-sdk/issues/9161 | ||
func TestVerifyProposerRewardAssignement(t *testing.T) { | ||
app := simapp.Setup(t, false) | ||
ctx := app.BaseApp.NewContext(false, tmproto.Header{}) | ||
addrs := simapp.AddTestAddrsIncremental(app, ctx, totalValidators, valTokens) | ||
tstaking := teststaking.NewHelper(t, ctx, app.StakingKeeper) | ||
tstaking.Commission = validatorCommissionRates | ||
|
||
// create validators | ||
validators := make([]validator, totalValidators-1) | ||
for i := range validators { | ||
validators[i].addr = sdk.ValAddress(addrs[i]) | ||
validators[i].pubkey = ed25519.GenPrivKey().PubKey() | ||
validators[i].votes = make([]abci.VoteInfo, totalValidators) | ||
tstaking.CreateValidatorWithValPower(validators[i].addr, validators[i].pubkey, power, true) | ||
} | ||
app.EndBlock(abci.RequestEndBlock{}) | ||
require.NotEmpty(t, app.Commit()) | ||
|
||
// verify validators lists | ||
require.Len(t, app.StakingKeeper.GetAllValidators(ctx), totalValidators) | ||
for i, val := range validators { | ||
// verify all validator exists | ||
require.NotNil(t, app.StakingKeeper.ValidatorByConsAddr(ctx, sdk.GetConsAddress(val.pubkey))) | ||
|
||
// populate last commit info | ||
voteInfos := []abci.VoteInfo{} | ||
for _, val2 := range validators { | ||
voteInfos = append(voteInfos, abci.VoteInfo{ | ||
Validator: abci.Validator{ | ||
Address: sdk.GetConsAddress(val2.pubkey), | ||
Power: power, | ||
}, | ||
SignedLastBlock: true, | ||
}) | ||
} | ||
|
||
// have this validator only submit the minimum amount of pre-commits | ||
if i == lazyValidatorIdx { | ||
for j := totalValidators * 2 / 3; j < len(voteInfos); j++ { | ||
voteInfos[j].SignedLastBlock = false | ||
} | ||
} | ||
|
||
validators[i].votes = voteInfos | ||
} | ||
|
||
// previous block submitted by validator n-1 (with 100% previous commits) and proposed by lazy validator | ||
app.BeginBlock(abci.RequestBeginBlock{ | ||
Header: tmproto.Header{Height: app.LastBlockHeight() + 1, ProposerAddress: sdk.GetConsAddress(validators[lazyValidatorIdx].pubkey)}, | ||
LastCommitInfo: abci.LastCommitInfo{Votes: validators[lazyValidatorIdx-1].votes}, | ||
}) | ||
require.NotEmpty(t, app.Commit()) | ||
|
||
// previous block submitted by lazy validator (with 67% previous commits) and proposed by validator n+1 | ||
app.BeginBlock(abci.RequestBeginBlock{ | ||
Header: tmproto.Header{Height: app.LastBlockHeight() + 1, ProposerAddress: sdk.GetConsAddress(validators[lazyValidatorIdx+1].pubkey)}, | ||
LastCommitInfo: abci.LastCommitInfo{Votes: validators[lazyValidatorIdx].votes}, | ||
}) | ||
require.NotEmpty(t, app.Commit()) | ||
|
||
// previous block submitted by validator n+1 (with 100% previous commits) and proposed by validator n+2 | ||
app.BeginBlock(abci.RequestBeginBlock{ | ||
Header: tmproto.Header{Height: app.LastBlockHeight() + 1, ProposerAddress: sdk.GetConsAddress(validators[lazyValidatorIdx+2].pubkey)}, | ||
LastCommitInfo: abci.LastCommitInfo{Votes: validators[lazyValidatorIdx+1].votes}, | ||
}) | ||
require.NotEmpty(t, app.Commit()) | ||
|
||
rewardsValidatorBeforeLazyValidator := app.DistrKeeper.GetValidatorOutstandingRewardsCoins(ctx, validators[lazyValidatorIdx+1].addr) | ||
rewardsLazyValidator := app.DistrKeeper.GetValidatorOutstandingRewardsCoins(ctx, validators[lazyValidatorIdx].addr) | ||
rewardsValidatorAfterLazyValidator := app.DistrKeeper.GetValidatorOutstandingRewardsCoins(ctx, validators[lazyValidatorIdx+1].addr) | ||
require.True(t, rewardsLazyValidator[0].Amount.LT(rewardsValidatorAfterLazyValidator[0].Amount)) | ||
require.Equal(t, rewardsValidatorBeforeLazyValidator, rewardsValidatorAfterLazyValidator) | ||
} |