summaryrefslogtreecommitdiff
path: root/data/transactions/logic/eval.go
diff options
context:
space:
mode:
Diffstat (limited to 'data/transactions/logic/eval.go')
-rw-r--r--data/transactions/logic/eval.go173
1 files changed, 107 insertions, 66 deletions
diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go
index 0dc7eb769..9a2530f36 100644
--- a/data/transactions/logic/eval.go
+++ b/data/transactions/logic/eval.go
@@ -349,8 +349,8 @@ type EvalContext struct {
version uint64
scratch scratchSpace
- subtxn *transactions.SignedTxn // place to build for itxn_submit
- // The transactions Performed() and their effects
+ subtxns []transactions.SignedTxn // place to build for itxn_submit
+ // Previous transactions Performed() and their effects
InnerTxns []transactions.SignedTxnWithAD
cost int // cost incurred so far
@@ -3676,33 +3676,68 @@ func authorizedSender(cx *EvalContext, addr basics.Address) bool {
return appAddr == authorizer
}
-func opTxBegin(cx *EvalContext) {
- if cx.subtxn != nil {
- cx.err = errors.New("itxn_begin without itxn_submit")
- return
- }
- // Start fresh
- cx.subtxn = &transactions.SignedTxn{}
- // Fill in defaults.
+// addInnerTxn appends a fresh SignedTxn to subtxns, populated with reasonable
+// defaults.
+func addInnerTxn(cx *EvalContext) error {
addr, err := cx.getApplicationAddress()
if err != nil {
- cx.err = err
- return
+ return err
}
- fee := cx.Proto.MinTxnFee
- if cx.FeeCredit != nil {
- // Use credit to shrink the fee, but don't change FeeCredit
- // here, because they might never itxn_submit, or they might
- // change the fee. Do it in itxn_submit.
- fee = basics.SubSaturate(fee, *cx.FeeCredit)
+ // For compatibility with v5, in which failures only occurred in the submit,
+ // we only fail here if we are OVER the MaxInnerTransactions limit. Thus
+ // this allows construction of one more Inner than is actually allowed, and
+ // will fail in submit. (But we do want the check here, so this can't become
+ // unbounded.) The MaxTxGroupSize check can be, and is, precise.
+ if len(cx.InnerTxns)+len(cx.subtxns) > cx.Proto.MaxInnerTransactions ||
+ len(cx.subtxns) >= cx.Proto.MaxTxGroupSize {
+ return errors.New("attempt to create too many inner transactions")
+ }
+
+ stxn := transactions.SignedTxn{}
+
+ groupFee := basics.MulSaturate(cx.Proto.MinTxnFee, uint64(len(cx.subtxns)+1))
+ groupPaid := uint64(0)
+ for _, ptxn := range cx.subtxns {
+ groupPaid = basics.AddSaturate(groupPaid, ptxn.Txn.Fee.Raw)
+ }
+
+ fee := uint64(0)
+ if groupPaid < groupFee {
+ fee = groupFee - groupPaid
+
+ if cx.FeeCredit != nil {
+ // Use credit to shrink the default populated fee, but don't change
+ // FeeCredit here, because they might never itxn_submit, or they
+ // might change the fee. Do it in itxn_submit.
+ fee = basics.SubSaturate(fee, *cx.FeeCredit)
+ }
}
- cx.subtxn.Txn.Header = transactions.Header{
- Sender: addr, // Default, to simplify usage
+
+ stxn.Txn.Header = transactions.Header{
+ Sender: addr,
Fee: basics.MicroAlgos{Raw: fee},
FirstValid: cx.Txn.Txn.FirstValid,
LastValid: cx.Txn.Txn.LastValid,
}
+ cx.subtxns = append(cx.subtxns, stxn)
+ return nil
+}
+
+func opTxBegin(cx *EvalContext) {
+ if len(cx.subtxns) > 0 {
+ cx.err = errors.New("itxn_begin without itxn_submit")
+ return
+ }
+ cx.err = addInnerTxn(cx)
+}
+
+func opTxNext(cx *EvalContext) {
+ if len(cx.subtxns) == 0 {
+ cx.err = errors.New("itxn_next without itxn_begin")
+ return
+ }
+ cx.err = addInnerTxn(cx)
}
// availableAccount is used instead of accountReference for more recent opcodes
@@ -3884,7 +3919,8 @@ func (cx *EvalContext) stackIntoTxnField(sv stackValue, fs txnFieldSpec, txn *tr
}
func opTxField(cx *EvalContext) {
- if cx.subtxn == nil {
+ itx := len(cx.subtxns) - 1
+ if itx < 0 {
cx.err = errors.New("itxn_field without itxn_begin")
return
}
@@ -3896,7 +3932,7 @@ func opTxField(cx *EvalContext) {
return
}
sv := cx.stack[last]
- cx.err = cx.stackIntoTxnField(sv, fs, &cx.subtxn.Txn)
+ cx.err = cx.stackIntoTxnField(sv, fs, &cx.subtxns[itx].Txn)
cx.stack = cx.stack[:last] // pop
}
@@ -3906,64 +3942,69 @@ func opTxSubmit(cx *EvalContext) {
return
}
- if cx.subtxn == nil {
- cx.err = errors.New("itxn_submit without itxn_begin")
+ // Should never trigger, since itxn_next checks these too.
+ if len(cx.InnerTxns)+len(cx.subtxns) > cx.Proto.MaxInnerTransactions ||
+ len(cx.subtxns) > cx.Proto.MaxTxGroupSize {
+ cx.err = errors.New("too many inner transactions")
return
}
- if len(cx.InnerTxns) >= cx.Proto.MaxInnerTransactions {
- cx.err = errors.New("itxn_submit with MaxInnerTransactions")
+ if len(cx.subtxns) == 0 {
+ cx.err = errors.New("itxn_submit without itxn_begin")
return
}
- // The goal is to follow the same invariants used by the
- // transaction pool. Namely that any transaction that makes it
- // to Perform (which is equivalent to eval.applyTransaction)
- // is authorized, and WellFormed.
- if !authorizedSender(cx, cx.subtxn.Txn.Sender) {
- cx.err = fmt.Errorf("unauthorized")
- return
+ // Check fees across the group first. Allows fee pooling in inner groups.
+ groupFee := basics.MulSaturate(cx.Proto.MinTxnFee, uint64(len(cx.subtxns)))
+ groupPaid := uint64(0)
+ for _, ptxn := range cx.subtxns {
+ groupPaid = basics.AddSaturate(groupPaid, ptxn.Txn.Fee.Raw)
}
-
- // Recall that WellFormed does not care about individual
- // transaction fees because of fee pooling. So we check below.
- cx.err = cx.subtxn.Txn.WellFormed(*cx.Specials, *cx.Proto)
- if cx.err != nil {
- return
- }
-
- paid := cx.subtxn.Txn.Fee.Raw
- if paid >= cx.Proto.MinTxnFee {
- // Over paying - accumulate into FeeCredit
- overpaid := paid - cx.Proto.MinTxnFee
+ if groupPaid < groupFee {
+ // See if the FeeCredit is enough to cover the shortfall
+ shortfall := groupFee - groupPaid
+ if cx.FeeCredit == nil || *cx.FeeCredit < shortfall {
+ cx.err = fmt.Errorf("fee too small %#v", cx.subtxns)
+ return
+ }
+ *cx.FeeCredit -= shortfall
+ } else {
+ overpay := groupPaid - groupFee
if cx.FeeCredit == nil {
cx.FeeCredit = new(uint64)
}
- *cx.FeeCredit = basics.AddSaturate(*cx.FeeCredit, overpaid)
- } else {
- underpaid := cx.Proto.MinTxnFee - paid
- // Try to pay with FeeCredit, else fail.
- if cx.FeeCredit != nil && *cx.FeeCredit >= underpaid {
- *cx.FeeCredit -= underpaid
- } else {
- // We allow changing the fee. One pattern might be for an
- // app to unilaterally set its Fee to 0. The idea would be
- // that other transactions were supposed to overpay.
- cx.err = fmt.Errorf("fee too small")
+ *cx.FeeCredit = basics.AddSaturate(*cx.FeeCredit, overpay)
+ }
+
+ for itx := range cx.subtxns {
+ // The goal is to follow the same invariants used by the
+ // transaction pool. Namely that any transaction that makes it
+ // to Perform (which is equivalent to eval.applyTransaction)
+ // is authorized, and WellFormed.
+ if !authorizedSender(cx, cx.subtxns[itx].Txn.Sender) {
+ cx.err = fmt.Errorf("unauthorized")
return
}
- }
- ad, err := cx.Ledger.Perform(&cx.subtxn.Txn, *cx.Specials)
- if err != nil {
- cx.err = err
- return
+ // Recall that WellFormed does not care about individual
+ // transaction fees because of fee pooling. So we check below.
+ cx.err = cx.subtxns[itx].Txn.WellFormed(*cx.Specials, *cx.Proto)
+ if cx.err != nil {
+ return
+ }
+
+ ad, err := cx.Ledger.Perform(&cx.subtxns[itx].Txn, *cx.Specials)
+ if err != nil {
+ cx.err = err
+ return
+ }
+
+ cx.InnerTxns = append(cx.InnerTxns, transactions.SignedTxnWithAD{
+ SignedTxn: cx.subtxns[itx],
+ ApplyData: ad,
+ })
}
- cx.InnerTxns = append(cx.InnerTxns, transactions.SignedTxnWithAD{
- SignedTxn: *cx.subtxn,
- ApplyData: ad,
- })
- cx.subtxn = nil
+ cx.subtxns = nil
}
// PcDetails return PC and disassembled instructions at PC up to 2 opcodes back