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 Oct 28, 2022
1 parent e501201 commit 97f40a0
Show file tree
Hide file tree
Showing 19 changed files with 392 additions and 89 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 @@ -864,7 +864,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 @@ -3,6 +3,7 @@ package computation
import (
"context"
"fmt"
"github.com/onflow/flow-go/fvm/errors"
"testing"

"github.com/onflow/cadence"
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, fmt.Sprintf("Error Code: %d", errors.ErrCodeInsufficientPayerBalance))
})

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, fmt.Sprintf("Error Code: %d", errors.ErrCodeInsufficientPayerBalance))

// 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, fmt.Sprintf("Error Code: %d", errors.ErrCodeInsufficientPayerBalance))

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("39c851fefc59d5e63e6a96770bf26a53fcfdd3db65fc21433759f5d120440ad3,")
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 @@ -262,8 +262,8 @@ func (b *BootstrapProcedure) Run(
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 @@ -391,7 +391,7 @@ func (b *BootstrapProcedure) deployFlowToken(service, fungibleToken flow.Address
return flowToken
}

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

txError, err := b.invokeMetaTransaction(
Expand All @@ -401,6 +401,7 @@ func (b *BootstrapProcedure) deployFlowFees(service, fungibleToken, flowToken fl
service,
fungibleToken,
flowToken,
storageFees,
flowFees,
),
0),
Expand All @@ -409,7 +410,7 @@ func (b *BootstrapProcedure) deployFlowFees(service, fungibleToken, flowToken fl
return flowFees
}

func (b *BootstrapProcedure) deployStorageFees(service, fungibleToken, flowToken flow.Address) {
func (b *BootstrapProcedure) deployStorageFees(service, fungibleToken, flowToken flow.Address) flow.Address {
contract := contracts.FlowStorageFees(
fungibleToken.HexWithPrefix(),
flowToken.HexWithPrefix(),
Expand All @@ -425,6 +426,7 @@ func (b *BootstrapProcedure) 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 @@ -42,6 +42,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 @@ -78,6 +78,8 @@ const (
ErrCodeScriptExecutionCancelledError ErrorCode = 1114
ErrCodeScriptExecutionTimedOutError ErrorCode = 1113
ErrCodeEventEncodingError ErrorCode = 1115
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.String(),
)
}

// 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
24 changes: 20 additions & 4 deletions fvm/fvm_blockcontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,6 @@ func TestBlockContext_ExecuteTransaction_InteractionLimitReached(t *testing.T) {
withContextOptions(fvm.WithTransactionFeesEnabled(true)).
run(
func(t *testing.T, vm *fvm.VirtualMachine, chain flow.Chain, ctx fvm.Context, view state.View, programs *programs.BlockPrograms) {
ctx.MaxStateInteractionSize = 500_000

// Create an account private key.
privateKeys, err := testutil.GenerateAccountPrivateKeys(1)
Expand All @@ -1033,13 +1032,30 @@ func TestBlockContext_ExecuteTransaction_InteractionLimitReached(t *testing.T) {
accounts, err := testutil.CreateAccounts(vm, view, programs, privateKeys, chain)
require.NoError(t, err)

txBody := testutil.CreateContractDeploymentTransaction(
// fund account so the payer can pay for the next transaction.
txBody := transferTokensTx(chain).
SetProposalKey(chain.ServiceAddress(), 0, 0).
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, programs.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, 1)
txBody.SetPayer(accounts[0])

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

tx := fvm.Transaction(txBody, programs.NextTxIndexForTestingOnly())
tx = fvm.Transaction(txBody, programs.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 @@ -893,6 +893,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 @@ -1978,7 +1992,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
Loading

0 comments on commit 97f40a0

Please sign in to comment.