Skip to content

Commit

Permalink
Inner clearstate (#3556)
Browse files Browse the repository at this point in the history
* Test current state of inner clear state behavior

* First half of clear state fixes. Prevents inner calls in CSP

Should also implement the "CSP requires 700 budget and can't spend
more than 700 budget" rule, but tests still required.

* Match program version. Don't allow downgrade < 6.

* partition test

* typo

Co-authored-by: Michael Diamant <[email protected]>

* typo

Co-authored-by: Michael Diamant <[email protected]>

* Make the default clear_state program match versions to approval

* Correct some tests that now need programs present

* Fix test to actually test INNER clear state

* Add tests for opcode budget and CSPs

* Improve tracing for inner app calls

* Error now reports what the cost *was* before the attempted inst.

* Simplify type switch and maybe kick CI?

* Fewer copies during inner processing

Co-authored-by: Michael Diamant <[email protected]>
  • Loading branch information
jannotti and michaeldiamant authored Feb 6, 2022
1 parent 7c2329c commit abe3c6a
Show file tree
Hide file tree
Showing 16 changed files with 1,106 additions and 88 deletions.
4 changes: 4 additions & 0 deletions config/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ type ConsensusParams struct {
// should the number of inner transactions be pooled across group?
EnableInnerTransactionPooling bool

// provide greater isolation for clear state programs
IsolateClearState bool

// maximum number of applications a single account can create and store
// AppParams for at once
MaxAppsCreated int
Expand Down Expand Up @@ -1060,6 +1063,7 @@ func initConsensusProtocols() {
// Enable TEAL 6 / AVM 1.1
vFuture.LogicSigVersion = 6
vFuture.EnableInnerTransactionPooling = true
vFuture.IsolateClearState = true

vFuture.MaxProposedExpiredOnlineAccounts = 32

Expand Down
105 changes: 87 additions & 18 deletions data/transactions/logic/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,8 @@ func NewEvalParams(txgroup []transactions.SignedTxnWithAD, proto *config.Consens
}

// NewInnerEvalParams creates an EvalParams to be used while evaluating an inner group txgroup
func NewInnerEvalParams(txg []transactions.SignedTxn, caller *EvalContext) *EvalParams {
txgroup := transactions.WrapSignedTxnsWithAD(txg)

minTealVersion := ComputeMinTealVersion(txgroup, true)
func NewInnerEvalParams(txg []transactions.SignedTxnWithAD, caller *EvalContext) *EvalParams {
minTealVersion := ComputeMinTealVersion(txg, true)
// Can't happen currently, since innerAppsEnabledVersion > than any minimum
// imposed otherwise. But is correct to check, in case of future restriction.
if minTealVersion < *caller.MinTealVersion {
Expand All @@ -351,7 +349,7 @@ func NewInnerEvalParams(txg []transactions.SignedTxn, caller *EvalContext) *Eval
// Unlike NewEvalParams, do not add fee credit here. opTxSubmit has already done so.

if caller.Proto.EnableAppCostPooling {
for _, tx := range txgroup {
for _, tx := range txg {
if tx.Txn.Type == protocol.ApplicationCallTx {
*caller.PooledApplicationBudget += caller.Proto.MaxAppProgramCost
}
Expand All @@ -360,8 +358,9 @@ func NewInnerEvalParams(txg []transactions.SignedTxn, caller *EvalContext) *Eval

ep := &EvalParams{
Proto: caller.Proto,
TxnGroup: copyWithClearAD(txgroup),
pastScratch: make([]*scratchSpace, len(txgroup)),
Trace: caller.Trace,
TxnGroup: txg,
pastScratch: make([]*scratchSpace, len(txg)),
MinTealVersion: &minTealVersion,
FeeCredit: caller.FeeCredit,
Specials: caller.Specials,
Expand Down Expand Up @@ -466,9 +465,9 @@ type EvalContext struct {
version uint64
scratch scratchSpace

subtxns []transactions.SignedTxn // place to build for itxn_submit
cost int // cost incurred so far
logSize int // total log size so far
subtxns []transactions.SignedTxnWithAD // place to build for itxn_submit
cost int // cost incurred so far
logSize int // total log size so far

// Set of PC values that branches we've seen so far might
// go. So, if checkStep() skips one, that branch is trying to
Expand Down Expand Up @@ -546,6 +545,19 @@ func (pe PanicError) Error() string {
var errLogicSigNotSupported = errors.New("LogicSig not supported")
var errTooManyArgs = errors.New("LogicSig has too many arguments")

// ClearStateBudgetError allows evaluation to signal that the caller should
// reject the transaction. Normally, an error in evaluation would not cause a
// ClearState txn to fail. However, callers fail a txn for ClearStateBudgetError
// because the transaction has not provided enough budget to let ClearState do
// its job.
type ClearStateBudgetError struct {
offered int
}

func (e ClearStateBudgetError) Error() string {
return fmt.Sprintf("Attempted ClearState execution with low OpcodeBudget %d", e.offered)
}

// EvalContract executes stateful TEAL program as the gi'th transaction in params
func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParams) (bool, *EvalContext, error) {
if params.Ledger == nil {
Expand All @@ -561,9 +573,26 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam
Txn: &params.TxnGroup[gi],
appID: aid,
}

if cx.Proto.IsolateClearState && cx.Txn.Txn.OnCompletion == transactions.ClearStateOC {
if cx.PooledApplicationBudget != nil && *cx.PooledApplicationBudget < cx.Proto.MaxAppProgramCost {
return false, nil, ClearStateBudgetError{*cx.PooledApplicationBudget}
}
}

if cx.Trace != nil && cx.caller != nil {
fmt.Fprintf(cx.Trace, "--- enter %d %s %v\n", aid, cx.Txn.Txn.OnCompletion, cx.Txn.Txn.ApplicationArgs)
}
pass, err := eval(program, &cx)
if cx.Trace != nil && cx.caller != nil {
fmt.Fprintf(cx.Trace, "--- exit %d accept=%t\n", aid, pass)
}

// update side effects
// update side effects. It is tempting, and maybe even a good idea, to store
// the pointer to cx.scratch instead. Since we don't modify them again,
// it's probably safe. However it may have poor GC characteristics (because
// we'd be storing a pointer into a much larger structure, the cx), and
// copying seems nice and clean.
cx.pastScratch[cx.GroupIndex] = &scratchSpace{}
*cx.pastScratch[cx.GroupIndex] = cx.scratch

Expand Down Expand Up @@ -749,12 +778,9 @@ func check(program []byte, params *EvalParams, mode runMode) (err error) {
}

func versionCheck(program []byte, params *EvalParams) (uint64, int, error) {
if len(program) == 0 {
return 0, 0, errors.New("invalid program (empty)")
}
version, vlen := binary.Uvarint(program)
if vlen <= 0 {
return 0, 0, errors.New("invalid version")
version, vlen, err := transactions.ProgramVersion(program)
if err != nil {
return 0, 0, err
}
if version > EvalMaxVersion {
return 0, 0, fmt.Errorf("program version %d greater than max supported version %d", version, EvalMaxVersion)
Expand Down Expand Up @@ -801,6 +827,16 @@ func (cx *EvalContext) remainingBudget() int {
if cx.runModeFlags == runModeSignature {
return int(cx.Proto.LogicSigMaxCost) - cx.cost
}

// restrict clear state programs from using more than standard unpooled budget
// cx.Txn is not set during check()
if cx.Proto.IsolateClearState && cx.Txn != nil && cx.Txn.Txn.OnCompletion == transactions.ClearStateOC {
// Need not confirm that *cx.PooledApplicationBudget is also >0, as
// ClearState programs are only run if *cx.PooledApplicationBudget >
// MaxAppProgramCost at the start.
return cx.Proto.MaxAppProgramCost - cx.cost
}

if cx.PooledApplicationBudget != nil {
return *cx.PooledApplicationBudget
}
Expand Down Expand Up @@ -856,6 +892,13 @@ func (cx *EvalContext) step() {
}

if cx.remainingBudget() < 0 {
// We're not going to execute the instruction, so give the cost back.
// This only matters if this is an inner ClearState - the caller should
// not be over debited. (Normally, failure causes total txtree failure.)
cx.cost -= deets.Cost
if cx.PooledApplicationBudget != nil {
*cx.PooledApplicationBudget += deets.Cost
}
cx.err = fmt.Errorf("pc=%3d dynamic cost budget exceeded, executing %s: local program cost was %d",
cx.pc, spec.Name, cx.cost)
return
Expand Down Expand Up @@ -3984,7 +4027,7 @@ func addInnerTxn(cx *EvalContext) error {
return fmt.Errorf("too many inner transactions %d with %d left", len(cx.subtxns), cx.remainingInners())
}

stxn := transactions.SignedTxn{}
stxn := transactions.SignedTxnWithAD{}

groupFee := basics.MulSaturate(cx.Proto.MinTxnFee, uint64(len(cx.subtxns)+1))
groupPaid := uint64(0)
Expand Down Expand Up @@ -4019,6 +4062,10 @@ func opTxBegin(cx *EvalContext) {
cx.err = errors.New("itxn_begin without itxn_submit")
return
}
if cx.Proto.IsolateClearState && cx.Txn.Txn.OnCompletion == transactions.ClearStateOC {
cx.err = errors.New("clear state programs can not issue inner transactions")
return
}
cx.err = addInnerTxn(cx)
}

Expand Down Expand Up @@ -4435,6 +4482,28 @@ func opTxSubmit(cx *EvalContext) {
cx.err = fmt.Errorf("appl depth (%d) exceeded", depth)
return
}

// Can't call version < innerAppsEnabledVersion, and apps with such
// versions will always match, so just check approval program
// version.
program := cx.subtxns[itx].Txn.ApprovalProgram
if cx.subtxns[itx].Txn.ApplicationID != 0 {
app, _, err := cx.Ledger.AppParams(cx.subtxns[itx].Txn.ApplicationID)
if err != nil {
cx.err = err
return
}
program = app.ApprovalProgram
}
v, _, err := transactions.ProgramVersion(program)
if err != nil {
cx.err = err
return
}
if v < innerAppsEnabledVersion {
cx.err = fmt.Errorf("inner app call with version %d < %d", v, innerAppsEnabledVersion)
return
}
}

if isGroup {
Expand Down
Loading

0 comments on commit abe3c6a

Please sign in to comment.