Skip to content

Commit

Permalink
Merge pull request #6004 from The-K-R-O-K/illia-malachyn/5823-payer-h…
Browse files Browse the repository at this point in the history
…as-enough-flow-to-pay-fee

[Access] Add check if tx payer has sufficient amount of flow to pay fee
  • Loading branch information
peterargue authored Jul 18, 2024
2 parents 3b62404 + aeee394 commit f6fdeab
Show file tree
Hide file tree
Showing 17 changed files with 501 additions and 79 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ generate-mocks: install-mock-generators
mockery --name '.*' --dir="./consensus/hotstuff" --case=underscore --output="./consensus/hotstuff/mocks" --outpkg="mocks"
mockery --name '.*' --dir="./engine/access/wrapper" --case=underscore --output="./engine/access/mock" --outpkg="mock"
mockery --name 'API' --dir="./access" --case=underscore --output="./access/mock" --outpkg="mock"
mockery --name 'Blocks' --dir="./access" --case=underscore --output="./access/mock" --outpkg="mock"
mockery --name 'API' --dir="./engine/protocol" --case=underscore --output="./engine/protocol/mock" --outpkg="mock"
mockery --name '.*' --dir="./engine/access/state_stream" --case=underscore --output="./engine/access/state_stream/mock" --outpkg="mock"
mockery --name 'BlockTracker' --dir="./engine/access/subscription" --case=underscore --output="./engine/access/subscription/mock" --outpkg="mock"
Expand Down
17 changes: 17 additions & 0 deletions access/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"errors"
"fmt"

"github.com/onflow/cadence"

"github.com/onflow/flow-go/model/flow"
)

Expand Down Expand Up @@ -98,3 +100,18 @@ type InvalidTxRateLimitedError struct {
func (e InvalidTxRateLimitedError) Error() string {
return fmt.Sprintf("transaction rate limited for payer (%s)", e.Payer)
}

type InsufficientBalanceError struct {
Payer flow.Address
RequiredBalance cadence.UFix64
}

func (e InsufficientBalanceError) Error() string {
return fmt.Sprintf("transaction payer (%s) has insufficient balance to pay transaction fee. "+
"Required balance: (%s). ", e.Payer, e.RequiredBalance.String())
}

func IsInsufficientBalanceError(err error) bool {
var balanceError InsufficientBalanceError
return errors.As(err, &balanceError)
}
117 changes: 117 additions & 0 deletions access/mock/blocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions access/utils/cadence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package utils

import (
"errors"

"github.com/onflow/cadence"
"github.com/onflow/cadence/encoding/json"
)

func EncodeArgs(argValues []cadence.Value) ([][]byte, error) {
args := make([][]byte, len(argValues))
for i, arg := range argValues {
var err error
args[i], err = json.Encode(arg)
if err != nil {
return nil, errors.New("could not encode cadence value: " + err.Error())
}
}
return args, nil
}
108 changes: 95 additions & 13 deletions access/validator.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
package access

import (
"context"
"errors"
"fmt"

"github.com/onflow/cadence"
jsoncdc "github.com/onflow/cadence/encoding/json"
"github.com/onflow/cadence/runtime/parser"
"github.com/onflow/crypto"
"github.com/onflow/flow-core-contracts/lib/go/templates"
"github.com/rs/zerolog/log"

cadenceutils "github.com/onflow/flow-go/access/utils"
"github.com/onflow/flow-go/fvm"
"github.com/onflow/flow-go/fvm/systemcontracts"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/module/execution"
"github.com/onflow/flow-go/state"
"github.com/onflow/flow-go/state/protocol"
)

type Blocks interface {
HeaderByID(id flow.Identifier) (*flow.Header, error)
FinalizedHeader() (*flow.Header, error)
SealedHeader() (*flow.Header, error)
}

type ProtocolStateBlocks struct {
Expand Down Expand Up @@ -42,6 +52,10 @@ func (b *ProtocolStateBlocks) FinalizedHeader() (*flow.Header, error) {
return b.state.Final().Head()
}

func (b *ProtocolStateBlocks) SealedHeader() (*flow.Header, error) {
return b.state.Sealed().Head()
}

// RateLimiter is an interface for checking if an address is rate limited.
// By convention, the address used is the payer field of a transaction.
// This rate limiter is applied when a transaction is first received by a
Expand Down Expand Up @@ -70,28 +84,40 @@ type TransactionValidationOptions struct {
CheckScriptsParse bool
MaxTransactionByteSize uint64
MaxCollectionByteSize uint64
CheckPayerBalance bool
}

type TransactionValidator struct {
blocks Blocks // for looking up blocks to check transaction expiry
chain flow.Chain // for checking validity of addresses
options TransactionValidationOptions
serviceAccountAddress flow.Address
limiter RateLimiter
blocks Blocks // for looking up blocks to check transaction expiry
chain flow.Chain // for checking validity of addresses
options TransactionValidationOptions
serviceAccountAddress flow.Address
limiter RateLimiter
scriptExecutor execution.ScriptExecutor
verifyPayerBalanceScript []byte
}

func NewTransactionValidator(
blocks Blocks,
chain flow.Chain,
options TransactionValidationOptions,
) *TransactionValidator {
return &TransactionValidator{
blocks: blocks,
chain: chain,
options: options,
serviceAccountAddress: chain.ServiceAddress(),
limiter: NewNoopLimiter(),
executor execution.ScriptExecutor,
) (*TransactionValidator, error) {
if options.CheckPayerBalance && executor == nil {
return nil, errors.New("transaction validator cannot use checkPayerBalance with nil executor")
}

env := systemcontracts.SystemContractsForChain(chain.ChainID()).AsTemplateEnv()

return &TransactionValidator{
blocks: blocks,
chain: chain,
options: options,
serviceAccountAddress: chain.ServiceAddress(),
limiter: NewNoopLimiter(),
scriptExecutor: executor,
verifyPayerBalanceScript: templates.GenerateVerifyPayerBalanceForTxExecution(env),
}, nil
}

func NewTransactionValidatorWithLimiter(
Expand All @@ -109,7 +135,7 @@ func NewTransactionValidatorWithLimiter(
}
}

func (v *TransactionValidator) Validate(tx *flow.TransactionBody) (err error) {
func (v *TransactionValidator) Validate(ctx context.Context, tx *flow.TransactionBody) (err error) {
// rate limit transactions for specific payers.
// a short term solution to prevent attacks that send too many failed transactions
// if a transaction is from a payer that should be rate limited, all the following
Expand Down Expand Up @@ -159,6 +185,20 @@ func (v *TransactionValidator) Validate(tx *flow.TransactionBody) (err error) {
return err
}

err = v.checkSufficientBalanceToPayForTransaction(ctx, tx)
if err != nil {
// we only return InsufficientBalanceError as it's a client-side issue
// that requires action from a user. Other errors (e.g. parsing errors)
// are 'internal' and related to script execution process. they shouldn't
// prevent the transaction from proceeding.
if IsInsufficientBalanceError(err) {
return err
}

// log and ignore all other errors
log.Info().Err(err).Msg("check payer validation skipped due to error")
}

// TODO replace checkSignatureFormat by verifying the account/payer signatures

return nil
Expand Down Expand Up @@ -346,6 +386,48 @@ func (v *TransactionValidator) checkSignatureFormat(tx *flow.TransactionBody) er
return nil
}

func (v *TransactionValidator) checkSufficientBalanceToPayForTransaction(ctx context.Context, tx *flow.TransactionBody) error {
if !v.options.CheckPayerBalance {
return nil
}

header, err := v.blocks.SealedHeader()
if err != nil {
return fmt.Errorf("could not fetch block header: %w", err)
}

payerAddress := cadence.NewAddress(tx.Payer)
inclusionEffort := cadence.UFix64(tx.InclusionEffort())
gasLimit := cadence.UFix64(tx.GasLimit)

args, err := cadenceutils.EncodeArgs([]cadence.Value{payerAddress, inclusionEffort, gasLimit})
if err != nil {
return fmt.Errorf("failed to encode cadence args for script executor: %w", err)
}

result, err := v.scriptExecutor.ExecuteAtBlockHeight(ctx, v.verifyPayerBalanceScript, args, header.Height)
if err != nil {
return fmt.Errorf("script finished with error: %w", err)
}

value, err := jsoncdc.Decode(nil, result)
if err != nil {
return fmt.Errorf("could not decode result value returned by script executor: %w", err)
}

canExecuteTransaction, requiredBalance, _, err := fvm.DecodeVerifyPayerBalanceResult(value)
if err != nil {
return fmt.Errorf("could not parse cadence value returned by script executor: %w", err)
}

// return no error if payer has sufficient balance
if bool(canExecuteTransaction) {
return nil
}

return InsufficientBalanceError{Payer: tx.Payer, RequiredBalance: requiredBalance}
}

func remove(s []string, r string) []string {
for i, v := range s {
if v == r {
Expand Down
Loading

0 comments on commit f6fdeab

Please sign in to comment.