mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
Executable
+60
@@ -0,0 +1,60 @@
|
||||
// Package ante provides ante handler implementations for transaction processing
|
||||
// in the Sonr blockchain. It supports both Cosmos SDK and Ethereum transactions.
|
||||
package ante
|
||||
|
||||
import (
|
||||
errorsmod "cosmossdk.io/errors"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
|
||||
)
|
||||
|
||||
// NewAnteHandler returns an ante handler responsible for attempting to route an
|
||||
// Ethereum or SDK transaction to an internal ante handler for performing
|
||||
// transaction-level processing (e.g. fee payment, signature verification) before
|
||||
// being passed onto it's respective handler.
|
||||
//
|
||||
// The handler inspects the transaction type and extension options to determine
|
||||
// the appropriate processing path:
|
||||
// - Ethereum transactions with ExtensionOptionsEthereumTx
|
||||
// - Cosmos SDK transactions with ExtensionOptionDynamicFeeTx
|
||||
// - Standard Cosmos SDK transactions
|
||||
func NewAnteHandler(options HandlerOptions) sdk.AnteHandler {
|
||||
return func(
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool,
|
||||
) (newCtx sdk.Context, err error) {
|
||||
var anteHandler sdk.AnteHandler
|
||||
|
||||
txWithExtensions, ok := tx.(authante.HasExtensionOptionsTx)
|
||||
if ok {
|
||||
opts := txWithExtensions.GetExtensionOptions()
|
||||
if len(opts) > 0 {
|
||||
switch typeURL := opts[0].GetTypeUrl(); typeURL {
|
||||
case "/cosmos.evm.vm.v1.ExtensionOptionsEthereumTx":
|
||||
// handle as *evmtypes.MsgEthereumTx
|
||||
anteHandler = newMonoEVMAnteHandler(options)
|
||||
case "/cosmos.evm.vm.v1.ExtensionOptionDynamicFeeTx":
|
||||
// cosmos-sdk tx with dynamic fee extension
|
||||
anteHandler = NewCosmosAnteHandler(options)
|
||||
default:
|
||||
return ctx, errorsmod.Wrapf(
|
||||
errortypes.ErrUnknownExtensionOptions,
|
||||
"rejecting tx with unsupported extension option: %s", typeURL,
|
||||
)
|
||||
}
|
||||
|
||||
return anteHandler(ctx, tx, sim)
|
||||
}
|
||||
}
|
||||
|
||||
// handle as totally normal Cosmos SDK tx
|
||||
switch tx.(type) {
|
||||
case sdk.Tx:
|
||||
anteHandler = NewCosmosAnteHandler(options)
|
||||
default:
|
||||
return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid transaction type: %T", tx)
|
||||
}
|
||||
|
||||
return anteHandler(ctx, tx, sim)
|
||||
}
|
||||
}
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/ante"
|
||||
sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
|
||||
evmoscosmosante "github.com/cosmos/evm/ante/cosmos"
|
||||
evmante "github.com/cosmos/evm/ante/evm"
|
||||
evmtypes "github.com/cosmos/evm/x/vm/types"
|
||||
|
||||
circuitante "cosmossdk.io/x/circuit/ante"
|
||||
ibcante "github.com/cosmos/ibc-go/v8/modules/core/ante"
|
||||
)
|
||||
|
||||
// NewCosmosAnteHandler creates the default ante handler for Cosmos SDK transactions.
|
||||
// It sets up a chain of decorators that perform various checks and operations:
|
||||
// - Rejects Ethereum transactions in Cosmos context
|
||||
// - Enforces authz limitations
|
||||
// - Sets up transaction context
|
||||
// - Validates basic transaction properties
|
||||
// - Handles WebAuthn gasless transactions
|
||||
// - Handles gas consumption and fee deduction
|
||||
// - Performs signature verification
|
||||
// - Manages account sequences
|
||||
// - Handles IBC-specific checks
|
||||
func NewCosmosAnteHandler(options HandlerOptions) sdk.AnteHandler {
|
||||
// Determine if we should use enhanced gasless mode
|
||||
// Enhanced mode allows address generation from credentials for true gasless onboarding
|
||||
enhancedGaslessMode := options.EnableEnhancedGasless
|
||||
|
||||
// Build the decorator chain
|
||||
decorators := []sdk.AnteDecorator{
|
||||
// WebAuthn bypass - must be first to intercept WebAuthn transactions
|
||||
NewWebAuthnBypassDecorator(),
|
||||
evmoscosmosante.NewRejectMessagesDecorator(), // reject MsgEthereumTxs
|
||||
evmoscosmosante.NewAuthzLimiterDecorator( // disable the Msg types that cannot be included on an authz.MsgExec msgs field
|
||||
sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}),
|
||||
sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}),
|
||||
),
|
||||
|
||||
ante.NewSetUpContextDecorator(),
|
||||
circuitante.NewCircuitBreakerDecorator(options.CircuitKeeper),
|
||||
ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker),
|
||||
ante.NewValidateBasicDecorator(),
|
||||
ante.NewTxTimeoutHeightDecorator(),
|
||||
ante.NewValidateMemoDecorator(options.AccountKeeper),
|
||||
|
||||
// UCAN validation - must come before fee deduction for gasless support
|
||||
NewConditionalUCANDecorator(NewUCANDecorator()),
|
||||
evmoscosmosante.NewMinGasPriceDecorator(
|
||||
options.FeeMarketKeeper,
|
||||
options.EvmKeeper,
|
||||
options.ControlPanelKeeper,
|
||||
),
|
||||
ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
|
||||
|
||||
// WebAuthn gasless transaction support - must come before fee deduction
|
||||
// Enhanced mode allows true gasless onboarding without pre-existing accounts
|
||||
NewWebAuthnGaslessDecorator(options.AccountKeeper, options.DidKeeper, enhancedGaslessMode),
|
||||
|
||||
// Conditional fee deduction - skips fees for gasless WebAuthn and UCAN
|
||||
NewUCANGaslessDecorator(
|
||||
NewConditionalFeeDecorator(ante.NewDeductFeeDecorator(
|
||||
options.AccountKeeper,
|
||||
options.BankKeeper,
|
||||
options.FeegrantKeeper,
|
||||
options.TxFeeChecker,
|
||||
)),
|
||||
),
|
||||
}
|
||||
|
||||
// Add signature verification decorators
|
||||
// In enhanced gasless mode, we wrap these to be conditional
|
||||
if enhancedGaslessMode {
|
||||
// Conditional decorators that skip verification for gasless transactions
|
||||
decorators = append(
|
||||
decorators,
|
||||
NewConditionalPubKeyDecorator(ante.NewSetPubKeyDecorator(options.AccountKeeper)),
|
||||
NewConditionalSigCountDecorator(
|
||||
ante.NewValidateSigCountDecorator(options.AccountKeeper),
|
||||
),
|
||||
NewConditionalSigGasDecorator(
|
||||
ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer),
|
||||
),
|
||||
NewConditionalSignatureDecorator(
|
||||
ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// Standard signature verification decorators
|
||||
decorators = append(decorators,
|
||||
ante.NewSetPubKeyDecorator(options.AccountKeeper),
|
||||
ante.NewValidateSigCountDecorator(options.AccountKeeper),
|
||||
ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer),
|
||||
ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler),
|
||||
)
|
||||
}
|
||||
|
||||
// Add remaining decorators
|
||||
decorators = append(decorators,
|
||||
ante.NewIncrementSequenceDecorator(options.AccountKeeper),
|
||||
ibcante.NewRedundantRelayDecorator(options.IBCKeeper),
|
||||
evmante.NewGasWantedDecorator(options.EvmKeeper, options.FeeMarketKeeper),
|
||||
)
|
||||
|
||||
return sdk.ChainAnteDecorators(decorators...)
|
||||
}
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
evmante "github.com/cosmos/evm/ante/evm"
|
||||
)
|
||||
|
||||
// newMonoEVMAnteHandler creates the ante handler for Ethereum Virtual Machine transactions.
|
||||
// It uses a single decorator that handles all EVM-specific validation and processing,
|
||||
// including gas calculation, fee market dynamics, and account management.
|
||||
//
|
||||
// The mono decorator performs all EVM ante operations in a single pass for efficiency.
|
||||
func newMonoEVMAnteHandler(options HandlerOptions) sdk.AnteHandler {
|
||||
return sdk.ChainAnteDecorators(
|
||||
evmante.NewEVMMonoDecorator(
|
||||
options.AccountKeeper,
|
||||
options.FeeMarketKeeper,
|
||||
options.EvmKeeper,
|
||||
options.ControlPanelKeeper,
|
||||
options.MaxTxGasWanted,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
anteinterfaces "github.com/cosmos/evm/ante/interfaces"
|
||||
)
|
||||
|
||||
// Ensure ControlPanelKeeper implements the interface
|
||||
var _ anteinterfaces.ControlPanelKeeper = (*ControlPanelKeeper)(nil)
|
||||
|
||||
// ControlPanelKeeper provides control panel functionality for sponsored transactions.
|
||||
// This is a simple implementation that can be extended to support sponsored addresses
|
||||
// and custom transaction priorities in the future.
|
||||
type ControlPanelKeeper struct {
|
||||
// sponsoredAddresses could be loaded from state or configuration
|
||||
sponsoredAddresses map[string]bool
|
||||
// priority for sponsored transactions
|
||||
sponsoredTxPriority int64
|
||||
}
|
||||
|
||||
// NewControlPanelKeeper creates a new ControlPanelKeeper instance
|
||||
func NewControlPanelKeeper() *ControlPanelKeeper {
|
||||
return &ControlPanelKeeper{
|
||||
sponsoredAddresses: make(map[string]bool),
|
||||
sponsoredTxPriority: 0, // Default priority
|
||||
}
|
||||
}
|
||||
|
||||
// IsSponsoredAddress checks if an address is sponsored for gasless transactions
|
||||
func (k *ControlPanelKeeper) IsSponsoredAddress(ctx context.Context, addr []byte) bool {
|
||||
// For now, return false for all addresses
|
||||
// This can be extended to check against a whitelist or state
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSponsoredTransactionPriority returns the priority for sponsored transactions
|
||||
func (k *ControlPanelKeeper) GetSponsoredTransactionPriority(ctx context.Context) int64 {
|
||||
// Return default priority
|
||||
// This can be made configurable or dynamic based on chain state
|
||||
return k.sponsoredTxPriority
|
||||
}
|
||||
|
||||
// SetSponsoredAddress adds or removes an address from the sponsored list
|
||||
// This is a helper method for future use
|
||||
func (k *ControlPanelKeeper) SetSponsoredAddress(addr sdk.AccAddress, sponsored bool) {
|
||||
if sponsored {
|
||||
k.sponsoredAddresses[addr.String()] = true
|
||||
} else {
|
||||
delete(k.sponsoredAddresses, addr.String())
|
||||
}
|
||||
}
|
||||
|
||||
// SetSponsoredTransactionPriority updates the priority for sponsored transactions
|
||||
// This is a helper method for future use
|
||||
func (k *ControlPanelKeeper) SetSponsoredTransactionPriority(priority int64) {
|
||||
k.sponsoredTxPriority = priority
|
||||
}
|
||||
Executable
+129
@@ -0,0 +1,129 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
addresscodec "cosmossdk.io/core/address"
|
||||
errorsmod "cosmossdk.io/errors"
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
circuitkeeper "cosmossdk.io/x/circuit/keeper"
|
||||
txsigning "cosmossdk.io/x/tx/signing"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/ante"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
anteinterfaces "github.com/cosmos/evm/ante/interfaces"
|
||||
ibckeeper "github.com/cosmos/ibc-go/v8/modules/core/keeper"
|
||||
)
|
||||
|
||||
// WebAuthnKeeperInterface defines the required methods from the DID keeper for WebAuthn gasless processing
|
||||
type WebAuthnKeeperInterface interface {
|
||||
// HasExistingCredential checks if this credential ID already exists
|
||||
HasExistingCredential(ctx sdk.Context, credentialId string) bool
|
||||
}
|
||||
|
||||
// BankKeeper defines the contract needed for supply related APIs.
|
||||
// It provides methods for checking send permissions and transferring coins
|
||||
// between accounts and modules.
|
||||
type BankKeeper interface {
|
||||
IsSendEnabledCoins(ctx context.Context, coins ...sdk.Coin) error
|
||||
SendCoins(ctx context.Context, from, to sdk.AccAddress, amt sdk.Coins) error
|
||||
SendCoinsFromAccountToModule(
|
||||
ctx context.Context,
|
||||
senderAddr sdk.AccAddress,
|
||||
recipientModule string,
|
||||
amt sdk.Coins,
|
||||
) error
|
||||
}
|
||||
|
||||
// AccountKeeper defines the account management interface required by ante handlers.
|
||||
// It provides methods for account creation, retrieval, modification, and
|
||||
// sequence number management.
|
||||
type AccountKeeper interface {
|
||||
NewAccountWithAddress(ctx context.Context, addr sdk.AccAddress) sdk.AccountI
|
||||
GetModuleAddress(moduleName string) sdk.AccAddress
|
||||
GetAccount(ctx context.Context, addr sdk.AccAddress) sdk.AccountI
|
||||
SetAccount(ctx context.Context, account sdk.AccountI)
|
||||
RemoveAccount(ctx context.Context, account sdk.AccountI)
|
||||
GetParams(ctx context.Context) (params authtypes.Params)
|
||||
GetSequence(ctx context.Context, addr sdk.AccAddress) (uint64, error)
|
||||
AddressCodec() addresscodec.Codec
|
||||
}
|
||||
|
||||
// HandlerOptions defines the list of module keepers and configurations required
|
||||
// to run the ante handler decorators. It includes both standard Cosmos SDK
|
||||
// keepers and EVM-specific components for processing different transaction types.
|
||||
type HandlerOptions struct {
|
||||
Cdc codec.BinaryCodec
|
||||
AccountKeeper AccountKeeper
|
||||
BankKeeper BankKeeper
|
||||
FeegrantKeeper ante.FeegrantKeeper
|
||||
ExtensionOptionChecker ante.ExtensionOptionChecker
|
||||
SignModeHandler *txsigning.HandlerMap
|
||||
SigGasConsumer func(meter storetypes.GasMeter, sig signing.SignatureV2, params authtypes.Params) error
|
||||
TxFeeChecker ante.TxFeeChecker // safe to be nil
|
||||
|
||||
MaxTxGasWanted uint64
|
||||
FeeMarketKeeper anteinterfaces.FeeMarketKeeper
|
||||
EvmKeeper anteinterfaces.EVMKeeper
|
||||
ControlPanelKeeper anteinterfaces.ControlPanelKeeper
|
||||
|
||||
IBCKeeper *ibckeeper.Keeper
|
||||
CircuitKeeper *circuitkeeper.Keeper
|
||||
|
||||
// WebAuthn gasless transaction support
|
||||
DidKeeper WebAuthnKeeperInterface
|
||||
EnableEnhancedGasless bool // Enable enhanced gasless mode for true onboarding without pre-existing accounts
|
||||
|
||||
// UCAN module keepers for permission validation
|
||||
DwnKeeper interface{} // Will be cast to proper type in decorator
|
||||
DexKeeper interface{} // Will be cast to proper type in decorator
|
||||
SvcKeeper interface{} // Will be cast to proper type in decorator
|
||||
}
|
||||
|
||||
// Validate checks if all required keepers and handlers are properly initialized.
|
||||
// It ensures that the HandlerOptions struct has all necessary components to
|
||||
// process transactions without nil pointer errors.
|
||||
func (options HandlerOptions) Validate() error {
|
||||
if options.Cdc == nil {
|
||||
return errorsmod.Wrap(errortypes.ErrLogic, "codec is required for AnteHandler")
|
||||
}
|
||||
if options.AccountKeeper == nil {
|
||||
return errorsmod.Wrap(errortypes.ErrLogic, "account keeper is required for AnteHandler")
|
||||
}
|
||||
if options.BankKeeper == nil {
|
||||
return errorsmod.Wrap(errortypes.ErrLogic, "bank keeper is required for AnteHandler")
|
||||
}
|
||||
if options.SigGasConsumer == nil {
|
||||
return errorsmod.Wrap(
|
||||
errortypes.ErrLogic,
|
||||
"signature gas consumer is required for AnteHandler",
|
||||
)
|
||||
}
|
||||
if options.SignModeHandler == nil {
|
||||
return errorsmod.Wrap(errortypes.ErrLogic, "sign mode handler is required for AnteHandler")
|
||||
}
|
||||
if options.CircuitKeeper == nil {
|
||||
return errorsmod.Wrap(errortypes.ErrLogic, "circuit keeper is required for ante builder")
|
||||
}
|
||||
|
||||
if options.TxFeeChecker == nil {
|
||||
return errorsmod.Wrap(errortypes.ErrLogic, "tx fee checker is required for AnteHandler")
|
||||
}
|
||||
if options.FeeMarketKeeper == nil {
|
||||
return errorsmod.Wrap(errortypes.ErrLogic, "fee market keeper is required for AnteHandler")
|
||||
}
|
||||
if options.EvmKeeper == nil {
|
||||
return errorsmod.Wrap(errortypes.ErrLogic, "evm keeper is required for AnteHandler")
|
||||
}
|
||||
if options.ControlPanelKeeper == nil {
|
||||
return errorsmod.Wrap(
|
||||
errortypes.ErrLogic,
|
||||
"control panel keeper is required for AnteHandler",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/sonr-io/sonr/crypto/keys"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
)
|
||||
|
||||
// UCANDecorator validates UCAN tokens in transactions
|
||||
// This is a placeholder implementation that sets up the infrastructure
|
||||
// for UCAN validation. In production, UCAN tokens would be passed
|
||||
// in message fields or transaction extensions.
|
||||
type UCANDecorator struct {
|
||||
verifier *ucan.Verifier
|
||||
}
|
||||
|
||||
// NewUCANDecorator creates a new UCAN decorator
|
||||
func NewUCANDecorator() UCANDecorator {
|
||||
// Create a basic DID resolver
|
||||
didResolver := &BasicDIDResolver{}
|
||||
verifier := ucan.NewVerifier(didResolver)
|
||||
|
||||
return UCANDecorator{
|
||||
verifier: verifier,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle validates UCAN tokens for transactions requiring authorization
|
||||
func (ud UCANDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
|
||||
// Skip validation in simulation mode
|
||||
if simulate {
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
|
||||
// Check if transaction has UCAN extension
|
||||
// This is where we would extract and validate UCAN tokens
|
||||
// For now, this is a placeholder that demonstrates the structure
|
||||
|
||||
// Future implementation would:
|
||||
// 1. Extract UCAN token from transaction extensions or memo
|
||||
// 2. Validate the token using the verifier
|
||||
// 3. Check capabilities against message types
|
||||
// 4. Mark transaction as gasless if appropriate
|
||||
|
||||
// Check if transaction qualifies for gasless execution
|
||||
if ud.isGaslessTransaction(ctx, tx) {
|
||||
// Mark context for gasless processing
|
||||
ctx = ctx.WithValue("gasless_ucan", true)
|
||||
}
|
||||
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
|
||||
// isGaslessTransaction checks if transaction qualifies for gasless execution
|
||||
// This is a placeholder implementation
|
||||
func (ud UCANDecorator) isGaslessTransaction(ctx sdk.Context, tx sdk.Tx) bool {
|
||||
// In production, this would check for UCAN tokens with gasless capabilities
|
||||
// For now, return false to maintain normal fee processing
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckTokenExpiration checks if UCAN token has expired
|
||||
func (ud UCANDecorator) CheckTokenExpiration(ctx sdk.Context, token *ucan.Token) error {
|
||||
if token.ExpiresAt > 0 {
|
||||
currentTime := ctx.BlockTime().Unix()
|
||||
if currentTime > token.ExpiresAt {
|
||||
return fmt.Errorf("UCAN token has expired")
|
||||
}
|
||||
}
|
||||
|
||||
// Check NotBefore
|
||||
if token.NotBefore > 0 {
|
||||
currentTime := ctx.BlockTime().Unix()
|
||||
if currentTime < token.NotBefore {
|
||||
return fmt.Errorf("UCAN token is not yet valid")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCapabilities validates UCAN capabilities against required permissions
|
||||
func (ud UCANDecorator) ValidateCapabilities(token *ucan.Token, requiredCapabilities []string) error {
|
||||
// Check if token grants required capabilities
|
||||
for _, att := range token.Attenuations {
|
||||
if att.Capability.Grants(requiredCapabilities) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("UCAN token does not grant required capabilities")
|
||||
}
|
||||
|
||||
// BasicDIDResolver implements ucan.DIDResolver for the ante handler
|
||||
type BasicDIDResolver struct{}
|
||||
|
||||
// ResolveDIDKey resolves DID to public key for UCAN verification
|
||||
func (r *BasicDIDResolver) ResolveDIDKey(ctx context.Context, did string) (keys.DID, error) {
|
||||
// This is a basic implementation that accepts all DIDs
|
||||
// In production, this would query the DID module
|
||||
return keys.Parse(did)
|
||||
}
|
||||
|
||||
// ConditionalUCANDecorator wraps UCAN decorator to skip for certain transactions
|
||||
type ConditionalUCANDecorator struct {
|
||||
decorator sdk.AnteDecorator
|
||||
}
|
||||
|
||||
// NewConditionalUCANDecorator creates a conditional UCAN decorator
|
||||
func NewConditionalUCANDecorator(decorator sdk.AnteDecorator) ConditionalUCANDecorator {
|
||||
return ConditionalUCANDecorator{decorator: decorator}
|
||||
}
|
||||
|
||||
// AnteHandle conditionally applies UCAN validation
|
||||
func (cud ConditionalUCANDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
|
||||
// Skip UCAN validation if already marked as gasless WebAuthn
|
||||
if ctx.Value("bypass_ucan") != nil {
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
|
||||
// Apply UCAN validation
|
||||
return cud.decorator.AnteHandle(ctx, tx, simulate, next)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
)
|
||||
|
||||
// TestUCANDecorator tests the UCAN decorator functionality
|
||||
func TestUCANDecorator(t *testing.T) {
|
||||
// Test decorator creation
|
||||
decorator := NewUCANDecorator()
|
||||
assert.NotNil(t, decorator)
|
||||
}
|
||||
|
||||
// TestUCANGaslessDecorator tests the gasless decorator
|
||||
func TestUCANGaslessDecorator(t *testing.T) {
|
||||
// Create mock fee decorator
|
||||
mockFeeDecorator := &mockAnteDecorator{}
|
||||
|
||||
// Test gasless decorator creation
|
||||
gaslessDecorator := NewUCANGaslessDecorator(mockFeeDecorator)
|
||||
assert.NotNil(t, gaslessDecorator)
|
||||
}
|
||||
|
||||
// TestConditionalUCANDecorator tests conditional UCAN decorator
|
||||
func TestConditionalUCANDecorator(t *testing.T) {
|
||||
// Create mock UCAN decorator
|
||||
mockUCANDecorator := &mockAnteDecorator{}
|
||||
|
||||
// Test conditional decorator creation
|
||||
conditionalDecorator := NewConditionalUCANDecorator(mockUCANDecorator)
|
||||
assert.NotNil(t, conditionalDecorator)
|
||||
}
|
||||
|
||||
// TestTokenExpiration tests UCAN token expiration validation
|
||||
func TestTokenExpiration(t *testing.T) {
|
||||
decorator := NewUCANDecorator()
|
||||
ctx := sdk.Context{}.WithBlockTime(time.Now())
|
||||
|
||||
// Test expired token
|
||||
expiredToken := &ucan.Token{
|
||||
ExpiresAt: time.Now().Unix() - 3600, // 1 hour ago
|
||||
}
|
||||
|
||||
err := decorator.CheckTokenExpiration(ctx, expiredToken)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "UCAN token has expired")
|
||||
|
||||
// Test valid token
|
||||
validToken := &ucan.Token{
|
||||
ExpiresAt: time.Now().Unix() + 3600, // 1 hour from now
|
||||
}
|
||||
|
||||
err = decorator.CheckTokenExpiration(ctx, validToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test token not yet valid
|
||||
futureToken := &ucan.Token{
|
||||
NotBefore: time.Now().Unix() + 3600, // 1 hour from now
|
||||
}
|
||||
|
||||
err = decorator.CheckTokenExpiration(ctx, futureToken)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "UCAN token is not yet valid")
|
||||
}
|
||||
|
||||
// TestValidateCapabilities tests capability validation
|
||||
func TestValidateCapabilities(t *testing.T) {
|
||||
decorator := NewUCANDecorator()
|
||||
|
||||
// Create token with single capability
|
||||
token := &ucan.Token{
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.SimpleCapability{
|
||||
Action: "did/update",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test with matching capability
|
||||
err := decorator.ValidateCapabilities(token, []string{"did/update"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test with non-matching capability
|
||||
err = decorator.ValidateCapabilities(token, []string{"did/delete"})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "UCAN token does not grant required capabilities")
|
||||
|
||||
// Test with multiple capabilities
|
||||
multiToken := &ucan.Token{
|
||||
Attenuations: []ucan.Attenuation{
|
||||
{
|
||||
Capability: &ucan.MultiCapability{
|
||||
Actions: []string{"did/update", "did/create"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = decorator.ValidateCapabilities(multiToken, []string{"did/update"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = decorator.ValidateCapabilities(multiToken, []string{"did/create"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = decorator.ValidateCapabilities(multiToken, []string{"did/delete"})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// mockAnteDecorator is a helper for testing
|
||||
type mockAnteDecorator struct{}
|
||||
|
||||
func (m *mockAnteDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// UCANGaslessDecorator allows gasless transactions for UCAN-authorized operations
|
||||
type UCANGaslessDecorator struct {
|
||||
feeDecorator sdk.AnteDecorator
|
||||
}
|
||||
|
||||
// NewUCANGaslessDecorator creates a new UCAN gasless decorator
|
||||
func NewUCANGaslessDecorator(feeDecorator sdk.AnteDecorator) UCANGaslessDecorator {
|
||||
return UCANGaslessDecorator{
|
||||
feeDecorator: feeDecorator,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle conditionally skips fee deduction for UCAN gasless transactions
|
||||
func (ugd UCANGaslessDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
|
||||
// Check if transaction is marked as UCAN gasless
|
||||
if ctx.Value("gasless_ucan") != nil {
|
||||
// Skip fee deduction for gasless UCAN transaction
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
|
||||
// Apply normal fee deduction
|
||||
return ugd.feeDecorator.AnteHandle(ctx, tx, simulate, next)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
|
||||
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||||
)
|
||||
|
||||
// WebAuthnBypassDecorator completely bypasses signature verification for WebAuthn registration.
|
||||
// This decorator must be placed FIRST in the ante handler chain to intercept WebAuthn
|
||||
// transactions before any signature validation occurs.
|
||||
type WebAuthnBypassDecorator struct{}
|
||||
|
||||
// NewWebAuthnBypassDecorator creates a new WebAuthnBypassDecorator
|
||||
func NewWebAuthnBypassDecorator() WebAuthnBypassDecorator {
|
||||
return WebAuthnBypassDecorator{}
|
||||
}
|
||||
|
||||
// AnteHandle validates WebAuthn transactions and marks them for controlled processing
|
||||
// This decorator performs essential security checks while allowing gasless processing
|
||||
func (wbd WebAuthnBypassDecorator) AnteHandle(
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool, next sdk.AnteHandler,
|
||||
) (newCtx sdk.Context, err error) {
|
||||
msgs := tx.GetMsgs()
|
||||
|
||||
// Check if this is a single WebAuthn registration transaction
|
||||
if len(msgs) != 1 {
|
||||
// Not a single message transaction, proceed normally
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
msg, ok := msgs[0].(*didtypes.MsgRegisterWebAuthnCredential)
|
||||
if !ok {
|
||||
// Not a WebAuthn registration, proceed normally
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
// This is a WebAuthn registration - perform security validation
|
||||
ctx.Logger().Info("Processing WebAuthn registration with controlled bypass",
|
||||
"username", msg.Username,
|
||||
"credential_id", msg.WebauthnCredential.CredentialId)
|
||||
|
||||
// CRITICAL SECURITY CHECK 1: Validate the WebAuthn credential structure
|
||||
if err := msg.WebauthnCredential.ValidateStructure(); err != nil {
|
||||
ctx.Logger().Error("WebAuthn credential structure validation failed", "error", err)
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY CHECK 2: Handle signatures (dummy signatures are allowed for mempool validation)
|
||||
if sigTx, ok := tx.(authsigning.SigVerifiableTx); ok {
|
||||
sigs, err := sigTx.GetSignaturesV2()
|
||||
if err != nil {
|
||||
ctx.Logger().Error("Failed to get signatures from WebAuthn transaction", "error", err)
|
||||
return ctx, err
|
||||
}
|
||||
if len(sigs) > 0 {
|
||||
ctx.Logger().
|
||||
Debug("WebAuthn transaction has dummy signatures for mempool validation", "sig_count", len(sigs))
|
||||
|
||||
// This is expected - dummy signatures are used to pass mempool validation
|
||||
// The actual signature verification will be bypassed by conditional decorators
|
||||
} else {
|
||||
ctx.Logger().Debug("WebAuthn transaction has no signatures - gasless flow")
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY CHECK 3: Validate credential uniqueness to prevent replay attacks
|
||||
// Note: This will be enforced in the WebAuthnGaslessDecorator with keeper access
|
||||
|
||||
// Mark context for controlled WebAuthn processing
|
||||
// These flags will be checked by conditional decorators
|
||||
ctx = ctx.WithValue("webauthn_bypass_validated", true)
|
||||
|
||||
ctx.Logger().Info("WebAuthn transaction validated - proceeding with controlled processing",
|
||||
"credential_id", msg.WebauthnCredential.CredentialId,
|
||||
"username", msg.Username)
|
||||
|
||||
// Continue to next decorator (WebAuthnGaslessDecorator) for full processing
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
// Package ante provides ante handler implementations for transaction processing
|
||||
// in the Sonr blockchain. It supports both Cosmos SDK and Ethereum transactions.
|
||||
package ante
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
errorsmod "cosmossdk.io/errors"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||||
)
|
||||
|
||||
// WebAuthnGaslessDecorator provides gasless transaction processing for WebAuthn registration.
|
||||
// This decorator identifies WebAuthn credential registration messages and bypasses fee deduction,
|
||||
// enabling users to create their first decentralized identity without requiring existing tokens.
|
||||
//
|
||||
// The decorator supports two modes:
|
||||
// 1. Standard mode: Requires a controller address (for users with existing accounts)
|
||||
// 2. Enhanced mode: Generates controller address from credential (for brand new users)
|
||||
//
|
||||
// Security considerations:
|
||||
// - Only applies to MsgRegisterWebAuthnCredential messages
|
||||
// - Validates WebAuthn credential authenticity before fee waiving
|
||||
// - Prevents abuse through cryptographic WebAuthn requirements
|
||||
// - Limited to one gasless transaction per unique credential
|
||||
type WebAuthnGaslessDecorator struct {
|
||||
accountKeeper AccountKeeper
|
||||
didKeeper WebAuthnKeeperInterface
|
||||
enhancedMode bool // If true, allows address generation from credentials
|
||||
}
|
||||
|
||||
// NewWebAuthnGaslessDecorator creates a new WebAuthn gasless transaction decorator.
|
||||
// This decorator must be placed in the ante handler chain BEFORE the fee deduction decorator
|
||||
// to effectively bypass fee requirements for qualifying WebAuthn transactions.
|
||||
//
|
||||
// Set enhancedMode to true to enable automatic address generation from credentials,
|
||||
// allowing truly gasless onboarding without pre-existing accounts.
|
||||
func NewWebAuthnGaslessDecorator(
|
||||
accountKeeper AccountKeeper,
|
||||
didKeeper WebAuthnKeeperInterface,
|
||||
enhancedMode bool,
|
||||
) WebAuthnGaslessDecorator {
|
||||
return WebAuthnGaslessDecorator{
|
||||
accountKeeper: accountKeeper,
|
||||
didKeeper: didKeeper,
|
||||
enhancedMode: enhancedMode,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle processes the transaction and determines if WebAuthn gasless processing applies.
|
||||
// For qualifying WebAuthn registration transactions, it sets transaction fees to zero
|
||||
// and validates the WebAuthn credential to prevent abuse.
|
||||
//
|
||||
// Gasless criteria:
|
||||
// 1. Transaction contains exactly one MsgRegisterWebAuthnCredential
|
||||
// 2. WebAuthn credential passes cryptographic validation
|
||||
// 3. Credential ID has not been used before (prevents replay attacks)
|
||||
// 4. Transaction sender account exists or can be created
|
||||
//
|
||||
// In enhanced mode, if no controller is provided, it generates one from the credential.
|
||||
func (wgd WebAuthnGaslessDecorator) AnteHandle(
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool, next sdk.AnteHandler,
|
||||
) (newCtx sdk.Context, err error) {
|
||||
// Process during both CheckTx (simulation) and DeliverTx (execution)
|
||||
// This ensures gasless flags are set during mempool validation
|
||||
|
||||
msgs := tx.GetMsgs()
|
||||
|
||||
// Debug: Log entry into gasless decorator
|
||||
ctx.Logger().Debug("WebAuthnGaslessDecorator: processing transaction",
|
||||
"msg_count", len(msgs), "sim", sim)
|
||||
|
||||
// Check if this is a single WebAuthn registration transaction
|
||||
if len(msgs) != 1 {
|
||||
// Multi-message transactions don't qualify for gasless processing
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
msg, ok := msgs[0].(*didtypes.MsgRegisterWebAuthnCredential)
|
||||
if !ok {
|
||||
// Not a WebAuthn registration message
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
// Check if WebAuthn bypass validation already occurred
|
||||
if bypassed, ok := ctx.Value("webauthn_bypass_validated").(bool); !ok || !bypassed {
|
||||
// Validate the WebAuthn credential to prevent abuse (if not already validated)
|
||||
if err := msg.WebauthnCredential.ValidateStructure(); err != nil {
|
||||
return ctx, errorsmod.Wrapf(
|
||||
errortypes.ErrInvalidRequest,
|
||||
"invalid WebAuthn credential for gasless transaction: %v", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent credential reuse (anti-replay protection)
|
||||
if wgd.didKeeper.HasExistingCredential(ctx, msg.WebauthnCredential.CredentialId) {
|
||||
return ctx, errorsmod.Wrapf(
|
||||
errortypes.ErrInvalidRequest,
|
||||
"WebAuthn credential already registered: %s", msg.WebauthnCredential.CredentialId,
|
||||
)
|
||||
}
|
||||
|
||||
// Handle controller address
|
||||
var controllerAddr sdk.AccAddress
|
||||
|
||||
// Enhanced mode: Generate address from credential if not provided
|
||||
if wgd.enhancedMode && msg.GetController() == "" {
|
||||
// Generate deterministic address from credential ID
|
||||
controllerAddr = GenerateAddressFromCredential(msg.WebauthnCredential.CredentialId)
|
||||
|
||||
// Note: We can't modify the message directly in most cases,
|
||||
// but we can pass the generated address through context
|
||||
ctx = ctx.WithValue("generated_controller", controllerAddr.String())
|
||||
|
||||
ctx.Logger().Info(
|
||||
"Generated controller address for gasless WebAuthn registration",
|
||||
"generated_address", controllerAddr.String(),
|
||||
"credential_id", msg.WebauthnCredential.CredentialId,
|
||||
)
|
||||
} else {
|
||||
// Standard mode or enhanced mode with provided controller
|
||||
controllerStr := msg.GetController()
|
||||
if controllerStr == "" {
|
||||
return ctx, errorsmod.Wrap(
|
||||
errortypes.ErrInvalidAddress,
|
||||
"controller address required for WebAuthn registration",
|
||||
)
|
||||
}
|
||||
|
||||
controllerAddr, err = sdk.AccAddressFromBech32(controllerStr)
|
||||
if err != nil {
|
||||
return ctx, errorsmod.Wrapf(
|
||||
errortypes.ErrInvalidAddress,
|
||||
"invalid controller address: %v", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the account exists (simulation-safe)
|
||||
account := wgd.accountKeeper.GetAccount(ctx, controllerAddr)
|
||||
if account == nil {
|
||||
// Create account if it doesn't exist (common for first-time WebAuthn users)
|
||||
// Only actually create during execution, not simulation
|
||||
if !sim {
|
||||
account = wgd.accountKeeper.NewAccountWithAddress(ctx, controllerAddr)
|
||||
wgd.accountKeeper.SetAccount(ctx, account)
|
||||
|
||||
ctx.Logger().Info(
|
||||
"Created new account for gasless WebAuthn registration",
|
||||
"address", controllerAddr.String(),
|
||||
)
|
||||
} else {
|
||||
// During simulation (CheckTx), just create a temporary account for validation
|
||||
// We don't assign it back to account variable since it's only for validation
|
||||
wgd.accountKeeper.NewAccountWithAddress(ctx, controllerAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this transaction as gasless
|
||||
gaslessCtx := ctx.WithValue("webauthn_gasless", true)
|
||||
|
||||
// In enhanced mode, also mark to skip signature verification
|
||||
if wgd.enhancedMode {
|
||||
gaslessCtx = gaslessCtx.WithValue("skip_sig_verification", true)
|
||||
gaslessCtx = gaslessCtx.WithValue("skip_pubkey_verification", true)
|
||||
}
|
||||
|
||||
// Log the gasless transaction for monitoring and security purposes
|
||||
ctx.Logger().Info(
|
||||
"Processing gasless WebAuthn registration",
|
||||
"controller", controllerAddr.String(),
|
||||
"credential_id", msg.WebauthnCredential.CredentialId,
|
||||
"username", msg.Username,
|
||||
"auto_vault", msg.AutoCreateVault,
|
||||
"enhanced_mode", wgd.enhancedMode,
|
||||
)
|
||||
|
||||
return next(gaslessCtx, tx, sim)
|
||||
}
|
||||
|
||||
// GenerateAddressFromCredential generates a deterministic address from a WebAuthn credential ID.
|
||||
// This ensures the same credential always generates the same address, allowing for
|
||||
// predictable account creation without requiring pre-existing blockchain state.
|
||||
func GenerateAddressFromCredential(credentialID string) sdk.AccAddress {
|
||||
// Create a deterministic hash from the credential ID
|
||||
// Add a domain separator to prevent collisions with other address generation methods
|
||||
domainSeparator := "webauthn_gasless_v1"
|
||||
data := domainSeparator + credentialID
|
||||
|
||||
// Generate SHA256 hash
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
|
||||
// Take the first 20 bytes for the address (Ethereum-compatible)
|
||||
return sdk.AccAddress(hash[:20])
|
||||
}
|
||||
|
||||
// GenerateDIDFromCredential generates a deterministic DID from a WebAuthn credential.
|
||||
// This creates a unique, reproducible DID for each WebAuthn credential.
|
||||
func GenerateDIDFromCredential(credentialID string, username string) string {
|
||||
// Create a deterministic hash from credential ID and username
|
||||
data := credentialID + ":" + username
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
|
||||
// Create a DID with the sonr method
|
||||
// Format: did:sonr:<hex-encoded-hash-prefix>
|
||||
didSuffix := hex.EncodeToString(hash[:16]) // Use first 16 bytes for shorter DIDs
|
||||
return fmt.Sprintf("did:sonr:%s", didSuffix)
|
||||
}
|
||||
|
||||
// ConditionalFeeDecorator wraps the standard fee deduction decorator to conditionally
|
||||
// skip fee deduction for gasless WebAuthn transactions marked by WebAuthnGaslessDecorator.
|
||||
// This is the simplest possible implementation that reuses all existing SDK infrastructure.
|
||||
type ConditionalFeeDecorator struct {
|
||||
standardFeeDecorator sdk.AnteDecorator
|
||||
}
|
||||
|
||||
// NewConditionalFeeDecorator creates a decorator that conditionally skips fee deduction
|
||||
// for gasless transactions while maintaining all standard fee logic for other transactions.
|
||||
func NewConditionalFeeDecorator(standardFeeDecorator sdk.AnteDecorator) ConditionalFeeDecorator {
|
||||
return ConditionalFeeDecorator{
|
||||
standardFeeDecorator: standardFeeDecorator,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle processes transactions and conditionally skips fee deduction for gasless WebAuthn transactions.
|
||||
func (cfd ConditionalFeeDecorator) AnteHandle(
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool, next sdk.AnteHandler,
|
||||
) (newCtx sdk.Context, err error) {
|
||||
// Check if this transaction was marked as gasless by WebAuthnGaslessDecorator
|
||||
if gasless, ok := ctx.Value("webauthn_gasless").(bool); ok && gasless {
|
||||
// Skip fee deduction for gasless WebAuthn transactions
|
||||
ctx.Logger().Info("Waiving fees for gasless WebAuthn registration")
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
// For all other transactions, use the standard fee deduction decorator
|
||||
return cfd.standardFeeDecorator.AnteHandle(ctx, tx, sim, next)
|
||||
}
|
||||
|
||||
// ConditionalSignatureDecorator wraps signature verification to skip it for gasless transactions
|
||||
// This is used in enhanced mode where WebAuthn itself is the authentication mechanism.
|
||||
type ConditionalSignatureDecorator struct {
|
||||
sigVerifyDecorator sdk.AnteDecorator
|
||||
}
|
||||
|
||||
// NewConditionalSignatureDecorator creates a decorator that conditionally skips signature verification
|
||||
func NewConditionalSignatureDecorator(
|
||||
sigVerifyDecorator sdk.AnteDecorator,
|
||||
) ConditionalSignatureDecorator {
|
||||
return ConditionalSignatureDecorator{
|
||||
sigVerifyDecorator: sigVerifyDecorator,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle conditionally skips signature verification for gasless transactions
|
||||
func (csd ConditionalSignatureDecorator) AnteHandle(
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool, next sdk.AnteHandler,
|
||||
) (newCtx sdk.Context, err error) {
|
||||
// Check if signature verification should be skipped
|
||||
if skip, ok := ctx.Value("skip_sig_verification").(bool); ok && skip {
|
||||
ctx.Logger().Debug("Skipping signature verification for gasless transaction")
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
// For all other transactions, use the standard signature verification
|
||||
return csd.sigVerifyDecorator.AnteHandle(ctx, tx, sim, next)
|
||||
}
|
||||
|
||||
// ConditionalPubKeyDecorator wraps pubkey setting to skip it for gasless transactions
|
||||
// This is used in enhanced mode where accounts are created without pre-existing keys.
|
||||
type ConditionalPubKeyDecorator struct {
|
||||
setPubKeyDecorator sdk.AnteDecorator
|
||||
}
|
||||
|
||||
// NewConditionalPubKeyDecorator creates a decorator that conditionally skips pubkey setting
|
||||
func NewConditionalPubKeyDecorator(
|
||||
setPubKeyDecorator sdk.AnteDecorator,
|
||||
) ConditionalPubKeyDecorator {
|
||||
return ConditionalPubKeyDecorator{
|
||||
setPubKeyDecorator: setPubKeyDecorator,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle conditionally skips pubkey setting for gasless transactions
|
||||
func (cpd ConditionalPubKeyDecorator) AnteHandle(
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool, next sdk.AnteHandler,
|
||||
) (newCtx sdk.Context, err error) {
|
||||
// Check if pubkey verification should be skipped
|
||||
if skip, ok := ctx.Value("skip_pubkey_verification").(bool); ok && skip {
|
||||
ctx.Logger().Debug("Skipping pubkey setting for gasless transaction")
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
// For all other transactions, use the standard pubkey decorator
|
||||
return cpd.setPubKeyDecorator.AnteHandle(ctx, tx, sim, next)
|
||||
}
|
||||
|
||||
// ConditionalSigCountDecorator wraps signature count validation to skip it for gasless transactions
|
||||
// This is critical for gasless WebAuthn transactions which have no signatures to validate.
|
||||
type ConditionalSigCountDecorator struct {
|
||||
sigCountDecorator sdk.AnteDecorator
|
||||
}
|
||||
|
||||
// NewConditionalSigCountDecorator creates a decorator that conditionally skips signature count validation
|
||||
func NewConditionalSigCountDecorator(
|
||||
sigCountDecorator sdk.AnteDecorator,
|
||||
) ConditionalSigCountDecorator {
|
||||
return ConditionalSigCountDecorator{
|
||||
sigCountDecorator: sigCountDecorator,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle conditionally skips signature count validation for gasless transactions
|
||||
func (cscd ConditionalSigCountDecorator) AnteHandle(
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool, next sdk.AnteHandler,
|
||||
) (newCtx sdk.Context, err error) {
|
||||
// Check if signature verification should be skipped (implies no signatures to count)
|
||||
if skip, ok := ctx.Value("skip_sig_verification").(bool); ok && skip {
|
||||
ctx.Logger().Debug("Skipping signature count validation for gasless transaction")
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
// For all other transactions, use the standard signature count validator
|
||||
return cscd.sigCountDecorator.AnteHandle(ctx, tx, sim, next)
|
||||
}
|
||||
|
||||
// ConditionalSigGasDecorator wraps signature gas consumption to skip it for gasless transactions
|
||||
// This prevents "no signatures supplied" errors for gasless WebAuthn transactions.
|
||||
type ConditionalSigGasDecorator struct {
|
||||
sigGasDecorator sdk.AnteDecorator
|
||||
}
|
||||
|
||||
// NewConditionalSigGasDecorator creates a decorator that conditionally skips signature gas consumption
|
||||
func NewConditionalSigGasDecorator(
|
||||
sigGasDecorator sdk.AnteDecorator,
|
||||
) ConditionalSigGasDecorator {
|
||||
return ConditionalSigGasDecorator{
|
||||
sigGasDecorator: sigGasDecorator,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle conditionally skips signature gas consumption for gasless transactions
|
||||
func (csgd ConditionalSigGasDecorator) AnteHandle(
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool, next sdk.AnteHandler,
|
||||
) (newCtx sdk.Context, err error) {
|
||||
// Check if signature verification should be skipped (implies no signature gas consumption needed)
|
||||
if skip, ok := ctx.Value("skip_sig_verification").(bool); ok && skip {
|
||||
ctx.Logger().Debug("Skipping signature gas consumption for gasless transaction")
|
||||
return next(ctx, tx, sim)
|
||||
}
|
||||
|
||||
// For all other transactions, use the standard signature gas decorator
|
||||
return csgd.sigGasDecorator.AnteHandle(ctx, tx, sim, next)
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package ante_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/sonr-io/sonr/app/ante"
|
||||
)
|
||||
|
||||
// MockWebAuthnKeeper implements the WebAuthnKeeperInterface for testing
|
||||
type MockWebAuthnKeeper struct {
|
||||
existingCredentials map[string]bool
|
||||
}
|
||||
|
||||
func NewMockWebAuthnKeeper() *MockWebAuthnKeeper {
|
||||
return &MockWebAuthnKeeper{
|
||||
existingCredentials: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockWebAuthnKeeper) HasExistingCredential(ctx sdk.Context, credentialId string) bool {
|
||||
return m.existingCredentials[credentialId]
|
||||
}
|
||||
|
||||
func (m *MockWebAuthnKeeper) AddCredential(credentialId string) {
|
||||
m.existingCredentials[credentialId] = true
|
||||
}
|
||||
|
||||
// MockAccountKeeper - Simple mock without complex interface implementation
|
||||
// For comprehensive testing, we'd implement the full AccountKeeper interface
|
||||
|
||||
// MockTransaction implements sdk.Tx for testing
|
||||
type MockTransaction struct {
|
||||
messages []sdk.Msg
|
||||
}
|
||||
|
||||
func (m *MockTransaction) GetMsgs() []sdk.Msg {
|
||||
return m.messages
|
||||
}
|
||||
|
||||
func (m *MockTransaction) ValidateBasic() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebAuthnGaslessTestSuite tests the WebAuthn gasless decorator
|
||||
type WebAuthnGaslessTestSuite struct {
|
||||
suite.Suite
|
||||
didKeeper *MockWebAuthnKeeper
|
||||
}
|
||||
|
||||
func TestWebAuthnGaslessTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(WebAuthnGaslessTestSuite))
|
||||
}
|
||||
|
||||
func (suite *WebAuthnGaslessTestSuite) SetupTest() {
|
||||
// Setup minimal test context and keepers
|
||||
suite.didKeeper = NewMockWebAuthnKeeper()
|
||||
// We'll focus on testing the logic without complex keeper setup
|
||||
}
|
||||
|
||||
func (suite *WebAuthnGaslessTestSuite) TestGenerateAddressFromCredential() {
|
||||
// Test that the same credential always generates the same address
|
||||
credentialID := "test-credential-id-123"
|
||||
|
||||
addr1 := ante.GenerateAddressFromCredential(credentialID)
|
||||
addr2 := ante.GenerateAddressFromCredential(credentialID)
|
||||
|
||||
suite.Require().Equal(addr1, addr2, "Same credential should generate same address")
|
||||
suite.Require().NotNil(addr1, "Generated address should not be nil")
|
||||
suite.Require().Equal(20, len(addr1), "Address should be 20 bytes")
|
||||
}
|
||||
|
||||
func (suite *WebAuthnGaslessTestSuite) TestGenerateDIDFromCredential() {
|
||||
// Test DID generation
|
||||
credentialID := "test-credential-id-123"
|
||||
username := "testuser"
|
||||
|
||||
did1 := ante.GenerateDIDFromCredential(credentialID, username)
|
||||
did2 := ante.GenerateDIDFromCredential(credentialID, username)
|
||||
|
||||
suite.Require().Equal(did1, did2, "Same inputs should generate same DID")
|
||||
suite.Require().Contains(did1, "did:sonr:", "DID should have correct prefix")
|
||||
|
||||
// Different username should generate different DID
|
||||
did3 := ante.GenerateDIDFromCredential(credentialID, "differentuser")
|
||||
suite.Require().NotEqual(did1, did3, "Different username should generate different DID")
|
||||
}
|
||||
|
||||
func (suite *WebAuthnGaslessTestSuite) TestWebAuthnGaslessDecorator_StandardMode() {
|
||||
// Test standard mode behavior
|
||||
decorator := ante.NewWebAuthnGaslessDecorator(
|
||||
nil, // accountKeeper would be mocked in full test
|
||||
suite.didKeeper,
|
||||
false, // standard mode
|
||||
)
|
||||
|
||||
suite.Require().NotNil(decorator, "Decorator should be created")
|
||||
}
|
||||
|
||||
func (suite *WebAuthnGaslessTestSuite) TestWebAuthnGaslessDecorator_EnhancedMode() {
|
||||
// Test enhanced mode behavior
|
||||
decorator := ante.NewWebAuthnGaslessDecorator(
|
||||
nil, // accountKeeper would be mocked in full test
|
||||
suite.didKeeper,
|
||||
true, // enhanced mode
|
||||
)
|
||||
|
||||
suite.Require().NotNil(decorator, "Decorator should be created in enhanced mode")
|
||||
}
|
||||
|
||||
func (suite *WebAuthnGaslessTestSuite) TestConditionalDecorators() {
|
||||
// Test that conditional decorators are created properly
|
||||
// We'll use a simple mock decorator for testing
|
||||
mockDecorator := &MockAnteDecorator{}
|
||||
|
||||
feeDecorator := ante.NewConditionalFeeDecorator(mockDecorator)
|
||||
suite.Require().NotNil(feeDecorator, "Conditional fee decorator should be created")
|
||||
|
||||
sigDecorator := ante.NewConditionalSignatureDecorator(mockDecorator)
|
||||
suite.Require().NotNil(sigDecorator, "Conditional signature decorator should be created")
|
||||
|
||||
pubKeyDecorator := ante.NewConditionalPubKeyDecorator(mockDecorator)
|
||||
suite.Require().NotNil(pubKeyDecorator, "Conditional pubkey decorator should be created")
|
||||
}
|
||||
|
||||
func (suite *WebAuthnGaslessTestSuite) TestWebAuthnCredentialValidation() {
|
||||
// Test WebAuthn credential structure validation
|
||||
// This tests the core logic without full ante handler setup
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
credentialID string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "valid credential ID",
|
||||
credentialID: "valid-credential-123",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty credential ID should be caught by message validation",
|
||||
credentialID: "",
|
||||
expectError: true, // This would be caught by ValidateStructure
|
||||
},
|
||||
{
|
||||
name: "duplicate credential",
|
||||
credentialID: "duplicate-cred",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Add a duplicate credential
|
||||
suite.didKeeper.AddCredential("duplicate-cred")
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
hasExisting := suite.didKeeper.HasExistingCredential(sdk.Context{}, tc.credentialID)
|
||||
|
||||
switch tc.credentialID {
|
||||
case "duplicate-cred":
|
||||
suite.Require().True(hasExisting, "Duplicate credential should exist")
|
||||
case "valid-credential-123":
|
||||
suite.Require().False(hasExisting, "New credential should not exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *WebAuthnGaslessTestSuite) TestEnhancedModeAddressGeneration() {
|
||||
// Test enhanced mode functionality for address generation
|
||||
testCredentialID := "test-enhanced-credential"
|
||||
|
||||
// Test that the same credential always generates the same address
|
||||
addr1 := ante.GenerateAddressFromCredential(testCredentialID)
|
||||
addr2 := ante.GenerateAddressFromCredential(testCredentialID)
|
||||
|
||||
suite.Require().Equal(addr1, addr2, "Same credential should generate same address")
|
||||
suite.Require().Equal(20, len(addr1), "Address should be 20 bytes")
|
||||
|
||||
// Test that different credentials generate different addresses
|
||||
addr3 := ante.GenerateAddressFromCredential("different-credential")
|
||||
suite.Require().
|
||||
NotEqual(addr1, addr3, "Different credentials should generate different addresses")
|
||||
}
|
||||
|
||||
// MockAnteDecorator is a simple mock implementation of sdk.AnteDecorator for testing
|
||||
type MockAnteDecorator struct{}
|
||||
|
||||
func (m *MockAnteDecorator) AnteHandle(
|
||||
ctx sdk.Context,
|
||||
tx sdk.Tx,
|
||||
simulate bool,
|
||||
next sdk.AnteHandler,
|
||||
) (sdk.Context, error) {
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
|
||||
// TestAddressGeneration tests the deterministic address generation
|
||||
func TestAddressGeneration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
credentialID string
|
||||
expectSame bool
|
||||
}{
|
||||
{
|
||||
name: "same credential generates same address",
|
||||
credentialID: "credential-1",
|
||||
expectSame: true,
|
||||
},
|
||||
{
|
||||
name: "different credential generates different address",
|
||||
credentialID: "credential-2",
|
||||
expectSame: false,
|
||||
},
|
||||
}
|
||||
|
||||
baseAddr := ante.GenerateAddressFromCredential("base-credential")
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
addr := ante.GenerateAddressFromCredential(tc.credentialID)
|
||||
require.NotNil(t, addr)
|
||||
require.Equal(t, 20, len(addr))
|
||||
|
||||
if tc.expectSame && tc.credentialID == "base-credential" {
|
||||
require.Equal(t, baseAddr, addr)
|
||||
} else if !tc.expectSame {
|
||||
require.NotEqual(t, baseAddr, addr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDIDGeneration tests the deterministic DID generation
|
||||
func TestDIDGeneration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
credentialID string
|
||||
username string
|
||||
expectedPrefix string
|
||||
}{
|
||||
{
|
||||
name: "generates valid DID",
|
||||
credentialID: "cred-1",
|
||||
username: "user1",
|
||||
expectedPrefix: "did:sonr:",
|
||||
},
|
||||
{
|
||||
name: "different username generates different DID",
|
||||
credentialID: "cred-1",
|
||||
username: "user2",
|
||||
expectedPrefix: "did:sonr:",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
did := ante.GenerateDIDFromCredential(tc.credentialID, tc.username)
|
||||
require.NotEmpty(t, did)
|
||||
require.Contains(t, did, tc.expectedPrefix)
|
||||
|
||||
// Verify deterministic generation
|
||||
did2 := ante.GenerateDIDFromCredential(tc.credentialID, tc.username)
|
||||
require.Equal(t, did, did2)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user