diff options
Diffstat (limited to 'data/transactions/logic/eval.go')
-rw-r--r-- | data/transactions/logic/eval.go | 173 |
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 |