Skip to content

Commit

Permalink
Add payer can pay check to transaction execution
Browse files Browse the repository at this point in the history
  • Loading branch information
janezpodhostnik committed Nov 29, 2022
1 parent b856110 commit 5355125
Show file tree
Hide file tree
Showing 25 changed files with 390 additions and 169 deletions.
2 changes: 1 addition & 1 deletion engine/execution/computation/computer/computer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ func Test_ExecutingSystemCollection(t *testing.T) {
mock.Anything, // memory used
expectedNumberOfEvents,
expectedEventSize,
49, // expected number of registers touched
50, // expected number of registers touched
3404, // expected number of bytes written
1). // expected number of transactions
Return(nil).
Expand Down
11 changes: 6 additions & 5 deletions engine/execution/computation/execution_verification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/onflow/flow-go/fvm"
"github.com/onflow/flow-go/fvm/blueprints"
"github.com/onflow/flow-go/fvm/environment"
"github.com/onflow/flow-go/fvm/errors"
"github.com/onflow/flow-go/fvm/programs"
completeLedger "github.com/onflow/flow-go/ledger/complete"
"github.com/onflow/flow-go/ledger/complete/wal/fixtures"
Expand Down Expand Up @@ -171,7 +172,7 @@ func Test_ExecutionMatchesVerification(t *testing.T) {
err = testutil.SignTransaction(addKeyTx, accountAddress, accountPrivKey, 0)
require.NoError(t, err)

minimumStorage, err := cadence.NewUFix64("0.00008312")
minimumStorage, err := cadence.NewUFix64("0.00010489")
require.NoError(t, err)

cr := executeBlockAndVerify(t, [][]*flow.TransactionBody{
Expand All @@ -190,7 +191,7 @@ func Test_ExecutionMatchesVerification(t *testing.T) {
// ensure fee deduction events are emitted even though tx fails
require.Len(t, cr.Events[1], 3)
// storage limit error
assert.Contains(t, cr.TransactionResults[1].ErrorMessage, "Error Code: 1103")
assert.Contains(t, cr.TransactionResults[1].ErrorMessage, errors.ErrCodeInsufficientPayerBalance.String())
})

t.Run("with failed transaction fee deduction", func(t *testing.T) {
Expand Down Expand Up @@ -260,8 +261,8 @@ func Test_ExecutionMatchesVerification(t *testing.T) {
}
require.Equal(t, 10, transactionEvents)

// minimum account balance error as account is put below minimum account balance due to fee deduction
assert.Contains(t, cr.TransactionResults[1].ErrorMessage, "Error Code: 1103")
// insufficient account balance error as account is at minimum account balance and inclusion fee is non-zero
assert.Contains(t, cr.TransactionResults[1].ErrorMessage, errors.ErrCodeInsufficientPayerBalance.String())

// ensure tx fee deduction events are emitted even though tx failed
transactionEvents = 0
Expand Down Expand Up @@ -476,7 +477,7 @@ func TestTransactionFeeDeduction(t *testing.T) {
checkResult: func(t *testing.T, cr *execution.ComputationResult) {
require.Empty(t, cr.TransactionResults[0].ErrorMessage)
require.Empty(t, cr.TransactionResults[1].ErrorMessage)
require.Contains(t, cr.TransactionResults[2].ErrorMessage, "Error Code: 1103")
require.Contains(t, cr.TransactionResults[2].ErrorMessage, errors.ErrCodeInsufficientPayerBalance.String())

var deposits []flow.Event
var withdraws []flow.Event
Expand Down
2 changes: 1 addition & 1 deletion engine/execution/state/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestBootstrapLedger(t *testing.T) {
}

func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) {
expectedStateCommitmentBytes, _ := hex.DecodeString("57c6944485f220b5be606121b86dd7cf105f102c7d2888d93f774c02f388a82d,")
expectedStateCommitmentBytes, _ := hex.DecodeString("2c1fcac4e97271af1bebb1a1aa7ba6962cb011291d409770ef3c489b8ca66727")
expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes)
require.NoError(t, err)

Expand Down
3 changes: 2 additions & 1 deletion fvm/blueprints/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ var setupFeesTransactionTemplate string
//go:embed scripts/setExecutionMemoryLimit.cdc
var setExecutionMemoryLimit string

func DeployTxFeesContractTransaction(service, fungibleToken, flowToken, flowFees flow.Address) *flow.TransactionBody {
func DeployTxFeesContractTransaction(service, fungibleToken, flowToken, storageFees, flowFees flow.Address) *flow.TransactionBody {
contract := contracts.FlowFees(
fungibleToken.HexWithPrefix(),
flowToken.HexWithPrefix(),
storageFees.HexWithPrefix(),
)

return flow.NewTransactionBody().
Expand Down
10 changes: 6 additions & 4 deletions fvm/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ func (b *bootstrapExecutor) Execute() error {
b.deployContractAuditVouchers(service)
fungibleToken := b.deployFungibleToken()
flowToken := b.deployFlowToken(service, fungibleToken)
feeContract := b.deployFlowFees(service, fungibleToken, flowToken)
b.deployStorageFees(service, fungibleToken, flowToken)
storageFees := b.deployStorageFees(service, fungibleToken, flowToken)
feeContract := b.deployFlowFees(service, fungibleToken, flowToken, storageFees)

if b.initialTokenSupply > 0 {
b.mintInitialTokens(service, fungibleToken, flowToken, b.initialTokenSupply)
Expand Down Expand Up @@ -423,7 +423,7 @@ func (b *bootstrapExecutor) deployFlowToken(service, fungibleToken flow.Address)
return flowToken
}

func (b *bootstrapExecutor) deployFlowFees(service, fungibleToken, flowToken flow.Address) flow.Address {
func (b *bootstrapExecutor) deployFlowFees(service, fungibleToken, flowToken, storageFees flow.Address) flow.Address {
flowFees := b.createAccount(b.accountKeys.FlowFeesAccountPublicKeys)

txError, err := b.invokeMetaTransaction(
Expand All @@ -433,6 +433,7 @@ func (b *bootstrapExecutor) deployFlowFees(service, fungibleToken, flowToken flo
service,
fungibleToken,
flowToken,
storageFees,
flowFees,
),
0),
Expand All @@ -441,7 +442,7 @@ func (b *bootstrapExecutor) deployFlowFees(service, fungibleToken, flowToken flo
return flowFees
}

func (b *bootstrapExecutor) deployStorageFees(service, fungibleToken, flowToken flow.Address) {
func (b *bootstrapExecutor) deployStorageFees(service, fungibleToken, flowToken flow.Address) flow.Address {
contract := contracts.FlowStorageFees(
fungibleToken.HexWithPrefix(),
flowToken.HexWithPrefix(),
Expand All @@ -457,6 +458,7 @@ func (b *bootstrapExecutor) deployStorageFees(service, fungibleToken, flowToken
0),
)
panicOnMetaInvokeErrf("failed to deploy storage fees contract: %s", txError, err)
return service
}

// deployContractAuditVouchers deploys audit vouchers contract to the service account
Expand Down
8 changes: 8 additions & 0 deletions fvm/environment/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ type Environment interface {

// SystemContracts
AccountsStorageCapacity(addresses []common.Address) (cadence.Value, error)
CheckPayerBalanceAndGetMaxTxFees(
payer flow.Address,
inclusionEffort uint64,
executionEffort uint64,
) (
cadence.Value,
error,
)
DeductTransactionFees(
payer flow.Address,
inclusionEffort uint64,
Expand Down
23 changes: 23 additions & 0 deletions fvm/environment/mock/environment.go

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

32 changes: 30 additions & 2 deletions fvm/environment/system_contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,34 @@ func ServiceAddress(chain flow.Chain) flow.Address {
return chain.ServiceAddress()
}

var verifyPayersBalanceForTransactionExecutionSpec = ContractFunctionSpec{
AddressFromChain: FlowFeesAddress,
LocationName: systemcontracts.ContractNameFlowFees,
FunctionName: systemcontracts.ContractServiceAccountFunction_verifyPayersBalanceForTransactionExecution,
ArgumentTypes: []sema.Type{
sema.AuthAccountType,
sema.UInt64Type,
sema.UInt64Type,
},
}

// CheckPayerBalanceAndGetMaxTxFees executes the verifyPayersBalanceForTransactionExecution
// on the FlowFees account.
func (sys *SystemContracts) CheckPayerBalanceAndGetMaxTxFees(
payer flow.Address,
inclusionEffort uint64,
maxExecutionEffort uint64,
) (cadence.Value, error) {
return sys.Invoke(
verifyPayersBalanceForTransactionExecutionSpec,
[]cadence.Value{
cadence.BytesToAddress(payer.Bytes()),
cadence.UFix64(inclusionEffort),
cadence.UFix64(maxExecutionEffort),
},
)
}

var deductTransactionFeeSpec = ContractFunctionSpec{
AddressFromChain: FlowFeesAddress,
LocationName: systemcontracts.ContractNameFlowFees,
Expand All @@ -103,8 +131,8 @@ var deductTransactionFeeSpec = ContractFunctionSpec{
},
}

// DeductTransactionFees executes the fee deduction contract on the service
// account.
// DeductTransactionFees executes the fee deduction function
// on the FlowFees account.
func (sys *SystemContracts) DeductTransactionFees(
payer flow.Address,
inclusionEffort uint64,
Expand Down
2 changes: 2 additions & 0 deletions fvm/errors/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ const (
ErrCodeScriptExecutionTimedOutError ErrorCode = 1113
ErrCodeEventEncodingError ErrorCode = 1115
ErrCodeInvalidFVMStateAccessError ErrorCode = 1116
ErrCodePayerBalanceCheckError ErrorCode = 1117
ErrCodeInsufficientPayerBalance ErrorCode = 1118

// accounts errors 1200 - 1250
// Deprecated: No longer used.
Expand Down
28 changes: 28 additions & 0 deletions fvm/errors/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package errors
import (
"strings"

"github.com/onflow/cadence"
"github.com/onflow/cadence/runtime"

"github.com/onflow/flow-go/model/flow"
Expand Down Expand Up @@ -47,6 +48,33 @@ func NewTransactionFeeDeductionFailedError(
payer)
}

// NewInsufficientPayerBalanceError constructs a new CodedError which
// indicates that the payer has insufficient balance to attempt transaction execution.
func NewInsufficientPayerBalanceError(
payer flow.Address,
requiredBalance cadence.UFix64,
) CodedError {
return NewCodedError(
ErrCodeInsufficientPayerBalance,
"payer %s has insufficient balance to attempt transaction execution (required balance: %s)",
payer,
requiredBalance,
)
}

// NewPayerBalanceCheckError constructs a new CodedError which
// indicates that a there was an error checking the payers balance.
func NewPayerBalanceCheckError(
payer flow.Address,
err error,
) CodedError {
return WrapCodedError(
ErrCodePayerBalanceCheckError,
err,
"failed to check if the payer %s has sufficient balance",
payer)
}

// NewComputationLimitExceededError constructs a new CodedError which indicates
// that computation has exceeded its limit.
func NewComputationLimitExceededError(limit uint64) CodedError {
Expand Down
29 changes: 26 additions & 3 deletions fvm/fvm_blockcontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1033,13 +1033,36 @@ func TestBlockContext_ExecuteTransaction_InteractionLimitReached(t *testing.T) {
accounts, err := testutil.CreateAccounts(vm, view, derivedBlockData, privateKeys, chain)
require.NoError(t, err)

txBody := testutil.CreateContractDeploymentTransaction(
n := 0
seqNum := func() uint64 {
sn := n
n++
return uint64(sn)
}
// fund account so the payer can pay for the next transaction.
txBody := transferTokensTx(chain).
SetProposalKey(chain.ServiceAddress(), 0, seqNum()).
AddAuthorizer(chain.ServiceAddress()).
AddArgument(jsoncdc.MustEncode(cadence.UFix64(100_000_000))).
AddArgument(jsoncdc.MustEncode(cadence.NewAddress(accounts[0]))).
SetPayer(chain.ServiceAddress())

err = testutil.SignEnvelope(txBody, chain.ServiceAddress(), unittest.ServiceAccountPrivateKey)
require.NoError(t, err)
tx := fvm.Transaction(txBody, derivedBlockData.NextTxIndexForTestingOnly())
err = vm.Run(ctx, tx, view)
require.NoError(t, err)
assert.NoError(t, tx.Err)

ctx.MaxStateInteractionSize = 500_000

txBody = testutil.CreateContractDeploymentTransaction(
"Container",
script,
accounts[0],
chain)

txBody.SetProposalKey(chain.ServiceAddress(), 0, 0)
txBody.SetProposalKey(chain.ServiceAddress(), 0, seqNum())
txBody.SetPayer(accounts[0])

err = testutil.SignPayload(txBody, chain.ServiceAddress(), unittest.ServiceAccountPrivateKey)
Expand All @@ -1048,7 +1071,7 @@ func TestBlockContext_ExecuteTransaction_InteractionLimitReached(t *testing.T) {
err = testutil.SignEnvelope(txBody, accounts[0], privateKeys[0])
require.NoError(t, err)

tx := fvm.Transaction(txBody, derivedBlockData.NextTxIndexForTestingOnly())
tx = fvm.Transaction(txBody, derivedBlockData.NextTxIndexForTestingOnly())

err = vm.Run(ctx, tx, view)
require.NoError(t, err)
Expand Down
16 changes: 15 additions & 1 deletion fvm/fvm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,20 @@ func TestTransactionFeeDeduction(t *testing.T) {
require.Equal(t, minimumStorageReservation, balanceAfter)
},
},
{
name: "If not enough balance, transaction fails because of insufficient funds",
fundWith: txFees - 1,
tryToTransfer: 0,
checkResult: func(t *testing.T, balanceBefore uint64, balanceAfter uint64, tx *fvm.TransactionProcedure) {
require.Error(t, tx.Err)
require.True(t, errors.HasErrorCode(tx.Err, errors.ErrCodeInsufficientPayerBalance))

// no computation used, due to transaction exiting early.
require.Equal(t, uint64(0), tx.ComputationUsed)

require.Equal(t, minimumStorageReservation-1, balanceAfter)
},
},
{
name: "If tx fails, fees are deducted",
fundWith: fundingAmount,
Expand Down Expand Up @@ -1976,7 +1990,7 @@ func TestInteractionLimit(t *testing.T) {
},
{
name: "low limit succeeds",
interactionLimit: 100000,
interactionLimit: 160000,
require: func(t *testing.T, tx *fvm.TransactionProcedure) {
require.NoError(t, tx.Err)
require.Len(t, tx.Events, 5)
Expand Down
15 changes: 8 additions & 7 deletions fvm/systemcontracts/system_contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ const (

// Unqualified names of service event contract functions (not including address prefix or contract name)

ContractServiceAccountFunction_setupNewAccount = "setupNewAccount"
ContractServiceAccountFunction_defaultTokenBalance = "defaultTokenBalance"
ContractServiceAccountFunction_deductTransactionFee = "deductTransactionFee"
ContractStorageFeesFunction_calculateAccountCapacity = "calculateAccountCapacity"
ContractStorageFeesFunction_calculateAccountsCapacity = "calculateAccountsCapacity"
ContractStorageFeesFunction_defaultTokenAvailableBalance = "defaultTokenAvailableBalance"
ContractDeploymentAuditsFunction_useVoucherForDeploy = "useVoucherForDeploy"
ContractServiceAccountFunction_setupNewAccount = "setupNewAccount"
ContractServiceAccountFunction_defaultTokenBalance = "defaultTokenBalance"
ContractServiceAccountFunction_deductTransactionFee = "deductTransactionFee"
ContractServiceAccountFunction_verifyPayersBalanceForTransactionExecution = "verifyPayersBalanceForTransactionExecution"
ContractStorageFeesFunction_calculateAccountCapacity = "calculateAccountCapacity"
ContractStorageFeesFunction_calculateAccountsCapacity = "calculateAccountsCapacity"
ContractStorageFeesFunction_defaultTokenAvailableBalance = "defaultTokenAvailableBalance"
ContractDeploymentAuditsFunction_useVoucherForDeploy = "useVoucherForDeploy"
)

// SystemContract represents a system contract on a particular chain.
Expand Down
10 changes: 10 additions & 0 deletions fvm/transactionInvoker.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type transactionExecutor struct {
TransactionVerifier
TransactionSequenceNumberChecker
TransactionStorageLimiter
TransactionPayerBalanceChecker

ctx Context
proc *TransactionProcedure
Expand Down Expand Up @@ -275,6 +276,15 @@ func (executor *transactionExecutor) normalExecution() (
modifiedSets programsCache.ModifiedSetsInvalidator,
err error,
) {
// TODO: max transaction fees returned from this function should be used in the storage check
_, err = executor.CheckPayerBalanceAndReturnMaxFees(
executor.proc,
executor.txnState,
executor.env)
if err != nil {
return
}

executor.txnBodyExecutor = executor.cadenceRuntime.NewTransactionExecutor(
runtime.Script{
Source: executor.proc.Transaction.Script,
Expand Down
Loading

0 comments on commit 5355125

Please sign in to comment.