Skip to content

Commit 848f132

Browse files
authored
Pool App call budget in group transactions (#2711)
* Add pooling for grouped app calls and add unit tests * Fix doc errors * Pool app call budget in ep * Check if pooledBudget is not nil in budget call * Fix app call integration test output and minor syntax changes * Minor change in comment * Modify some tests to get better coverage * Refactor tests * Clean up * Remove a hardcoded constant * Add pointer equivalence test
1 parent 58bc231 commit 848f132

File tree

9 files changed

+273
-48
lines changed

9 files changed

+273
-48
lines changed

config/consensus.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ type ConsensusParams struct {
108108
// each Txn has a MinFee.
109109
EnableFeePooling bool
110110

111+
// EnableAppCostPooling specifies that the sum of fees for application calls
112+
// in a group is checked against the sum of the budget for application calls,
113+
// rather than check each individual app call is within the budget.
114+
EnableAppCostPooling bool
115+
111116
// RewardUnit specifies the number of MicroAlgos corresponding to one reward
112117
// unit.
113118
//
@@ -1005,6 +1010,9 @@ func initConsensusProtocols() {
10051010
// Enable TEAL 5 / AVM 1.0
10061011
vFuture.LogicSigVersion = 5
10071012

1013+
// Enable App calls to pool budget in grouped transactions
1014+
vFuture.EnableAppCostPooling = true
1015+
10081016
Consensus[protocol.ConsensusFuture] = vFuture
10091017
}
10101018

data/transactions/logic/TEAL_opcodes.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ Ops have a 'cost' of 1 unless otherwise specified.
4747
- Pushes: uint64
4848
- for (data A, signature B, pubkey C) verify the signature of ("ProgData" || program_hash || data) against the pubkey => {0 or 1}
4949
- **Cost**: 1900
50-
- Mode: Signature
5150

5251
The 32 byte public key is the last element on the stack, preceded by the 64 byte signature at the second-to-last element on the stack, preceded by the data which was signed at the third-to-last element on the stack.
5352

data/transactions/logic/eval.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ type EvalParams struct {
224224

225225
// determines eval mode: runModeSignature or runModeApplication
226226
runModeFlags runMode
227+
228+
// Total pool of app call budget in a group transaction
229+
PooledApplicationBudget *uint64
227230
}
228231

229232
type opEvalFunc func(cx *evalContext)
@@ -263,6 +266,9 @@ func (ep EvalParams) budget() int {
263266
if ep.runModeFlags == runModeSignature {
264267
return int(ep.Proto.LogicSigMaxCost)
265268
}
269+
if ep.Proto.EnableAppCostPooling && ep.PooledApplicationBudget != nil {
270+
return int(*ep.PooledApplicationBudget)
271+
}
266272
return ep.Proto.MaxAppProgramCost
267273
}
268274

@@ -365,6 +371,10 @@ func EvalStateful(program []byte, params EvalParams) (pass bool, err error) {
365371
cx.EvalParams = params
366372
cx.runModeFlags = runModeApplication
367373
pass, err = eval(program, &cx)
374+
if cx.EvalParams.Proto.EnableAppCostPooling && cx.EvalParams.PooledApplicationBudget != nil {
375+
// if eval passes, then budget is always greater than cost, so should not have underflow
376+
*cx.EvalParams.PooledApplicationBudget = basics.SubSaturate(*cx.EvalParams.PooledApplicationBudget, uint64(cx.cost))
377+
}
368378

369379
// set side effects
370380
cx.PastSideEffects[cx.GroupIndex].setScratchSpace(cx.scratch)
@@ -640,7 +650,8 @@ func (cx *evalContext) step() {
640650
}
641651
cx.cost += deets.Cost
642652
if cx.cost > cx.budget() {
643-
cx.err = fmt.Errorf("pc=%3d dynamic cost budget of %d exceeded, executing %s", cx.pc, cx.budget(), spec.Name)
653+
cx.err = fmt.Errorf("pc=%3d dynamic cost budget exceeded, executing %s: remaining budget is %d but program cost was %d",
654+
cx.pc, spec.Name, cx.budget(), cx.cost)
644655
return
645656
}
646657

data/transactions/logic/evalStateful_test.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,15 +697,33 @@ log
697697
})
698698
}
699699

700-
// check ed25519verify and arg are not allowed in statefull mode
701-
disallowed := []string{
700+
// check that ed25519verify and arg is not allowed in stateful mode between v2-v4
701+
disallowedV4 := []string{
702702
"byte 0x01\nbyte 0x01\nbyte 0x01\ned25519verify",
703703
"arg 0",
704704
"arg_0",
705705
"arg_1",
706706
"arg_2",
707707
"arg_3",
708708
}
709+
for _, source := range disallowedV4 {
710+
ops := testProg(t, source, 4)
711+
ep := defaultEvalParams(nil, nil)
712+
err := CheckStateful(ops.Program, ep)
713+
require.Error(t, err)
714+
_, err = EvalStateful(ops.Program, ep)
715+
require.Error(t, err)
716+
require.Contains(t, err.Error(), "not allowed in current mode")
717+
}
718+
719+
// check that arg is not allowed in stateful mode beyond v5
720+
disallowed := []string{
721+
"arg 0",
722+
"arg_0",
723+
"arg_1",
724+
"arg_2",
725+
"arg_3",
726+
}
709727
for _, source := range disallowed {
710728
ops := testProg(t, source, AssemblerMaxVersion)
711729
ep := defaultEvalParams(nil, nil)
@@ -2929,3 +2947,29 @@ bnz loop
29292947
require.Empty(t, delta.LocalDeltas)
29302948
require.Len(t, delta.Logs, 29)
29312949
}
2950+
2951+
func TestPooledAppCallsVerifyOp(t *testing.T) {
2952+
partitiontest.PartitionTest(t)
2953+
t.Parallel()
2954+
2955+
source := `#pragma version 5
2956+
global CurrentApplicationID
2957+
pop
2958+
byte 0x01
2959+
byte "ZC9KNzlnWTlKZ1pwSkNzQXVzYjNBcG1xTU9YbkRNWUtIQXNKYVk2RzRBdExPakQx"
2960+
addr DROUIZXGT3WFJR3QYVZWTR5OJJXJCMOLS7G4FUGZDSJM5PNOVOREH6HIZE
2961+
ed25519verify
2962+
pop
2963+
int 1`
2964+
2965+
ep, _ := makeSampleEnv()
2966+
ep.Proto.EnableAppCostPooling = true
2967+
ep.PooledApplicationBudget = new(uint64)
2968+
// Simulate test with 2 grouped txn
2969+
*ep.PooledApplicationBudget = uint64(ep.Proto.MaxAppProgramCost * 2)
2970+
testApp(t, source, ep, "pc=107 dynamic cost budget exceeded, executing ed25519verify: remaining budget is 1400 but program cost was 1905")
2971+
2972+
// Simulate test with 3 grouped txn
2973+
*ep.PooledApplicationBudget = uint64(ep.Proto.MaxAppProgramCost * 3)
2974+
testApp(t, source, ep)
2975+
}

data/transactions/logic/opcodes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ var OpSpecs = []OpSpec{
142142
{0x03, "sha512_256", opSHA512_256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, costly(45)},
143143

144144
{0x04, "ed25519verify", opEd25519verify, asmDefault, disDefault, threeBytes, oneInt, 1, runModeSignature, costly(1900)},
145+
{0x04, "ed25519verify", opEd25519verify, asmDefault, disDefault, threeBytes, oneInt, 5, modeAny, costly(1900)},
145146
{0x08, "+", opPlus, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault},
146147
{0x09, "-", opMinus, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault},
147148
{0x0a, "/", opDiv, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault},

ledger/apply/application_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ func (b *testBalancesPass) Put(addr basics.Address, ad basics.AccountData) error
242242
func (b *testBalancesPass) ConsensusParams() config.ConsensusParams {
243243
return b.proto
244244
}
245+
245246
func (b *testBalancesPass) Allocate(addr basics.Address, aidx basics.AppIndex, global bool, space basics.StateSchema) error {
246247
b.allocatedAppIdx = aidx
247248
return nil

ledger/eval.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -672,12 +672,18 @@ func (eval *BlockEvaluator) prepareEvalParams(txgroup []transactions.SignedTxnWi
672672
var groupNoAD []transactions.SignedTxn
673673
var pastSideEffects []logic.EvalSideEffects
674674
var minTealVersion uint64
675+
pooledApplicationBudget := uint64(0)
675676
res = make([]*logic.EvalParams, len(txgroup))
676677
for i, txn := range txgroup {
677678
// Ignore any non-ApplicationCall transactions
678679
if txn.SignedTxn.Txn.Type != protocol.ApplicationCallTx {
679680
continue
680681
}
682+
if eval.proto.EnableAppCostPooling {
683+
pooledApplicationBudget += uint64(eval.proto.MaxAppProgramCost)
684+
} else {
685+
pooledApplicationBudget = uint64(eval.proto.MaxAppProgramCost)
686+
}
681687

682688
// Initialize side effects and group without ApplyData lazily
683689
if groupNoAD == nil {
@@ -690,12 +696,13 @@ func (eval *BlockEvaluator) prepareEvalParams(txgroup []transactions.SignedTxnWi
690696
}
691697

692698
res[i] = &logic.EvalParams{
693-
Txn: &groupNoAD[i],
694-
Proto: &eval.proto,
695-
TxnGroup: groupNoAD,
696-
GroupIndex: i,
697-
PastSideEffects: pastSideEffects,
698-
MinTealVersion: &minTealVersion,
699+
Txn: &groupNoAD[i],
700+
Proto: &eval.proto,
701+
TxnGroup: groupNoAD,
702+
GroupIndex: i,
703+
PastSideEffects: pastSideEffects,
704+
MinTealVersion: &minTealVersion,
705+
PooledApplicationBudget: &pooledApplicationBudget,
699706
}
700707
}
701708
return

0 commit comments

Comments
 (0)