Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Access] Add check if tx payer has sufficient amount of flow to pay fee #6004

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7124cb4
Add check if tx payer has sufficient amount of flow to pay fee
illia-malachyn May 30, 2024
f34cc31
add skeleton code
illia-malachyn Jun 5, 2024
47f50da
skeleton code with extending VM interface
illia-malachyn Jun 5, 2024
5d0a37e
add env
illia-malachyn Jun 12, 2024
b72cbb8
Extend validator with check if payer has balance to pay for tx
illia-malachyn Jun 12, 2024
6835088
Add option to enable check if tx payer has sufficient balance
illia-malachyn Jun 17, 2024
b344d98
add option to enable payer balance check
illia-malachyn Jun 18, 2024
f45ca34
look up sealed header to execute script on
illia-malachyn Jun 18, 2024
fd820b0
Merge branch 'master' into illia-malachyn/5823-payer-has-enough-flow-…
illia-malachyn Jun 18, 2024
83ff795
fix tx validator creation
illia-malachyn Jun 18, 2024
577e146
set checkPayerBalance default value to false
illia-malachyn Jun 18, 2024
8ba0b63
remove unnecessary ctx passing for validator in CN ingestion engine
illia-malachyn Jun 19, 2024
cfbfd3c
replace templates in insecure package
illia-malachyn Jun 19, 2024
3ec0447
replace templates in integration/ folder
illia-malachyn Jun 19, 2024
3253784
add integration test skeleton
illia-malachyn Jun 20, 2024
fbdcf0e
update flow-core-contracts commit to latest
illia-malachyn Jun 24, 2024
cd3633a
Merge branch 'master' into illia-malachyn/5823-payer-has-enough-flow-…
illia-malachyn Jun 24, 2024
4863c42
add code to pay for tx
illia-malachyn Jun 27, 2024
47c5d8c
replace integration test with simple unit test
illia-malachyn Jul 2, 2024
6f39b25
Merge branch 'master' into illia-malachyn/5823-payer-has-enough-flow-…
illia-malachyn Jul 3, 2024
5c67d99
fix args types for script in tx validator
illia-malachyn Jul 4, 2024
f34875b
remove mock for access/blocks
illia-malachyn Jul 4, 2024
5960ea2
fix integration's go.mod and account creatation fees
illia-malachyn Jul 4, 2024
140009d
remove tx fees in localnet
illia-malachyn Jul 4, 2024
09ab561
move cadence utils to access directory
illia-malachyn Jul 4, 2024
5509392
return mock blocks as it is used in unit test
illia-malachyn Jul 4, 2024
3f0e13f
run goimports
illia-malachyn Jul 5, 2024
6619460
replace emulator dep
illia-malachyn Jul 5, 2024
8cbb685
add unit tests for balance check
illia-malachyn Jul 8, 2024
6e57f41
remove reduntant space
illia-malachyn Jul 8, 2024
b8a98c2
Merge branch 'master' into illia-malachyn/5823-payer-has-enough-flow-…
illia-malachyn Jul 8, 2024
8af686f
run goimport
illia-malachyn Jul 8, 2024
3e56296
remove querying of payer actual balance
illia-malachyn Jul 11, 2024
4442af2
Merge branch 'master' into illia-malachyn/5823-payer-has-enough-flow-…
illia-malachyn Jul 11, 2024
891c3ec
run go mod tidy
illia-malachyn Jul 11, 2024
807d5f3
update core contracts version
illia-malachyn Jul 16, 2024
817db93
Merge branch 'master' into illia-malachyn/5823-payer-has-enough-flow-…
illia-malachyn Jul 16, 2024
5866b5c
update replace statement with correct version
illia-malachyn Jul 16, 2024
f7d331c
update flow-emulator replace
illia-malachyn Jul 16, 2024
5b4abda
update flow-emulator replace
illia-malachyn Jul 16, 2024
58850e2
Merge branch 'master' into illia-malachyn/5823-payer-has-enough-flow-…
illia-malachyn Jul 18, 2024
5fc2b1d
update emulator replace
illia-malachyn Jul 18, 2024
69d112a
run go mod tidy
illia-malachyn Jul 18, 2024
aeee394
update to merged version of emulator
peterargue Jul 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
illia-malachyn marked this conversation as resolved.
Show resolved Hide resolved
// 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)
illia-malachyn marked this conversation as resolved.
Show resolved Hide resolved
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)
illia-malachyn marked this conversation as resolved.
Show resolved Hide resolved
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
Loading