Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AVM: Show opcode context for logicsigs (not just apps) #5336

Merged
merged 6 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,9 @@ type ConsensusParams struct {
// returning false, if pubkey is not on the curve.
EnablePrecheckECDSACurve bool

// EnableBareBudgetError specifies that I/O budget overruns should not be considered EvalError
EnableBareBudgetError bool

// StateProofUseTrackerVerification specifies whether the node will use data from state proof verification tracker
// in order to verify state proofs.
StateProofUseTrackerVerification bool
Expand Down Expand Up @@ -1262,6 +1265,7 @@ func initConsensusProtocols() {

vFuture.LogicSigVersion = 9 // When moving this to a release, put a new higher LogicSigVersion here
vFuture.EnablePrecheckECDSACurve = true
vFuture.EnableBareBudgetError = true

vFuture.StateProofUseTrackerVerification = true
vFuture.EnableCatchpointsWithSPContexts = true
Expand Down
4 changes: 2 additions & 2 deletions data/transactions/logic/backwardCompat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func TestBackwardCompatTEALv1(t *testing.T) {
ep.TxnGroup[0].Lsig.Logic = program
ep.TxnGroup[0].Lsig.Args = [][]byte{data[:], sig[:], pk[:], tx.Sender[:], tx.Note}

// ensure v1 program runs well on latest TEAL evaluator
// ensure v1 program runs well on latest evaluator
require.Equal(t, uint8(1), program[0])

// Cost should stay exactly 2140
Expand Down Expand Up @@ -315,7 +315,7 @@ func TestBackwardCompatTEALv1(t *testing.T) {
ep2.Proto.LogicSigMaxCost = 2308
testLogicBytes(t, opsV2.Program, ep2)

// ensure v0 program runs well on latest TEAL evaluator
// ensure v0 program runs well on latest evaluator
ep, tx, _ = makeSampleEnv()
program[0] = 0
sig = c.Sign(Msg{
Expand Down
77 changes: 53 additions & 24 deletions data/transactions/logic/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,33 +678,46 @@ func (st StackType) Typed() bool {
return false
}

// PanicError wraps a recover() catching a panic()
type PanicError struct {
// panicError wraps a recover() catching a panic()
type panicError struct {
PanicValue interface{}
StackTrace string
}

func (pe PanicError) Error() string {
func (pe panicError) Error() string {
return fmt.Sprintf("panic in TEAL Eval: %v\n%s", pe.PanicValue, pe.StackTrace)
}

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
// EvalError indicates AVM evaluation failure
type EvalError struct {
Err error
details string
groupIndex int
logicsig bool
}

func (e ClearStateBudgetError) Error() string {
return fmt.Sprintf("Attempted ClearState execution with low OpcodeBudget %d", e.offered)
// Error satisfies builtin interface `error`
func (err EvalError) Error() string {
var msg string
if err.logicsig {
msg = fmt.Sprintf("rejected by logic err=%v", err.Err)
} else {
msg = fmt.Sprintf("logic eval error: %v", err.Err)
bbroder-algo marked this conversation as resolved.
Show resolved Hide resolved
}
if err.details == "" {
return msg
}
return msg + ". Details: " + err.details
}

// EvalContract executes stateful TEAL program as the gi'th transaction in params
func (err EvalError) Unwrap() error {
return err.Err
}

// EvalContract executes stateful 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 {
return false, nil, errors.New("no ledger in contract eval")
Expand All @@ -725,7 +738,7 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam

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}
return false, nil, fmt.Errorf("Attempted ClearState execution with low OpcodeBudget %d", *cx.PooledApplicationBudget)
}
}

Expand Down Expand Up @@ -766,7 +779,11 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam

used = basics.AddSaturate(used, size)
if used > cx.ioBudget {
return false, nil, fmt.Errorf("box read budget (%d) exceeded", cx.ioBudget)
err = fmt.Errorf("box read budget (%d) exceeded", cx.ioBudget)
if !cx.Proto.EnableBareBudgetError {
err = EvalError{err, "", gi, false}
}
return false, nil, err
}
}
cx.readBudgetChecked = true
Expand All @@ -776,6 +793,11 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam
fmt.Fprintf(cx.Trace, "--- enter %d %s %v\n", aid, cx.txn.Txn.OnCompletion, cx.txn.Txn.ApplicationArgs)
}
pass, err := eval(program, &cx)
if err != nil {
pc, det := cx.pcDetails()
details := fmt.Sprintf("pc=%d, opcodes=%s", pc, det)
err = EvalError{err, details, gi, false}
}

if cx.Trace != nil && cx.caller != nil {
fmt.Fprintf(cx.Trace, "--- exit %d accept=%t\n", aid, pass)
Expand All @@ -798,7 +820,7 @@ func EvalApp(program []byte, gi int, aid basics.AppIndex, params *EvalParams) (b
// EvalSignatureFull evaluates the logicsig of the ith transaction in params.
// A program passes successfully if it finishes with one int element on the stack that is non-zero.
// It returns EvalContext suitable for obtaining additional info about the execution.
func EvalSignatureFull(gi int, params *EvalParams) (pass bool, pcx *EvalContext, err error) {
func EvalSignatureFull(gi int, params *EvalParams) (bool, *EvalContext, error) {
if params.SigLedger == nil {
return false, nil, errors.New("no sig ledger in signature eval")
}
Expand All @@ -808,14 +830,21 @@ func EvalSignatureFull(gi int, params *EvalParams) (pass bool, pcx *EvalContext,
groupIndex: gi,
txn: &params.TxnGroup[gi],
}
pass, err = eval(cx.txn.Lsig.Logic, &cx)
pass, err := eval(cx.txn.Lsig.Logic, &cx)

if err != nil {
pc, det := cx.pcDetails()
details := fmt.Sprintf("pc=%d, opcodes=%s", pc, det)
err = EvalError{err, details, gi, true}
}

return pass, &cx, err
}

// EvalSignature evaluates the logicsig of the ith transaction in params.
// A program passes successfully if it finishes with one int element on the stack that is non-zero.
func EvalSignature(gi int, params *EvalParams) (pass bool, err error) {
pass, _, err = EvalSignatureFull(gi, params)
func EvalSignature(gi int, params *EvalParams) (bool, error) {
pass, _, err := EvalSignatureFull(gi, params)
return pass, err
}

Expand All @@ -831,7 +860,7 @@ func eval(program []byte, cx *EvalContext) (pass bool, err error) {
if cx.Trace != nil {
errstr += cx.Trace.String()
}
err = PanicError{x, errstr}
err = panicError{x, errstr}
cx.EvalParams.log().Errorf("recovered panic in Eval: %v", err)
}
}()
Expand Down Expand Up @@ -941,7 +970,7 @@ func check(program []byte, params *EvalParams, mode RunMode) (err error) {
if params.Trace != nil {
errstr += params.Trace.String()
}
err = PanicError{x, errstr}
err = panicError{x, errstr}
params.log().Errorf("recovered panic in Check: %s", err)
}
}()
Expand Down Expand Up @@ -5522,8 +5551,8 @@ func opBlock(cx *EvalContext) error {
}
}

// PcDetails return PC and disassembled instructions at PC up to 2 opcodes back
func (cx *EvalContext) PcDetails() (pc int, dis string) {
// pcDetails return PC and disassembled instructions at PC up to 2 opcodes back
func (cx *EvalContext) pcDetails() (pc int, dis string) {
jannotti marked this conversation as resolved.
Show resolved Hide resolved
const maxNumAdditionalOpcodes = 2
text, ds, err := disassembleInstrumented(cx.program, nil)
if err != nil {
Expand All @@ -5547,7 +5576,7 @@ func (cx *EvalContext) PcDetails() (pc int, dis string) {
break
}
}
return cx.pc, dis
return cx.pc, strings.ReplaceAll(strings.TrimSuffix(dis, "\n"), "\n", "; ")
}

func base64Decode(encoded []byte, encoding *base64.Encoding) ([]byte, error) {
Expand Down
45 changes: 16 additions & 29 deletions data/transactions/logic/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2961,7 +2961,7 @@ func isNotPanic(t *testing.T, err error) {
if err == nil {
return
}
if pe, ok := err.(PanicError); ok {
if pe, ok := err.(panicError); ok {
t.Error(pe)
}
}
Expand Down Expand Up @@ -3188,14 +3188,10 @@ func TestPanic(t *testing.T) { //nolint:paralleltest // Uses withPanicOpcode
params := defaultEvalParams()
params.TxnGroup[0].Lsig.Logic = ops.Program
err := CheckSignature(0, params)
require.Error(t, err)
if pe, ok := err.(PanicError); ok {
require.Equal(t, panicString, pe.PanicValue)
pes := pe.Error()
require.True(t, strings.Contains(pes, "panic"))
} else {
t.Errorf("expected PanicError object but got %T %#v", err, err)
}
var pe panicError
require.ErrorAs(t, err, &pe)
require.Equal(t, panicString, pe.PanicValue)
require.ErrorContains(t, pe, "panic")

var txn transactions.SignedTxn
txn.Lsig.Logic = ops.Program
Expand All @@ -3206,13 +3202,9 @@ func TestPanic(t *testing.T) { //nolint:paralleltest // Uses withPanicOpcode
t.Log(params.Trace.String())
}
require.False(t, pass)
if pe, ok := err.(PanicError); ok {
require.Equal(t, panicString, pe.PanicValue)
pes := pe.Error()
require.True(t, strings.Contains(pes, "panic"))
} else {
t.Errorf("expected PanicError object but got %T %#v", err, err)
}
require.ErrorAs(t, err, &pe)
require.Equal(t, panicString, pe.PanicValue)
require.ErrorContains(t, pe, "panic")

if v >= appsEnabledVersion {
txn = transactions.SignedTxn{
Expand All @@ -3224,13 +3216,9 @@ func TestPanic(t *testing.T) { //nolint:paralleltest // Uses withPanicOpcode
params.Ledger = NewLedger(nil)
pass, err = EvalApp(ops.Program, 0, 1, params)
require.False(t, pass)
if pe, ok := err.(PanicError); ok {
require.Equal(t, panicString, pe.PanicValue)
pes := pe.Error()
require.True(t, strings.Contains(pes, "panic"))
} else {
t.Errorf("expected PanicError object but got %T %#v", err, err)
}
require.ErrorAs(t, err, &pe)
require.Equal(t, panicString, pe.PanicValue)
require.ErrorContains(t, pe, "panic")
}
})
})
Expand Down Expand Up @@ -5150,9 +5138,9 @@ func TestPcDetails(t *testing.T) {
pc int
det string
}{
{"int 1; int 2; -", 5, "pushint 1\npushint 2\n-\n"},
{"int 1; err", 3, "pushint 1\nerr\n"},
{"int 1; dup; int 2; -; +", 6, "dup\npushint 2\n-\n"},
{"int 1; int 2; -", 5, "pushint 1; pushint 2; -"},
{"int 1; err", 3, "pushint 1; err"},
{"int 1; dup; int 2; -; +", 6, "dup; pushint 2; -"},
{"b end; end:", 4, ""},
}
for i, test := range tests {
Expand All @@ -5170,7 +5158,7 @@ func TestPcDetails(t *testing.T) {

assert.Equal(t, test.pc, cx.pc, ep.Trace.String())

pc, det := cx.PcDetails()
pc, det := cx.pcDetails()
assert.Equal(t, test.pc, pc)
assert.Equal(t, test.det, det)
})
Expand Down Expand Up @@ -5789,8 +5777,7 @@ func TestOpJSONRef(t *testing.T) {

pass, _, err := EvalContract(ops.Program, 0, 888, ep)
require.False(t, pass)
require.Error(t, err)
require.EqualError(t, err, s.error)
require.ErrorContains(t, err, s.error)

// reset pooled budget for new "app call"
*ep.PooledApplicationBudget = ep.Proto.MaxAppProgramCost
Expand Down
2 changes: 1 addition & 1 deletion data/transactions/verify/txn.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ func logicSigVerify(txn *transactions.SignedTxn, groupIndex int, groupCtx *Group
pass, cx, err := logic.EvalSignatureFull(groupIndex, &ep)
if err != nil {
logicErrTotal.Inc(nil)
return LogicSigError{groupIndex, fmt.Errorf("transaction %v: rejected by logic err=%w", txn.ID(), err)}
return LogicSigError{groupIndex, fmt.Errorf("transaction %v: %w", txn.ID(), err)}
}
if !pass {
logicRejTotal.Inc(nil)
Expand Down
4 changes: 2 additions & 2 deletions ledger/apply/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,8 @@ func ApplicationCall(ac transactions.ApplicationCallTxnFields, header transactio
if exists {
pass, evalDelta, err := balances.StatefulEval(gi, evalParams, appIdx, params.ClearStateProgram)
if err != nil {
// Fail on non-logic eval errors and ignore LogicEvalError errors
if _, ok := err.(ledgercore.LogicEvalError); !ok {
// ClearStateProgram evaluation can't make the txn fail.
if _, ok := err.(logic.EvalError); !ok {
return err
}
}
Expand Down
2 changes: 1 addition & 1 deletion ledger/apply/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ func TestAppCallClearState(t *testing.T) {
// one to opt out, one deallocate, no error from ApplicationCall
b.pass = true
b.delta = transactions.EvalDelta{GlobalDelta: nil}
b.err = ledgercore.LogicEvalError{Err: fmt.Errorf("test error")}
b.err = logic.EvalError{Err: fmt.Errorf("test error")}
err = ApplicationCall(ac, h, b, ad, 0, &ep, txnCounter)
a.NoError(err)
a.Equal(1, b.put)
Expand Down
12 changes: 6 additions & 6 deletions ledger/boxtxn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,28 +171,28 @@ func TestBoxCreate(t *testing.T) {
}

dl.txn(adam.Args("check", "adam", "\x00\x00"))
dl.txgroup("box_create\nassert", adam.Noted("one"), adam.Noted("two"))
dl.txgroup("box_create; assert", adam.Noted("one"), adam.Noted("two"))

bobo := call.Args("create", "bobo")
dl.txn(bobo, fmt.Sprintf("invalid Box reference %#x", "bobo"))
bobo.Boxes = []transactions.BoxRef{{Index: 0, Name: []byte("bobo")}}
dl.txn(bobo)
dl.txgroup("box_create\nassert", bobo.Noted("one"), bobo.Noted("two"))
dl.txgroup("box_create; assert", bobo.Noted("one"), bobo.Noted("two"))

dl.beginBlock()
chaz := call.Args("create", "chaz")
chaz.Boxes = []transactions.BoxRef{{Index: 0, Name: []byte("chaz")}}
dl.txn(chaz)
dl.txn(chaz.Noted("again"), "box_create\nassert")
dl.txn(chaz.Noted("again"), "box_create; assert")
dl.endBlock()

// new block
dl.txn(chaz.Noted("again"), "box_create\nassert")
dl.txn(chaz.Noted("again"), "box_create; assert")
dogg := call.Args("create", "dogg")
dogg.Boxes = []transactions.BoxRef{{Index: 0, Name: []byte("dogg")}}
dl.txn(dogg, "below min")
dl.txn(chaz.Args("delete", "chaz"))
dl.txn(chaz.Args("delete", "chaz").Noted("again"), "box_del\nassert")
dl.txn(chaz.Args("delete", "chaz").Noted("again"), "box_del; assert")
dl.txn(dogg)
dl.txn(bobo.Args("delete", "bobo"))

Expand Down Expand Up @@ -231,7 +231,7 @@ func TestBoxRecreate(t *testing.T) {
create := call.Args("create", "adam", "\x04") // box value size is 4 bytes
recreate := call.Args("recreate", "adam", "\x04")

dl.txn(recreate, "box_create\n!\nassert")
dl.txn(recreate, "box_create; !; assert")
dl.txn(create)
dl.txn(recreate)
dl.txn(call.Args("set", "adam", "\x01\x02\x03\x04"))
Expand Down
14 changes: 2 additions & 12 deletions ledger/eval/appcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,19 +467,9 @@ func (cb *roundCowState) StatefulEval(gi int, params *logic.EvalParams, aidx bas
params.Ledger = calf
params.SigLedger = calf

// Eval the program
pass, cx, err := logic.EvalContract(program, gi, aidx, params)
pass, err = logic.EvalApp(program, gi, aidx, params)
if err != nil {
var details string
if cx != nil {
pc, det := cx.PcDetails()
details = fmt.Sprintf("pc=%d, opcodes=%s", pc, det)
}
// Don't wrap ClearStateBudgetError, so it will be taken seriously
if _, ok := err.(logic.ClearStateBudgetError); ok {
return false, transactions.EvalDelta{}, err
}
return false, transactions.EvalDelta{}, ledgercore.LogicEvalError{Err: err, Details: details}
return false, transactions.EvalDelta{}, err
}

// If program passed, build our eval delta, and commit to state changes
Expand Down
2 changes: 1 addition & 1 deletion ledger/eval/cow.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type roundCowState struct {

// storage deltas populated as side effects of AppCall transaction
// 1. Opt-in/Close actions (see Allocate/Deallocate)
// 2. Stateful TEAL evaluation (see setKey/delKey)
// 2. Application evaluation (see setKey/delKey)
// must be incorporated into mods.accts before passing deltas forward
sdeltas map[basics.Address]map[storagePtr]*storageDelta

Expand Down
Loading