feat: add capability to reformat Transactions before broadcasting (#12936)

## Description
Increase customized signing capabilities by:
- Adding a hook to preprocess transactions before broadcasting

This PR introduces the ability to preprocess transactions before broadcasting, a feature that provides the ability to add extensions, or otherwise customize the payload to satisfy a chain's requirements. For adaptability, the parameters include the keytype to allow differentiation between formatting transactions for Ledger vs. software keys.

## PR
This PR introduces a hook to preprocess transactions, chiefly to add extensions, from a higher-level chain. Key changes and relevant files:

- Add `PreprocessTxFn` type and fields to both `Context` and `Factory`
- Instantiate the `Factory` with the `PreprocessTxFn` provided by the `Context`
- Call `PreprocessTx` with the appropriate parameters after signing

Closes: #XXXX



---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
Austin Chandra 2022-08-29 18:16:12 -07:00 committed by GitHub
parent 0024a0bf44
commit f8b55ff8ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 3042 additions and 2882 deletions

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,9 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// PreprocessTxFn defines a hook by which chains can preprocess transactions before broadcasting
type PreprocessTxFn func(chainID string, key keyring.KeyType, tx TxBuilder) error
// Context implements a typical context created in SDK modules for transaction
// handling and queries.
type Context struct {
@ -50,6 +53,7 @@ type Context struct {
FeePayer sdk.AccAddress
FeeGranter sdk.AccAddress
Viper *viper.Viper
PreprocessTxHook PreprocessTxFn
// IsAux is true when the signer is an auxiliary signer (e.g. the tipper).
IsAux bool
@ -262,6 +266,13 @@ func (ctx Context) WithAux(isAux bool) Context {
return ctx
}
// WithPreprocessTxHook returns the context with the provided preprocessing hook, which
// enables chains to preprocess the transaction using the builder.
func (ctx Context) WithPreprocessTxHook(preprocessFn PreprocessTxFn) Context {
ctx.PreprocessTxHook = preprocessFn
return ctx
}
// PrintString prints the raw string to ctx.Output if it's defined, otherwise to os.Stdout
func (ctx Context) PrintString(str string) error {
return ctx.PrintBytes([]byte(str))

View File

@ -40,6 +40,7 @@ type Factory struct {
gasPrices sdk.DecCoins
signMode signing.SignMode
simulateAndExecute bool
preprocessTxHook client.PreprocessTxFn
}
// NewFactoryCLI creates a new Factory.
@ -97,6 +98,8 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) Factory {
gasPricesStr, _ := flagSet.GetString(flags.FlagGasPrices)
f = f.WithGasPrices(gasPricesStr)
f = f.WithPreprocessTxHook(clientCtx.PreprocessTxHook)
return f
}
@ -242,6 +245,29 @@ func (f Factory) WithFeePayer(fp sdk.AccAddress) Factory {
return f
}
// WithPreprocessTxHook returns a copy of the Factory with an updated preprocess tx function,
// allows for preprocessing of transaction data using the TxBuilder.
func (f Factory) WithPreprocessTxHook(preprocessFn client.PreprocessTxFn) Factory {
f.preprocessTxHook = preprocessFn
return f
}
// PreprocessTx calls the preprocessing hook with the factory parameters and
// returns the result.
func (f Factory) PreprocessTx(keyname string, builder client.TxBuilder) error {
if f.preprocessTxHook == nil {
// Allow pass-through
return nil
}
key, err := f.Keybase().Key(keyname)
if err != nil {
return fmt.Errorf("error retrieving key from keyring: %w", err)
}
return f.preprocessTxHook(f.chainID, key.GetType(), builder)
}
// BuildUnsignedTx builds a transaction to be signed given a set of messages.
// Once created, the fee, memo, and messages are set.
func (f Factory) BuildUnsignedTx(msgs ...sdk.Msg) (client.TxBuilder, error) {

View File

@ -321,10 +321,19 @@ func Sign(txf Factory, name string, txBuilder client.TxBuilder, overwriteSig boo
}
if overwriteSig {
return txBuilder.SetSignatures(sig)
err = txBuilder.SetSignatures(sig)
} else {
prevSignatures = append(prevSignatures, sig)
err = txBuilder.SetSignatures(prevSignatures...)
}
prevSignatures = append(prevSignatures, sig)
return txBuilder.SetSignatures(prevSignatures...)
if err != nil {
return fmt.Errorf("unable to set signatures on payload: %w", err)
}
// Run optional preprocessing if specified. By default, this is unset
// and will return nil.
return txf.PreprocessTx(name, txBuilder)
}
// GasEstimateResponse defines a response definition for tx gas estimation.

View File

@ -13,12 +13,15 @@ import (
clienttestutil "github.com/cosmos/cosmos-sdk/client/testutil"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing"
ante "github.com/cosmos/cosmos-sdk/x/auth/ante"
"github.com/cosmos/cosmos-sdk/x/auth/signing"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
@ -314,6 +317,81 @@ func TestSign(t *testing.T) {
}
}
func TestPreprocessHook(t *testing.T) {
txConfig, cdc := newTestTxConfig(t)
requireT := require.New(t)
path := hd.CreateHDPath(118, 0, 0).String()
kb, err := keyring.New(t.Name(), "test", t.TempDir(), nil, cdc)
requireT.NoError(err)
from := "test_key"
kr, _, err := kb.NewMnemonic(from, keyring.English, path, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
requireT.NoError(err)
extVal := &testdata.Cat{
Moniker: "einstein",
Lives: 9,
}
extAny, err := codectypes.NewAnyWithValue(extVal)
requireT.NoError(err)
coin := sdk.Coin{
Denom: "atom",
Amount: sdk.NewInt(20),
}
newTip := &txtypes.Tip{
Amount: sdk.Coins{coin},
Tipper: "galaxy",
}
preprocessHook := client.PreprocessTxFn(func(chainID string, key keyring.KeyType, tx client.TxBuilder) error {
extensionBuilder, ok := tx.(authtx.ExtensionOptionsTxBuilder)
requireT.True(ok)
// Set new extension and tip
extensionBuilder.SetExtensionOptions(extAny)
tx.SetTip(newTip)
return nil
})
txfDirect := tx.Factory{}.
WithTxConfig(txConfig).
WithAccountNumber(50).
WithSequence(23).
WithFees("50stake").
WithMemo("memo").
WithChainID("test-chain").
WithKeybase(kb).
WithSignMode(signingtypes.SignMode_SIGN_MODE_DIRECT).
WithPreprocessTxHook(preprocessHook)
addr1, err := kr.GetAddress()
requireT.NoError(err)
msg1 := banktypes.NewMsgSend(addr1, sdk.AccAddress("to"), nil)
msg2 := banktypes.NewMsgSend(addr2, sdk.AccAddress("to"), nil)
txb, err := txfDirect.BuildUnsignedTx(msg1, msg2)
err = tx.Sign(txfDirect, from, txb, false)
requireT.NoError(err)
// Run preprocessing
err = txfDirect.PreprocessTx(from, txb)
requireT.NoError(err)
hasExtOptsTx, ok := txb.(ante.HasExtensionOptionsTx)
requireT.True(ok)
hasOneExt := len(hasExtOptsTx.GetExtensionOptions()) == 1
requireT.True(hasOneExt)
opt := hasExtOptsTx.GetExtensionOptions()[0]
requireT.Equal(opt, extAny)
tip := txb.GetTx().GetTip()
requireT.Equal(tip, newTip)
}
func testSigners(require *require.Assertions, tr signing.Tx, pks ...cryptotypes.PubKey) []signingtypes.SignatureV2 {
sigs, err := tr.GetSignaturesV2()
require.Len(sigs, len(pks))