diff --git a/client/v2/CHANGELOG.md b/client/v2/CHANGELOG.md index 831ee40ce922..f7e469069c98 100644 --- a/client/v2/CHANGELOG.md +++ b/client/v2/CHANGELOG.md @@ -42,6 +42,25 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#18626](https://github.com/cosmos/cosmos-sdk/pull/18626) Support for off-chain signing and verification of a file. * [#18461](https://github.com/cosmos/cosmos-sdk/pull/18461) Support governance proposals. +<<<<<<< HEAD +======= +* [#20623](https://github.com/cosmos/cosmos-sdk/pull/20623) Introduce client/v2 tx factory. +* [#20623](https://github.com/cosmos/cosmos-sdk/pull/20623) Extend client/v2 keyring interface with `KeyType` and `KeyInfo`. + +### Improvements + +* [#21936](https://github.com/cosmos/cosmos-sdk/pull/21936) Print possible enum values in error message after an invalid input was provided. + +### API Breaking Changes + +* [#17709](https://github.com/cosmos/cosmos-sdk/pull/17709) Address codecs have been removed from `autocli.AppOptions` and `flag.Builder`. Instead client/v2 uses the address codecs present in the context (introduced in [#17503](https://github.com/cosmos/cosmos-sdk/pull/17503)). + +### Bug Fixes + +* [#21853](https://github.com/cosmos/cosmos-sdk/pull/21853) Fix `*big.Int` unmarshalling in txs. + +## [v2.0.0-beta.5] - 2024-09-18 +>>>>>>> 43c41be13 (fix(client/v2): *big.Int unmarshal (#21853)) ### Improvements diff --git a/client/v2/autocli/flag/builder.go b/client/v2/autocli/flag/builder.go index 6ff325c53bdb..bdd42634dd35 100644 --- a/client/v2/autocli/flag/builder.go +++ b/client/v2/autocli/flag/builder.go @@ -26,6 +26,7 @@ const ( ValidatorAddressStringScalarType = "cosmos.ValidatorAddressString" ConsensusAddressStringScalarType = "cosmos.ConsensusAddressString" PubkeyScalarType = "cosmos.Pubkey" + DecScalarType = "cosmos.Dec" ) // Builder manages options for building pflag flags for protobuf messages. @@ -67,6 +68,7 @@ func (b *Builder) init() { b.scalarFlagTypes[ValidatorAddressStringScalarType] = validatorAddressStringType{} b.scalarFlagTypes[ConsensusAddressStringScalarType] = consensusAddressStringType{} b.scalarFlagTypes[PubkeyScalarType] = pubkeyType{} + b.scalarFlagTypes[DecScalarType] = decType{} } } diff --git a/client/v2/autocli/flag/legacy_dec.go b/client/v2/autocli/flag/legacy_dec.go new file mode 100644 index 000000000000..073afa94f1f5 --- /dev/null +++ b/client/v2/autocli/flag/legacy_dec.go @@ -0,0 +1,48 @@ +package flag + +import ( + "context" + + "google.golang.org/protobuf/reflect/protoreflect" + + "cosmossdk.io/math" +) + +type decType struct{} + +func (a decType) NewValue(_ *context.Context, _ *Builder) Value { + return &decValue{} +} + +func (a decType) DefaultValue() string { + return "0" +} + +type decValue struct { + value string +} + +func (a decValue) Get(protoreflect.Value) (protoreflect.Value, error) { + return protoreflect.ValueOf(a.value), nil +} + +func (a decValue) String() string { + return a.value +} + +func (a *decValue) Set(s string) error { + dec, err := math.LegacyNewDecFromStr(s) + if err != nil { + return err + } + + // we need to convert from float representation to non-float representation using default precision + // 0.5 -> 500000000000000000 + a.value = dec.BigInt().String() + + return nil +} + +func (a decValue) Type() string { + return "cosmos.Dec" +} diff --git a/x/auth/tx/builder.go b/x/auth/tx/builder.go index 423bca3a0870..2b1c866e8e0a 100644 --- a/x/auth/tx/builder.go +++ b/x/auth/tx/builder.go @@ -112,7 +112,7 @@ var marshalOption = proto.MarshalOptions{ func (w *builder) getTx() (*gogoTxWrapper, error) { anyMsgs, err := msgsV1toAnyV2(w.msgs) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to convert messages: %w", err) } body := &txv1beta1.TxBody{ Messages: anyMsgs, @@ -136,12 +136,12 @@ func (w *builder) getTx() (*gogoTxWrapper, error) { bodyBytes, err := marshalOption.Marshal(body) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to marshal body: %w", err) } authInfoBytes, err := marshalOption.Marshal(authInfo) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to marshal auth info: %w", err) } txRawBytes, err := marshalOption.Marshal(&txv1beta1.TxRaw{ @@ -150,12 +150,12 @@ func (w *builder) getTx() (*gogoTxWrapper, error) { Signatures: w.signatures, }) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to marshal tx raw: %w", err) } decodedTx, err := w.decoder.Decode(txRawBytes) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to decode tx: %w", err) } return newWrapperFromDecodedTx(w.addressCodec, w.codec, decodedTx) diff --git a/x/tx/decode/decode.go b/x/tx/decode/decode.go new file mode 100644 index 000000000000..bf4f3a54f31f --- /dev/null +++ b/x/tx/decode/decode.go @@ -0,0 +1,226 @@ +package decode + +import ( + "crypto/sha256" + "errors" + "fmt" + "reflect" + "strings" + + gogoproto "github.com/cosmos/gogoproto/proto" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/dynamicpb" + + v1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" + "cosmossdk.io/core/transaction" + errorsmod "cosmossdk.io/errors" + "cosmossdk.io/x/tx/signing" +) + +// DecodedTx contains the decoded transaction, its signers, and other flags. +type DecodedTx struct { + DynamicMessages []proto.Message + Messages []gogoproto.Message + Tx *v1beta1.Tx + TxRaw *v1beta1.TxRaw + Signers [][]byte + TxBodyHasUnknownNonCriticals bool + + // Cache for hash and full bytes + cachedHash [32]byte + cachedBytes []byte + cachedHashed bool +} + +var _ transaction.Tx = &DecodedTx{} + +type gogoProtoCodec interface { + Unmarshal([]byte, gogoproto.Message) error +} + +// Decoder contains the dependencies required for decoding transactions. +type Decoder struct { + signingCtx *signing.Context + codec gogoProtoCodec +} + +// Options are options for creating a Decoder. +type Options struct { + SigningContext *signing.Context + ProtoCodec gogoProtoCodec +} + +// NewDecoder creates a new Decoder for decoding transactions. +func NewDecoder(options Options) (*Decoder, error) { + if options.SigningContext == nil { + return nil, errors.New("signing context is required") + } + if options.ProtoCodec == nil { + return nil, errors.New("proto codec is required for unmarshalling gogoproto messages") + } + return &Decoder{ + signingCtx: options.SigningContext, + codec: options.ProtoCodec, + }, nil +} + +// Decode decodes raw protobuf encoded transaction bytes into a DecodedTx. +func (d *Decoder) Decode(txBytes []byte) (*DecodedTx, error) { + // Make sure txBytes follow ADR-027. + err := rejectNonADR027TxRaw(txBytes) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, err.Error()) + } + + var raw v1beta1.TxRaw + + // reject all unknown proto fields in the root TxRaw + fileResolver := d.signingCtx.FileResolver() + err = RejectUnknownFieldsStrict(txBytes, raw.ProtoReflect().Descriptor(), fileResolver) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, err.Error()) + } + + err = proto.Unmarshal(txBytes, &raw) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, err.Error()) + } + + var body v1beta1.TxBody + + // allow non-critical unknown fields in TxBody + txBodyHasUnknownNonCriticals, err := RejectUnknownFields(raw.BodyBytes, body.ProtoReflect().Descriptor(), true, fileResolver) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, err.Error()) + } + + err = proto.Unmarshal(raw.BodyBytes, &body) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, err.Error()) + } + + var authInfo v1beta1.AuthInfo + + // reject all unknown proto fields in AuthInfo + err = RejectUnknownFieldsStrict(raw.AuthInfoBytes, authInfo.ProtoReflect().Descriptor(), fileResolver) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, err.Error()) + } + + err = proto.Unmarshal(raw.AuthInfoBytes, &authInfo) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, err.Error()) + } + + theTx := &v1beta1.Tx{ + Body: &body, + AuthInfo: &authInfo, + Signatures: raw.Signatures, + } + + var ( + signers [][]byte + dynamicMsgs []proto.Message + msgs []gogoproto.Message + ) + seenSigners := map[string]struct{}{} + for _, anyMsg := range body.Messages { + typeURL := strings.TrimPrefix(anyMsg.TypeUrl, "/") + + // unmarshal into dynamic message + msgDesc, err := fileResolver.FindDescriptorByName(protoreflect.FullName(typeURL)) + if err != nil { + return nil, fmt.Errorf("protoFiles does not have descriptor %s: %w", anyMsg.TypeUrl, err) + } + dynamicMsg := dynamicpb.NewMessageType(msgDesc.(protoreflect.MessageDescriptor)).New().Interface() + err = anyMsg.UnmarshalTo(dynamicMsg) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, fmt.Sprintf("cannot unmarshal Any message: %v", err)) + } + dynamicMsgs = append(dynamicMsgs, dynamicMsg) + + // unmarshal into gogoproto message + gogoType := gogoproto.MessageType(typeURL) + if gogoType == nil { + return nil, fmt.Errorf("cannot find type: %s", anyMsg.TypeUrl) + } + msg := reflect.New(gogoType.Elem()).Interface().(gogoproto.Message) + err = d.codec.Unmarshal(anyMsg.Value, msg) + if err != nil { + return nil, errorsmod.Wrap(ErrTxDecode, err.Error()) + } + msgs = append(msgs, msg) + + // fetch signers with dynamic message + ss, signerErr := d.signingCtx.GetSigners(dynamicMsg) + if signerErr != nil { + return nil, errorsmod.Wrap(ErrTxDecode, signerErr.Error()) + } + for _, s := range ss { + _, seen := seenSigners[string(s)] + if seen { + continue + } + signers = append(signers, s) + seenSigners[string(s)] = struct{}{} + } + } + + return &DecodedTx{ + Messages: msgs, + DynamicMessages: dynamicMsgs, + Tx: theTx, + TxRaw: &raw, + TxBodyHasUnknownNonCriticals: txBodyHasUnknownNonCriticals, + Signers: signers, + }, nil +} + +// Hash implements the interface for the Tx interface. +func (dtx *DecodedTx) Hash() [32]byte { + if !dtx.cachedHashed { + dtx.computeHashAndBytes() + } + return dtx.cachedHash +} + +func (dtx *DecodedTx) GetGasLimit() (uint64, error) { + if dtx == nil || dtx.Tx == nil || dtx.Tx.AuthInfo == nil || dtx.Tx.AuthInfo.Fee == nil { + return 0, errors.New("gas limit not available or one or more required fields are nil") + } + return dtx.Tx.AuthInfo.Fee.GasLimit, nil +} + +func (dtx *DecodedTx) GetMessages() ([]transaction.Msg, error) { + if dtx == nil || dtx.Messages == nil { + return nil, errors.New("messages not available or are nil") + } + + return dtx.Messages, nil +} + +func (dtx *DecodedTx) GetSenders() ([][]byte, error) { + if dtx == nil || dtx.Signers == nil { + return nil, errors.New("senders not available or are nil") + } + return dtx.Signers, nil +} + +func (dtx *DecodedTx) Bytes() []byte { + if !dtx.cachedHashed { + dtx.computeHashAndBytes() + } + return dtx.cachedBytes +} + +func (dtx *DecodedTx) computeHashAndBytes() { + bz, err := proto.Marshal(dtx.TxRaw) + if err != nil { + panic(err) + } + + dtx.cachedBytes = bz + dtx.cachedHash = sha256.Sum256(bz) + dtx.cachedHashed = true +}