From 2ec1adebb3fb7c44f31fd45f99a5abbfbc907654 Mon Sep 17 00:00:00 2001 From: jayy04 <103467857+jayy04@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:26:50 -0400 Subject: [PATCH] [CT-1161] add signature verification authenticator (#2183) --- .../authenticator/authentication_request.go | 284 ++++++++++++ .../x/accountplus/authenticator/base_test.go | 180 ++++++++ protocol/x/accountplus/authenticator/iface.go | 51 +++ .../x/accountplus/authenticator/requests.go | 49 +++ .../authenticator/signature_authenticator.go | 114 +++++ .../signature_authenticator_test.go | 406 ++++++++++++++++++ 6 files changed, 1084 insertions(+) create mode 100644 protocol/x/accountplus/authenticator/authentication_request.go create mode 100644 protocol/x/accountplus/authenticator/base_test.go create mode 100644 protocol/x/accountplus/authenticator/iface.go create mode 100644 protocol/x/accountplus/authenticator/requests.go create mode 100644 protocol/x/accountplus/authenticator/signature_authenticator.go create mode 100644 protocol/x/accountplus/authenticator/signature_authenticator_test.go diff --git a/protocol/x/accountplus/authenticator/authentication_request.go b/protocol/x/accountplus/authenticator/authentication_request.go new file mode 100644 index 0000000000..d7ff749812 --- /dev/null +++ b/protocol/x/accountplus/authenticator/authentication_request.go @@ -0,0 +1,284 @@ +package authenticator + +import ( + "fmt" + + txsigning "cosmossdk.io/x/tx/signing" + + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + + "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/codec" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// +// These structs define the data structure for authentication, used with AuthenticationRequest struct. +// + +// SignModeData represents the signing modes with direct bytes and textual representation. +type SignModeData struct { + Direct []byte `json:"sign_mode_direct"` + Textual string `json:"sign_mode_textual"` +} + +// LocalAny holds a message with its type URL and byte value. This is necessary because the type Any fails +// to serialize and deserialize properly in nested contexts. +type LocalAny struct { + TypeURL string `json:"type_url"` + Value []byte `json:"value"` +} + +// SimplifiedSignatureData contains lists of signers and their corresponding signatures. +type SimplifiedSignatureData struct { + Signers []sdk.AccAddress `json:"signers"` + Signatures [][]byte `json:"signatures"` +} + +// ExplicitTxData encapsulates key transaction data like chain ID, account info, and messages. +type ExplicitTxData struct { + ChainID string `json:"chain_id"` + AccountNumber uint64 `json:"account_number"` + AccountSequence uint64 `json:"sequence"` + TimeoutHeight uint64 `json:"timeout_height"` + Msgs []LocalAny `json:"msgs"` + Memo string `json:"memo"` +} + +// GetSignerAndSignatures gets an array of signer and an array of signatures from the transaction +// checks they're the same length and returns both. +// +// A signer can only have one signature, so if it appears in multiple messages, the signatures must be +// the same, and it will only be returned once by this function. This is to mimic the way the classic +// sdk authentication works, and we will probably want to change this in the future +func GetSignerAndSignatures(tx sdk.Tx) (signers []sdk.AccAddress, signatures []signing.SignatureV2, err error) { + // Attempt to cast the provided transaction to an authsigning.Tx. + sigTx, ok := tx.(authsigning.Tx) + if !ok { + return nil, nil, + errorsmod.Wrap(sdkerrors.ErrTxDecode, "invalid transaction type") + } + + // Retrieve signatures from the transaction. + signatures, err = sigTx.GetSignaturesV2() + if err != nil { + return nil, nil, err + } + + // Retrieve messages from the transaction. + signerBytes, err := sigTx.GetSigners() + if err != nil { + return nil, nil, err + } + + for _, signer := range signerBytes { + signers = append(signers, sdk.AccAddress(signer)) + } + + // check that signer length and signature length are the same + if len(signatures) != len(signers) { + return nil, + nil, + errorsmod.Wrap( + sdkerrors.ErrTxDecode, + fmt.Sprintf( + "invalid number of signer; expected: %d, got %d", + len(signers), + len(signatures), + ), + ) + } + + return signers, signatures, nil +} + +// getSignerData returns the signer data for a given account. This is part of the data that needs to be signed. +func getSignerData(ctx sdk.Context, ak authante.AccountKeeper, account sdk.AccAddress) authsigning.SignerData { + // Retrieve and build the signer data struct + baseAccount := ak.GetAccount(ctx, account) + genesis := ctx.BlockHeight() == 0 + chainID := ctx.ChainID() + var accNum uint64 + if !genesis { + accNum = baseAccount.GetAccountNumber() + } + var sequence uint64 + if baseAccount != nil { + sequence = baseAccount.GetSequence() + } + + return authsigning.SignerData{ + ChainID: chainID, + AccountNumber: accNum, + Sequence: sequence, + } +} + +// extractExplicitTxData makes the transaction data concrete for the authentication request. This is necessary to +// pass the parsed data to the cosmwasm authenticator. +func extractExplicitTxData(tx sdk.Tx, signerData authsigning.SignerData) (ExplicitTxData, error) { + timeoutTx, ok := tx.(sdk.TxWithTimeoutHeight) + if !ok { + return ExplicitTxData{}, errorsmod.Wrap(sdkerrors.ErrInvalidType, "failed to cast tx to TxWithTimeoutHeight") + } + memoTx, ok := tx.(sdk.TxWithMemo) + if !ok { + return ExplicitTxData{}, errorsmod.Wrap(sdkerrors.ErrInvalidType, "failed to cast tx to TxWithMemo") + } + + // Encode messages as Anys and manually convert them to a struct we can serialize to json for cosmwasm. + txMsgs := tx.GetMsgs() + msgs := make([]LocalAny, len(txMsgs)) + for i, txMsg := range txMsgs { + encodedMsg, err := types.NewAnyWithValue(txMsg) + if err != nil { + return ExplicitTxData{}, errorsmod.Wrap(err, "failed to encode msg") + } + msgs[i] = LocalAny{ + TypeURL: encodedMsg.TypeUrl, + Value: encodedMsg.Value, + } + } + + return ExplicitTxData{ + ChainID: signerData.ChainID, + AccountNumber: signerData.AccountNumber, + AccountSequence: signerData.Sequence, + TimeoutHeight: timeoutTx.GetTimeoutHeight(), + Msgs: msgs, + Memo: memoTx.GetMemo(), + }, nil +} + +// extractSignatures returns the signature data for each signature in the transaction and +// the one for the current signer. +// +// This function also checks for replay attacks. The replay protection needs to be able to match the signature to the +// corresponding signer, which involves iterating over the signatures. To avoid iterating over the signatures twice, +// we do replay protection here instead of in a separate replay protection function. +// +// Only SingleSignatureData is supported. Multisigs can be implemented by using partitioned compound authenticators +func extractSignatures( + txSigners []sdk.AccAddress, + txSignatures []signing.SignatureV2, + account sdk.AccAddress, +) (signatures [][]byte, msgSignature []byte, err error) { + for i, signature := range txSignatures { + single, ok := signature.Data.(*signing.SingleSignatureData) + if !ok { + return nil, + nil, + errorsmod.Wrap( + sdkerrors.ErrInvalidType, + "failed to cast signature to SingleSignatureData", + ) + } + + signatures = append(signatures, single.Signature) + + if txSigners[i].Equals(account) { + msgSignature = single.Signature + } + } + return signatures, msgSignature, nil +} + +// GenerateAuthenticationRequest creates an AuthenticationRequest for the transaction. +func GenerateAuthenticationRequest( + ctx sdk.Context, + cdc codec.Codec, + ak authante.AccountKeeper, + sigModeHandler *txsigning.HandlerMap, + account sdk.AccAddress, + feePayer sdk.AccAddress, + feeGranter sdk.AccAddress, + fee sdk.Coins, + msg sdk.Msg, + tx sdk.Tx, + msgIndex int, + simulate bool, +) (AuthenticationRequest, error) { + // Only supporting one signer per message. This will be enforced in sdk v0.50 + signers, _, err := cdc.GetMsgV1Signers(msg) + if err != nil { + return AuthenticationRequest{}, err + } + signer := sdk.AccAddress(signers[0]) + if !signer.Equals(account) { + return AuthenticationRequest{}, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "invalid signer") + } + + // Get the signers and signatures from the transaction. A signer can only have one signature, so if it + // appears in multiple messages, the signatures must be the same, and it will only be returned once by + // this function. This is to mimic the way the classic sdk authentication works, and we will probably want + // to change this in the future + txSigners, txSignatures, err := GetSignerAndSignatures(tx) + if err != nil { + return AuthenticationRequest{}, errorsmod.Wrap(err, "failed to get signers and signatures") + } + + // Get the signer data for the account. This is needed in the SignDoc + signerData := getSignerData(ctx, ak, account) + + // Get the concrete transaction data to be passed to the authenticators + txData, err := extractExplicitTxData(tx, signerData) + if err != nil { + return AuthenticationRequest{}, errorsmod.Wrap(err, "failed to get explicit tx data") + } + + // Get the signatures for the transaction and execute replay protection + signatures, msgSignature, err := extractSignatures(txSigners, txSignatures, account) + if err != nil { + return AuthenticationRequest{}, errorsmod.Wrap(err, "failed to get signatures") + } + + // Build the authentication request + authRequest := AuthenticationRequest{ + Account: account, + FeePayer: feePayer, + FeeGranter: feeGranter, + Fee: fee, + Msg: txData.Msgs[msgIndex], + MsgIndex: uint64(msgIndex), + Signature: msgSignature, + TxData: txData, + SignModeTxData: SignModeData{ + Direct: []byte("signBytes"), + }, + SignatureData: SimplifiedSignatureData{ + Signers: txSigners, + Signatures: signatures, + }, + Simulate: simulate, + AuthenticatorParams: nil, + } + + // We do not generate the sign bytes if simulate is true + if simulate { + return authRequest, nil + } + + // Get the sign bytes for the transaction + signBytes, err := authsigning.GetSignBytesAdapter( + ctx, + sigModeHandler, + signing.SignMode_SIGN_MODE_DIRECT, + signerData, + tx, + ) + if err != nil { + return AuthenticationRequest{}, errorsmod.Wrap(err, "failed to get signBytes") + } + + // TODO: Add other sign modes. Specifically json when it becomes available + authRequest.SignModeTxData = SignModeData{ + Direct: signBytes, + } + + return authRequest, nil +} diff --git a/protocol/x/accountplus/authenticator/base_test.go b/protocol/x/accountplus/authenticator/base_test.go new file mode 100644 index 0000000000..dbc6a89f0a --- /dev/null +++ b/protocol/x/accountplus/authenticator/base_test.go @@ -0,0 +1,180 @@ +package authenticator_test + +import ( + "encoding/hex" + "fmt" + "math/rand" + + storetypes "cosmossdk.io/store/types" + "github.com/cometbft/cometbft/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/bank/testutil" + "github.com/dydxprotocol/v4-chain/protocol/app" + testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + "github.com/dydxprotocol/v4-chain/protocol/x/accountplus/authenticator" + smartaccounttypes "github.com/dydxprotocol/v4-chain/protocol/x/accountplus/types" + "github.com/stretchr/testify/suite" +) + +type BaseAuthenticatorSuite struct { + suite.Suite + tApp *testapp.TestApp + Ctx sdk.Context + EncodingConfig app.EncodingConfig + SigVerificationAuthenticator authenticator.SignatureVerification + TestKeys []string + TestAccAddress []sdk.AccAddress + TestPrivKeys []*secp256k1.PrivKey + HomeDir string +} + +func (s *BaseAuthenticatorSuite) SetupKeys() { + // Test data for authenticator signature verification + TestKeys := []string{ + "6cf5103c60c939a5f38e383b52239c5296c968579eec1c68a47d70fbf1d19159", + "0dd4d1506e18a5712080708c338eb51ecf2afdceae01e8162e890b126ac190fe", + "49006a359803f0602a7ec521df88bf5527579da79112bb71f285dd3e7d438033", + } + accounts := make([]sdk.AccountI, 0) + // Set up test accounts + for _, key := range TestKeys { + bz, _ := hex.DecodeString(key) + priv := &secp256k1.PrivKey{Key: bz} + + // add the test private keys to array for later use + s.TestPrivKeys = append(s.TestPrivKeys, priv) + + accAddress := sdk.AccAddress(priv.PubKey().Address()) + accounts = append( + accounts, + authtypes.NewBaseAccount(accAddress, priv.PubKey(), 0, 0), + ) + + // add the test accounts to array for later use + s.TestAccAddress = append(s.TestAccAddress, accAddress) + } + + s.HomeDir = fmt.Sprintf("%d", rand.Int()) + s.tApp = testapp.NewTestAppBuilder(s.T()).WithGenesisDocFn(func() (genesis types.GenesisDoc) { + genesis = testapp.DefaultGenesis() + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *authtypes.GenesisState) { + for _, acct := range accounts { + genesisState.Accounts = append(genesisState.Accounts, codectypes.UnsafePackAny(acct)) + } + }, + ) + return genesis + }).Build() + s.Ctx = s.tApp.InitChain() + s.Ctx = s.Ctx.WithGasMeter(storetypes.NewGasMeter(1_000_000)) + + s.EncodingConfig = app.GetEncodingConfig() +} + +func (s *BaseAuthenticatorSuite) GenSimpleTx(msgs []sdk.Msg, signers []cryptotypes.PrivKey) (sdk.Tx, error) { + txconfig := app.GetEncodingConfig().TxConfig + feeCoins := constants.TestFeeCoins_5Cents + var accNums []uint64 + var accSeqs []uint64 + + ak := s.tApp.App.AccountKeeper + + for _, signer := range signers { + var account sdk.AccountI + if ak.HasAccount(s.Ctx, sdk.AccAddress(signer.PubKey().Address())) { + account = ak.GetAccount(s.Ctx, sdk.AccAddress(signer.PubKey().Address())) + } else { + account = authtypes.NewBaseAccount( + sdk.AccAddress(signer.PubKey().Address()), + signer.PubKey(), + ak.NextAccountNumber(s.Ctx), + 0, + ) + } + accNums = append(accNums, account.GetAccountNumber()) + accSeqs = append(accSeqs, account.GetSequence()) + } + + tx, err := GenTx( + s.Ctx, + txconfig, + msgs, + feeCoins, + 300000, + "", + accNums, + accSeqs, + signers, + signers, + ) + if err != nil { + return nil, err + } + return tx, nil +} + +func (s *BaseAuthenticatorSuite) GenSimpleTxWithSelectedAuthenticators( + msgs []sdk.Msg, + signers []cryptotypes.PrivKey, + selectedAuthenticators []uint64, +) (sdk.Tx, error) { + txconfig := app.GetEncodingConfig().TxConfig + feeCoins := constants.TestFeeCoins_5Cents + var accNums []uint64 + var accSeqs []uint64 + + ak := s.tApp.App.AccountKeeper + + for _, signer := range signers { + account := ak.GetAccount(s.Ctx, sdk.AccAddress(signer.PubKey().Address())) + accNums = append(accNums, account.GetAccountNumber()) + accSeqs = append(accSeqs, account.GetSequence()) + } + + baseTxBuilder, err := MakeTxBuilder( + s.Ctx, + txconfig, + msgs, + feeCoins, + 300000, + "", + accNums, + accSeqs, + signers, + signers, + ) + if err != nil { + return nil, err + } + + txBuilder, ok := baseTxBuilder.(authtx.ExtensionOptionsTxBuilder) + if !ok { + return nil, fmt.Errorf("expected authtx.ExtensionOptionsTxBuilder, got %T", baseTxBuilder) + } + if len(selectedAuthenticators) > 0 { + value, err := codectypes.NewAnyWithValue(&smartaccounttypes.TxExtension{ + SelectedAuthenticators: selectedAuthenticators, + }) + if err != nil { + return nil, err + } + txBuilder.SetNonCriticalExtensionOptions(value) + } + + tx := txBuilder.GetTx() + return tx, nil +} + +// FundAcc funds target address with specified amount. +func (s *BaseAuthenticatorSuite) FundAcc(acc sdk.AccAddress, amounts sdk.Coins) { + err := testutil.FundAccount(s.Ctx, s.tApp.App.BankKeeper, acc, amounts) + s.Require().NoError(err) +} diff --git a/protocol/x/accountplus/authenticator/iface.go b/protocol/x/accountplus/authenticator/iface.go new file mode 100644 index 0000000000..556d6d0367 --- /dev/null +++ b/protocol/x/accountplus/authenticator/iface.go @@ -0,0 +1,51 @@ +package authenticator + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// InitializedAuthenticator denotes an authenticator fetched from the store and prepared for use. +type InitializedAuthenticator struct { + Id uint64 + Authenticator Authenticator +} + +// Authenticator is an interface that encapsulates all authentication functionalities essential for +// verifying transactions, paying transaction fees, and managing gas consumption during verification. +type Authenticator interface { + // Type returns the specific type of the authenticator, such as SignatureVerification. + // This type is used for registering and identifying the authenticator within the AuthenticatorManager. + Type() string + + // StaticGas provides the fixed gas amount consumed for each invocation of this authenticator. + // This is used for managing gas consumption during transaction verification. + StaticGas() uint64 + + // Initialize prepares the authenticator with necessary data from storage, specific to an account-authenticator pair. + // This method is used for setting up the authenticator with data like a PublicKey for signature verification. + Initialize(config []byte) (Authenticator, error) + + // Authenticate confirms the validity of a message using the provided authentication data. + // NOTE: Any state changes made by this function will be discarded. + // It's a core function within an ante handler to ensure message authenticity and enforce gas consumption. + Authenticate(ctx sdk.Context, request AuthenticationRequest) error + + // Track allows the authenticator to record information, regardless of the transaction's authentication method. + // NOTE: Any state changes made by this function will be written to the store as long as Authenticate succeeds + // and will not be reverted if the message execution fails. + // This function is used for the authenticator to acknowledge the execution of specific messages by an account. + Track(ctx sdk.Context, request AuthenticationRequest) error + + // ConfirmExecution enforces transaction rules post-transaction, like spending and transaction limits. + // It is used to verify execution-specific state and values, to allow authentication to be dependent on the + // effects of a transaction. + ConfirmExecution(ctx sdk.Context, request AuthenticationRequest) error + + // OnAuthenticatorAdded handles the addition of an authenticator to an account. + // It checks the data format and compatibility, to maintain account security and authenticator integrity. + OnAuthenticatorAdded(ctx sdk.Context, account sdk.AccAddress, config []byte, authenticatorId string) error + + // OnAuthenticatorRemoved manages the removal of an authenticator from an account. + // This function is used for updating global data or preventing removal when necessary to maintain system stability. + OnAuthenticatorRemoved(ctx sdk.Context, account sdk.AccAddress, config []byte, authenticatorId string) error +} diff --git a/protocol/x/accountplus/authenticator/requests.go b/protocol/x/accountplus/authenticator/requests.go new file mode 100644 index 0000000000..f1629f862e --- /dev/null +++ b/protocol/x/accountplus/authenticator/requests.go @@ -0,0 +1,49 @@ +package authenticator + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type TrackRequest struct { + AuthenticatorId string `json:"authenticator_id"` + Account sdk.AccAddress `json:"account"` + FeePayer sdk.AccAddress `json:"fee_payer"` + FeeGranter sdk.AccAddress `json:"fee_granter,omitempty"` + Fee sdk.Coins `json:"fee"` + Msg LocalAny `json:"msg"` + MsgIndex uint64 `json:"msg_index"` + AuthenticatorParams []byte `json:"authenticator_params,omitempty"` +} + +type ConfirmExecutionRequest struct { + AuthenticatorId string `json:"authenticator_id"` + Account sdk.AccAddress `json:"account"` + FeePayer sdk.AccAddress `json:"fee_payer"` + FeeGranter sdk.AccAddress `json:"fee_granter,omitempty"` + Fee sdk.Coins `json:"fee"` + Msg LocalAny `json:"msg"` + MsgIndex uint64 `json:"msg_index"` + AuthenticatorParams []byte `json:"authenticator_params,omitempty"` +} + +type AuthenticationRequest struct { + AuthenticatorId string `json:"authenticator_id"` + Account sdk.AccAddress `json:"account"` + FeePayer sdk.AccAddress `json:"fee_payer"` + FeeGranter sdk.AccAddress `json:"fee_granter,omitempty"` + Fee sdk.Coins `json:"fee"` + Msg LocalAny `json:"msg"` + + // Since array size is int, and size depends on the system architecture, + // we use uint64 to cover all available architectures. + // It is unsigned, so at this point, it can't be negative. + MsgIndex uint64 `json:"msg_index"` + + // Only allowing messages with a single signer, so the signature can be a single byte array. + Signature []byte `json:"signature"` + SignModeTxData SignModeData `json:"sign_mode_tx_data"` + TxData ExplicitTxData `json:"tx_data"` + SignatureData SimplifiedSignatureData `json:"signature_data"` + Simulate bool `json:"simulate"` + AuthenticatorParams []byte `json:"authenticator_params,omitempty"` +} diff --git a/protocol/x/accountplus/authenticator/signature_authenticator.go b/protocol/x/accountplus/authenticator/signature_authenticator.go new file mode 100644 index 0000000000..de051ad2b9 --- /dev/null +++ b/protocol/x/accountplus/authenticator/signature_authenticator.go @@ -0,0 +1,114 @@ +package authenticator + +import ( + "fmt" + + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + + errorsmod "cosmossdk.io/errors" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// Compile time type assertion for the SignatureData using the +// SignatureVerification struct +var _ Authenticator = &SignatureVerification{} + +const ( + // SignatureVerificationType represents a type of authenticator specifically designed for + // secp256k1 signature verification. + SignatureVerificationType = "SignatureVerification" +) + +// signature authenticator +type SignatureVerification struct { + ak authante.AccountKeeper + PubKey cryptotypes.PubKey +} + +func (sva SignatureVerification) Type() string { + return SignatureVerificationType +} + +func (sva SignatureVerification) StaticGas() uint64 { + // using 0 gas here. The gas is consumed based on the pubkey type in Authenticate() + return 0 +} + +// NewSignatureVerification creates a new SignatureVerification +func NewSignatureVerification(ak authante.AccountKeeper) SignatureVerification { + return SignatureVerification{ak: ak} +} + +// Initialize sets up the public key to the data supplied from the account-authenticator configuration +func (sva SignatureVerification) Initialize(config []byte) (Authenticator, error) { + if len(config) != secp256k1.PubKeySize { + sva.PubKey = nil + } + sva.PubKey = &secp256k1.PubKey{Key: config} + return sva, nil +} + +// Authenticate takes a SignaturesVerificationData struct and validates +// each signer and signature using signature verification +func (sva SignatureVerification) Authenticate(ctx sdk.Context, request AuthenticationRequest) error { + // First consume gas for verifying the signature + params := sva.ak.GetParams(ctx) + ctx.GasMeter().ConsumeGas(params.SigVerifyCostSecp256k1, "secp256k1 signature verification") + // after gas consumption continue to verify signatures + + if request.Simulate || ctx.IsReCheckTx() { + return nil + } + if sva.PubKey == nil { + return errorsmod.Wrap(sdkerrors.ErrInvalidPubKey, "pubkey on not set on account or authenticator") + } + + if !sva.PubKey.VerifySignature(request.SignModeTxData.Direct, request.Signature) { + return errorsmod.Wrapf( + sdkerrors.ErrUnauthorized, + "signature verification failed; please verify account number (%d), sequence (%d) and chain-id (%s)", + request.TxData.AccountNumber, + request.TxData.AccountSequence, + request.TxData.ChainID, + ) + } + return nil +} + +func (sva SignatureVerification) Track(ctx sdk.Context, request AuthenticationRequest) error { + return nil +} + +func (sva SignatureVerification) ConfirmExecution(ctx sdk.Context, request AuthenticationRequest) error { + return nil +} + +func (sva SignatureVerification) OnAuthenticatorAdded( + ctx sdk.Context, + account sdk.AccAddress, + config []byte, + authenticatorId string, +) error { + // We allow users to pass no data or a valid public key for signature verification. + if len(config) != secp256k1.PubKeySize { + return fmt.Errorf( + "invalid secp256k1 public key size, expected %d, got %d", + secp256k1.PubKeySize, + len(config), + ) + } + return nil +} + +func (sva SignatureVerification) OnAuthenticatorRemoved( + ctx sdk.Context, + account sdk.AccAddress, + config []byte, + authenticatorId string, +) error { + return nil +} diff --git a/protocol/x/accountplus/authenticator/signature_authenticator_test.go b/protocol/x/accountplus/authenticator/signature_authenticator_test.go new file mode 100644 index 0000000000..081e52345c --- /dev/null +++ b/protocol/x/accountplus/authenticator/signature_authenticator_test.go @@ -0,0 +1,406 @@ +package authenticator_test + +import ( + "math/rand" + "os" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/client" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/stretchr/testify/suite" + + "github.com/dydxprotocol/v4-chain/protocol/app" + "github.com/dydxprotocol/v4-chain/protocol/app/config" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + "github.com/dydxprotocol/v4-chain/protocol/x/accountplus/authenticator" +) + +type SigVerifyAuthenticationSuite struct { + BaseAuthenticatorSuite + + SigVerificationAuthenticator authenticator.SignatureVerification +} + +func TestSigVerifyAuthenticationSuite(t *testing.T) { + suite.Run(t, new(SigVerifyAuthenticationSuite)) +} + +func (s *SigVerifyAuthenticationSuite) SetupTest() { + s.SetupKeys() + + s.EncodingConfig = app.GetEncodingConfig() + ak := s.tApp.App.AccountKeeper + + // Create a new Secp256k1SignatureAuthenticator for testing + s.SigVerificationAuthenticator = authenticator.NewSignatureVerification( + ak, + ) +} + +func (s *SigVerifyAuthenticationSuite) TearDownTest() { + os.RemoveAll(s.HomeDir) +} + +type SignatureVerificationTestData struct { + Msgs []sdk.Msg + AccNums []uint64 + AccSeqs []uint64 + Signers []cryptotypes.PrivKey + Signatures []cryptotypes.PrivKey + NumberOfExpectedSigners int + NumberOfExpectedSignatures int + ShouldSucceedGettingData bool + ShouldSucceedSignatureVerification bool +} + +type SignatureVerificationTest struct { + Description string + TestData SignatureVerificationTestData +} + +// TestSignatureAuthenticator test a non-smart account signature verification +func (s *SigVerifyAuthenticationSuite) TestSignatureAuthenticator() { + bech32Prefix := config.Bech32PrefixAccAddr + coins := sdk.Coins{sdk.NewInt64Coin(constants.TestNativeTokenDenom, 2500)} + + // Create a test messages for signing + testMsg1 := &banktypes.MsgSend{ + FromAddress: sdk.MustBech32ifyAddressBytes(bech32Prefix, s.TestAccAddress[0]), + ToAddress: sdk.MustBech32ifyAddressBytes(bech32Prefix, s.TestAccAddress[1]), + Amount: coins, + } + testMsg2 := &banktypes.MsgSend{ + FromAddress: sdk.MustBech32ifyAddressBytes(bech32Prefix, s.TestAccAddress[1]), + ToAddress: sdk.MustBech32ifyAddressBytes(bech32Prefix, s.TestAccAddress[1]), + Amount: coins, + } + testMsg3 := &banktypes.MsgSend{ + FromAddress: sdk.MustBech32ifyAddressBytes(bech32Prefix, s.TestAccAddress[2]), + ToAddress: sdk.MustBech32ifyAddressBytes(bech32Prefix, s.TestAccAddress[1]), + Amount: coins, + } + feeCoins := constants.TestFeeCoins_5Cents + + tests := []SignatureVerificationTest{ + { + Description: "Test: successfully verified authenticator with one signer: base case: PASS", + TestData: SignatureVerificationTestData{ + []sdk.Msg{ + testMsg1, + }, + []uint64{5}, + []uint64{0}, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + }, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + }, + 1, + 1, + true, + true, + }, + }, + + { + Description: "Test: successfully verified authenticator: multiple signers: PASS", + TestData: SignatureVerificationTestData{ + []sdk.Msg{ + testMsg1, + testMsg2, + testMsg3, + }, + []uint64{5, 5, 5, 5}, + []uint64{0, 0, 0, 0}, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + s.TestPrivKeys[1], + s.TestPrivKeys[2], + }, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + s.TestPrivKeys[1], + s.TestPrivKeys[2], + }, + 3, + 3, + true, + true, + }, + }, + + { + // This test case tests if there are two messages with the same signer + // with two successful signatures. + Description: "Test: verified authenticator with 2 messages signed correctly with the same address: PASS", + TestData: SignatureVerificationTestData{ + []sdk.Msg{ + testMsg1, + testMsg2, + testMsg2, + }, + []uint64{5, 5}, + []uint64{0, 0}, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + s.TestPrivKeys[1], + }, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + s.TestPrivKeys[1], + }, + 2, + 2, + true, + true, + }, + }, + + { + // This test case tests if there are two messages with the same signer + // with two successful signatures. + Description: "Test: verified authenticator with 2 messages but only first signed signed correctly: Fail", + TestData: SignatureVerificationTestData{ + []sdk.Msg{ + testMsg1, + testMsg2, + testMsg2, + }, + []uint64{5, 5}, + []uint64{0, 0}, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + s.TestPrivKeys[1], + }, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + s.TestPrivKeys[0], + }, + 2, + 2, + true, + false, + }, + }, + + { + // This test case tests if there are two messages with the same signer + // with two successful signatures. + Description: "Test: verified authenticator with 2 messages but only second signed signed correctly: Fail", + TestData: SignatureVerificationTestData{ + []sdk.Msg{ + testMsg1, + testMsg2, + testMsg2, + }, + []uint64{5, 5}, + []uint64{0, 0}, + []cryptotypes.PrivKey{ + s.TestPrivKeys[0], + s.TestPrivKeys[1], + }, + []cryptotypes.PrivKey{ + s.TestPrivKeys[1], + s.TestPrivKeys[1], + }, + 2, + 2, + true, + false, + }, + }, + + { + Description: "Test: unsuccessful signature authentication invalid signatures: FAIL", + TestData: SignatureVerificationTestData{ + []sdk.Msg{ + testMsg1, + testMsg2, + }, + []uint64{5, 5}, + []uint64{0, 0}, + []cryptotypes.PrivKey{ + s.TestPrivKeys[1], + s.TestPrivKeys[0], + }, + []cryptotypes.PrivKey{ + s.TestPrivKeys[2], + s.TestPrivKeys[0], + }, + 2, + 2, + false, + false, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.Description, func() { + // Generate a transaction based on the test cases + tx, _ := GenTx( + s.Ctx, + s.EncodingConfig.TxConfig, + tc.TestData.Msgs, + feeCoins, + 300000, + s.Ctx.ChainID(), + tc.TestData.AccNums, + tc.TestData.AccSeqs, + tc.TestData.Signers, + tc.TestData.Signatures, + ) + ak := s.tApp.App.AccountKeeper + sigModeHandler := s.EncodingConfig.TxConfig.SignModeHandler() + + // Only the first message is tested for authenticate + addr := sdk.AccAddress(tc.TestData.Signers[0].PubKey().Address()) + + if tc.TestData.ShouldSucceedGettingData { + // request for the first message + request, err := authenticator.GenerateAuthenticationRequest( + s.Ctx, + s.tApp.App.AppCodec(), + ak, + sigModeHandler, + addr, + addr, + nil, + sdk.NewCoins(), + tc.TestData.Msgs[0], + tx, + 0, + false, + ) + s.Require().NoError(err) + + // Test Authenticate method + if tc.TestData.ShouldSucceedSignatureVerification { + initialized, err := s.SigVerificationAuthenticator.Initialize(tc.TestData.Signers[0].PubKey().Bytes()) + s.Require().NoError(err) + err = initialized.Authenticate(s.Ctx, request) + s.Require().NoError(err) + } else { + err = s.SigVerificationAuthenticator.Authenticate(s.Ctx, request) + s.Require().Error(err) + } + } else { + _, err := authenticator.GenerateAuthenticationRequest( + s.Ctx, + s.tApp.App.AppCodec(), + ak, + sigModeHandler, + addr, + addr, + nil, + sdk.NewCoins(), + tc.TestData.Msgs[0], + tx, + 0, + false, + ) + s.Require().Error(err) + } + }) + } +} + +func MakeTxBuilder(ctx sdk.Context, + gen client.TxConfig, + msgs []sdk.Msg, + feeAmt sdk.Coins, + gas uint64, + chainID string, + accNums, + accSeqs []uint64, + signers []cryptotypes.PrivKey, + signatures []cryptotypes.PrivKey, +) (client.TxBuilder, error) { + sigs := make([]signing.SignatureV2, len(signatures)) + + // create a random length memo + r := rand.New(rand.NewSource(time.Now().UnixNano())) + memo := simulation.RandStringOfLength(r, simulation.RandIntBetween(r, 0, 100)) + signMode, err := authsigning.APISignModeToInternal(gen.SignModeHandler().DefaultMode()) + if err != nil { + return nil, err + } + + // 1st round: set SignatureV2 with empty signatures, to set correct + // signer infos. + for i, p := range signers { + sigs[i] = signing.SignatureV2{ + PubKey: p.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: signMode, + }, + Sequence: accSeqs[i], + } + } + + tx := gen.NewTxBuilder() + err = tx.SetMsgs(msgs...) + if err != nil { + return nil, err + } + err = tx.SetSignatures(sigs...) + if err != nil { + return nil, err + } + tx.SetMemo(memo) + tx.SetFeeAmount(feeAmt) + tx.SetGasLimit(gas) + + // 2nd round: once all signer infos are set, every signer can sign. + for i, p := range signatures { + signerData := authsigning.SignerData{ + ChainID: chainID, + AccountNumber: accNums[i], + Sequence: accSeqs[i], + } + signBytes, err := authsigning.GetSignBytesAdapter( + ctx, gen.SignModeHandler(), signMode, signerData, tx.GetTx()) + if err != nil { + panic(err) + } + sig, err := p.Sign(signBytes) + if err != nil { + panic(err) + } + sigs[i].Data.(*signing.SingleSignatureData).Signature = sig + } + + err = tx.SetSignatures(sigs...) + if err != nil { + panic(err) + } + return tx, nil +} + +// GenTx generates a signed mock transaction. +func GenTx( + ctx sdk.Context, + gen client.TxConfig, + msgs []sdk.Msg, + feeAmt sdk.Coins, + gas uint64, + chainID string, + accNums, + accSeqs []uint64, + signers []cryptotypes.PrivKey, + signatures []cryptotypes.PrivKey, +) (sdk.Tx, error) { + tx, err := MakeTxBuilder(ctx, gen, msgs, feeAmt, gas, chainID, accNums, accSeqs, signers, signatures) + if err != nil { + return nil, err + } + return tx.GetTx(), nil +}